Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

- Added `AlphaMode` to allow configuring transparency and zero-copy on Web. Set it with `Surface::configure`.
- Added `Surface::supports_alpha_mode` for querying supported alpha modes.
- Added `PixelFormat` enum.
- Added `Buffer::pixels()` for accessing the buffer's pixel data.
- Added `Buffer::pixel_rows()` for iterating over rows of the buffer data.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ drm = { version = "0.14.1", default-features = false, optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.61.2", features = [
"Win32_Graphics_Gdi",
"Win32_UI_ColorSystem",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation",
Expand Down
166 changes: 166 additions & 0 deletions examples/transparency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! An example to test transparent rendering.
//!
//! Press `o`, `i`, `m` or `t` to change the alpha mode to `Opaque`, `Ignored`, `Premultiplied` and
//! `Postmultiplied` respectively.
//!
//! This should render 6 rectangular areas. For details on the terminology, see:
//! <https://html.spec.whatwg.org/multipage/canvas.html#premultiplied-alpha-and-the-2d-rendering-context>
//!
//! (255, 127, 0, 255):
//! - Opaque/Ignored: Completely-opaque orange.
//! - Postmultiplied: Completely-opaque orange.
//! - Premultiplied: Completely-opaque orange.
//!
//! (255, 255, 0, 127):
//! - Opaque/Ignored: Completely-opaque yellow.
//! - Postmultiplied: Halfway-opaque yellow.
//! - Premultiplied: Additive halfway-opaque yellow.
//!
//! (127, 127, 0, 127):
//! - Opaque/Ignored: Completely-opaque dark yellow.
//! - Postmultiplied: Halfway-opaque dark yellow.
//! - Premultiplied: Halfway-opaque yellow.
//!
//! (255, 127, 0, 127):
//! - Opaque/Ignored: Completely-opaque orange.
//! - Postmultiplied: Halfway-opaque orange.
//! - Premultiplied: Additive halfway-opaque orange.
//!
//! (255, 127, 0, 0):
//! - Opaque/Ignored: Completely-opaque orange.
//! - Postmultiplied: Fully-transparent orange.
//! - Premultiplied: Additive fully-transparent orange.
//!
//! (0, 0, 0, 0):
//! - Opaque/Ignored: Completely-opaque black.
//! - Postmultiplied: Fully-transparent.
//! - Premultiplied: Fully-transparent.
use softbuffer::{AlphaMode, Context, Pixel, Surface};
use std::num::NonZeroU32;
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::keyboard::{Key, NamedKey};

mod util;

fn main() {
util::setup();

let event_loop = EventLoop::new().unwrap();

let context = Context::new(event_loop.owned_display_handle()).unwrap();

let app = util::WinitAppBuilder::with_init(
|elwt| util::make_window(elwt, |w| w),
move |_elwt, window| Surface::new(&context, window.clone()).unwrap(),
)
.with_event_handler(|window, surface, window_id, event, elwt| {
elwt.set_control_flow(ControlFlow::Wait);

if window_id != window.id() {
return;
}

match event {
WindowEvent::Resized(size) => {
let Some(surface) = surface else {
tracing::error!("Resized fired before Resumed or after Suspended");
return;
};

if let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
{
surface.resize(width, height).unwrap();
}
}
WindowEvent::RedrawRequested => {
let Some(surface) = surface else {
tracing::error!("RedrawRequested fired before Resumed or after Suspended");
return;
};

tracing::info!(alpha_mode = ?surface.alpha_mode(), "redraw");

let alpha_mode = surface.alpha_mode();
let mut buffer = surface.buffer_mut().unwrap();
let width = buffer.width().get();
for (x, _, pixel) in buffer.pixels_iter() {
let rectangle_number = (x * 6) / width;
*pixel = match rectangle_number {
0 => Pixel::new_rgba(255, 127, 0, 255),
1 => Pixel::new_rgba(255, 255, 0, 127),
2 => Pixel::new_rgba(127, 127, 0, 127),
3 => Pixel::new_rgba(255, 127, 0, 127),
4 => Pixel::new_rgba(255, 127, 0, 0),
_ => Pixel::new_rgba(0, 0, 0, 0),
};

// Convert `AlphaMode::Opaque` -> `AlphaMode::Ignored`.
if alpha_mode == AlphaMode::Opaque {
pixel.a = 255;
};
}

buffer.present().unwrap();
}
WindowEvent::CloseRequested
| WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Named(NamedKey::Escape),
repeat: false,
..
},
..
} => {
elwt.exit();
}
WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key,
repeat: false,
state: ElementState::Pressed,
..
},
..
} => {
let Some(surface) = surface else {
tracing::error!("KeyboardInput fired before Resumed or after Suspended");
return;
};

let alpha_mode = match logical_key.to_text() {
Some("o") => AlphaMode::Opaque,
Some("i") => AlphaMode::Ignored,
Some("m") => AlphaMode::Premultiplied,
Some("t") => AlphaMode::Postmultiplied,
_ => return,
};

if !surface.supports_alpha_mode(alpha_mode) {
tracing::warn!(?alpha_mode, "not supported by the backend");
return;
}

tracing::info!(?alpha_mode, "set alpha");
let size = window.inner_size();
let width = NonZeroU32::new(size.width).unwrap();
let height = NonZeroU32::new(size.height).unwrap();
surface.configure(width, height, alpha_mode).unwrap();
assert_eq!(surface.alpha_mode(), alpha_mode);

window.set_transparent(matches!(
alpha_mode,
AlphaMode::Premultiplied | AlphaMode::Postmultiplied
));

window.request_redraw();
}
_ => {}
}
});

util::run_app(event_loop, app);
}
20 changes: 15 additions & 5 deletions src/backend_dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Implements `buffer_interface::*` traits for enums dispatching to backends

use crate::{backend_interface::*, backends, InitError, Pixel, Rect, SoftBufferError};
use crate::{backend_interface::*, backends, AlphaMode, InitError, Pixel, Rect, SoftBufferError};

use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::fmt;
Expand Down Expand Up @@ -99,20 +99,30 @@ macro_rules! make_dispatch {
}
}

fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
#[inline]
fn supports_alpha_mode(&self, alpha_mode: AlphaMode) -> bool {
match self {
$(
$(#[$attr])*
Self::$name(inner) => inner.supports_alpha_mode(alpha_mode),
)*
}
}

fn configure(&mut self, width: NonZeroU32, height: NonZeroU32, alpha_mode: AlphaMode) -> Result<(), SoftBufferError> {
match self {
$(
$(#[$attr])*
Self::$name(inner) => inner.resize(width, height),
Self::$name(inner) => inner.configure(width, height, alpha_mode),
)*
}
}

fn buffer_mut(&mut self) -> Result<BufferDispatch<'_>, SoftBufferError> {
fn buffer_mut(&mut self, alpha_mode: AlphaMode) -> Result<BufferDispatch<'_>, SoftBufferError> {
match self {
$(
$(#[$attr])*
Self::$name(inner) => Ok(BufferDispatch::$name(inner.buffer_mut()?)),
Self::$name(inner) => Ok(BufferDispatch::$name(inner.buffer_mut(alpha_mode)?)),
)*
}
}
Expand Down
19 changes: 15 additions & 4 deletions src/backend_interface.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Interface implemented by backends

use crate::{InitError, Pixel, Rect, SoftBufferError};
use crate::{AlphaMode, InitError, Pixel, Rect, SoftBufferError};

use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::num::NonZeroU32;
Expand All @@ -22,12 +22,23 @@ pub(crate) trait SurfaceInterface<D: HasDisplayHandle + ?Sized, W: HasWindowHand
where
W: Sized,
Self: Sized;

/// Get the inner window handle.
fn window(&self) -> &W;
/// Resize the internal buffer to the given width and height.
fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError>;

fn supports_alpha_mode(&self, alpha_mode: AlphaMode) -> bool;

/// Reconfigure the internal buffer(s).
fn configure(
&mut self,
width: NonZeroU32,
height: NonZeroU32,
alpha_mode: AlphaMode,
) -> Result<(), SoftBufferError>;

/// Get a mutable reference to the buffer.
fn buffer_mut(&mut self) -> Result<Self::Buffer<'_>, SoftBufferError>;
fn buffer_mut(&mut self, alpha_mode: AlphaMode) -> Result<Self::Buffer<'_>, SoftBufferError>;

/// Fetch the buffer from the window.
fn fetch(&mut self) -> Result<Vec<Pixel>, SoftBufferError> {
Err(SoftBufferError::Unimplemented)
Expand Down
36 changes: 25 additions & 11 deletions src/backends/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use raw_window_handle::AndroidNdkWindowHandle;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};

use crate::error::InitError;
use crate::{util, BufferInterface, Pixel, Rect, SoftBufferError, SurfaceInterface};
use crate::{util, AlphaMode, BufferInterface, Pixel, Rect, SoftBufferError, SurfaceInterface};

/// The handle to a window for software buffering.
#[derive(Debug)]
Expand Down Expand Up @@ -52,31 +52,45 @@ impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for Android
&self.window
}

/// Also changes the pixel format to [`HardwareBufferFormat::R8G8B8A8_UNORM`].
fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
#[inline]
fn supports_alpha_mode(&self, alpha_mode: AlphaMode) -> bool {
matches!(alpha_mode, AlphaMode::Opaque | AlphaMode::Ignored)
}

/// Also changes the pixel format.
fn configure(
&mut self,
width: NonZeroU32,
height: NonZeroU32,
alpha_mode: AlphaMode,
) -> Result<(), SoftBufferError> {
let (width, height) = (|| {
let width = NonZeroI32::try_from(width).ok()?;
let height = NonZeroI32::try_from(height).ok()?;
Some((width, height))
})()
.ok_or(SoftBufferError::SizeOutOfRange { width, height })?;

// Default is typically R5G6B5 16bpp, switch to 32bpp
let pixel_format = match alpha_mode {
AlphaMode::Opaque | AlphaMode::Ignored => HardwareBufferFormat::R8G8B8X8_UNORM,
AlphaMode::Premultiplied => todo!("HardwareBufferFormat::R8G8B8A8_UNORM"),
_ => unimplemented!(),
};

self.native_window
.set_buffers_geometry(
width.into(),
height.into(),
// Default is typically R5G6B5 16bpp, switch to 32bpp
Some(HardwareBufferFormat::R8G8B8X8_UNORM),
)
.set_buffers_geometry(width.into(), height.into(), Some(pixel_format))
.map_err(|err| {
SoftBufferError::PlatformError(
Some("Failed to set buffer geometry on ANativeWindow".to_owned()),
Some(Box::new(err)),
)
})
})?;

Ok(())
}

fn buffer_mut(&mut self) -> Result<BufferImpl<'_>, SoftBufferError> {
fn buffer_mut(&mut self, _alpha_mode: AlphaMode) -> Result<BufferImpl<'_>, SoftBufferError> {
let native_window_buffer = self.native_window.lock(None).map_err(|err| {
SoftBufferError::PlatformError(
Some("Failed to lock ANativeWindow".to_owned()),
Expand Down
34 changes: 29 additions & 5 deletions src/backends/cg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Softbuffer implementation using CoreGraphics.
use crate::backend_interface::*;
use crate::error::InitError;
use crate::{backend_interface::*, AlphaMode};
use crate::{util, Pixel, Rect, SoftBufferError};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Bool};
Expand Down Expand Up @@ -252,19 +252,42 @@ impl<D: HasDisplayHandle, W: HasWindowHandle> SurfaceInterface<D, W> for CGImpl<
&self.window_handle
}

fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> {
#[inline]
fn supports_alpha_mode(&self, _alpha_mode: AlphaMode) -> bool {
true
}

fn configure(
&mut self,
width: NonZeroU32,
height: NonZeroU32,
alpha_mode: AlphaMode,
) -> Result<(), SoftBufferError> {
let opaque = matches!(alpha_mode, AlphaMode::Opaque | AlphaMode::Ignored);
self.layer.setOpaque(opaque);
// TODO: Set opaque-ness on root layer too? Is that our responsibility, or Winit's?
// self.root_layer.setOpaque(opaque);

self.width = width.get() as usize;
self.height = height.get() as usize;
Ok(())
}

fn buffer_mut(&mut self) -> Result<BufferImpl<'_>, SoftBufferError> {
fn buffer_mut(&mut self, alpha_mode: AlphaMode) -> Result<BufferImpl<'_>, SoftBufferError> {
let buffer_size = util::byte_stride(self.width as u32) as usize * self.height / 4;
Ok(BufferImpl {
buffer: util::PixelBuffer(vec![Pixel::default(); buffer_size]),
width: self.width,
height: self.height,
color_space: &self.color_space,
alpha_info: match (alpha_mode, cfg!(target_endian = "little")) {
(AlphaMode::Opaque | AlphaMode::Ignored, true) => CGImageAlphaInfo::NoneSkipFirst,
(AlphaMode::Opaque | AlphaMode::Ignored, false) => CGImageAlphaInfo::NoneSkipLast,
(AlphaMode::Premultiplied, true) => CGImageAlphaInfo::PremultipliedFirst,
(AlphaMode::Premultiplied, false) => CGImageAlphaInfo::PremultipliedLast,
(AlphaMode::Postmultiplied, true) => CGImageAlphaInfo::First,
(AlphaMode::Postmultiplied, false) => CGImageAlphaInfo::Last,
},
layer: &mut self.layer,
})
}
Expand All @@ -276,6 +299,7 @@ pub struct BufferImpl<'a> {
height: usize,
color_space: &'a CGColorSpace,
buffer: util::PixelBuffer,
alpha_info: CGImageAlphaInfo,
layer: &'a mut SendCALayer,
}

Expand Down Expand Up @@ -331,9 +355,9 @@ impl BufferInterface for BufferImpl<'_> {
//
// TODO: Use `CGBitmapInfo::new` once the next version of objc2-core-graphics is released.
let bitmap_info = CGBitmapInfo(
CGImageAlphaInfo::NoneSkipFirst.0
self.alpha_info.0
| CGImageComponentInfo::Integer.0
| CGImageByteOrderInfo::Order32Little.0
| CGImageByteOrderInfo::Order32Host.0
| CGImagePixelFormatInfo::Packed.0,
);

Expand Down
Loading
Loading