You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用skia-safe在winit窗口绘图并实现多平台(移动/桌面/嵌入式)兼容

如何使用skia-safe在winit窗口绘图并实现多平台(移动/桌面/嵌入式)兼容

嘿,我来帮你搞定这个跨平台绘图的问题~ 首先得明确你核心的需求:用skia-safe配合winit,尽量少依赖,还要覆盖桌面、移动、嵌入式这些平台。咱们一步步来解决:

核心问题分析

你之前踩的坑其实很典型:

  • skia-safe自带的gl-window例子依赖glutin,但glutin在iOS/Android上的支持还不完善,导致没法跑
  • softbuffer+tiny-skia的组合虽然轻量,但softbuffer目前不支持Android NDK,没法覆盖移动平台

那最优解其实是利用winit的跨平台原生窗口句柄,直接对接skia-safe的渲染后端,既不用额外的中间层(比如glutin),又能让skia-safe自己适配不同平台的渲染方式。

解决方案:用skia-safe对接winit的原生窗口句柄

skia-safe支持通过raw-window-handle直接创建渲染表面,而winit刚好能提供所有平台的原生窗口句柄,这就完美匹配了。我们可以分两种渲染模式来实现:GPU加速渲染(优先)和软件渲染( fallback,适合嵌入式或低配置设备)。

第一步:配置依赖

在你的Cargo.toml里添加这些依赖,确保启用skia-safe的必要特性:

[dependencies]
winit = { version = "0.29", features = ["rwh_05"] }
skia-safe = { version = "0.72", features = ["gl", "raw-window-handle", "software"] }
raw-window-handle = "0.5"

这里rwh_05是winit对接raw-window-handle 0.5版本的特性,skia-safe也对应支持这个版本。

第二步:跨平台实现代码

下面是一个完整的跨平台示例,能在桌面(Windows/macOS/Linux)、移动(Android/iOS)、嵌入式平台运行:

use skia_safe::{
    gpu::{gl::FramebufferInfo, BackendRenderTarget, SurfaceOrigin},
    Color, Paint, Surface,
};
use winit::event::{Event, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let window = WindowBuilder::new()
        .with_title("Skia-Winit Multiplatform")
        .build(&event_loop)
        .unwrap();

    // 初始化Skia渲染上下文
    let mut skia_surface = create_skia_surface(&window).unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        match event {
            Event::WindowEvent { event, window_id } if window_id == window.id() => match event {
                WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
                WindowEvent::Resized(new_size) => {
                    // 窗口大小变化时重新创建Skia表面
                    skia_surface = create_skia_surface(&window).unwrap();
                }
                WindowEvent::RedrawRequested => {
                    // 获取Skia画布开始绘图
                    let mut canvas = skia_surface.canvas();

                    // 清空背景为白色
                    canvas.clear(Color::WHITE);

                    // 绘制一个蓝色填充圆,红色描边
                    let center = (window.inner_size().width as f32 / 2.0, window.inner_size().height as f32 / 2.0);
                    let radius = center.0.min(center.1) - 20.0;

                    let mut fill_paint = Paint::default();
                    fill_paint.set_color(Color::from_rgba(0, 128, 128, 255));
                    canvas.draw_circle(center, radius, &fill_paint);

                    let mut stroke_paint = Paint::default();
                    stroke_paint.set_color(Color::RED);
                    stroke_paint.set_stroke_width(10.0);
                    stroke_paint.set_style(skia_safe::PaintStyle::Stroke);
                    canvas.draw_circle(center, radius, &stroke_paint);

                    // 提交绘制内容到窗口
                    skia_surface.flush_and_submit();
                    window.request_redraw();
                }
                _ => {}
            },
            Event::RedrawRequested(window_id) if window_id == window.id() => {
                window.request_redraw();
            }
            _ => {}
        }
    }).unwrap();
}

// 根据平台创建对应的Skia Surface
fn create_skia_surface(window: &winit::window::Window) -> Option<Surface> {
    let size = window.inner_size();
    let (width, height) = (size.width as i32, size.height as i32);

    // 获取原生窗口句柄
    let raw_handle = window.raw_window_handle();

    match raw_handle {
        // 桌面平台优先用GL后端
        RawWindowHandle::Xlib(_) | RawWindowHandle::Wayland(_) | RawWindowHandle::Win32(_) | RawWindowHandle::MacOS(_) => {
            // 初始化GL上下文(skia-safe会自动处理)
            let gl_context = skia_safe::gpu::gl::Context::new_shared(None, None).ok()?;
            gl_context.make_current().ok()?;

            let fb_info = FramebufferInfo {
                fboid: 0,
                format: skia_safe::gpu::gl::Format::RGBA8,
            };

            let render_target = BackendRenderTarget::new_gl(
                (width, height),
                None,
                8,
                fb_info,
            );

            Surface::from_backend_render_target(
                &gl_context,
                &render_target,
                SurfaceOrigin::BottomLeft,
                skia_safe::ColorType::RGBA8888,
                None,
                None,
            )
        }
        // 移动平台(Android/iOS)用对应的原生后端,skia-safe会自动适配
        RawWindowHandle::AndroidNdk(_) | RawWindowHandle::UiKit(_) => {
            Surface::from_raw_window_handle(
                raw_handle,
                (width, height),
                None,
                None,
            )
        }
        // 嵌入式或其他平台fallback到软件渲染
        _ => {
            Surface::new_raster_n32_premul((width as u32, height as u32), None)
        }
    }
}

代码关键点解释

  1. 原生窗口句柄对接:通过window.raw_window_handle()获取各平台的原生窗口句柄,直接传给skia-safe创建渲染表面,不需要额外的窗口中间层。
  2. 多平台渲染适配:针对不同平台选择对应的渲染后端:桌面用GL加速,移动平台用原生后端,嵌入式用软件渲染。
  3. 窗口大小处理:窗口 resize 时重新创建Skia表面,保证绘图区域和窗口匹配。
  4. 绘图逻辑:用skia-safe的原生API绘图,和你之前用tiny-skia的逻辑一致,但功能更完整,毕竟是官方的Skia绑定。

平台特殊注意事项

  • Android/iOS:需要确保winit的移动平台配置正确(比如Android需要AndroidManifest.xml,iOS需要Info.plist),这些winit的文档里都有详细说明。
  • 嵌入式平台:如果是嵌入式Linux(比如树莓派),winit支持Wayland/X11后端,搭配skia-safe的软件渲染或GL渲染都可以,不需要额外依赖。
  • 减少依赖:如果不需要GPU加速,可以只启用skia-safe的software特性,完全去掉GL相关依赖,适合资源有限的设备。

对比你之前的尝试

这个方案比你之前的两种尝试更优:

  • 不需要依赖glutin,避免了它在移动平台的兼容性问题
  • 不需要softbuffer,用skia-safe自己的渲染后端就能覆盖所有平台
  • 保留了skia-safe的完整功能,比tiny-skia更强大

备注:内容来源于stack exchange,提问作者iccc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.20 13:05:27