use crate::asset::{Wisp, WispHandle};
use crate::render::supports_compute;
use crate::schema::{PassSchema, PassStage, TargetSchema, WispSchema, eval_size, requires_compute};
use bevy_asset::prelude::{Assets, Handle};
use bevy_camera::{Camera, RenderTarget};
use bevy_ecs::prelude::*;
use bevy_image::prelude::Image;
use bevy_log::warn_once;
use bevy_math::UVec2;
use bevy_render::extract_component::ExtractComponent;
use bevy_render::render_resource::{Extent3d, TextureUsages};
use bevy_render::renderer::RenderAdapter;
use bevy_utils::default;
use bevy_window::{PrimaryWindow, Window, WindowRef};
#[derive(Component, ExtractComponent, Clone, Debug, Default)]
pub struct WispPassTargets(pub Vec<Option<PassTarget>>);
#[derive(Clone, Debug)]
pub struct PassTarget {
pub size: UVec2,
pub clear: bool,
pub dispatch: Option<[u32; 3]>,
images: [Handle<Image>; 2],
flip: bool,
}
impl PassTarget {
pub fn write(&self) -> &Handle<Image> {
&self.images[self.flip as usize]
}
pub fn read(&self) -> &Handle<Image> {
&self.images[1 - self.flip as usize]
}
}
pub(crate) fn update_pass_targets(
mut commands: Commands,
render_adapter: Option<Res<RenderAdapter>>,
wisps: Res<Assets<Wisp>>,
windows: Query<(&Window, Option<&PrimaryWindow>)>,
mut images: ResMut<Assets<Image>>,
mut cameras: Query<(
Entity,
&Camera,
&RenderTarget,
&WispHandle,
Option<&mut WispPassTargets>,
)>,
) {
for (entity, camera, render_target, wisp, targets) in cameras.iter_mut() {
let Some(wisp) = wisps.get(&**wisp) else {
continue;
};
if requires_compute(&wisp.schema)
&& render_adapter
.as_ref()
.is_some_and(|adapter| !supports_compute(adapter))
{
continue;
}
let viewport_size = camera.viewport.as_ref().map(|v| v.physical_size);
let Some(view_size) =
viewport_size.or_else(|| render_target_size(render_target, &windows, &images))
else {
warn_once!("wisp: unsupported camera render target; only windows and images work");
continue;
};
match targets {
Some(mut targets) => sync_targets(&mut targets, &wisp.schema, view_size, &mut images),
None => {
let mut targets = WispPassTargets::default();
sync_targets(&mut targets, &wisp.schema, view_size, &mut images);
commands.entity(entity).insert(targets);
}
}
}
}
fn render_target_size(
render_target: &RenderTarget,
windows: &Query<(&Window, Option<&PrimaryWindow>)>,
images: &Assets<Image>,
) -> Option<UVec2> {
match render_target {
RenderTarget::Window(WindowRef::Primary) => windows
.iter()
.find(|(_, primary)| primary.is_some())
.map(|(window, _)| window.resolution.physical_size()),
RenderTarget::Window(WindowRef::Entity(entity)) => windows
.get(*entity)
.ok()
.map(|(window, _)| window.resolution.physical_size()),
RenderTarget::Image(target) => images.get(&target.handle).map(|image| image.size()),
RenderTarget::TextureView(_) | RenderTarget::None { .. } => None,
}
}
fn sync_targets(
targets: &mut WispPassTargets,
schema: &WispSchema,
view_size: UVec2,
images: &mut Assets<Image>,
) {
targets.0.resize(schema.passes.len(), None);
for (index, pass) in schema.passes.iter().enumerate() {
let slot = &mut targets.0[index];
let Some(target) = &pass.target else {
*slot = None;
continue;
};
let size = target_size(target, view_size);
let clear = !target.persistent;
let dispatch = match pass.stage {
PassStage::Fragment => None,
PassStage::Compute => Some(dispatch_size(pass, size)),
};
let up_to_date = slot.as_ref().is_some_and(|t| {
t.size == size
&& t.clear == clear
&& t.dispatch == dispatch
&& (t.images[0] != t.images[1]) == pass.self_feedback
&& images
.get(&t.images[0])
.is_some_and(|image| image.texture_descriptor.format == target.format)
});
if up_to_date {
if let Some(t) = slot
&& pass.self_feedback
{
t.flip = !t.flip;
}
continue;
}
let mut usage = TextureUsages::RENDER_ATTACHMENT
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST;
if pass.stage == PassStage::Compute {
usage |= TextureUsages::STORAGE_BINDING;
}
let mut new_image = || {
let mut image = Image::default();
image.texture_descriptor.format = target.format;
image.texture_descriptor.usage = usage;
image.resize(Extent3d {
width: size.x,
height: size.y,
..default()
});
images.add(image)
};
let first = new_image();
let second = match pass.self_feedback {
true => new_image(),
false => first.clone(),
};
*slot = Some(PassTarget {
size,
clear,
dispatch,
images: [first, second],
flip: false,
});
}
}
fn dispatch_size(pass: &PassSchema, target_size: UVec2) -> [u32; 3] {
match &pass.dispatch {
Some(exprs) => {
let eval = |expr: &String| {
eval_size(expr, target_size).unwrap_or_else(|err| {
warn_once!("wisp: {err}; dispatching a single workgroup");
1
})
};
[eval(&exprs[0]), eval(&exprs[1]), eval(&exprs[2])]
}
None => {
let [x, y, _] = pass.workgroup_size;
[
target_size.x.div_ceil(x.max(1)),
target_size.y.div_ceil(y.max(1)),
1,
]
}
}
}
fn target_size(target: &TargetSchema, view_size: UVec2) -> UVec2 {
let dim = |expr: &Option<String>, base: u32| match expr {
None => base,
Some(expr) => eval_size(expr, view_size).unwrap_or_else(|err| {
warn_once!("wisp: {err}; falling back to the view size");
base
}),
};
UVec2::new(
dim(&target.width, view_size.x),
dim(&target.height, view_size.y),
)
}