Skip to content

Conversation

@madsmtm
Copy link
Member

@madsmtm madsmtm commented Jan 20, 2026

Add:

impl<D, W> Surface<D, W> {
    pub fn alpha_mode(&self) -> AlphaMode { ... }
    pub fn pixel_mode(&self) -> PixelMode { ... }
    pub fn configure(
        &mut self,
        width: NonZeroU32,
        height: NonZeroU32,
        alpha_mode: AlphaMode,
        pixel_mode: PixelMode,
    ) -> Result<(), SoftBufferError> { ... }
    
    // `resize` now calls `configure`:
    pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
        self.configure(width, height, self.alpha_mode(), self.pixel_mode())
    }

    // Earlier formats are preferred.
    //
    // TODO: We could possibly instead return:
    // `impl IntoIterator<Item = &[PixelFormat]>`
    //
    // To support "tranches" like Wayland does, i.e. groups of pixel formats with the same preference.
    pub fn supported_pixel_formats(&self, alpha_mode: AlphaMode) -> &[PixelFormat] { ... }
}

pub enum AlphaMode {
    #[default]
    Opaque,
    PreMultiplied,
    PostMultiplied,
}

// From https://github.com/rust-windowing/softbuffer/pull/289
pub const DEFAULT_PIXEL_FORMAT: PixelFormat = ...;

// Notice the lack of `Bgrx8` and similar!
// If `AlphaMode::Opaque`, we will treat `A` as `X` (i.e. ignore the alpha channel).
pub enum PixelFormat {
    // Normal RGB formats
    Bgr8,
    Rgb8,
    Bgra8,
    Rgba8,
    Abgr8,
    Argb8,
    Bgr16,
    Rgb16,
    Bgra16,
    Rgba16,
    Abgr16,
    Argb16,

    // Grayscale formats
    R1,
    R2,
    R4,
    R8,
    R16,

    // Packed formats
    B2g3r3,
    R3g3b2,
    B5g6r5,
    R5g6b5,
    Bgra4,
    Rgba4,
    Abgr4,
    Argb4,
    Bgr5a1,
    Rgb5a1,
    A1bgr5,
    A1rgb5,
    Bgr10a2,
    Rgb10a2,
    A2bgr10,
    A2rgb10,

    // Floating point formats
    // Uses the `half` crate for the `f16` type.
    #[cfg(feature = "f16")]
    Bgr16f,
    #[cfg(feature = "f16")]
    Rgb16f,
    #[cfg(feature = "f16")]
    Bgra16f,
    #[cfg(feature = "f16")]
    Rgba16f,
    #[cfg(feature = "f16")]
    Abgr16f,
    #[cfg(feature = "f16")]
    Argb16f,
    Bgr32f,
    Rgb32f,
    Bgra32f,
    Rgba32f,
    Abgr32f,
    Argb32f,
}

impl PixelFormat {
    pub const fn bits_per_pixel(self) -> u8 { ... }
    pub const fn components(self) -> u8 { ... }
}

// Which one of these methods is desirable depends on the format:
// - Normal formats will want to use the format with the same size as their component.
//   - Bgr8 -> `data`, `Bgr16` -> `data_u16`, `Bgr16f` -> `data_f16`, `Bgr32f` -> `data_f32`.
//   - Users will want to use `as_chunks_mut::<{ format.components() }>()` on each row.
// - Packed formats will want to use the format with the same size as _the entire format_:
//   - `B2g3r3` -> `data`, `Bgra4` -> `data_u16`, `Bgr10a2` -> `data_u32`.
impl Buffer<'_> {
    pub fn data(&mut self) -> &mut [u8] { ... }
    pub fn rows(&mut self) -> impl DoubleEndedIterator<Item = &mut [u8]> + ExactSizeIterator { ... }

    pub fn data_u16(&mut self) -> &mut [u16] { ... }
    pub fn rows_u16(&mut self) -> impl DoubleEndedIterator<Item = &mut [u16]> + ExactSizeIterator { ... }

    pub fn data_u32(&mut self) -> &mut [u32] { ... }
    pub fn rows_u32(&mut self) -> impl DoubleEndedIterator<Item = &mut [u32]> + ExactSizeIterator { ... }

    #[cfg(feature = "f16")]
    pub fn data_f16(&mut self) -> &mut [f16] { ... }
    #[cfg(feature = "f16")]
    pub fn rows_f16(&mut self) -> impl DoubleEndedIterator<Item = &mut [f16]> + ExactSizeIterator { ... }
    
    pub fn data_f32(&mut self) -> &mut [f32] { ... }
    pub fn rows_f32(&mut self) -> impl DoubleEndedIterator<Item = &mut [f32]> + ExactSizeIterator { ... }
}

// Existing are only valid when `pixel_format == DEFAULT_PIXEL_FORMAT`.
// If that is not the case, the `Pixel` values are non-sensical (but still sound).
// (Or perhaps we make these methods panic then?).
impl Buffer<'_> {
    fn pixels(&mut self) -> &mut [Pixel] { ... }
    fn pixel_rows(&mut self) -> impl Iterator<Item = &mut [Pixel]> { ... }
    fn pixels_iter(&mut self) -> impl Iterator<Item = (u32, u32, &mut Pixel)> { ... }
}

When the pixel format or alpha mode is not supported by the backend, we allocate an intermediate buffer which is what the user is given to render into, and then we copy from that buffer to the actual buffer in present. The actual buffer in this case is chosen to be DEFAULT_PIXEL_FORMAT to reduce the different permutations of pixel format conversions.

Fixes #17 by adding AlphaMode.
Fixes #98 by adding PixelFormat and conversions between those.
Replaces #186.
Replaces #241.

Draft because this relies on a bunch of other PRs to land first, and because I haven't actually implemented it fully yet. I'll probably split this PR out further too.

@madsmtm madsmtm added the enhancement New feature or request label Jan 20, 2026
Comment on lines +23 to +39
/// Convert and write pixel data from one row to another.
///
/// This is primarily meant as a fallback, so while it may be fairly efficient, that is not the
/// primary purpose. Users should instead.
///
/// # Strategy
///
/// Doing a generic conversion from data in one pixel format to another is difficult, so we allow
/// ourselves to assume that the output format is [`FALLBACK_FORMAT`].
///
/// If we didn't do this, we'd have to introduce a round-trip to a format like `Rgba32f` that is
/// (mostly) capable of storing all the other formats.
///
/// # Prior art
///
/// `SDL_ConvertPixels` + `SDL_PremultiplyAlpha` maybe?
pub(crate) fn convert_fallback(i: Input<'_>, o: Output<'_>) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I'm fairly certain that we want this in some form, but the question is how?

Asked another way, what should happen when a requested pixel format is not supported by the backend?

  1. We allocate an additional buffer, give that to the user as Buffer::pixels, and convert in Buffer::present.
  2. The above, but as an optimization, when the format only differs in order (PixelFormat::Rgba vs PixelFormat::Bgra), we convert it in place in Surface::buffer_mut and Buffer::present (and Buffer::drop).
  3. We expose a conversion function publicly, and error out on unsupported pixel formats. The user is expected to check the supported formats, and then use the conversion function to convert the buffer themselves.

See also the discussion in #98.

I haven't yet explored option 3, but it's likely it would be the most desirable, it will be surprising for the user when extra allocations and copying happens, and it seems more in line with Rust's philosophy to allow them to control this.

Especially relevant is that cargo bloat --example winit --crates --release reveals:

  • Std: 279.1KiB
  • Winit: 107.8KiB
  • Softbuffer before this PR: 4.3KiB
  • Softbuffer with convert_fallback symbol (current, alpha monomorphized): 90.7KiB
  • Softbuffer with convert_fallback symbol (alpha dynamic): 37.7KiB

Which would impact option 1 and 2 unless we change convert_fallback to be much more dynamic (and thus much less performant). Option 3 would be able to strip it out when unused, and probably also allow only emitting the relevant conversion code needed for a given application if they specify the pixel formats to be converted in line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

Pixel format API design Transparency

2 participants