Filter API + Blur (#40)
Co-authored-by: Newspicel <newspicel+claude@pm.me>
This commit is contained in:
co-authored by
Newspicel
parent
cfec5ff014
commit
69d467e4d1
@@ -23,11 +23,29 @@ use crate::directx_renderer::shader_resources::{RawShaderBytes, ShaderModule, Sh
|
||||
use crate::*;
|
||||
use gpui::*;
|
||||
|
||||
/// The largest blur radius in a scene-space filter chain, in device pixels — used to size the
|
||||
/// blur kernel and the dilated region the blur passes are scissored to.
|
||||
///
|
||||
/// The `match` is exhaustive on purpose: adding a [`ScaledFilter`] variant breaks it here,
|
||||
/// forcing this backend to handle (or deliberately ignore) the new filter rather than silently
|
||||
/// dropping it.
|
||||
fn max_blur_radius(filters: &[ScaledFilter]) -> f32 {
|
||||
filters.iter().fold(0.0, |acc, filter| match filter {
|
||||
ScaledFilter::Blur(radius) => acc.max(radius.0),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) const DISABLE_DIRECT_COMPOSITION: &str = "GPUI_DISABLE_DIRECT_COMPOSITION";
|
||||
const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
// This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11.
|
||||
const PATH_MULTISAMPLE_COUNT: u32 = 4;
|
||||
|
||||
/// Number of content-filter (`filter`) nesting levels that get their own isolated group target.
|
||||
/// Two covers the realistic "a blurred element inside another blurred element" case; deeper nests
|
||||
/// render inline (unblurred at the inner level) rather than allocating unbounded VRAM. Must match
|
||||
/// the wgpu backend's `MAX_FILTER_DEPTH` so nested blur renders consistently across platforms.
|
||||
const MAX_FILTER_DEPTH: usize = 2;
|
||||
|
||||
pub(crate) struct FontInfo {
|
||||
pub gamma_ratios: [f32; 4],
|
||||
pub grayscale_enhanced_contrast: f32,
|
||||
@@ -53,6 +71,12 @@ pub(crate) struct DirectXRenderer {
|
||||
/// In that case we want to discard the first frame that we draw as we got reset in the middle of a frame
|
||||
/// meaning we lost all the allocated gpu textures and scene resources.
|
||||
skip_draws: bool,
|
||||
|
||||
/// The render target currently bound for the main scene this frame (the offscreen
|
||||
/// `scene_color` when blur filters are present, a content-filter group texture inside such a
|
||||
/// group, or the swapchain otherwise). `draw_paths_to_intermediate` restores to this after
|
||||
/// its own pass so paths land on the correct target.
|
||||
active_render_target: Option<ID3D11RenderTargetView>,
|
||||
}
|
||||
|
||||
/// Direct3D objects
|
||||
@@ -77,10 +101,68 @@ struct DirectXResources {
|
||||
path_intermediate_msaa_texture: ID3D11Texture2D,
|
||||
path_intermediate_msaa_view: Option<ID3D11RenderTargetView>,
|
||||
|
||||
// Offscreen targets for blur filters (each is render-target + shader-resource).
|
||||
blur: BlurResources,
|
||||
|
||||
// Cached viewport
|
||||
viewport: D3D11_VIEWPORT,
|
||||
}
|
||||
|
||||
/// Offscreen render targets used by the blur filters. The scene is rendered into `scene_color`
|
||||
/// (so filters can sample it), `ping`/`pong` are half-resolution scratch for the separable
|
||||
/// gaussian, and `groups` isolate content-filter (`filter`) subtrees — one per nesting level
|
||||
/// (indexed by isolation depth), up to [`MAX_FILTER_DEPTH`], so nested content blurs isolate
|
||||
/// correctly; deeper nests render inline.
|
||||
struct BlurResources {
|
||||
scene_color: ID3D11Texture2D,
|
||||
scene_color_rtv: Option<ID3D11RenderTargetView>,
|
||||
scene_color_srv: Option<ID3D11ShaderResourceView>,
|
||||
ping: ID3D11Texture2D,
|
||||
ping_rtv: Option<ID3D11RenderTargetView>,
|
||||
ping_srv: Option<ID3D11ShaderResourceView>,
|
||||
pong: ID3D11Texture2D,
|
||||
pong_rtv: Option<ID3D11RenderTargetView>,
|
||||
pong_srv: Option<ID3D11ShaderResourceView>,
|
||||
// Kept alive for the lifetime of their views; indexed by isolation depth.
|
||||
groups: Vec<ID3D11Texture2D>,
|
||||
group_rtvs: Vec<Option<ID3D11RenderTargetView>>,
|
||||
group_srvs: Vec<Option<ID3D11ShaderResourceView>>,
|
||||
}
|
||||
|
||||
impl BlurResources {
|
||||
fn new(device: &ID3D11Device, width: u32, height: u32) -> Result<Self> {
|
||||
let half_w = (width / 2).max(1);
|
||||
let half_h = (height / 2).max(1);
|
||||
let (scene_color, scene_color_rtv, scene_color_srv) =
|
||||
create_color_target(device, width, height)?;
|
||||
let (ping, ping_rtv, ping_srv) = create_color_target(device, half_w, half_h)?;
|
||||
let (pong, pong_rtv, pong_srv) = create_color_target(device, half_w, half_h)?;
|
||||
let mut groups = Vec::with_capacity(MAX_FILTER_DEPTH);
|
||||
let mut group_rtvs = Vec::with_capacity(MAX_FILTER_DEPTH);
|
||||
let mut group_srvs = Vec::with_capacity(MAX_FILTER_DEPTH);
|
||||
for _ in 0..MAX_FILTER_DEPTH {
|
||||
let (group, group_rtv, group_srv) = create_color_target(device, width, height)?;
|
||||
groups.push(group);
|
||||
group_rtvs.push(group_rtv);
|
||||
group_srvs.push(group_srv);
|
||||
}
|
||||
Ok(Self {
|
||||
scene_color,
|
||||
scene_color_rtv,
|
||||
scene_color_srv,
|
||||
ping,
|
||||
ping_rtv,
|
||||
ping_srv,
|
||||
pong,
|
||||
pong_rtv,
|
||||
pong_srv,
|
||||
groups,
|
||||
group_rtvs,
|
||||
group_srvs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DirectXRenderPipelines {
|
||||
shadow_pipeline: PipelineState<Shadow>,
|
||||
quad_pipeline: PipelineState<Quad>,
|
||||
@@ -90,6 +172,18 @@ struct DirectXRenderPipelines {
|
||||
mono_sprites: PipelineState<MonochromeSprite>,
|
||||
subpixel_sprites: PipelineState<SubpixelSprite>,
|
||||
poly_sprites: PipelineState<PolychromeSprite>,
|
||||
// Blur (backdrop-filter / filter). These don't use the generic PipelineState since they
|
||||
// sample a texture rather than read a structured instance buffer; their parameters live in
|
||||
// a dedicated constant buffer at register b1.
|
||||
blur_downsample_vertex: ID3D11VertexShader,
|
||||
blur_downsample_fragment: ID3D11PixelShader,
|
||||
blur_vertex: ID3D11VertexShader,
|
||||
blur_fragment: ID3D11PixelShader,
|
||||
blur_composite_vertex: ID3D11VertexShader,
|
||||
blur_composite_fragment: ID3D11PixelShader,
|
||||
blur_params_buffer: ID3D11Buffer,
|
||||
blur_blend_replace: ID3D11BlendState,
|
||||
blur_blend_composite: ID3D11BlendState,
|
||||
}
|
||||
|
||||
struct DirectXGlobalElements {
|
||||
@@ -174,6 +268,7 @@ impl DirectXRenderer {
|
||||
width: 1,
|
||||
height: 1,
|
||||
skip_draws: false,
|
||||
active_render_target: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,6 +413,56 @@ impl DirectXRenderer {
|
||||
|
||||
self.upload_scene_buffers(scene)?;
|
||||
|
||||
// Only route through the offscreen scene texture when the scene contains blur filters;
|
||||
// otherwise render straight to the swapchain exactly as before.
|
||||
let use_offscreen =
|
||||
!scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty();
|
||||
|
||||
// Clone the views we need (AddRef) so the loop can rebind render targets without holding a
|
||||
// borrow of `self` across the `&mut self` draw_* calls.
|
||||
let (scene_rtv, scene_srv, group_rtvs, group_srvs, swapchain_rtv) = {
|
||||
let r = self.resources.as_ref().context("resources missing")?;
|
||||
(
|
||||
r.blur.scene_color_rtv.clone(),
|
||||
r.blur.scene_color_srv.clone(),
|
||||
r.blur.group_rtvs.clone(),
|
||||
r.blur.group_srvs.clone(),
|
||||
r.render_target_view.clone(),
|
||||
)
|
||||
};
|
||||
let ctx = self
|
||||
.devices
|
||||
.as_ref()
|
||||
.context("devices missing")?
|
||||
.device_context
|
||||
.clone();
|
||||
|
||||
if use_offscreen {
|
||||
unsafe {
|
||||
if let Some(rtv) = scene_rtv.as_ref() {
|
||||
ctx.ClearRenderTargetView(rtv, &[0.0; 4]);
|
||||
}
|
||||
ctx.OMSetRenderTargets(Some(slice::from_ref(&scene_rtv)), None);
|
||||
}
|
||||
self.active_render_target = scene_rtv.clone();
|
||||
} else {
|
||||
self.active_render_target = swapchain_rtv.clone();
|
||||
}
|
||||
|
||||
// Current target for the main scene + a parent stack for content-filter groups.
|
||||
let mut current_rtv = self.active_render_target.clone();
|
||||
let mut current_srv = if use_offscreen {
|
||||
scene_srv.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// (parent_rtv, parent_srv, isolated)
|
||||
let mut filter_stack: Vec<(
|
||||
Option<ID3D11RenderTargetView>,
|
||||
Option<ID3D11ShaderResourceView>,
|
||||
bool,
|
||||
)> = Vec::new();
|
||||
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
PrimitiveBatch::Shadows(range) => self.draw_shadows(range.start, range.len()),
|
||||
@@ -338,6 +483,77 @@ impl DirectXRenderer {
|
||||
self.draw_polychrome_sprites(texture_id, range.start, range.len())
|
||||
}
|
||||
PrimitiveBatch::Surfaces(range) => self.draw_surfaces(&scene.surfaces[range]),
|
||||
PrimitiveBatch::BackdropFilters(range) => {
|
||||
let result = (|| {
|
||||
for filter in &scene.backdrop_filters[range] {
|
||||
self.dx_blur_and_composite(
|
||||
¤t_srv,
|
||||
¤t_rtv,
|
||||
filter.bounds,
|
||||
filter.content_mask.bounds,
|
||||
corner_radii_array(filter.corner_radii),
|
||||
max_blur_radius(&filter.filters),
|
||||
filter.opacity,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})();
|
||||
// Restore the current target for subsequent batches.
|
||||
unsafe {
|
||||
ctx.OMSetRenderTargets(Some(slice::from_ref(¤t_rtv)), None);
|
||||
}
|
||||
result
|
||||
}
|
||||
PrimitiveBatch::FilterBoundary(ix) => {
|
||||
let boundary = scene.filter_boundaries[ix].clone();
|
||||
if boundary.is_start {
|
||||
// Each isolated nesting level uses its own group target from the pool
|
||||
// (indexed by current isolation depth). Beyond the pool size
|
||||
// (MAX_FILTER_DEPTH) deeper filters render inline without isolation rather
|
||||
// than corrupting an outer group.
|
||||
let depth = filter_stack.iter().filter(|entry| entry.2).count();
|
||||
if depth < group_rtvs.len() {
|
||||
filter_stack.push((current_rtv.clone(), current_srv.clone(), true));
|
||||
current_rtv = group_rtvs[depth].clone();
|
||||
current_srv = group_srvs[depth].clone();
|
||||
self.active_render_target = current_rtv.clone();
|
||||
unsafe {
|
||||
if let Some(rtv) = current_rtv.as_ref() {
|
||||
ctx.ClearRenderTargetView(rtv, &[0.0; 4]);
|
||||
}
|
||||
ctx.OMSetRenderTargets(Some(slice::from_ref(¤t_rtv)), None);
|
||||
}
|
||||
} else {
|
||||
filter_stack.push((current_rtv.clone(), current_srv.clone(), false));
|
||||
}
|
||||
Ok(())
|
||||
} else if let Some((parent_rtv, parent_srv, isolated)) = filter_stack.pop() {
|
||||
let result = if isolated {
|
||||
self.dx_blur_and_composite(
|
||||
¤t_srv,
|
||||
&parent_rtv,
|
||||
boundary.bounds,
|
||||
boundary.content_mask.bounds,
|
||||
corner_radii_array(boundary.corner_radii),
|
||||
max_blur_radius(&boundary.filters),
|
||||
boundary.opacity,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
current_rtv = parent_rtv;
|
||||
current_srv = parent_srv;
|
||||
self.active_render_target = current_rtv.clone();
|
||||
unsafe {
|
||||
ctx.OMSetRenderTargets(Some(slice::from_ref(¤t_rtv)), None);
|
||||
}
|
||||
result
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
.context(format!(
|
||||
"scene too large:\
|
||||
@@ -352,6 +568,12 @@ impl DirectXRenderer {
|
||||
scene.surfaces.len(),
|
||||
))?;
|
||||
}
|
||||
|
||||
// Present the offscreen scene by blitting it into the swapchain.
|
||||
if use_offscreen {
|
||||
self.dx_blit(&scene_srv, &swapchain_rtv)?;
|
||||
}
|
||||
self.active_render_target = None;
|
||||
self.present()
|
||||
}
|
||||
|
||||
@@ -553,10 +775,16 @@ impl DirectXRenderer {
|
||||
0,
|
||||
RENDER_TARGET_FORMAT,
|
||||
);
|
||||
// Restore main render target
|
||||
// Restore the active render target (the offscreen scene/group target when blurring,
|
||||
// otherwise the swapchain) so the path sprites land on the correct surface.
|
||||
let restore_target = if self.active_render_target.is_some() {
|
||||
&self.active_render_target
|
||||
} else {
|
||||
&resources.render_target_view
|
||||
};
|
||||
devices
|
||||
.device_context
|
||||
.OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
|
||||
.OMSetRenderTargets(Some(slice::from_ref(restore_target)), None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -704,6 +932,208 @@ impl DirectXRenderer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a single blur pass: a full-screen (or composite) draw sampling `source_srv` into
|
||||
/// `target_rtv`, with `params` in the blur constant buffer (b1).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dx_blur_pass(
|
||||
&self,
|
||||
vertex: &ID3D11VertexShader,
|
||||
fragment: &ID3D11PixelShader,
|
||||
blend: &ID3D11BlendState,
|
||||
target_rtv: &Option<ID3D11RenderTargetView>,
|
||||
source_srv: &Option<ID3D11ShaderResourceView>,
|
||||
params: BlurParams,
|
||||
viewport: &D3D11_VIEWPORT,
|
||||
topology: D3D_PRIMITIVE_TOPOLOGY,
|
||||
vertex_count: u32,
|
||||
clear: bool,
|
||||
) -> Result<()> {
|
||||
let devices = self.devices.as_ref().context("devices missing")?;
|
||||
let ctx = &devices.device_context;
|
||||
update_buffer(ctx, &self.pipelines.blur_params_buffer, &[params])?;
|
||||
let null_srv: [Option<ID3D11ShaderResourceView>; 1] = [None];
|
||||
let blur_params = [Some(self.pipelines.blur_params_buffer.clone())];
|
||||
unsafe {
|
||||
// Unbind any SRV at slot 0 so the target texture isn't simultaneously bound as input.
|
||||
ctx.PSSetShaderResources(0, Some(&null_srv));
|
||||
if clear {
|
||||
ctx.ClearRenderTargetView(
|
||||
target_rtv.as_ref().context("blur target view missing")?,
|
||||
&[0.0; 4],
|
||||
);
|
||||
}
|
||||
ctx.OMSetRenderTargets(Some(slice::from_ref(target_rtv)), None);
|
||||
ctx.RSSetViewports(Some(slice::from_ref(viewport)));
|
||||
ctx.IASetPrimitiveTopology(topology);
|
||||
ctx.VSSetShader(vertex, None);
|
||||
ctx.PSSetShader(fragment, None);
|
||||
ctx.VSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer)));
|
||||
ctx.PSSetConstantBuffers(0, Some(slice::from_ref(&self.globals.global_params_buffer)));
|
||||
ctx.VSSetConstantBuffers(1, Some(&blur_params));
|
||||
ctx.PSSetConstantBuffers(1, Some(&blur_params));
|
||||
ctx.PSSetSamplers(0, Some(slice::from_ref(&self.globals.sampler)));
|
||||
ctx.PSSetShaderResources(0, Some(slice::from_ref(source_srv)));
|
||||
ctx.OMSetBlendState(blend, None, 0xFFFFFFFF);
|
||||
ctx.DrawInstanced(vertex_count, 1, 0, 0);
|
||||
// Unbind the source so the target can be rebound as a render target next.
|
||||
ctx.PSSetShaderResources(0, Some(&null_srv));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Blur `source_srv` (full-resolution) using the half-res ping/pong textures and composite the
|
||||
/// result into `target_rtv`, clipped to `bounds`/`corner_radii`/`content_mask` and modulated
|
||||
/// by `opacity`. Shared by the backdrop and content-filter paths.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dx_blur_and_composite(
|
||||
&self,
|
||||
source_srv: &Option<ID3D11ShaderResourceView>,
|
||||
target_rtv: &Option<ID3D11RenderTargetView>,
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
content_mask: Bounds<ScaledPixels>,
|
||||
corner_radii: [f32; 4],
|
||||
blur_radius: f32,
|
||||
opacity: f32,
|
||||
// Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds.
|
||||
clip_rounded: bool,
|
||||
) -> Result<()> {
|
||||
// Sigma is halved because the blur runs at half resolution.
|
||||
let sigma = (blur_radius * 0.5).max(0.0);
|
||||
if sigma <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
// Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather
|
||||
// than truncating the kernel — keeps very large radii from clipping. Matches wgpu.
|
||||
let ideal_taps = (3.0 * sigma).ceil();
|
||||
let tap_count = ideal_taps.clamp(1.0, 32.0);
|
||||
let tap_step = (ideal_taps / tap_count).max(1.0);
|
||||
// Content blur bleeds ~3·radius past the box, so its composite quad covers a dilated rect.
|
||||
let composite_bounds = if clip_rounded {
|
||||
bounds
|
||||
} else {
|
||||
bounds.dilate(ScaledPixels(3.0 * blur_radius))
|
||||
};
|
||||
let half_w = (self.width / 2).max(1);
|
||||
let half_h = (self.height / 2).max(1);
|
||||
let half_vp = D3D11_VIEWPORT {
|
||||
TopLeftX: 0.0,
|
||||
TopLeftY: 0.0,
|
||||
Width: half_w as f32,
|
||||
Height: half_h as f32,
|
||||
MinDepth: 0.0,
|
||||
MaxDepth: 1.0,
|
||||
};
|
||||
let (full_vp, ping_rtv, ping_srv, pong_rtv, pong_srv) = {
|
||||
let r = self.resources.as_ref().context("resources missing")?;
|
||||
(
|
||||
r.viewport,
|
||||
r.blur.ping_rtv.clone(),
|
||||
r.blur.ping_srv.clone(),
|
||||
r.blur.pong_rtv.clone(),
|
||||
r.blur.pong_srv.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// Downsample source -> ping, then separable gaussian ping -> pong -> ping.
|
||||
self.dx_blur_pass(
|
||||
&self.pipelines.blur_downsample_vertex,
|
||||
&self.pipelines.blur_downsample_fragment,
|
||||
&self.pipelines.blur_blend_replace,
|
||||
&ping_rtv,
|
||||
source_srv,
|
||||
BlurParams {
|
||||
downsample: 1.0,
|
||||
..Default::default()
|
||||
},
|
||||
&half_vp,
|
||||
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
||||
3,
|
||||
true,
|
||||
)?;
|
||||
self.dx_blur_pass(
|
||||
&self.pipelines.blur_vertex,
|
||||
&self.pipelines.blur_fragment,
|
||||
&self.pipelines.blur_blend_replace,
|
||||
&pong_rtv,
|
||||
&ping_srv,
|
||||
BlurParams {
|
||||
direction: [1.0 / half_w as f32, 0.0],
|
||||
sigma,
|
||||
tap_count,
|
||||
tap_step,
|
||||
..Default::default()
|
||||
},
|
||||
&half_vp,
|
||||
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
||||
3,
|
||||
true,
|
||||
)?;
|
||||
self.dx_blur_pass(
|
||||
&self.pipelines.blur_vertex,
|
||||
&self.pipelines.blur_fragment,
|
||||
&self.pipelines.blur_blend_replace,
|
||||
&ping_rtv,
|
||||
&pong_srv,
|
||||
BlurParams {
|
||||
direction: [0.0, 1.0 / half_h as f32],
|
||||
sigma,
|
||||
tap_count,
|
||||
tap_step,
|
||||
..Default::default()
|
||||
},
|
||||
&half_vp,
|
||||
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
||||
3,
|
||||
true,
|
||||
)?;
|
||||
// Composite the blurred result into the target (preserving its contents).
|
||||
self.dx_blur_pass(
|
||||
&self.pipelines.blur_composite_vertex,
|
||||
&self.pipelines.blur_composite_fragment,
|
||||
&self.pipelines.blur_blend_composite,
|
||||
target_rtv,
|
||||
&ping_srv,
|
||||
BlurParams {
|
||||
bounds: composite_bounds,
|
||||
content_mask,
|
||||
corner_radii,
|
||||
opacity,
|
||||
clip_rounded: if clip_rounded { 1.0 } else { 0.0 },
|
||||
..Default::default()
|
||||
},
|
||||
&full_vp,
|
||||
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,
|
||||
4,
|
||||
false,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy the offscreen scene texture into the swapchain render target.
|
||||
fn dx_blit(
|
||||
&self,
|
||||
source_srv: &Option<ID3D11ShaderResourceView>,
|
||||
target_rtv: &Option<ID3D11RenderTargetView>,
|
||||
) -> Result<()> {
|
||||
let full_vp = self
|
||||
.resources
|
||||
.as_ref()
|
||||
.context("resources missing")?
|
||||
.viewport;
|
||||
self.dx_blur_pass(
|
||||
&self.pipelines.blur_downsample_vertex,
|
||||
&self.pipelines.blur_downsample_fragment,
|
||||
&self.pipelines.blur_blend_replace,
|
||||
target_rtv,
|
||||
source_srv,
|
||||
BlurParams::default(),
|
||||
&full_vp,
|
||||
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
|
||||
3,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn gpu_specs(&self) -> Result<GpuSpecs> {
|
||||
let devices = self.devices.as_ref().context("devices missing")?;
|
||||
let desc = unsafe { devices.adapter.GetDesc1() }?;
|
||||
@@ -783,6 +1213,7 @@ impl DirectXResources {
|
||||
viewport,
|
||||
) = create_resources(devices, &swap_chain, width, height)?;
|
||||
set_rasterizer_state(&devices.device, &devices.device_context)?;
|
||||
let blur = BlurResources::new(&devices.device, width, height)?;
|
||||
|
||||
Ok(Self {
|
||||
swap_chain,
|
||||
@@ -792,6 +1223,7 @@ impl DirectXResources {
|
||||
path_intermediate_msaa_texture,
|
||||
path_intermediate_msaa_view,
|
||||
path_intermediate_srv,
|
||||
blur,
|
||||
viewport,
|
||||
})
|
||||
}
|
||||
@@ -818,6 +1250,7 @@ impl DirectXResources {
|
||||
self.path_intermediate_msaa_texture = path_intermediate_msaa_texture;
|
||||
self.path_intermediate_msaa_view = path_intermediate_msaa_view;
|
||||
self.path_intermediate_srv = path_intermediate_srv;
|
||||
self.blur = BlurResources::new(&devices.device, width, height)?;
|
||||
self.viewport = viewport;
|
||||
Ok(())
|
||||
}
|
||||
@@ -882,6 +1315,36 @@ impl DirectXRenderPipelines {
|
||||
create_blend_state(device)?,
|
||||
)?;
|
||||
|
||||
let blur_downsample_vertex = create_vertex_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Vertex)?.as_bytes(),
|
||||
)?;
|
||||
let blur_downsample_fragment = create_fragment_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::BlurDownsample, ShaderTarget::Fragment)?.as_bytes(),
|
||||
)?;
|
||||
let blur_vertex = create_vertex_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Vertex)?.as_bytes(),
|
||||
)?;
|
||||
let blur_fragment = create_fragment_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::Blur, ShaderTarget::Fragment)?.as_bytes(),
|
||||
)?;
|
||||
let blur_composite_vertex = create_vertex_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Vertex)?.as_bytes(),
|
||||
)?;
|
||||
let blur_composite_fragment = create_fragment_shader(
|
||||
device,
|
||||
RawShaderBytes::new(ShaderModule::BlurComposite, ShaderTarget::Fragment)?.as_bytes(),
|
||||
)?;
|
||||
let blur_params_buffer = create_constant_buffer(device, std::mem::size_of::<BlurParams>())?;
|
||||
let blur_blend_replace = create_blend_state_no_blend(device)?;
|
||||
// Premultiplied (One / InvSrcAlpha) — the composite outputs a premultiplied blurred sample;
|
||||
// straight-alpha blending would darken the faded edges.
|
||||
let blur_blend_composite = create_blend_state_for_path_sprite(device)?;
|
||||
|
||||
Ok(Self {
|
||||
shadow_pipeline,
|
||||
quad_pipeline,
|
||||
@@ -891,6 +1354,15 @@ impl DirectXRenderPipelines {
|
||||
mono_sprites,
|
||||
subpixel_sprites,
|
||||
poly_sprites,
|
||||
blur_downsample_vertex,
|
||||
blur_downsample_fragment,
|
||||
blur_vertex,
|
||||
blur_fragment,
|
||||
blur_composite_vertex,
|
||||
blur_composite_fragment,
|
||||
blur_params_buffer,
|
||||
blur_blend_replace,
|
||||
blur_blend_composite,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -969,6 +1441,47 @@ struct GlobalParams {
|
||||
_pad: [u32; 3],
|
||||
}
|
||||
|
||||
/// Mirrors the `BlurParams` cbuffer (register b1) in `shaders.hlsl`. 80 bytes (a multiple of 16,
|
||||
/// as constant buffers require). Updated per blur pass via `update_buffer`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct BlurParams {
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
content_mask: Bounds<ScaledPixels>,
|
||||
corner_radii: [f32; 4],
|
||||
direction: [f32; 2],
|
||||
sigma: f32,
|
||||
opacity: f32,
|
||||
tap_count: f32,
|
||||
/// 1.0 clips the composite to the rounded rect (backdrop); 0.0 lets content blur bleed past
|
||||
/// its bounds like CSS `filter: blur`.
|
||||
clip_rounded: f32,
|
||||
/// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the
|
||||
/// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy
|
||||
/// (the scene blit, which must not downsample). Downsample pass only.
|
||||
downsample: f32,
|
||||
/// Spacing between taps in pixels (gaussian passes only); >1 lets `tap_count` taps span very
|
||||
/// large radii without truncating the gaussian, matching the wgpu backend.
|
||||
tap_step: f32,
|
||||
}
|
||||
|
||||
impl Default for BlurParams {
|
||||
fn default() -> Self {
|
||||
BlurParams {
|
||||
bounds: Bounds::default(),
|
||||
content_mask: Bounds::default(),
|
||||
corner_radii: [0.0; 4],
|
||||
direction: [0.0, 0.0],
|
||||
sigma: 0.0,
|
||||
opacity: 1.0,
|
||||
tap_count: 0.0,
|
||||
clip_rounded: 0.0,
|
||||
downsample: 0.0,
|
||||
tap_step: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PipelineState<T> {
|
||||
label: &'static str,
|
||||
vertex: ID3D11VertexShader,
|
||||
@@ -1267,6 +1780,16 @@ fn create_resources(
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Flatten a `Corners` into the `[tl, tr, br, bl]` order expected by the blur composite shader.
|
||||
fn corner_radii_array(corners: Corners<ScaledPixels>) -> [f32; 4] {
|
||||
[
|
||||
corners.top_left.0,
|
||||
corners.top_right.0,
|
||||
corners.bottom_right.0,
|
||||
corners.bottom_left.0,
|
||||
]
|
||||
}
|
||||
|
||||
fn create_render_target_and_its_view(
|
||||
swap_chain: &IDXGISwapChain1,
|
||||
device: &ID3D11Device,
|
||||
@@ -1310,6 +1833,45 @@ fn create_path_intermediate_texture(
|
||||
Ok((texture, Some(shader_resource_view.unwrap())))
|
||||
}
|
||||
|
||||
/// Create a color texture usable as both a render target and a shader resource, returning both
|
||||
/// views. Used for the blur offscreen targets.
|
||||
#[inline]
|
||||
fn create_color_target(
|
||||
device: &ID3D11Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(
|
||||
ID3D11Texture2D,
|
||||
Option<ID3D11RenderTargetView>,
|
||||
Option<ID3D11ShaderResourceView>,
|
||||
)> {
|
||||
let texture = unsafe {
|
||||
let mut output = None;
|
||||
let desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: width.max(1),
|
||||
Height: height.max(1),
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: RENDER_TARGET_FORMAT,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_DEFAULT,
|
||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||
CPUAccessFlags: 0,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
device.CreateTexture2D(&desc, None, Some(&mut output))?;
|
||||
output.unwrap()
|
||||
};
|
||||
let mut rtv = None;
|
||||
unsafe { device.CreateRenderTargetView(&texture, None, Some(&mut rtv))? };
|
||||
let mut srv = None;
|
||||
unsafe { device.CreateShaderResourceView(&texture, None, Some(&mut srv))? };
|
||||
Ok((texture, rtv, srv))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_path_intermediate_msaa_texture_and_view(
|
||||
device: &ID3D11Device,
|
||||
@@ -1458,6 +2020,35 @@ fn create_blend_state_for_path_sprite(device: &ID3D11Device) -> Result<ID3D11Ble
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a CPU-writable dynamic constant buffer of the given byte size (rounded up to 16).
|
||||
#[inline]
|
||||
fn create_constant_buffer(device: &ID3D11Device, byte_size: usize) -> Result<ID3D11Buffer> {
|
||||
let desc = D3D11_BUFFER_DESC {
|
||||
ByteWidth: byte_size.next_multiple_of(16) as u32,
|
||||
Usage: D3D11_USAGE_DYNAMIC,
|
||||
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mut buffer = None;
|
||||
unsafe { device.CreateBuffer(&desc, None, Some(&mut buffer)) }?;
|
||||
Ok(buffer.unwrap())
|
||||
}
|
||||
|
||||
/// A blend state that overwrites the target (no blending) — used for the blur downsample and
|
||||
/// gaussian passes.
|
||||
#[inline]
|
||||
fn create_blend_state_no_blend(device: &ID3D11Device) -> Result<ID3D11BlendState> {
|
||||
let mut desc = D3D11_BLEND_DESC::default();
|
||||
desc.RenderTarget[0].BlendEnable = false.into();
|
||||
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8;
|
||||
unsafe {
|
||||
let mut state = None;
|
||||
device.CreateBlendState(&desc, Some(&mut state))?;
|
||||
Ok(state.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_vertex_shader(device: &ID3D11Device, bytes: &[u8]) -> Result<ID3D11VertexShader> {
|
||||
unsafe {
|
||||
@@ -1604,6 +2195,9 @@ pub(crate) mod shader_resources {
|
||||
SubpixelSprite,
|
||||
PolychromeSprite,
|
||||
EmojiRasterization,
|
||||
BlurDownsample,
|
||||
Blur,
|
||||
BlurComposite,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
@@ -1681,6 +2275,18 @@ pub(crate) mod shader_resources {
|
||||
ShaderTarget::Vertex => EMOJI_RASTERIZATION_VERTEX_BYTES,
|
||||
ShaderTarget::Fragment => EMOJI_RASTERIZATION_FRAGMENT_BYTES,
|
||||
},
|
||||
ShaderModule::BlurDownsample => match target {
|
||||
ShaderTarget::Vertex => BLUR_DOWNSAMPLE_VERTEX_BYTES,
|
||||
ShaderTarget::Fragment => BLUR_DOWNSAMPLE_FRAGMENT_BYTES,
|
||||
},
|
||||
ShaderModule::Blur => match target {
|
||||
ShaderTarget::Vertex => BLUR_VERTEX_BYTES,
|
||||
ShaderTarget::Fragment => BLUR_FRAGMENT_BYTES,
|
||||
},
|
||||
ShaderModule::BlurComposite => match target {
|
||||
ShaderTarget::Vertex => BLUR_COMPOSITE_VERTEX_BYTES,
|
||||
ShaderTarget::Fragment => BLUR_COMPOSITE_FRAGMENT_BYTES,
|
||||
},
|
||||
};
|
||||
Self { inner: bytes }
|
||||
}
|
||||
@@ -1768,6 +2374,9 @@ pub(crate) mod shader_resources {
|
||||
ShaderModule::SubpixelSprite => "subpixel_sprite",
|
||||
ShaderModule::PolychromeSprite => "polychrome_sprite",
|
||||
ShaderModule::EmojiRasterization => "emoji_rasterization",
|
||||
ShaderModule::BlurDownsample => "blur_downsample",
|
||||
ShaderModule::Blur => "blur",
|
||||
ShaderModule::BlurComposite => "blur_composite",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,3 +1256,122 @@ float4 polychrome_sprite_fragment(PolychromeSpriteFragmentInput input): SV_Targe
|
||||
color.a *= sprite.opacity * saturate(0.5 - distance);
|
||||
return color;
|
||||
}
|
||||
|
||||
/*
|
||||
**
|
||||
** Blur (backdrop-filter / filter)
|
||||
**
|
||||
** Shared by backdrop and content blur. The source texture is bound at t0 (t_sprite) and the
|
||||
** parameters in the BlurParams constant buffer at b1. Three passes: downsample (full -> half
|
||||
** res), separable gaussian (run twice), and a composite into a rounded rectangle.
|
||||
*/
|
||||
|
||||
cbuffer BlurParams: register(b1) {
|
||||
Bounds blur_bounds;
|
||||
Bounds blur_content_mask;
|
||||
float4 blur_corner_radii;
|
||||
float2 blur_direction;
|
||||
float blur_sigma;
|
||||
float blur_opacity;
|
||||
float blur_tap_count;
|
||||
float blur_clip_rounded;
|
||||
// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the origin
|
||||
// so a stationary element blurs identically at every window size); 0.0 = 1:1 copy (scene blit).
|
||||
float blur_downsample;
|
||||
// Spacing between taps in pixels (gaussian passes only); >1 lets `blur_tap_count` taps span
|
||||
// very large radii without truncating the gaussian.
|
||||
float blur_tap_step;
|
||||
};
|
||||
|
||||
struct BlurVertexOutput {
|
||||
float4 position: SV_Position;
|
||||
float2 uv: TEXCOORD0;
|
||||
};
|
||||
|
||||
BlurVertexOutput blur_fullscreen(uint vertex_id) {
|
||||
float2 uv = float2(float((vertex_id << 1u) & 2u), float(vertex_id & 2u));
|
||||
BlurVertexOutput output;
|
||||
output.uv = uv;
|
||||
output.position = float4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
|
||||
return output;
|
||||
}
|
||||
|
||||
BlurVertexOutput blur_downsample_vertex(uint vertex_id: SV_VertexID) {
|
||||
return blur_fullscreen(vertex_id);
|
||||
}
|
||||
|
||||
float4 blur_downsample_fragment(BlurVertexOutput input): SV_Target {
|
||||
if (blur_downsample > 0.5) {
|
||||
// Snapped 2:1 box downsample. Half-res texel `px` samples source at full-res coordinate
|
||||
// 2*px + 1 (the boundary between source texels 2*px and 2*px+1), so one bilinear tap
|
||||
// averages exactly that pair. Anchored to the origin and independent of the viewport size,
|
||||
// so an element at fixed pixels blurs identically at every window size — otherwise the
|
||||
// implicit floor(W/2) grid stretches and the halo wobbles by ~1px on resize.
|
||||
uint sw, sh;
|
||||
t_sprite.GetDimensions(sw, sh);
|
||||
float2 src_uv = (floor(input.position.xy) * 2.0 + 1.0) / float2(sw, sh);
|
||||
return t_sprite.SampleLevel(s_sprite, src_uv, 0.0);
|
||||
}
|
||||
// 1:1 copy at matching resolution (used to blit the offscreen scene into the swapchain).
|
||||
return t_sprite.SampleLevel(s_sprite, input.uv, 0.0);
|
||||
}
|
||||
|
||||
BlurVertexOutput blur_vertex(uint vertex_id: SV_VertexID) {
|
||||
return blur_fullscreen(vertex_id);
|
||||
}
|
||||
|
||||
float4 blur_fragment(BlurVertexOutput input): SV_Target {
|
||||
int taps = int(blur_tap_count);
|
||||
float4 color = float4(0.0, 0.0, 0.0, 0.0);
|
||||
float weight_sum = 0.0;
|
||||
[loop]
|
||||
for (int i = -taps; i <= taps; i++) {
|
||||
float offset = float(i) * blur_tap_step;
|
||||
float weight = gaussian(offset, blur_sigma);
|
||||
color += t_sprite.SampleLevel(s_sprite, input.uv + blur_direction * offset, 0.0) * weight;
|
||||
weight_sum += weight;
|
||||
}
|
||||
return color / max(weight_sum, 1e-5);
|
||||
}
|
||||
|
||||
struct BlurCompositeVertexOutput {
|
||||
float4 position: SV_Position;
|
||||
float4 clip_distance: SV_ClipDistance;
|
||||
};
|
||||
|
||||
struct BlurCompositeFragmentInput {
|
||||
float4 position: SV_Position;
|
||||
};
|
||||
|
||||
BlurCompositeVertexOutput blur_composite_vertex(uint vertex_id: SV_VertexID) {
|
||||
float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
|
||||
BlurCompositeVertexOutput output;
|
||||
output.position = to_device_position(unit_vertex, blur_bounds);
|
||||
output.clip_distance = distance_from_clip_rect(unit_vertex, blur_bounds, blur_content_mask);
|
||||
return output;
|
||||
}
|
||||
|
||||
float4 blur_composite_fragment(BlurCompositeFragmentInput input): SV_Target {
|
||||
// Sample the half-res blur by screen position, on the SAME fixed 2:1 grid the snapped downsample
|
||||
// wrote (anchored at the origin, independent of viewport parity): 2 * the half-res texture size
|
||||
// maps screen pixel p to half-res texel p/2 at every window size, so it doesn't wobble on resize.
|
||||
uint hw, hh;
|
||||
t_sprite.GetDimensions(hw, hh);
|
||||
float2 uv = input.position.xy / (2.0 * float2(hw, hh));
|
||||
float4 blurred = t_sprite.SampleLevel(s_sprite, uv, 0.0);
|
||||
Corners radii;
|
||||
radii.top_left = blur_corner_radii.x;
|
||||
radii.top_right = blur_corner_radii.y;
|
||||
radii.bottom_right = blur_corner_radii.z;
|
||||
radii.bottom_left = blur_corner_radii.w;
|
||||
float distance = quad_sdf(input.position.xy, blur_bounds, radii);
|
||||
// Backdrop clips to the rounded rect (the panel has a defined shape); content blur bleeds past
|
||||
// its bounds like CSS `filter: blur`, so its shape comes from the blurred group's own alpha.
|
||||
float coverage = blur_clip_rounded > 0.5 ? saturate(0.5 - distance) : 1.0;
|
||||
// The blurred sample is premultiplied (blurring against the transparent surround scales rgb
|
||||
// with the fading alpha), so output premultiplied and use a premultiplied-blend state. A
|
||||
// backdrop's scene is opaque (so this replaces); a content-filter group is transparent outside
|
||||
// its subtree (so the target shows through there instead of darkening).
|
||||
float a = coverage * blur_opacity;
|
||||
return float4(blurred.rgb * a, blurred.a * a);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user