Skip to content
Merged
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
99 changes: 94 additions & 5 deletions crates/rendering/src/layers/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,7 @@ impl From<BackgroundSource> for Background {
noise_scale: noise_scale.unwrap_or(3.0),
}),
BackgroundSource::Image { path } | BackgroundSource::Wallpaper { path } => {
if let Some(clean_path) = path.as_deref().and_then(clean_background_path) {
Background::Image { path: clean_path }
} else {
Background::Color([1.0, 1.0, 1.0, 1.0])
}
Background::image_or_white(path.as_deref().and_then(clean_background_path))
}
}
}
Expand All @@ -324,6 +320,13 @@ fn background_source_is_empty(source: &BackgroundSource) -> bool {
}

impl Background {
fn image_or_white(path: Option<String>) -> Self {
match path {
Some(path) => Background::Image { path },
None => Background::Color([1.0, 1.0, 1.0, 1.0]),
}
}

pub fn from_source(source: BackgroundSource, transparent_when_empty: bool) -> Self {
if transparent_when_empty && background_source_is_empty(&source) {
Background::Color([0.0, 0.0, 0.0, 0.0])
Expand All @@ -333,6 +336,38 @@ impl Background {
}
}

/// The last image source path and what it resolved to on disk, so the path
/// is only cleaned and checked again when the source changes.
#[derive(Default)]
struct ResolvedImagePath(Option<(String, Option<String>)>);

impl ResolvedImagePath {
fn background(
&mut self,
source: &BackgroundSource,
transparent_when_empty: bool,
) -> Background {
let (BackgroundSource::Image { path: Some(path) }
| BackgroundSource::Wallpaper { path: Some(path) }) = source
else {
return Background::from_source(source.clone(), transparent_when_empty);
};
if path.is_empty() {
return Background::from_source(source.clone(), transparent_when_empty);
}

let resolved = match &self.0 {
Some((cached, resolved)) if cached == path => resolved.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing background paths remain cached after the file is restored

Medium Severity

If a project's image is temporarily unavailable on its first render, clean_background_path returns None and this cache retains that result. Restoring the file or reconnecting its drive never reaches BackgroundTextureCache::ensure: subsequent frames keep producing the white fallback. Switching to a color and reselecting the same image also fails because the non-image branches leave this entry intact. Previously, path existence was checked again and the image recovered. Avoid indefinitely caching failed lookups, or provide invalidation and retry when the source is reselected.

Open in Capy

_ => {
let resolved = clean_background_path(path);
self.0 = Some((path.clone(), resolved.clone()));
resolved
}
};
Background::image_or_white(resolved)
}
}

pub enum Inner {
AnimatedGradient(Box<AnimatedGradientLayer>),
Image {
Expand All @@ -353,6 +388,7 @@ pub struct BackgroundLayer {
/// Bumped whenever `inner` is replaced, so consumers can cache anything
/// derived from a static background (e.g. its blurred copy).
generation: u64,
resolved_image_path: ResolvedImagePath,
image_pipeline: ImageBackgroundPipeline,
color_pipeline: GradientOrColorPipeline,
}
Expand All @@ -362,11 +398,21 @@ impl BackgroundLayer {
Self {
inner: None,
generation: 0,
resolved_image_path: ResolvedImagePath::default(),
image_pipeline: ImageBackgroundPipeline::new(device),
color_pipeline: GradientOrColorPipeline::new(device),
}
}

pub fn background_for(
&mut self,
source: &BackgroundSource,
transparent_when_empty: bool,
) -> Background {
self.resolved_image_path
.background(source, transparent_when_empty)
}

pub async fn prepare(
&mut self,
constants: &RenderVideoConstants,
Expand Down Expand Up @@ -836,6 +882,49 @@ mod tests {
}
}

#[test]
fn image_path_is_checked_on_disk_only_when_the_source_changes() {
let directory = tempfile::tempdir().unwrap();
let first = directory.path().join("first.png");
let second = directory.path().join("second.png");
std::fs::write(&first, b"").unwrap();
std::fs::write(&second, b"").unwrap();
let source = |path: &std::path::Path| BackgroundSource::Wallpaper {
path: Some(path.to_string_lossy().into_owned()),
};
let is_image = |background: Background, path: &std::path::Path| match background {
Background::Image { path: resolved } => resolved == path.to_string_lossy(),
_ => false,
};

let mut resolved = ResolvedImagePath::default();
assert!(is_image(
resolved.background(&source(&first), false),
&first
));

std::fs::remove_file(&first).unwrap();
for _ in 0..3 {
assert!(is_image(
resolved.background(&source(&first), false),
&first
));
}

assert!(is_image(
resolved.background(&source(&second), false),
&second
));
assert!(matches!(
resolved.background(&source(&first), false),
Background::Color([1.0, 1.0, 1.0, 1.0])
));
assert!(matches!(
resolved.background(&BackgroundSource::Wallpaper { path: None }, true),
Background::Color([0.0, 0.0, 0.0, 0.0])
));
}

#[test]
fn test_gradient_conversion() {
let source = BackgroundSource::Gradient {
Expand Down
43 changes: 30 additions & 13 deletions crates/rendering/src/layers/blur.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;

use super::SourceBindGroups;
use crate::ProjectUniforms;

pub struct BlurLayer {
Expand All @@ -11,6 +12,7 @@ pub struct BlurLayer {
pipeline: BlurPipeline,
cached_uniforms: Option<BlurUniforms>,
result: Option<(BlurResultKey, wgpu::Texture)>,
bind_groups: SourceBindGroups,
}

/// Everything the blurred background depends on. While it matches, the
Expand Down Expand Up @@ -53,6 +55,7 @@ impl BlurLayer {
pipeline: BlurPipeline::new(device),
cached_uniforms: None,
result: None,
bind_groups: SourceBindGroups::default(),
}
}

Expand Down Expand Up @@ -132,39 +135,53 @@ impl BlurLayer {
}
}

pub fn retain_views(&mut self, views: [&wgpu::TextureView; 2]) {
self.bind_groups.retain_views(views);
}

#[cfg(test)]
pub fn bind_groups_created(&self) -> usize {
self.bind_groups.created()
}

pub fn render_h(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
source_texture: &wgpu::TextureView,
) {
self.render_pass(pass, device, source_texture, &self.uniforms_buffer_h);
self.render_pass(pass, device, source_texture, false);
}

pub fn render_v(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
source_texture: &wgpu::TextureView,
) {
self.render_pass(pass, device, source_texture, &self.uniforms_buffer_v);
self.render_pass(pass, device, source_texture, true);
}

fn render_pass(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
source_texture: &wgpu::TextureView,
uniforms_buffer: &wgpu::Buffer,
vertical: bool,
) {
let uniforms_buffer = if vertical {
&self.uniforms_buffer_v
} else {
&self.uniforms_buffer_h
};
let bind_group = self
.bind_groups
.get_or_create(uniforms_buffer, source_texture, || {
self.pipeline
.bind_group(device, uniforms_buffer, source_texture, &self.sampler)
});
pass.set_pipeline(&self.pipeline.render_pipeline);
pass.set_bind_group(
0,
&self
.pipeline
.bind_group(device, uniforms_buffer, source_texture, &self.sampler),
&[],
);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..4, 0..1);
}
}
Expand Down
64 changes: 40 additions & 24 deletions crates/rendering/src/layers/camera3d.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;

use super::SourceBindGroups;
use crate::ProjectUniforms;
use crate::camera3d::{CAMERA3D_BLUR_BASE_HEIGHT, camera3d_inverse_homography};
use cap_project::Camera3DBlurMode;
Expand Down Expand Up @@ -30,6 +31,7 @@ pub struct Camera3DLayer {
blur_v_buffer: wgpu::Buffer,
blur_pipeline: Camera3DBlurPipeline,
cached_blur_uniforms: Option<(Camera3DBlurUniforms, Camera3DBlurUniforms)>,
bind_groups: SourceBindGroups,
}

impl Camera3DLayer {
Expand Down Expand Up @@ -64,6 +66,7 @@ impl Camera3DLayer {
blur_v_buffer: make_blur_buffer("Camera3D Blur V Uniform Buffer"),
blur_pipeline: Camera3DBlurPipeline::new(device),
cached_blur_uniforms: None,
bind_groups: SourceBindGroups::default(),
}
}

Expand Down Expand Up @@ -153,61 +156,74 @@ impl Camera3DLayer {
}
}

pub fn retain_views(&mut self, views: [&wgpu::TextureView; 2]) {
self.bind_groups.retain_views(views);
}

#[cfg(test)]
pub fn bind_groups_created(&self) -> usize {
self.bind_groups.created()
}

pub fn render(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
content_texture: &wgpu::TextureView,
) {
let bind_group =
self.bind_groups
.get_or_create(&self.warp_uniforms_buffer, content_texture, || {
self.warp_pipeline.bind_group(
device,
&self.warp_uniforms_buffer,
content_texture,
&self.sampler,
)
});
pass.set_pipeline(&self.warp_pipeline.render_pipeline);
pass.set_bind_group(
0,
&self.warp_pipeline.bind_group(
device,
&self.warp_uniforms_buffer,
content_texture,
&self.sampler,
),
&[],
);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}

/// Horizontal gaussian pass (or the single bokeh pass, which uses the V
/// buffer via [`Self::render_blur_v`] instead).
pub fn render_blur_h(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
source_texture: &wgpu::TextureView,
) {
self.render_blur(pass, device, &self.blur_h_buffer, source_texture);
self.render_blur(pass, device, false, source_texture);
}

pub fn render_blur_v(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
source_texture: &wgpu::TextureView,
) {
self.render_blur(pass, device, &self.blur_v_buffer, source_texture);
self.render_blur(pass, device, true, source_texture);
}

fn render_blur(
&self,
&mut self,
pass: &mut wgpu::RenderPass<'_>,
device: &wgpu::Device,
buffer: &wgpu::Buffer,
vertical: bool,
source_texture: &wgpu::TextureView,
) {
let buffer = if vertical {
&self.blur_v_buffer
} else {
&self.blur_h_buffer
};
let bind_group = self.bind_groups.get_or_create(buffer, source_texture, || {
self.blur_pipeline
.bind_group(device, buffer, source_texture, &self.sampler)
});
pass.set_pipeline(&self.blur_pipeline.render_pipeline);
pass.set_bind_group(
0,
&self
.blur_pipeline
.bind_group(device, buffer, source_texture, &self.sampler),
&[],
);
pass.set_bind_group(0, bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
Expand Down
Loading
Loading