mod encode;
mod resolve;
mod scaler;
#[cfg(feature = "frame-interpolation")]
use std::ffi::c_void;
use std::sync::Mutex;
use bevy::core_pipeline::blit::{BlitPipeline, BlitPipelineKey};
use bevy::core_pipeline::prepass::ViewPrepassTextures;
use bevy::prelude::*;
use bevy::render::camera::TemporalJitter;
use bevy::render::render_graph::{NodeRunError, RenderGraphContext, ViewNode};
use bevy::render::render_resource::{
BindGroup, CachedRenderPipelineId, Extent3d, PipelineCache, RenderPassDescriptor,
SpecializedRenderPipeline, TextureView, TextureViewId,
};
use bevy::render::renderer::RenderContext;
use bevy::render::view::ViewTarget;
#[cfg(feature = "frame-interpolation")]
use foreign_types::ForeignType;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
#[cfg(feature = "frame-interpolation")]
use objc2_metal_fx::MTLFXFrameInterpolator;
use objc2_metal_fx::MTLFXSpatialScaler;
#[cfg(feature = "temporal")]
use objc2_metal_fx::MTLFXTemporalScaler;
use crate::platform::wgpu_format_to_mtl;
use crate::MetalFxMode;
#[derive(Resource, Clone, Copy, bevy::render::extract_resource::ExtractResource)]
pub struct MetalFxConfig {
pub(crate) render_scale: f32,
pub(crate) mode: MetalFxMode,
pub(crate) dynamic_res_range: Option<(f32, f32)>,
}
#[derive(Resource, Clone, Copy, bevy::render::extract_resource::ExtractResource)]
pub struct MetalFxFrameTiming {
pub(crate) delta_seconds: f32,
}
impl Default for MetalFxFrameTiming {
fn default() -> Self {
Self {
delta_seconds: 1.0 / 60.0,
}
}
}
pub(crate) enum SendScaler {
Spatial(Retained<ProtocolObject<dyn MTLFXSpatialScaler>>),
#[cfg(feature = "temporal")]
Temporal(Retained<ProtocolObject<dyn MTLFXTemporalScaler>>),
#[cfg(feature = "frame-interpolation")]
FrameInterpolator {
scaler: Retained<ProtocolObject<dyn MTLFXTemporalScaler>>,
interpolator: Retained<ProtocolObject<dyn MTLFXFrameInterpolator>>,
},
}
unsafe impl Send for SendScaler {}
unsafe impl Sync for SendScaler {}
impl SendScaler {
fn is_temporal_like(&self) -> bool {
match self {
SendScaler::Spatial(_) => false,
#[cfg(feature = "temporal")]
SendScaler::Temporal(_) => true,
#[cfg(feature = "frame-interpolation")]
SendScaler::FrameInterpolator { .. } => true,
}
}
}
#[cfg(feature = "frame-interpolation")]
const PRESENT_FORMAT: bevy::render::render_resource::TextureFormat =
bevy::render::render_resource::TextureFormat::Bgra8UnormSrgb;
struct CachedState {
scaler: SendScaler,
input_texture: bevy::render::render_resource::Texture,
output_texture: bevy::render::render_resource::Texture,
output_view: TextureView,
#[cfg(feature = "frame-interpolation")]
prev_color_texture: Option<bevy::render::render_resource::Texture>,
#[cfg(feature = "frame-interpolation")]
interp_output_texture: Option<bevy::render::render_resource::Texture>,
#[cfg(feature = "frame-interpolation")]
interp_output_view: Option<TextureView>,
#[cfg(feature = "frame-interpolation")]
interp_bgra: Option<bevy::render::render_resource::Texture>,
#[cfg(feature = "frame-interpolation")]
interp_bgra_view: Option<TextureView>,
#[cfg(feature = "frame-interpolation")]
real_bgra: Option<bevy::render::render_resource::Texture>,
#[cfg(feature = "frame-interpolation")]
real_bgra_view: Option<TextureView>,
content_depth_texture: Option<bevy::render::render_resource::Texture>,
content_depth_view: Option<TextureView>,
content_motion_texture: Option<bevy::render::render_resource::Texture>,
content_motion_view: Option<TextureView>,
input_w: u32,
input_h: u32,
output_w: u32,
output_h: u32,
frame_count: u64,
}
struct PendingScaler {
receiver: std::sync::mpsc::Receiver<Option<SendScaler>>,
input_w: u32,
input_h: u32,
output_w: u32,
output_h: u32,
}
struct ResolvePipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
}
pub struct MetalFxUpscaleNode {
cached: Mutex<Option<CachedState>>,
pending: Mutex<Option<PendingScaler>>,
cached_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
cached_pipeline: Mutex<Option<CachedRenderPipelineId>>,
depth_resolve: Mutex<Option<ResolvePipeline>>,
depth_resolve_bind_group: Mutex<Option<(TextureViewId, TextureViewId, wgpu::BindGroup)>>,
motion_resolve: Mutex<Option<ResolvePipeline>>,
motion_resolve_bind_group: Mutex<Option<(TextureViewId, wgpu::BindGroup)>>,
#[cfg(feature = "frame-interpolation")]
cached_interp_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
#[cfg(feature = "frame-interpolation")]
cached_real_present_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
#[cfg(feature = "frame-interpolation")]
cached_present_pipeline: Mutex<Option<CachedRenderPipelineId>>,
}
impl Default for MetalFxUpscaleNode {
fn default() -> Self {
Self {
cached: Mutex::new(None),
pending: Mutex::new(None),
cached_bind_group: Mutex::new(None),
cached_pipeline: Mutex::new(None),
depth_resolve: Mutex::new(None),
depth_resolve_bind_group: Mutex::new(None),
motion_resolve: Mutex::new(None),
motion_resolve_bind_group: Mutex::new(None),
#[cfg(feature = "frame-interpolation")]
cached_interp_bind_group: Mutex::new(None),
#[cfg(feature = "frame-interpolation")]
cached_real_present_bind_group: Mutex::new(None),
#[cfg(feature = "frame-interpolation")]
cached_present_pipeline: Mutex::new(None),
}
}
}
impl ViewNode for MetalFxUpscaleNode {
type ViewQuery = (
&'static ViewTarget,
Option<&'static ViewPrepassTextures>,
Option<&'static TemporalJitter>,
Option<&'static Projection>,
);
#[cfg_attr(not(feature = "frame-interpolation"), allow(unused_variables))]
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(target, prepass_textures, temporal_jitter, projection): bevy::ecs::query::QueryItem<
'w,
'_,
Self::ViewQuery,
>,
world: &'w World,
) -> Result<(), NodeRunError> {
let main_tex = target.main_texture();
let main_size = main_tex.size();
let main_format = main_tex.format();
let Some(color_mtl_fmt) = wgpu_format_to_mtl(main_format) else {
log::error!("MetalFxUpscaleNode: unsupported format {:?}", main_format);
return Ok(());
};
let config = world.get_resource::<MetalFxConfig>();
let render_scale = config.map_or(0.5, |c| c.render_scale);
let mode = config.map_or(MetalFxMode::Spatial, |c| c.mode);
let dynamic_res_range = config.and_then(|c| c.dynamic_res_range);
let full_w = main_size.width;
let full_h = main_size.height;
let input_w = (full_w as f32 * render_scale).round() as u32;
let input_h = (full_h as f32 * render_scale).round() as u32;
let output_w = full_w;
let output_h = full_h;
let content_w = input_w;
let content_h = input_h;
let (scaler_input_w, scaler_input_h) = match dynamic_res_range {
Some(_) => (output_w, output_h),
None => (input_w, input_h),
};
let device = render_context.render_device().clone();
let mut cached = self.cached.lock().unwrap();
if !self.ensure_scaler(
&device,
&mut cached,
scaler::ScalerDims {
scaler_input_w,
scaler_input_h,
input_w,
input_h,
output_w,
output_h,
},
mode,
main_format,
color_mtl_fmt,
dynamic_res_range,
) {
return Ok(());
}
let state = cached.as_mut().unwrap();
render_context.command_encoder().copy_texture_to_texture(
main_tex.as_image_copy(),
state.input_texture.as_image_copy(),
Extent3d {
width: content_w,
height: content_h,
depth_or_array_layers: 1,
},
);
let is_temporal_like = state.scaler.is_temporal_like();
if is_temporal_like {
let Some(prepass) = prepass_textures else {
log::warn!("MetalFxUpscaleNode: temporal mode but no prepass textures");
return Ok(());
};
let Some(depth_attachment) = &prepass.depth else {
log::warn!("MetalFxUpscaleNode: no depth prepass texture");
return Ok(());
};
let Some(motion_attachment) = &prepass.motion_vectors else {
log::warn!("MetalFxUpscaleNode: no motion vector prepass texture");
return Ok(());
};
if state.frame_count == 0 {
let depth_size = depth_attachment.texture.texture.size();
let motion_size = motion_attachment.texture.texture.size();
log::info!(
"MetalFxUpscaleNode temporal: prepass depth={}x{} ({:?}), motion={}x{} ({:?}), \
content-sized={}x{}, scaler input={}x{} -> output={}x{}",
depth_size.width, depth_size.height,
depth_attachment.texture.texture.format(),
motion_size.width, motion_size.height,
motion_attachment.texture.texture.format(),
content_w, content_h,
state.input_w, state.input_h,
state.output_w, state.output_h,
);
}
let content_motion_view = state.content_motion_view.as_ref().unwrap();
self.resolve_motion(
&device,
render_context,
&motion_attachment.texture.texture,
content_motion_view,
content_w,
content_h,
);
let content_depth_view = state.content_depth_view.as_ref().unwrap();
self.resolve_depth(
&device,
render_context,
&depth_attachment.texture.texture,
content_depth_view,
content_w,
content_h,
);
}
if !self.encode_metalfx(
world,
&device,
render_context,
state,
is_temporal_like,
temporal_jitter,
projection,
main_format,
content_w,
content_h,
input_w,
input_h,
output_w,
output_h,
) {
return Ok(());
}
let pipeline_cache = world.resource::<PipelineCache>();
let blit_pipeline = world.resource::<BlitPipeline>();
let mut cached_pipeline = self.cached_pipeline.lock().unwrap();
let pipeline_id = match *cached_pipeline {
Some(id) => id,
None => {
let key = BlitPipelineKey {
texture_format: target.out_texture_view_format(),
blend_state: None,
samples: 1,
};
let descriptor = blit_pipeline.specialize(key);
let id = pipeline_cache.queue_render_pipeline(descriptor);
*cached_pipeline = Some(id);
id
}
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
log::warn!("MetalFxUpscaleNode: blit pipeline not ready yet");
drop(cached);
return Ok(());
};
#[cfg(feature = "frame-interpolation")]
let interp_view_for_present = state.interp_output_view.clone();
#[cfg(feature = "frame-interpolation")]
let real_view_for_present = state.output_view.clone();
#[cfg(feature = "frame-interpolation")]
let staging = match (
state.interp_bgra_view.clone(),
state.real_bgra_view.clone(),
state.interp_bgra.clone(),
state.real_bgra.clone(),
) {
(Some(iv), Some(rv), Some(it), Some(rt)) => Some((iv, rv, it, rt)),
_ => None,
};
#[cfg(feature = "frame-interpolation")]
let dual_active = interp_view_for_present.is_some()
&& world
.get_resource::<crate::present::MetalFxDualPresent>()
.and_then(|d| d.layer())
.is_some();
let swapchain_view = &state.output_view;
let mut cached_bg = self.cached_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bg {
Some((id, bg)) if swapchain_view.id() == *id => bg,
slot => {
let bg = blit_pipeline.create_bind_group(
render_context.render_device(),
swapchain_view,
pipeline_cache,
);
let (_, bg) = slot.insert((swapchain_view.id(), bg));
bg
}
};
let pass_descriptor = RenderPassDescriptor {
label: Some("metalfx_blit"),
color_attachments: &[Some(target.out_texture_color_attachment(None))],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
drop(cached);
drop(cached_pipeline);
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1);
drop(render_pass);
drop(cached_bg);
#[cfg(feature = "frame-interpolation")]
if dual_active {
if let (Some(dual), Some((interp_src, real_src, interp_tex, real_tex))) = (
world.get_resource::<crate::present::MetalFxDualPresent>(),
staging,
) {
let mut present_pipeline = self.cached_present_pipeline.lock().unwrap();
let present_id = match *present_pipeline {
Some(id) => id,
None => {
let id = pipeline_cache.queue_render_pipeline(blit_pipeline.specialize(
BlitPipelineKey {
texture_format: PRESENT_FORMAT,
blend_state: None,
samples: 1,
},
));
*present_pipeline = Some(id);
id
}
};
drop(present_pipeline);
if let (Some(present_pipe), Some(layer), Some(queue)) = (
pipeline_cache.get_render_pipeline(present_id),
dual.layer(),
dual.queue(),
) {
self.convert_for_present(
render_context,
blit_pipeline,
pipeline_cache,
present_pipe,
interp_view_for_present.as_ref().unwrap(),
&interp_src,
true,
);
self.convert_for_present(
render_context,
blit_pipeline,
pipeline_cache,
present_pipe,
&real_view_for_present,
&real_src,
false,
);
let ptrs = unsafe {
let i = interp_tex.as_hal::<wgpu_hal::metal::Api>();
let r = real_tex.as_hal::<wgpu_hal::metal::Api>();
match (i, r) {
(Some(i), Some(r)) => Some((
i.raw_handle().as_ptr() as *mut c_void,
r.raw_handle().as_ptr() as *mut c_void,
)),
_ => None,
}
};
if let Some((interp_ptr, real_ptr)) = ptrs {
unsafe {
render_context
.command_encoder()
.as_hal_mut::<wgpu_hal::metal::Api, _, ()>(|hal_encoder| {
let Some(enc) = hal_encoder else { return };
let Some(cmd_buf) = enc.raw_command_buffer() else {
return;
};
crate::present::present_pair_deferred(
cmd_buf.as_ptr() as *mut c_void,
layer,
queue,
interp_ptr,
real_ptr,
dual.refresh_interval,
&dual.sink,
dual.single_present,
);
});
}
}
}
}
}
Ok(())
}
}
#[cfg(feature = "frame-interpolation")]
impl MetalFxUpscaleNode {
#[allow(clippy::too_many_arguments)]
fn convert_for_present(
&self,
render_context: &mut RenderContext,
blit_pipeline: &BlitPipeline,
pipeline_cache: &PipelineCache,
pipeline: &bevy::render::render_resource::RenderPipeline,
source: &TextureView,
target: &TextureView,
is_interpolated: bool,
) {
let mut slot = if is_interpolated {
self.cached_interp_bind_group.lock().unwrap()
} else {
self.cached_real_present_bind_group.lock().unwrap()
};
let bind_group = match &mut *slot {
Some((id, bg)) if source.id() == *id => bg,
s => {
let bg = blit_pipeline.create_bind_group(
render_context.render_device(),
source,
pipeline_cache,
);
let (_, bg) = s.insert((source.id(), bg));
bg
}
};
let mut pass = render_context
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
label: Some(if is_interpolated {
"metalfx_present_convert_interp"
} else {
"metalfx_present_convert_real"
}),
color_attachments: &[Some(
bevy::render::render_resource::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: bevy::render::render_resource::Operations {
load: bevy::render::render_resource::LoadOp::Clear(
bevy::color::LinearRgba::BLACK.into(),
),
store: bevy::render::render_resource::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}
}