はじめに
前回までで、FFM API の使い方を見てきました。
- Java から FFM API で Rust を呼ぶ(1/4)
- Java から FFM API で Rust を呼ぶ(2/4)
- Java から FFM API で Rust を呼ぶ(3/4)
- Java から FFM API で Rust を呼ぶ(4/4)
- Java から FFM API で Rust を呼ぶ(おまけ) いまココ
今回は、Rust 側で Vello によるレンダリング結果を Java 側でアニメーション表示する例とテキストを表示する例をおまけとして乗せておきます。
Rust 側実装(アニメーション表示)
Rust 側は Vello で描画するため以下の依存を追加します。
[dependencies] vello = "0.10.0" wgpu = "29.0.3" pollster = "0.4.0"
描画コードは以下となります。
use std::slice; use vello::{ kurbo::{Affine, Circle, Rect}, peniko::{Color, Fill}, Scene, }; pub struct RenderContext { device: wgpu::Device, queue: wgpu::Queue, renderer: vello::Renderer, texture: wgpu::Texture, readback_buffer: wgpu::Buffer, width: u32, height: u32, bytes_per_row: u32, } #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_create(width: u32, height: u32) -> *mut RenderContext { let bytes_per_row = width * 4; assert!(bytes_per_row % 256 == 0, "Width * 4 must be a multiple of 256"); let ctx = pollster::block_on(async move { let instance = wgpu::Instance::default(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions::default()) .await .unwrap(); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor::default()) .await .unwrap(); let format = wgpu::TextureFormat::Rgba8Unorm; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Vello Target Texture"), size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::STORAGE_BINDING, view_formats: &[], }); let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Readback Buffer"), size: (width * height * 4) as wgpu::BufferAddress, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let renderer = vello::Renderer::new( &device, vello::RendererOptions { use_cpu: false, antialiasing_support: vello::AaSupport::all(), num_init_threads: None, pipeline_cache: None, }, ).unwrap(); RenderContext { device, queue, renderer, texture, readback_buffer, width, height, bytes_per_row, } }); // detach from Rust's memory management and pass to Java as a raw pointer Box::into_raw(Box::new(ctx)) } #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_render(ctx_ptr: *mut RenderContext, buffer: *mut u8, time: f64) { if ctx_ptr.is_null() || buffer.is_null() { return; } // restore the reference from the raw pointer (without taking ownership) let ctx = unsafe { &mut *ctx_ptr }; let length = (ctx.width * ctx.height * 4) as usize; let view = ctx.texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut scene = Scene::new(); scene.fill( Fill::NonZero, Affine::IDENTITY, Color::from_rgb8(30, 30, 35), None, &Rect::new(0.0, 0.0, ctx.width as f64, ctx.height as f64), ); let cx = (ctx.width as f64 / 2.0) + time.sin() * 200.0; scene.fill( Fill::NonZero, Affine::IDENTITY, Color::from_rgb8(255, 80, 80), None, &Circle::new((cx, ctx.height as f64 / 2.0), 100.0), ); scene.fill( Fill::NonZero, Affine::rotate_about(time, (ctx.width as f64 / 2.0, ctx.height as f64 / 2.0)), Color::from_rgb8(80, 150, 255), None, &Rect::from_center_size((ctx.width as f64 / 2.0, ctx.height as f64 / 2.0), (150.0, 150.0)), ); // drow to the GPU ctx.renderer.render_to_texture( &ctx.device, &ctx.queue, &scene, &view, &vello::RenderParams { base_color: vello::peniko::Color::TRANSPARENT, width: ctx.width, height: ctx.height, antialiasing_method: vello::AaConfig::Msaa16, }, ).unwrap(); let mut encoder = ctx.device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &ctx.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &ctx.readback_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(ctx.bytes_per_row), rows_per_image: Some(ctx.height), }, }, wgpu::Extent3d { width: ctx.width, height: ctx.height, depth_or_array_layers: 1 }, ); ctx.queue.submit(Some(encoder.finish())); let buffer_slice = ctx.readback_buffer.slice(..); let (tx, rx) = std::sync::mpsc::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |v| tx.send(v).unwrap()); ctx.device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); rx.recv().unwrap().unwrap(); let mapped_data = buffer_slice.get_mapped_range(); // SIMD copy let src_bytes = unsafe { slice::from_raw_parts(mapped_data.as_ptr(), length) }; let dst_bytes = unsafe { slice::from_raw_parts_mut(buffer, length) }; for (src, dst) in src_bytes.chunks_exact(4).zip(dst_bytes.chunks_exact_mut(4)) { dst[0] = src[2]; // B <- R dst[1] = src[1]; // G <- G dst[2] = src[0]; // R <- B dst[3] = src[3]; // A <- A } drop(mapped_data); ctx.readback_buffer.unmap(); } #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_destroy(ctx_ptr: *mut RenderContext) { if !ctx_ptr.is_null() { // reconstruct the Box from the raw pointer to trigger // automatic dropping (memory deallocation) when going out of scope unsafe { let _ = Box::from_raw(ctx_ptr); } println!(" Vello RenderContext destroyed."); } }
wgpu で描画するので、ごちゃごちゃしていますが、以下の関数を公開しています。
vello_ctx_create():wgpu::Instanceを生成。呼び出し元で使いまわしvello_ctx_render(): レンダリング処理vello_ctx_destroy(): リソースの後始末
wgpu は1行のバイト数が 256 の倍数である必要があります。
JavaFX 側では Bgba として扱うため、Rgba で描画後に Bgba に変換しています(現バージョンのVelloではBgbaでの描画ができない)。
Java 側実装(アニメーション表示)
vello_ctx_create() で取得したコンテキストを使いまわし、vello_ctx_render() で定期的に描画処理を行っているだけです。
package org.example; import com.example.rust_lib.rust_lib_h; import javafx.animation.AnimationTimer; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.ImageView; import javafx.scene.image.PixelBuffer; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.nio.ByteBuffer; public class App extends Application { // Set so that width * 4 is a multiple of 256 (e.g., 1024 * 4 = 4096 = 256 * 16) private static final int WIDTH = 1024; private static final int HEIGHT = 768; private Arena arena; private MemorySegment ctxPtr; private AnimationTimer timer; @Override public void start(Stage stage) { arena = Arena.ofShared(); // initialize the Vello context on the Rust side and receive its pointer ctxPtr = rust_lib_h.vello_ctx_create(WIDTH, HEIGHT); if (ctxPtr.equals(MemorySegment.NULL)) { throw new RuntimeException("Failed to initialize Vello context"); } // allocate a shared memory region for image transfer long bufferSize = (long) WIDTH * HEIGHT * 4; MemorySegment pixelData = arena.allocate(bufferSize); // set up PixelBuffer and WritableImage ByteBuffer byteBuffer = pixelData.asByteBuffer(); PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraPreInstance(); PixelBuffer<ByteBuffer> pixelBuffer = new PixelBuffer<>(WIDTH, HEIGHT, byteBuffer, format); WritableImage image = new WritableImage(pixelBuffer); ImageView imageView = new ImageView(image); StackPane root = new StackPane(imageView); Scene scene = new Scene(root, WIDTH, HEIGHT); stage.setTitle("Vello Realtime Rendering"); stage.setScene(scene); stage.show(); long startTime = System.nanoTime(); timer = new AnimationTimer() { @Override public void handle(long now) { double time = (now - startTime) / 1_000_000_000.0; // request Rust to perform rendering rust_lib_h.vello_ctx_render(ctxPtr, pixelData, time); // notify JavaFX that the PixelBuffer has changed to refresh the screen pixelBuffer.updateBuffer(_ -> null); } }; timer.start(); } @Override public void stop() { if (timer != null) { timer.stop(); } if (ctxPtr != null && !ctxPtr.equals(MemorySegment.NULL)) { rust_lib_h.vello_ctx_destroy(ctxPtr); } if (arena != null) arena.close(); } }
実行結果(アプリケーション表示)

Rust 側実装(テキスト表示)
Vello で直接フォント表示するのは少し面倒です。
レイアウトエンジンである parley を使う方が良いのですが、長くなるので skrifa でグリフを直接描画します。
skrifa = "0.47"
フォントファイルを Rust の src 直下に配備します。ここでは Monaco.ttf を準備しました。
rust のソースはほとんど今までと同じですが、フォントを読み込んでグリフを作成し、Vello で描画するように変えています。
use std::slice; use std::sync::Arc; use vello::{ kurbo::{Affine, Rect}, peniko::{Blob, Color, Fill}, Glyph, Scene, }; use skrifa::{FontRef, MetadataProvider}; use vello::peniko::{Brush, FontData}; use std::ffi::{c_char, CStr}; pub struct RenderContext { device: wgpu::Device, queue: wgpu::Queue, renderer: vello::Renderer, texture: wgpu::Texture, readback_buffer: wgpu::Buffer, width: u32, height: u32, bytes_per_row: u32, } const FONT_DATA: &[u8] = include_bytes!("Monaco.ttf"); #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_create(width: u32, height: u32) -> *mut RenderContext { let bytes_per_row = width * 4; assert_eq!(bytes_per_row % 256, 0, "Width * 4 must be a multiple of 256"); let ctx = pollster::block_on(async move { let instance = wgpu::Instance::default(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions::default()) .await .unwrap(); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor::default()) .await .unwrap(); let format = wgpu::TextureFormat::Rgba8Unorm; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Vello Target Texture"), size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::STORAGE_BINDING, view_formats: &[], }); let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Readback Buffer"), size: (width * height * 4) as wgpu::BufferAddress, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let renderer = vello::Renderer::new( &device, vello::RendererOptions { use_cpu: false, antialiasing_support: vello::AaSupport::all(), num_init_threads: None, pipeline_cache: None, }, ).unwrap(); RenderContext { device, queue, renderer, texture, readback_buffer, width, height, bytes_per_row, } }); // detach from Rust's memory management and pass to Java as a raw pointer Box::into_raw(Box::new(ctx)) } #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_render(ctx_ptr: *mut RenderContext, buffer: *mut u8, text_ptr: *const c_char) { if ctx_ptr.is_null() || buffer.is_null() || text_ptr.is_null() { return; } let c_str = unsafe { CStr::from_ptr(text_ptr) }; let text = match c_str.to_str() { Ok(s) => s, Err(_) => return, // illegal UTF-8 }; // restore the reference from the raw pointer (without taking ownership) let ctx = unsafe { &mut *ctx_ptr }; let length = (ctx.width * ctx.height * 4) as usize; let view = ctx.texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut scene = Scene::new(); scene.fill( Fill::NonZero, Affine::IDENTITY, Color::from_rgb8(30, 30, 35), None, &Rect::new(0.0, 0.0, ctx.width as f64, ctx.height as f64), ); // read font let font_ref = FontRef::new(FONT_DATA).expect("Failed to load font data"); let char_map = font_ref.charmap(); let font_size = 80.0; let metrics = font_ref.glyph_metrics( skrifa::instance::Size::new(font_size), skrifa::instance::LocationRef::default() ); let font_blob = Blob::new(Arc::new(FONT_DATA)); let vello_font = FontData::new(font_blob, 0); // build glyph list let mut glyphs = Vec::new(); let mut current_x = 0.0; for ch in text.chars() { if let Some(glyph_id) = char_map.map(ch) { let glyph = Glyph { id: glyph_id.to_u32(), x: current_x, y: 0.0, }; glyphs.push(glyph); let advance = metrics.advance_width(glyph_id).unwrap_or(0.0); current_x += advance; } } // draw glyphs scene .draw_glyphs(&vello_font) .font_size(font_size) .brush(&Brush::Solid(Color::WHITE)) .transform(Affine::translate((50.0, 100.0))) .draw(Fill::NonZero, glyphs.into_iter()); // drow to the GPU ctx.renderer.render_to_texture( &ctx.device, &ctx.queue, &scene, &view, &vello::RenderParams { base_color: vello::peniko::Color::TRANSPARENT, width: ctx.width, height: ctx.height, antialiasing_method: vello::AaConfig::Msaa16, }, ).unwrap(); let mut encoder = ctx.device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &ctx.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &ctx.readback_buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(ctx.bytes_per_row), rows_per_image: Some(ctx.height), }, }, wgpu::Extent3d { width: ctx.width, height: ctx.height, depth_or_array_layers: 1 }, ); ctx.queue.submit(Some(encoder.finish())); let buffer_slice = ctx.readback_buffer.slice(..); let (tx, rx) = std::sync::mpsc::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |v| tx.send(v).unwrap()); ctx.device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); rx.recv().unwrap().unwrap(); let mapped_data = buffer_slice.get_mapped_range(); // SIMD copy let src_bytes = unsafe { slice::from_raw_parts(mapped_data.as_ptr(), length) }; let dst_bytes = unsafe { slice::from_raw_parts_mut(buffer, length) }; for (src, dst) in src_bytes.chunks_exact(4).zip(dst_bytes.chunks_exact_mut(4)) { dst[0] = src[2]; // B <- R dst[1] = src[1]; // G <- G dst[2] = src[0]; // R <- B dst[3] = src[3]; // A <- A } drop(mapped_data); ctx.readback_buffer.unmap(); } #[unsafe(no_mangle)] pub extern "C" fn vello_ctx_destroy(ctx_ptr: *mut RenderContext) { if !ctx_ptr.is_null() { // reconstruct the Box from the raw pointer to trigger // automatic dropping (memory deallocation) when going out of scope unsafe { let _ = Box::from_raw(ctx_ptr); } println!(" Vello RenderContext destroyed."); } }
Java 側実装(フォント表示)
こちらは特に説明は不要と思います。
package org.example; import com.example.rust_lib.rust_lib_h; import javafx.animation.AnimationTimer; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.image.ImageView; import javafx.scene.image.PixelBuffer; import javafx.scene.image.PixelFormat; import javafx.scene.image.WritableImage; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.nio.ByteBuffer; public class App extends Application { // Set so that width * 4 is a multiple of 256 (e.g., 1024 * 4 = 4096 = 256 * 16) private static final int WIDTH = 1024; private static final int HEIGHT = 768; private Arena arena; private MemorySegment ctxPtr; private AnimationTimer timer; @Override public void start(Stage stage) { arena = Arena.ofShared(); // initialize the Vello context on the Rust side and receive its pointer ctxPtr = rust_lib_h.vello_ctx_create(WIDTH, HEIGHT); if (ctxPtr.equals(MemorySegment.NULL)) { throw new RuntimeException("Failed to initialize Vello context"); } // allocate a shared memory region for image transfer long bufferSize = (long) WIDTH * HEIGHT * 4; MemorySegment pixelData = arena.allocate(bufferSize); // set up PixelBuffer and WritableImage ByteBuffer byteBuffer = pixelData.asByteBuffer(); PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraPreInstance(); PixelBuffer<ByteBuffer> pixelBuffer = new PixelBuffer<>(WIDTH, HEIGHT, byteBuffer, format); WritableImage image = new WritableImage(pixelBuffer); ImageView imageView = new ImageView(image); StackPane root = new StackPane(imageView); Scene scene = new Scene(root, WIDTH, HEIGHT); stage.setTitle("Vello Font Rendering"); stage.setScene(scene); stage.show(); rust_lib_h.vello_ctx_render(ctxPtr, pixelData, arena.allocateFrom("Hello")); pixelBuffer.updateBuffer(_ -> null); } @Override public void stop() { if (timer != null) { timer.stop(); } if (ctxPtr != null && !ctxPtr.equals(MemorySegment.NULL)) { rust_lib_h.vello_ctx_destroy(ctxPtr); } if (arena != null) arena.close(); } }
実行結果(フォント表示)

なお、サイズの小さなフォント描画時には、font hinting を指定することで、明瞭なレンダリングが期待できます。
// draw glyphs scene .draw_glyphs(&vello_font) .font_size(font_size) .brush(&Brush::Solid(Color::WHITE)) .transform(Affine::translate((50.0, 100.0))) .hint(true) // font hinting .draw(Fill::NonZero, glyphs.into_iter());


品の横線を比べると分かりやすいと思います。