Bilinear-sample upscaled raster proxies (smooth, not blocky)
The canvas blit used a nearest sampler, so the upscaled low-res proxy looked blocky. Added a Linear sampler + `CanvasBlitPipeline::blit_smooth`; the raster render uses it only for the proxy path (full-res canvas stays nearest/crisp). The bind-group layout already declares the canvas texture filterable, so no layout change was needed.
This commit is contained in:
parent
3c302f0215
commit
a35cc6fa9f
|
|
@ -1970,6 +1970,9 @@ pub struct CanvasBlitPipeline {
|
||||||
pub pipeline: wgpu::RenderPipeline,
|
pub pipeline: wgpu::RenderPipeline,
|
||||||
pub bg_layout: wgpu::BindGroupLayout,
|
pub bg_layout: wgpu::BindGroupLayout,
|
||||||
pub sampler: wgpu::Sampler,
|
pub sampler: wgpu::Sampler,
|
||||||
|
/// Bilinear sampler for smooth upscaling (used by `blit_smooth`, e.g. low-res
|
||||||
|
/// proxies). The default `sampler` stays nearest to keep the real canvas crisp.
|
||||||
|
pub linear_sampler: wgpu::Sampler,
|
||||||
/// Nearest-neighbour sampler used for the selection mask texture.
|
/// Nearest-neighbour sampler used for the selection mask texture.
|
||||||
pub mask_sampler: wgpu::Sampler,
|
pub mask_sampler: wgpu::Sampler,
|
||||||
}
|
}
|
||||||
|
|
@ -2145,6 +2148,17 @@ impl CanvasBlitPipeline {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("canvas_blit_linear_sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Linear,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mask_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
let mask_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
label: Some("canvas_mask_sampler"),
|
label: Some("canvas_mask_sampler"),
|
||||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
|
@ -2156,7 +2170,7 @@ impl CanvasBlitPipeline {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { pipeline, bg_layout, sampler, mask_sampler }
|
Self { pipeline, bg_layout, sampler, linear_sampler, mask_sampler }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the canvas texture into `target_view` (Rgba16Float) with the given camera.
|
/// Render the canvas texture into `target_view` (Rgba16Float) with the given camera.
|
||||||
|
|
@ -2164,6 +2178,7 @@ impl CanvasBlitPipeline {
|
||||||
/// `target_view` is cleared to transparent before writing.
|
/// `target_view` is cleared to transparent before writing.
|
||||||
/// `mask_view` is an R8Unorm texture in canvas-pixel space: 255 = keep, 0 = discard.
|
/// `mask_view` is an R8Unorm texture in canvas-pixel space: 255 = keep, 0 = discard.
|
||||||
/// Pass `None` to use the built-in 1×1 all-white default (no masking).
|
/// Pass `None` to use the built-in 1×1 all-white default (no masking).
|
||||||
|
/// Blit with the default nearest-neighbour sampler (crisp; for real canvas pixels).
|
||||||
pub fn blit(
|
pub fn blit(
|
||||||
&self,
|
&self,
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
|
|
@ -2172,6 +2187,32 @@ impl CanvasBlitPipeline {
|
||||||
target_view: &wgpu::TextureView,
|
target_view: &wgpu::TextureView,
|
||||||
transform: &BlitTransform,
|
transform: &BlitTransform,
|
||||||
mask_view: Option<&wgpu::TextureView>,
|
mask_view: Option<&wgpu::TextureView>,
|
||||||
|
) {
|
||||||
|
self.blit_with(device, queue, canvas_view, target_view, transform, mask_view, &self.sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blit with a bilinear sampler — smooth upscaling for low-res sources (proxies).
|
||||||
|
pub fn blit_smooth(
|
||||||
|
&self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
canvas_view: &wgpu::TextureView,
|
||||||
|
target_view: &wgpu::TextureView,
|
||||||
|
transform: &BlitTransform,
|
||||||
|
mask_view: Option<&wgpu::TextureView>,
|
||||||
|
) {
|
||||||
|
self.blit_with(device, queue, canvas_view, target_view, transform, mask_view, &self.linear_sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blit_with(
|
||||||
|
&self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
canvas_view: &wgpu::TextureView,
|
||||||
|
target_view: &wgpu::TextureView,
|
||||||
|
transform: &BlitTransform,
|
||||||
|
mask_view: Option<&wgpu::TextureView>,
|
||||||
|
canvas_sampler: &wgpu::Sampler,
|
||||||
) {
|
) {
|
||||||
// When no mask is provided, create a temporary 1×1 all-white texture.
|
// When no mask is provided, create a temporary 1×1 all-white texture.
|
||||||
// (queue is already available here, unlike in new())
|
// (queue is already available here, unlike in new())
|
||||||
|
|
@ -2224,7 +2265,7 @@ impl CanvasBlitPipeline {
|
||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
resource: wgpu::BindingResource::Sampler(canvas_sampler),
|
||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 2,
|
binding: 2,
|
||||||
|
|
|
||||||
|
|
@ -1313,23 +1313,31 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
let blit = if let Some(use_kf_id) = full_kf {
|
let blit = if let Some(use_kf_id) = full_kf {
|
||||||
gpu_brush.canvases.get(&use_kf_id)
|
gpu_brush.canvases.get(&use_kf_id)
|
||||||
.or_else(|| gpu_brush.raster_layer_cache.get(&use_kf_id))
|
.or_else(|| gpu_brush.raster_layer_cache.get(&use_kf_id))
|
||||||
.map(|c| (c, c.width, c.height))
|
.map(|c| (c, c.width, c.height, false))
|
||||||
} else if let Some((pkf, lw, lh)) = raster_proxy_blit {
|
} else if let Some((pkf, lw, lh)) = raster_proxy_blit {
|
||||||
gpu_brush.get_proxy_texture(&pkf).map(|c| (c, lw, lh))
|
gpu_brush.get_proxy_texture(&pkf).map(|c| (c, lw, lh, true))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if let Some((canvas, logical_w, logical_h)) = blit {
|
if let Some((canvas, logical_w, logical_h, is_proxy)) = blit {
|
||||||
let bt = crate::gpu_brush::BlitTransform::new(
|
let bt = crate::gpu_brush::BlitTransform::new(
|
||||||
*layer_transform,
|
*layer_transform,
|
||||||
logical_w, logical_h,
|
logical_w, logical_h,
|
||||||
width, height,
|
width, height,
|
||||||
);
|
);
|
||||||
|
// Proxies are upscaled, so sample them bilinearly;
|
||||||
|
// the real canvas stays nearest (crisp pixels).
|
||||||
|
if is_proxy {
|
||||||
|
shared.canvas_blit.blit_smooth(
|
||||||
|
device, queue, canvas.src_view(), hdr_layer_view, &bt, None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
shared.canvas_blit.blit(
|
shared.canvas_blit.blit(
|
||||||
device, queue, canvas.src_view(), hdr_layer_view, &bt, None,
|
device, queue, canvas.src_view(), hdr_layer_view, &bt, None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let compositor_layer = lightningbeam_core::gpu::CompositorLayer::new(
|
let compositor_layer = lightningbeam_core::gpu::CompositorLayer::new(
|
||||||
hdr_layer_handle,
|
hdr_layer_handle,
|
||||||
rendered_layer.opacity,
|
rendered_layer.opacity,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue