use anyhow::{Result, anyhow};
pub const SURFACE_FORMATS: [wgpu::TextureFormat; 2] = [
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Bgra8Unorm,
];
const DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SurfaceAlphaRequest {
Opaque,
TranslucentPreferred,
}
pub fn resolve_alpha_mode(
request: SurfaceAlphaRequest,
capabilities: &wgpu::SurfaceCapabilities,
) -> wgpu::CompositeAlphaMode {
match request {
SurfaceAlphaRequest::Opaque => wgpu::CompositeAlphaMode::Auto,
SurfaceAlphaRequest::TranslucentPreferred => {
const PREFERRED: [wgpu::CompositeAlphaMode; 3] = [
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PostMultiplied,
wgpu::CompositeAlphaMode::PreMultiplied,
];
PREFERRED
.into_iter()
.find(|mode| capabilities.alpha_modes.contains(mode))
.unwrap_or_else(|| {
log::warn!(
"frust-gpu: translucency requested but unavailable \
(alpha_modes={:?}) — falling back to Auto (opaque)",
capabilities.alpha_modes
);
wgpu::CompositeAlphaMode::Auto
})
}
}
}
pub fn alpha_mode_is_translucent(mode: wgpu::CompositeAlphaMode) -> bool {
matches!(
mode,
wgpu::CompositeAlphaMode::Inherit
| wgpu::CompositeAlphaMode::PostMultiplied
| wgpu::CompositeAlphaMode::PreMultiplied
)
}
pub fn alpha_mode_needs_premultiply(mode: wgpu::CompositeAlphaMode) -> bool {
matches!(
mode,
wgpu::CompositeAlphaMode::PreMultiplied | wgpu::CompositeAlphaMode::Inherit
)
}
pub fn alpha_mode_is_straight_translucent(mode: wgpu::CompositeAlphaMode) -> bool {
alpha_mode_is_translucent(mode) && !alpha_mode_needs_premultiply(mode)
}
pub fn compositor_expects_premultiplied(
backend: wgpu::Backend,
mode: wgpu::CompositeAlphaMode,
) -> bool {
backend == wgpu::Backend::Metal && mode == wgpu::CompositeAlphaMode::PostMultiplied
}
pub fn select_surface_format(
capabilities: &wgpu::SurfaceCapabilities,
) -> Result<wgpu::TextureFormat> {
capabilities
.formats
.iter()
.copied()
.find(|format| SURFACE_FORMATS.contains(format))
.ok_or_else(|| {
anyhow!(
"frust-gpu: no supported surface format (Rgba8Unorm/Bgra8Unorm) in {:?}",
capabilities.formats
)
})
}
pub fn surface_config(
format: wgpu::TextureFormat,
alpha_mode: wgpu::CompositeAlphaMode,
size: (u32, u32),
present_mode: wgpu::PresentMode,
) -> wgpu::SurfaceConfiguration {
let (width, height) = size;
wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
color_space: wgpu::SurfaceColorSpace::Auto,
width,
height,
present_mode,
desired_maximum_frame_latency: DESIRED_MAXIMUM_FRAME_LATENCY,
alpha_mode,
view_formats: vec![],
}
}
#[derive(Clone)]
pub struct SurfaceFactory {
instance: wgpu::Instance,
}
impl SurfaceFactory {
pub fn new(instance: &wgpu::Instance) -> Self {
Self {
instance: instance.clone(),
}
}
pub fn create_detached_surface(
&self,
target: impl Into<wgpu::SurfaceTarget<'static>>,
) -> Result<DetachedSurface> {
let surface = self
.instance
.create_surface(target)
.map_err(|e| anyhow!("frust-gpu: failed to create surface: {e}"))?;
Ok(DetachedSurface { surface })
}
}
pub struct DetachedSurface {
surface: wgpu::Surface<'static>,
}
impl DetachedSurface {
#[doc(hidden)]
pub fn into_surface(self) -> wgpu::Surface<'static> {
self.surface
}
}
pub struct ConfiguredSurface {
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
resolved_translucent: bool,
}
impl ConfiguredSurface {
pub fn configure(
surface: wgpu::Surface<'static>,
device: &wgpu::Device,
format: wgpu::TextureFormat,
alpha_mode: wgpu::CompositeAlphaMode,
size: (u32, u32),
present_mode: wgpu::PresentMode,
) -> Self {
let config = surface_config(format, alpha_mode, size, present_mode);
surface.configure(device, &config);
Self {
surface,
config,
resolved_translucent: alpha_mode_is_translucent(alpha_mode),
}
}
pub fn reconfigure(&self, device: &wgpu::Device) {
self.surface.configure(device, &self.config);
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.config.width = width;
self.config.height = height;
self.reconfigure(device);
}
pub fn surface(&self) -> &wgpu::Surface<'static> {
&self.surface
}
pub fn config(&self) -> &wgpu::SurfaceConfiguration {
&self.config
}
pub fn size(&self) -> (u32, u32) {
(self.config.width, self.config.height)
}
pub fn resolved_translucent(&self) -> bool {
self.resolved_translucent
}
pub fn straight_alpha_translucent(&self) -> bool {
self.resolved_translucent && alpha_mode_is_straight_translucent(self.config.alpha_mode)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn caps_with_alpha_modes(
alpha_modes: &[wgpu::CompositeAlphaMode],
) -> wgpu::SurfaceCapabilities {
wgpu::SurfaceCapabilities {
alpha_modes: alpha_modes.to_vec(),
..Default::default()
}
}
fn caps_with_formats(formats: &[wgpu::TextureFormat]) -> wgpu::SurfaceCapabilities {
wgpu::SurfaceCapabilities {
formats: formats.to_vec(),
..Default::default()
}
}
#[test]
fn opaque_request_always_resolves_to_auto() {
for modes in [
[wgpu::CompositeAlphaMode::Inherit].as_slice(),
&[
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::PostMultiplied,
],
&[wgpu::CompositeAlphaMode::Opaque],
] {
let caps = caps_with_alpha_modes(modes);
assert_eq!(
resolve_alpha_mode(SurfaceAlphaRequest::Opaque, &caps),
wgpu::CompositeAlphaMode::Auto
);
}
}
#[test]
fn translucent_preferred_picks_inherit_first() {
let caps = caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Inherit]);
assert_eq!(
resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
wgpu::CompositeAlphaMode::Inherit
);
}
#[test]
fn translucent_preferred_picks_post_multiplied_when_inherit_absent() {
let caps = caps_with_alpha_modes(&[
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::PostMultiplied,
]);
assert_eq!(
resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
wgpu::CompositeAlphaMode::PostMultiplied
);
}
#[test]
fn translucent_preferred_falls_back_to_pre_multiplied_last() {
let caps = caps_with_alpha_modes(&[
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::PreMultiplied,
]);
assert_eq!(
resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
wgpu::CompositeAlphaMode::PreMultiplied
);
}
#[test]
fn translucent_preferred_falls_back_to_auto_when_none_available() {
let caps = caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Opaque]);
assert_eq!(
resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
wgpu::CompositeAlphaMode::Auto
);
}
#[test]
fn resolved_translucency_is_true_only_for_the_three_translucent_modes() {
for mode in [
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PostMultiplied,
wgpu::CompositeAlphaMode::PreMultiplied,
] {
assert!(alpha_mode_is_translucent(mode), "{mode:?}");
}
for mode in [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
] {
assert!(!alpha_mode_is_translucent(mode), "{mode:?}");
}
}
#[test]
fn premultiplied_expecting_modes_take_engine_output_unchanged() {
for mode in [
wgpu::CompositeAlphaMode::PreMultiplied,
wgpu::CompositeAlphaMode::Inherit,
] {
assert!(alpha_mode_needs_premultiply(mode), "{mode:?}");
assert!(!alpha_mode_is_straight_translucent(mode), "{mode:?}");
}
for mode in [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
] {
assert!(!alpha_mode_needs_premultiply(mode), "{mode:?}");
}
}
#[test]
fn only_post_multiplied_is_translucent_with_straight_alpha() {
assert!(alpha_mode_is_straight_translucent(
wgpu::CompositeAlphaMode::PostMultiplied
));
assert!(!alpha_mode_needs_premultiply(
wgpu::CompositeAlphaMode::PostMultiplied
));
for mode in [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
] {
assert!(!alpha_mode_is_straight_translucent(mode), "{mode:?}");
}
}
#[test]
fn the_surfaces_own_order_decides_when_both_formats_are_reported() {
let bgra_first = caps_with_formats(&[
wgpu::TextureFormat::Bgra8Unorm,
wgpu::TextureFormat::Rgba8Unorm,
]);
assert_eq!(
select_surface_format(&bgra_first).unwrap(),
wgpu::TextureFormat::Bgra8Unorm,
"the surface's own preference order decides, not this module's"
);
let rgba_first = caps_with_formats(&[
wgpu::TextureFormat::Rgba8Unorm,
wgpu::TextureFormat::Bgra8Unorm,
]);
assert_eq!(
select_surface_format(&rgba_first).unwrap(),
wgpu::TextureFormat::Rgba8Unorm
);
}
#[test]
fn unsupported_formats_are_skipped_over_rather_than_taken() {
let caps = caps_with_formats(&[
wgpu::TextureFormat::Bgra8UnormSrgb,
wgpu::TextureFormat::Bgra8Unorm,
]);
assert_eq!(
select_surface_format(&caps).unwrap(),
wgpu::TextureFormat::Bgra8Unorm
);
}
#[test]
fn a_surface_supporting_neither_format_is_an_error() {
let caps = caps_with_formats(&[wgpu::TextureFormat::Rgba16Float]);
assert!(select_surface_format(&caps).is_err());
}
#[test]
fn config_asks_for_render_attachment_only() {
let config = surface_config(
wgpu::TextureFormat::Bgra8Unorm,
wgpu::CompositeAlphaMode::Auto,
(800, 600),
wgpu::PresentMode::AutoVsync,
);
assert_eq!(config.usage, wgpu::TextureUsages::RENDER_ATTACHMENT);
}
#[test]
fn config_carries_the_callers_format_alpha_mode_and_size() {
for format in SURFACE_FORMATS {
let config = surface_config(
format,
wgpu::CompositeAlphaMode::Inherit,
(1080, 2400),
wgpu::PresentMode::Fifo,
);
assert_eq!(config.format, format);
assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Inherit);
assert_eq!((config.width, config.height), (1080, 2400));
assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
assert!(config.view_formats.is_empty());
assert_eq!(
config.desired_maximum_frame_latency,
DESIRED_MAXIMUM_FRAME_LATENCY
);
assert_eq!(config.color_space, wgpu::SurfaceColorSpace::Auto);
}
}
#[test]
fn forced_mismatch_translucent_request_resolves_not_translucent() {
for modes in [
[wgpu::CompositeAlphaMode::Opaque].as_slice(),
&[wgpu::CompositeAlphaMode::Auto],
&[],
] {
let caps = caps_with_alpha_modes(modes);
let resolved = resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps);
assert_eq!(resolved, wgpu::CompositeAlphaMode::Auto, "{modes:?}");
assert!(
!alpha_mode_is_translucent(resolved),
"a fallback-to-opaque surface must never report translucent ({modes:?})"
);
}
}
#[test]
fn happy_path_translucent_request_resolves_translucent() {
for modes in [
[wgpu::CompositeAlphaMode::Inherit].as_slice(),
&[
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::PostMultiplied,
],
] {
let resolved = resolve_alpha_mode(
SurfaceAlphaRequest::TranslucentPreferred,
&caps_with_alpha_modes(modes),
);
assert!(alpha_mode_is_translucent(resolved), "{modes:?}");
}
assert!(!alpha_mode_is_translucent(resolve_alpha_mode(
SurfaceAlphaRequest::Opaque,
&caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Inherit]),
)));
}
const EVERY_ALPHA_MODE: [wgpu::CompositeAlphaMode; 5] = [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PreMultiplied,
wgpu::CompositeAlphaMode::PostMultiplied,
];
#[test]
fn compositor_expects_premultiplied_is_metal_post_multiplied_only() {
for backend in wgpu::Backend::ALL {
for mode in EVERY_ALPHA_MODE {
let expected = backend == wgpu::Backend::Metal
&& mode == wgpu::CompositeAlphaMode::PostMultiplied;
assert_eq!(
compositor_expects_premultiplied(backend, mode),
expected,
"compositor_expects_premultiplied({backend:?}, {mode:?})"
);
}
}
}
#[test]
fn the_truth_bug_only_ever_contradicts_a_straight_translucent_mode() {
for backend in wgpu::Backend::ALL {
for mode in EVERY_ALPHA_MODE {
if compositor_expects_premultiplied(backend, mode) {
assert!(
alpha_mode_is_straight_translucent(mode),
"{backend:?}/{mode:?} is contradicted without being straight-translucent"
);
}
}
}
}
}