softbuffer

repository·master·Indexed 19 days ago

https://github.com/rust-windowing/softbuffer

A cross-platform Rust library for software-based pixel buffer rendering. It allows developers to draw pixels to a window using the CPU via a consistent API across various operating systems and windowing backends. Softbuffer provides a raw pixel buffer and implements double-buffering and zero-copy presentation where possible, making it suitable for simple 2D scenes, GUIs, or fallback rendering paths when a GPU is unavailable.

Tokens
5.6K
Snippets
15
Records
19
Agent score
67%

What's inside softbuffer

  1. Overview of Softbuffer

    master

    Softbuffer allows you to render images on the CPU and display them in a window across multiple platforms. It is an ideal choice for learning purposes, simple 2D scenes, GUIs, or as a fallback rendering path when a GPU is unavailable.

    Key Workflow:

    1. Create a Window: Use a crate like winit or sdl3 that provides a type implementing raw_window_handle::HasWindowHandle.
    2. Initialize Softbuffer: Create a [Context] and a [Surface] from that window.
    3. Draw: Call [Surface::next_buffer()] to obtain a [Buffer].
    4. Present: Call [Buffer::present()] to show the drawn buffer on the window.

    Note: Softbuffer only provides the raw &mut [...] pixel buffer. It does not include rendering primitives (like drawing circles or rectangles). For drawing logic, use crates like tiny-skia or vello_cpu.

  2. How Softbuffer works

    master

    Softbuffer works by creating a shared memory region (a Buffer) that can be written to by the CPU. This buffer is then handed to the system's compositor (e.g., WindowServer on macOS, DWM on Windows, or Wayland) via Buffer::present.

    To maintain performance, Softbuffer implements double-buffering per surface and strives for zero-copy presentation. On platforms without unified memory architecture, some copying may occur, often handled via hardware DMA.

  3. Untitled record

    master

    Test on WebAssembly

    To run an example with the web backend, add the following to .cargo/config.toml:

    [target.'cfg(target_family = "wasm")']
    runner = "wasm-server-runner"

    And then run:

    cargo install wasm-server-runner
    cargo run --target wasm32-unknown-unknown --example winit

    Test on Android

    To run the Android-specific example on an Android phone:

    cargo apk r --example winit_android
    # or
    cargo apk r --example winit_multithread_android
  4. Understand AlphaMode options

    master

    The AlphaMode determines how the compositor handles the alpha channel. You should query surface.supports_alpha_mode(mode) before configuring.

    • Opaque (Default): The alpha channel must be 0xff. Supported on all platforms.
    • Ignored: The alpha channel is ignored; contents are treated as opaque. Supported on most platforms (Android, macOS/iOS, DRM/KMS, etc.), but cannot be used in a zero-copy manner on Web.
    • Premultiplied: Non-alpha channels are already multiplied by alpha. Supported on Wayland and DRM/KMS.
    • Postmultiplied (Straight Alpha): The compositor multiplies non-alpha channels by alpha. Supported on Web and macOS/iOS.
    pub enum AlphaMode {
        Opaque,
        Ignored,
        Premultiplied,
        Postmultiplied,
    }
  5. Handle Buffer Stride and Row Alignment

    master

    On many platforms, the buffer's row length (stride) is larger than width * 4 to ensure rows are aligned with cache lines.

    Never assume the buffer is exactly width * height * 4 bytes.

    To write data correctly:

    • Use buffer.byte_stride() to get the number of bytes per row.
    • Use buffer.pixel_rows() to iterate over rows safely.
    • If using raw u8 slices, chunk your data by buffer.byte_stride() rather than width * 4.
    pub struct Buffer<'surface> {
        pub fn byte_stride(&self) -> NonZeroU32
        pub fn width(&self) -> NonZeroU32
        pub fn height(&self) -> NonZeroU32
    }
  6. Handle premultiplied alpha with `Pixel`

    master

    If you are rendering to buffers using AlphaMode::Premultiplied, you must premultiply your color components by the alpha value. Conversely, if you have premultiplied data and need standard RGBA, you must un-premultiply it.

    Premultiply formula: component = (component * alpha) / 255 Un-premultiply formula: component = (component * 255) / alpha (with rounding and zero-alpha handling).

    // Example of manual premultiplication
    #[inline]
    fn premultiply(val: u8, alpha: u8) -> u8 {
        ((val as u16 * alpha as u16) / 0xff) as u8
    }
    
    let pixel = Pixel::new_rgba(0, 128, 255, 50);
    let premultiplied_pixel = Pixel {
        r: premultiply(pixel.r, pixel.a),
        g: premultiply(pixel.g, pixel.a),
        b: premultiply(pixel.b, pixel.a),
        a: pixel.a,
    };
    assert_eq!(premultiplied_pixel, Pixel::new_rgba(0, 25, 50, 50));
  7. Configure the PixelFormat for a surface and buffer

    master

    The PixelFormat enum defines the 32-bit pixel layout used by a surface and its buffer. This is primarily relevant when bitcasting Pixel to/from a u32 to avoid unnecessary copying.

    Alpha Channel Handling

    While the names (e.g., Rgba8) include an alpha channel, you can choose to ignore the alpha channel by using AlphaMode::Ignored. For example, using AlphaMode::Ignored with Rgba8 will treat the format as Rgbx8.

    Default Format

    Using PixelFormat::default() returns the format optimized for the current target platform:

    • macOS/iOS, KMS/DRM, Orbital, Wayland, Windows, and X11: Defaults to Bgra8.
    • WebAssembly (Wasm) and Android: Defaults to Rgba8 (as these platforms do not support BGRA via their APIs).

    Note that the default format for a platform may change in non-breaking releases to improve performance.

    // Use the default format for the current platform
    let format = softbuffer::PixelFormat::default();
    
    // Check if a specific format is the platform default
    if format.is_default() {
        // ...
    }
  8. Render to a Buffer and Present

    master

    The rendering lifecycle follows these steps:

    1. Call surface.next_buffer() to obtain a Buffer.
    2. Write pixel data to the buffer using one of the provided iteration or data access methods.
    3. Call buffer.present() to display the frame.

    Note that the buffer contents may be garbage or contain the previous frame; use buffer.age() to check if the buffer is new (age 0) or reused.

    let mut buffer = surface.next_buffer().unwrap();
    // ... write to buffer ...
    buffer.present().unwrap();
  9. Untitled record

    master

    This example demonstrates how to integrate Softbuffer with winit to create a window and render a dynamic color pattern using Surface::next_buffer() and Buffer::present().

    use std::num::NonZeroU32;
    use std::rc::Rc;
    
    use softbuffer::{Context, Pixel, Surface};
    use winit::application::ApplicationHandler;
    use winit::event::{StartCause, WindowEvent};
    use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
    use winit::window::{Window, WindowId};
    
    fn main() {
        let event_loop = EventLoop::new().unwrap();
        let context = Context::new(event_loop.owned_display_handle()).unwrap();
        let mut app = App {
            context,
            state: AppState::Initial,
        };
        event_loop.run_app(&mut app).unwrap();
    }
    
    #[derive(Debug)]
    struct App {
        context: Context<OwnedDisplayHandle>,
        state: AppState,
    }
    
    #[derive(Debug)]
    enum AppState {
        Initial,
        Suspended {
            window: Rc<Window>,
        },
        Running {
            surface: Surface<OwnedDisplayHandle, Rc<Window>>,
        },
    }
    
    impl ApplicationHandler for App {
        fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
            if let StartCause::Init = cause {
                let window_attrs = Window::default_attributes();
                let window = event_loop
                    .create_window(window_attrs)
                    .expect("failed creating window");
                self.state = AppState::Suspended {
                    window: Rc::new(window),
                };
            }
        }
    
        fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
            let AppState::Suspended { window } = &mut self.state else {
                unreachable!("got resumed event while not suspended");
            };
            let mut surface =
                Surface::new(&self.context, window.clone()).expect("failed creating surface");
    
            let size = window.inner_size();
            if let (Some(width), Some(height)) =
                (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
            {
                surface.resize(width, height).unwrap();
            }
    
            self.state = AppState::Running { surface };
        }
    
        fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
            let AppState::Running { surface } = &mut self.state else {
                unreachable!("got resumed event while not running");
            };
            let window = surface.window().clone();
            self.state = AppState::Suspended { window };
        }
    
        fn window_event(
            &mut self, 
            event_loop: &ActiveEventLoop, 
            window_id: WindowId, 
            event: WindowEvent
        ) {
            let AppState::Running { surface } = &mut self.state else {
                unreachable!("got window event while suspended");
            };
    
            if surface.window().id() != window_id {
                return;
            }
    
            match event {
                WindowEvent::Resized(size) => {
                    if let (Some(width), Some(height)) =
                        (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
                    {
                        surface.resize(width, height).unwrap();
                    }
                }
                WindowEvent::RedrawRequested => {
                    let mut buffer = surface.next_buffer().unwrap();
    
                    for (x, y, pixel) in buffer.pixels_iter() {
                        let red = (x % 255) as u8;
                        let green = (y % 255) as u8;
                        let blue = ((x * y) % 255) as u8;
    
                        *pixel = Pixel::new_rgb(red, green, blue);
                    }
    
                    buffer.present().unwrap();
                }
                WindowEvent::CloseRequested => {
                    event_loop.exit();
                }
                _ => {}
            }
        }
    }
  10. Platform support tiers

    master

    Softbuffer uses a tier system to define platform support:

    PlatformTierAvailable
    AppKit (macOS)1
    Wayland1
    Win321
    XCB / Xlib (X11)1
    Android NDK2
    UIKit (iOS)2
    WebAssembly2
    DRM/KMS3
    Orbital3
    GBM/KMSN/A
    HaikuN/A
    OpenHarmony OS NDKN/A
    WinRTN/A
    UEFIN/A

    Note: Big endian targets are less tested and may behave incorrectly.

    Tier Definitions:

    • Tier 1: Tested and guaranteed to work.
    • Tier 2: Will likely work.
    • Tier 3: Builds in CI.
  11. Convert `Pixel` to `u8` arrays or `u32` values

    master

    Because Pixel has a fixed memory layout and alignment, you can use unsafe transmutes to reinterpret it for low-level buffer operations.

    Warning: The resulting byte order or integer value depends on the platform's default PixelFormat (e.g., Rgba8 vs Bgra8).

    use softbuffer::Pixel;
    
    let red = Pixel::new_rgb(0xff, 0, 0);
    
    // Convert to [u8; 4]
    // SAFETY: `Pixel` can be reinterpreted as `[u8; 4]`.
    let bytes = unsafe { core::mem::transmute::<Pixel, [u8; 4]>(red) };
    
    // Convert to u32
    // SAFETY: `Pixel` can be reinterpreted as `u32`.
    let val = unsafe { core::mem::transmute::<Pixel, u32>(red) };
  12. Create and Configure a Surface

    master

    A Surface<D, W> is the primary object used for drawing to a window. It is created using a Context<D> and a window handle W that implements HasWindowHandle.

    To prepare the surface for drawing, you must call configure or resize to set the buffer dimensions and AlphaMode. If the buffer size does not match the window size, the buffer will be drawn in the upper-left corner of the window.

    impl<D: HasDisplayHandle, W: HasWindowHandle> Surface<D, W> {
        pub fn new(context: &Context<D>, window: W) -> Result<Self, SoftBufferError>
        pub fn configure(&mut self, width: NonZeroU32, height: NonZeroU32, alpha_mode: AlphaMode) -> Result<(), SoftBufferError>
        pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError>
        pub fn supports_alpha_mode(&self, alpha_mode: AlphaMode) -> bool
    }