use anyhow::{Result, anyhow};
#[cfg(feature = "perf-trace")]
use std::sync::OnceLock;
use frust_gpu::{ConfiguredSurface, DeviceHandle, RenderContext, SurfaceAlphaRequest};
pub(crate) use frust_gpu::context::{effective_limits, is_ios_simulator, optional_device_features};
use frust_gpu::surface::{alpha_mode_is_straight_translucent, compositor_expects_premultiplied};
#[cfg_attr(not(feature = "perf-trace"), allow(dead_code))]
fn env_flag_enabled(name_compile_time: Option<&str>, name_runtime: Option<String>) -> bool {
fn is_set_non_zero(value: Option<&str>) -> bool {
matches!(value, Some(v) if v != "0")
}
is_set_non_zero(name_compile_time) || is_set_non_zero(name_runtime.as_deref())
}
#[cfg(feature = "perf-trace")]
fn perf_tracing_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
env_flag_enabled(
option_env!("FRUST_TRACE"),
std::env::var("FRUST_TRACE").ok(),
)
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RenderPathKind {
EngineDirect,
EngineDirectUnpremultiply,
}
pub(crate) fn choose_engine_render_path(
backend: wgpu::Backend,
alpha_mode: wgpu::CompositeAlphaMode,
) -> RenderPathKind {
if alpha_mode_is_straight_translucent(alpha_mode)
&& !compositor_expects_premultiplied(backend, alpha_mode)
{
RenderPathKind::EngineDirectUnpremultiply
} else {
RenderPathKind::EngineDirect
}
}
fn check_engine_support(adapter: &wgpu::Adapter) -> Result<()> {
let caps = crate::tier::TierCaps {
downlevel_flags: adapter.get_downlevel_capabilities().flags,
adapter_name: adapter.get_info().name,
};
crate::tier::engine_support(&caps).map_err(|refusal| anyhow!(refusal.to_string()))
}
#[cfg(feature = "perf-trace")]
fn log_render_path(path: RenderPathKind) {
static LOGGED: OnceLock<()> = OnceLock::new();
if !perf_tracing_enabled() {
return;
}
LOGGED.get_or_init(|| {
let (name, reason) = match path {
RenderPathKind::EngineDirect => (
"engine-direct",
"frust-engine into the acquired swapchain view (RENDER_ATTACHMENT, \
surface-reported format, surface-owned depth)",
),
RenderPathKind::EngineDirectUnpremultiply => (
"engine-direct-unpremultiply",
"frust-engine into a surface-owned intermediate, un-premultiplied into the \
swapchain by one fragment pass (straight-alpha translucent surface)",
),
};
log::info!("frust-perf render-path {name} ({reason})");
});
}
#[cfg(not(feature = "perf-trace"))]
#[inline]
fn log_render_path(_path: RenderPathKind) {}
#[cfg(feature = "perf-trace")]
fn log_surface_alpha_caps(surface: &ConfiguredSurface, handle: &DeviceHandle) {
if !perf_tracing_enabled() {
return;
}
let capabilities = surface.surface().get_capabilities(&handle.adapter);
log::info!(
"frust-render surface-caps: alpha_modes={:?} chosen={:?}",
capabilities.alpha_modes,
surface.config().alpha_mode
);
}
#[cfg(not(feature = "perf-trace"))]
#[inline]
fn log_surface_alpha_caps(_surface: &ConfiguredSurface, _handle: &DeviceHandle) {}
pub(crate) fn extent_within_limits(width: u32, height: u32, max_dimension_2d: u32) -> bool {
width <= max_dimension_2d && height <= max_dimension_2d
}
pub(crate) enum RenderPath {
EngineDirect {
depth: frust_engine::DepthTexture,
},
EngineDirectUnpremultiply {
depth: frust_engine::DepthTexture,
intermediate_view: wgpu::TextureView,
present: frust_engine::gpu::present::UnpremultiplyPass,
},
}
pub(crate) struct EngineSurface {
pub(crate) gpu: ConfiguredSurface,
pub(crate) path: RenderPath,
}
impl EngineSurface {
pub(crate) fn config(&self) -> &wgpu::SurfaceConfiguration {
self.gpu.config()
}
pub(crate) fn resolved_translucent(&self) -> bool {
self.gpu.resolved_translucent()
}
pub(crate) fn reconfigure(&self, device: &wgpu::Device) {
self.gpu.reconfigure(device);
}
pub(crate) fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
match &mut self.path {
RenderPath::EngineDirect { depth } => {
let max_dimension_2d = device.limits().max_texture_dimension_2d;
if !extent_within_limits(width, height, max_dimension_2d) {
log::warn!(
"frust-render: engine tier cannot resize to {width}x{height} — the \
device's max_texture_dimension_2d is {max_dimension_2d}; keeping the \
previous depth attachment"
);
} else if !depth.matches(width, height) {
*depth = frust_engine::DepthTexture::new(device, width, height);
}
}
RenderPath::EngineDirectUnpremultiply {
depth,
intermediate_view,
..
} => {
let max_dimension_2d = device.limits().max_texture_dimension_2d;
if !extent_within_limits(width, height, max_dimension_2d) {
log::warn!(
"frust-render: engine tier cannot resize to {width}x{height} — the \
device's max_texture_dimension_2d is {max_dimension_2d}; keeping the \
previous depth attachment and intermediate"
);
} else {
if !depth.matches(width, height) {
*depth = frust_engine::DepthTexture::new(device, width, height);
}
*intermediate_view = create_engine_intermediate(width, height, device);
}
}
}
self.gpu.resize(device, width, height);
}
}
fn create_engine_intermediate(width: u32, height: u32, device: &wgpu::Device) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("frust-render engine intermediate"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
format: frust_engine::gpu::pipelines::INTERMEDIATE_FORMAT,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
pub(crate) async fn create_engine_surface(
ctx: &mut RenderContext,
surface: wgpu::Surface<'static>,
width: u32,
height: u32,
present_mode: wgpu::PresentMode,
alpha: SurfaceAlphaRequest,
) -> Result<EngineSurface> {
ctx.ensure_device(&surface).await?;
check_engine_support(&ctx.device_handle().adapter)?;
let max_dimension_2d = ctx.device_handle().device.limits().max_texture_dimension_2d;
if !extent_within_limits(width, height, max_dimension_2d) {
return Err(anyhow!(
"frust-render: the engine refuses a {width}x{height} surface — the device's \
max_texture_dimension_2d is {max_dimension_2d}, and both the swapchain and \
the depth attachment paired with it are sized from this extent"
));
}
let configured = ctx
.create_render_surface(surface, width, height, present_mode, alpha)
.await?;
let handle = ctx.device_handle();
let alpha_mode = configured.config().alpha_mode;
let path_kind = choose_engine_render_path(handle.adapter.get_info().backend, alpha_mode);
log_render_path(path_kind);
log_surface_alpha_caps(&configured, handle);
let format = configured.config().format;
let path = match path_kind {
RenderPathKind::EngineDirect => RenderPath::EngineDirect {
depth: frust_engine::DepthTexture::new(&handle.device, width, height),
},
RenderPathKind::EngineDirectUnpremultiply => RenderPath::EngineDirectUnpremultiply {
depth: frust_engine::DepthTexture::new(&handle.device, width, height),
intermediate_view: create_engine_intermediate(width, height, &handle.device),
present: frust_engine::gpu::present::UnpremultiplyPass::new(
&handle.device,
format,
None,
),
},
};
Ok(EngineSurface {
gpu: configured,
path,
})
}
pub(crate) async fn create_engine_surface_from_target(
ctx: &mut RenderContext,
target: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
present_mode: wgpu::PresentMode,
alpha: SurfaceAlphaRequest,
) -> Result<EngineSurface> {
let detached = ctx.surface_factory().create_detached_surface(target)?;
create_engine_surface(
ctx,
detached.into_surface(),
width,
height,
present_mode,
alpha,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_flag_unset_both_halves_is_disabled() {
assert!(!env_flag_enabled(None, None));
}
#[test]
fn env_flag_compile_time_non_zero_enables() {
assert!(env_flag_enabled(Some("1"), None));
}
#[test]
fn env_flag_compile_time_zero_is_disabled() {
assert!(!env_flag_enabled(Some("0"), None));
}
#[test]
fn env_flag_runtime_non_zero_enables() {
assert!(env_flag_enabled(None, Some("1".to_string())));
}
#[test]
fn env_flag_runtime_zero_is_disabled() {
assert!(!env_flag_enabled(None, Some("0".to_string())));
}
#[test]
fn env_flag_either_half_set_non_zero_enables() {
assert!(env_flag_enabled(Some("0"), Some("1".to_string())));
assert!(env_flag_enabled(Some("1"), Some("0".to_string())));
}
const EVERY_ALPHA_MODE: [wgpu::CompositeAlphaMode; 5] = [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PreMultiplied,
wgpu::CompositeAlphaMode::PostMultiplied,
];
#[test]
fn every_alpha_mode_routes_to_an_engine_arm() {
for backend in wgpu::Backend::ALL {
for mode in EVERY_ALPHA_MODE {
let kind = choose_engine_render_path(backend, mode);
assert!(
matches!(
kind,
RenderPathKind::EngineDirect | RenderPathKind::EngineDirectUnpremultiply
),
"{backend:?}/{mode:?} routed to {kind:?}, which is not an engine arm"
);
}
}
}
#[test]
fn engine_serves_opaque_and_premultiplied_modes_directly() {
for backend in wgpu::Backend::ALL {
for mode in [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PreMultiplied,
] {
assert_eq!(
choose_engine_render_path(backend, mode),
RenderPathKind::EngineDirect,
"{mode:?} on {backend:?} took a conversion it does not need"
);
}
}
}
#[test]
fn straight_alpha_translucency_routes_through_the_unpremultiply_arm() {
for backend in wgpu::Backend::ALL {
if backend == wgpu::Backend::Metal {
continue;
}
for mode in EVERY_ALPHA_MODE {
assert_eq!(
choose_engine_render_path(backend, mode)
== RenderPathKind::EngineDirectUnpremultiply,
alpha_mode_is_straight_translucent(mode),
"the conversion arm must be exactly the straight-alpha translucent modes on \
{backend:?}, not {mode:?}"
);
}
}
}
#[test]
fn metal_post_multiplied_skips_the_unpremultiply_arm() {
assert_eq!(
choose_engine_render_path(
wgpu::Backend::Metal,
wgpu::CompositeAlphaMode::PostMultiplied
),
RenderPathKind::EngineDirect,
);
for mode in [
wgpu::CompositeAlphaMode::Auto,
wgpu::CompositeAlphaMode::Opaque,
wgpu::CompositeAlphaMode::Inherit,
wgpu::CompositeAlphaMode::PreMultiplied,
] {
assert_eq!(
choose_engine_render_path(wgpu::Backend::Metal, mode),
RenderPathKind::EngineDirect,
"{mode:?} on Metal took a conversion it does not need"
);
}
}
#[test]
fn non_metal_post_multiplied_keeps_the_unpremultiply_arm() {
for backend in wgpu::Backend::ALL {
if backend == wgpu::Backend::Metal {
continue;
}
assert_eq!(
choose_engine_render_path(backend, wgpu::CompositeAlphaMode::PostMultiplied),
RenderPathKind::EngineDirectUnpremultiply,
"{backend:?} PostMultiplied must keep the unpremultiply conversion"
);
}
}
#[test]
fn extent_limits_refuse_only_an_over_ceiling_axis() {
assert!(extent_within_limits(4096, 4096, 4096));
assert!(extent_within_limits(1, 1, 4096));
assert!(!extent_within_limits(4097, 4096, 4096));
assert!(!extent_within_limits(4096, 4097, 4096));
}
}