use std::sync::Arc;
pub use raw_window_handle;
pub use wgpu;
pub use winit;
use winit::window::Window;
#[derive(Debug)]
pub enum SurfaceError {
Lost,
Outdated,
OutOfMemory,
Timeout,
Other(String),
}
impl std::fmt::Display for SurfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Lost => write!(f, "surface lost"),
Self::Outdated => write!(f, "surface outdated"),
Self::OutOfMemory => write!(f, "surface out of memory"),
Self::Timeout => write!(f, "surface timeout"),
Self::Other(s) => write!(f, "surface error: {s}"),
}
}
}
impl std::error::Error for SurfaceError {}
#[derive(Debug)]
pub enum HalError {
NoAdapter,
RequestDevice(String),
CreateSurface(String),
}
impl std::fmt::Display for HalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoAdapter => write!(f, "no GPU adapter available"),
Self::RequestDevice(s) => write!(f, "request_device failed: {s}"),
Self::CreateSurface(s) => write!(f, "create_surface failed: {s}"),
}
}
}
impl std::error::Error for HalError {}
pub trait Surface {
fn size(&self) -> (u32, u32);
fn resize(&mut self, width: u32, height: u32);
fn acquire(&mut self) -> Result<Frame, SurfaceError>;
fn present(&mut self, frame: Frame, hal: &Hal);
}
pub struct Frame {
surface_texture: wgpu::SurfaceTexture,
surface_view: wgpu::TextureView,
intermediate_view: wgpu::TextureView,
overlay_view: wgpu::TextureView,
width: u32,
height: u32,
}
impl Frame {
pub fn view(&self) -> &wgpu::TextureView {
&self.intermediate_view
}
pub fn overlay_view(&self) -> &wgpu::TextureView {
&self.overlay_view
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
}
pub struct Hal {
pub instance: wgpu::Instance,
pub adapter: wgpu::Adapter,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
}
impl Hal {
pub async fn new(
compatible_surface: Option<&wgpu::Surface<'static>>,
) -> Result<Self, HalError> {
let opts = wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface,
};
let primary = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let (instance, adapter) = match primary.request_adapter(&opts).await {
Ok(a) => (primary, a),
Err(_) => {
let all = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let a = all
.request_adapter(&opts)
.await
.map_err(|_| HalError::NoAdapter)?;
(all, a)
}
};
let limits = wgpu::Limits::default().using_resolution(adapter.limits());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("llimphi-hal-device"),
required_features: wgpu::Features::empty(),
required_limits: limits,
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::default(),
trace: wgpu::Trace::Off,
})
.await
.map_err(|e| HalError::RequestDevice(e.to_string()))?;
Ok(Self {
instance,
adapter,
device,
queue,
})
}
pub async unsafe fn new_for_raw_surface(
make_target: impl Fn() -> wgpu::SurfaceTargetUnsafe,
width: u32,
height: u32,
) -> Result<(Self, RawSurface), HalError> {
let primary = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let prim_surface = unsafe { primary.create_surface_unsafe(make_target()) }
.map_err(|e| HalError::CreateSurface(e.to_string()))?;
let prim_adapter = primary
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: Some(&prim_surface),
})
.await;
let (instance, adapter, wgpu_surface) = match prim_adapter {
Ok(a) => (primary, a, prim_surface),
Err(_) => {
let all = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let surface = unsafe { all.create_surface_unsafe(make_target()) }
.map_err(|e| HalError::CreateSurface(e.to_string()))?;
let a = all
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: Some(&surface),
})
.await
.map_err(|_| HalError::NoAdapter)?;
(all, a, surface)
}
};
let limits = wgpu::Limits::default().using_resolution(adapter.limits());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("llimphi-hal-device"),
required_features: wgpu::Features::empty(),
required_limits: limits,
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::default(),
trace: wgpu::Trace::Off,
})
.await
.map_err(|e| HalError::RequestDevice(e.to_string()))?;
let hal = Self {
instance,
adapter,
device,
queue,
};
let surface = RawSurface::from_surface(&hal, wgpu_surface, width, height)?;
Ok((hal, surface))
}
}
pub struct WinitSurface {
_window: Arc<Window>,
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
device: wgpu::Device,
intermediate: wgpu::Texture,
intermediate_view: wgpu::TextureView,
overlay: wgpu::Texture,
overlay_view: wgpu::TextureView,
blitter: wgpu::util::TextureBlitter,
}
const INTERMEDIATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
impl WinitSurface {
pub fn new(hal: &Hal, window: Arc<Window>) -> Result<Self, HalError> {
let surface = hal
.instance
.create_surface(window.clone())
.map_err(|e| HalError::CreateSurface(e.to_string()))?;
Self::from_surface(hal, window, surface)
}
pub fn from_surface(
hal: &Hal,
window: Arc<Window>,
surface: wgpu::Surface<'static>,
) -> Result<Self, HalError> {
let size = window.inner_size();
let caps = surface.get_capabilities(&hal.adapter);
let format = caps
.formats
.iter()
.copied()
.find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm))
.unwrap_or(caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: choose_present_mode(&caps),
desired_maximum_frame_latency: 2,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
};
surface.configure(&hal.device, &config);
let (intermediate, intermediate_view) =
create_intermediate(&hal.device, config.width, config.height);
let (overlay, overlay_view) =
create_intermediate(&hal.device, config.width, config.height);
let blitter = wgpu::util::TextureBlitter::new(&hal.device, format);
Ok(Self {
_window: window,
surface,
config,
device: hal.device.clone(),
intermediate,
intermediate_view,
overlay,
overlay_view,
blitter,
})
}
pub fn format(&self) -> wgpu::TextureFormat {
self.config.format
}
}
pub struct RawSurface {
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
device: wgpu::Device,
intermediate: wgpu::Texture,
intermediate_view: wgpu::TextureView,
overlay: wgpu::Texture,
overlay_view: wgpu::TextureView,
blitter: wgpu::util::TextureBlitter,
}
impl RawSurface {
pub fn from_surface(
hal: &Hal,
surface: wgpu::Surface<'static>,
width: u32,
height: u32,
) -> Result<Self, HalError> {
let caps = surface.get_capabilities(&hal.adapter);
let info = hal.adapter.get_info();
let format = match caps
.formats
.iter()
.copied()
.find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm))
.or_else(|| caps.formats.first().copied())
{
Some(f) => f,
None => {
return Err(HalError::CreateSurface(format!(
"la superficie no expone formatos (adapter {:?}/{:?}): el compositor no la soporta por {:?} WSI",
info.backend, info.device_type, info.backend
)))
}
};
let alpha_mode = {
use wgpu::CompositeAlphaMode as Mode;
let want = [
Mode::PreMultiplied,
Mode::PostMultiplied,
Mode::Inherit,
Mode::Auto,
];
want.iter()
.copied()
.find(|m| caps.alpha_modes.contains(m))
.or_else(|| caps.alpha_modes.first().copied())
.unwrap_or(Mode::Auto)
};
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: width.max(1),
height: height.max(1),
present_mode: choose_present_mode(&caps),
desired_maximum_frame_latency: 2,
alpha_mode,
view_formats: vec![],
};
surface.configure(&hal.device, &config);
let (intermediate, intermediate_view) =
create_intermediate(&hal.device, config.width, config.height);
let (overlay, overlay_view) =
create_intermediate(&hal.device, config.width, config.height);
let blitter = wgpu::util::TextureBlitter::new(&hal.device, format);
Ok(Self {
surface,
config,
device: hal.device.clone(),
intermediate,
intermediate_view,
overlay,
overlay_view,
blitter,
})
}
pub fn format(&self) -> wgpu::TextureFormat {
self.config.format
}
}
impl Surface for RawSurface {
fn size(&self) -> (u32, u32) {
(self.config.width, self.config.height)
}
fn resize(&mut self, width: u32, height: u32) {
let (w, h) = (width.max(1), height.max(1));
if self.config.width == w && self.config.height == h {
return;
}
self.config.width = w;
self.config.height = h;
self.surface.configure(&self.device, &self.config);
let (tex, view) = create_intermediate(&self.device, self.config.width, self.config.height);
self.intermediate = tex;
self.intermediate_view = view;
let (otex, oview) =
create_intermediate(&self.device, self.config.width, self.config.height);
self.overlay = otex;
self.overlay_view = oview;
}
fn acquire(&mut self) -> Result<Frame, SurfaceError> {
let texture = match self.surface.get_current_texture() {
Ok(t) => t,
Err(e @ (wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost)) => {
self.surface.configure(&self.device, &self.config);
self.surface.get_current_texture().map_err(|_| match e {
wgpu::SurfaceError::Lost => SurfaceError::Lost,
_ => SurfaceError::Outdated,
})?
}
Err(wgpu::SurfaceError::OutOfMemory) => return Err(SurfaceError::OutOfMemory),
Err(wgpu::SurfaceError::Timeout) => return Err(SurfaceError::Timeout),
Err(other) => return Err(SurfaceError::Other(format!("{other:?}"))),
};
let surface_view = texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Frame {
surface_texture: texture,
surface_view,
intermediate_view: self.intermediate_view.clone(),
overlay_view: self.overlay_view.clone(),
width: self.config.width,
height: self.config.height,
})
}
fn present(&mut self, frame: Frame, hal: &Hal) {
let mut encoder = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("llimphi-blit-raw"),
});
self.blitter.copy(
&hal.device,
&mut encoder,
&frame.intermediate_view,
&frame.surface_view,
);
hal.queue.submit(std::iter::once(encoder.finish()));
frame.surface_texture.present();
}
}
fn choose_present_mode(caps: &wgpu::SurfaceCapabilities) -> wgpu::PresentMode {
use wgpu::PresentMode::{Fifo, FifoRelaxed, Immediate, Mailbox};
if let Ok(v) = std::env::var("LLIMPHI_PRESENT_MODE") {
let want = match v.trim().to_ascii_lowercase().as_str() {
"fifo" | "vsync" => Some(Fifo),
"fifo_relaxed" | "fiforelaxed" => Some(FifoRelaxed),
"mailbox" => Some(Mailbox),
"immediate" | "novsync" => Some(Immediate),
_ => None,
};
if let Some(m) = want {
if caps.present_modes.contains(&m) {
return m;
}
}
}
if caps.present_modes.contains(&Mailbox) {
Mailbox
} else {
Fifo
}
}
fn create_intermediate(
device: &wgpu::Device,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("llimphi-intermediate"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
pub struct OverlayCompositor {
pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
bind_layout: wgpu::BindGroupLayout,
}
impl OverlayCompositor {
pub fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("llimphi-overlay-composite"),
source: wgpu::ShaderSource::Wgsl(OVERLAY_COMPOSITE_WGSL.into()),
});
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("llimphi-overlay-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("llimphi-overlay-pl"),
bind_group_layouts: &[&bind_layout],
push_constant_ranges: &[],
});
let straight = std::env::var("LLIMPHI_OVERLAY_BLEND")
.map(|v| v.trim().eq_ignore_ascii_case("straight"))
.unwrap_or(false);
let color_src = if straight {
wgpu::BlendFactor::SrcAlpha
} else {
wgpu::BlendFactor::One
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("llimphi-overlay-pipe"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: INTERMEDIATE_FORMAT,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: color_src,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("llimphi-overlay-sampler"),
..Default::default()
});
OverlayCompositor {
pipeline,
sampler,
bind_layout,
}
}
pub fn composite(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
source: &wgpu::TextureView,
) {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("llimphi-overlay-bg"),
layout: &self.bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-overlay-composite-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
const OVERLAY_COMPOSITE_WGSL: &str = r#"
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
var corners = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let xy = corners[vi];
var out: VsOut;
out.pos = vec4<f32>(xy, 0.0, 1.0);
out.uv = vec2<f32>((xy.x + 1.0) * 0.5, (1.0 - xy.y) * 0.5);
return out;
}
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var src_samp: sampler;
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {
return textureSample(src_tex, src_samp, in.uv);
}
"#;
pub struct BlurCompositor {
pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
bind_layout: wgpu::BindGroupLayout,
scratch: Option<BlurScratch>,
}
struct BlurScratch {
_texture: wgpu::Texture,
view: wgpu::TextureView,
width: u32,
height: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct BlurUniforms {
direction: [f32; 2],
pixel_size: [f32; 2],
sigma: f32,
radius: f32,
_pad: [f32; 2],
}
const BLUR_UBO_SIZE: u64 = std::mem::size_of::<BlurUniforms>() as u64;
const BLUR_MAX_RADIUS: f32 = 32.0;
impl BlurCompositor {
pub fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("llimphi-blur-shader"),
source: wgpu::ShaderSource::Wgsl(BLUR_WGSL.into()),
});
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("llimphi-blur-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("llimphi-blur-pl"),
bind_group_layouts: &[&bind_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("llimphi-blur-pipe"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: INTERMEDIATE_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("llimphi-blur-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::Nearest,
..Default::default()
});
BlurCompositor {
pipeline,
sampler,
bind_layout,
scratch: None,
}
}
pub fn blur(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
viewport: (u32, u32),
rect: (f32, f32, f32, f32),
sigma: f32,
) {
let (vw, vh) = viewport;
if vw == 0 || vh == 0 || sigma <= 0.0 {
return;
}
let (rx, ry, rw, rh) = rect;
let x0 = rx.max(0.0) as u32;
let y0 = ry.max(0.0) as u32;
let x1 = (rx + rw).min(vw as f32).max(0.0) as u32;
let y1 = (ry + rh).min(vh as f32).max(0.0) as u32;
if x1 <= x0 || y1 <= y0 {
return;
}
let scissor = (x0, y0, x1 - x0, y1 - y0);
let need_new = match &self.scratch {
Some(s) => s.width != vw || s.height != vh,
None => true,
};
if need_new {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("llimphi-blur-scratch"),
size: wgpu::Extent3d {
width: vw,
height: vh,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.scratch = Some(BlurScratch {
_texture: texture,
view,
width: vw,
height: vh,
});
}
let scratch_view = &self.scratch.as_ref().expect("scratch creado arriba").view;
let radius = (sigma * 3.0).ceil().min(BLUR_MAX_RADIUS);
let pixel_size = [1.0 / vw as f32, 1.0 / vh as f32];
let ubo_h_data = BlurUniforms {
direction: [1.0, 0.0],
pixel_size,
sigma,
radius,
_pad: [0.0, 0.0],
};
let ubo_v_data = BlurUniforms {
direction: [0.0, 1.0],
pixel_size,
sigma,
radius,
_pad: [0.0, 0.0],
};
let ubo_h = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-blur-ubo-h"),
size: BLUR_UBO_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let ubo_v = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-blur-ubo-v"),
size: BLUR_UBO_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&ubo_h, 0, bytemuck_cast(&ubo_h_data));
queue.write_buffer(&ubo_v, 0, bytemuck_cast(&ubo_v_data));
let bg_h = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("llimphi-blur-bg-h"),
layout: &self.bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(target),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: ubo_h.as_entire_binding(),
},
],
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-blur-pass-h"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: scratch_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bg_h, &[]);
pass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
pass.draw(0..3, 0..1);
}
let bg_v = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("llimphi-blur-bg-v"),
layout: &self.bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(scratch_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: ubo_v.as_entire_binding(),
},
],
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-blur-pass-v"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bg_v, &[]);
pass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
pass.draw(0..3, 0..1);
}
}
}
pub struct ColorFilterCompositor {
pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
bind_layout: wgpu::BindGroupLayout,
scratch: Option<BlurScratch>,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct ColorUniforms {
r: [f32; 4],
g: [f32; 4],
b: [f32; 4],
a: [f32; 4],
bias: [f32; 4],
}
const COLOR_UBO_SIZE: u64 = std::mem::size_of::<ColorUniforms>() as u64;
const COLOR_IDENTITY: ColorUniforms = ColorUniforms {
r: [1.0, 0.0, 0.0, 0.0],
g: [0.0, 1.0, 0.0, 0.0],
b: [0.0, 0.0, 1.0, 0.0],
a: [0.0, 0.0, 0.0, 1.0],
bias: [0.0, 0.0, 0.0, 0.0],
};
impl ColorFilterCompositor {
pub fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("llimphi-color-filter-shader"),
source: wgpu::ShaderSource::Wgsl(COLOR_WGSL.into()),
});
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("llimphi-color-filter-bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("llimphi-color-filter-pl"),
bind_group_layouts: &[&bind_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("llimphi-color-filter-pipe"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: INTERMEDIATE_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("llimphi-color-filter-sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
ColorFilterCompositor {
pipeline,
sampler,
bind_layout,
scratch: None,
}
}
pub fn apply(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
viewport: (u32, u32),
rect: (f32, f32, f32, f32),
matrix: [f32; 20],
) {
let (vw, vh) = viewport;
if vw == 0 || vh == 0 {
return;
}
let (rx, ry, rw, rh) = rect;
let x0 = rx.max(0.0) as u32;
let y0 = ry.max(0.0) as u32;
let x1 = (rx + rw).min(vw as f32).max(0.0) as u32;
let y1 = (ry + rh).min(vh as f32).max(0.0) as u32;
if x1 <= x0 || y1 <= y0 {
return;
}
let scissor = (x0, y0, x1 - x0, y1 - y0);
let need_new = match &self.scratch {
Some(s) => s.width != vw || s.height != vh,
None => true,
};
if need_new {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("llimphi-color-filter-scratch"),
size: wgpu::Extent3d {
width: vw,
height: vh,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.scratch = Some(BlurScratch {
_texture: texture,
view,
width: vw,
height: vh,
});
}
let scratch_view = &self.scratch.as_ref().expect("scratch creado arriba").view;
let apply = ColorUniforms {
r: [matrix[0], matrix[1], matrix[2], matrix[3]],
g: [matrix[5], matrix[6], matrix[7], matrix[8]],
b: [matrix[10], matrix[11], matrix[12], matrix[13]],
a: [matrix[15], matrix[16], matrix[17], matrix[18]],
bias: [matrix[4], matrix[9], matrix[14], matrix[19]],
};
let ubo_apply = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-color-filter-ubo-apply"),
size: COLOR_UBO_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let ubo_copy = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("llimphi-color-filter-ubo-copy"),
size: COLOR_UBO_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&ubo_apply, 0, bytemuck_cast(&apply));
queue.write_buffer(&ubo_copy, 0, bytemuck_cast(&COLOR_IDENTITY));
let bg_apply = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("llimphi-color-filter-bg-apply"),
layout: &self.bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(target),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: ubo_apply.as_entire_binding(),
},
],
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-color-filter-pass-apply"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: scratch_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bg_apply, &[]);
pass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
pass.draw(0..3, 0..1);
}
let bg_copy = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("llimphi-color-filter-bg-copy"),
layout: &self.bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(scratch_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: ubo_copy.as_entire_binding(),
},
],
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("llimphi-color-filter-pass-copy"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bg_copy, &[]);
pass.set_scissor_rect(scissor.0, scissor.1, scissor.2, scissor.3);
pass.draw(0..3, 0..1);
}
}
}
fn bytemuck_cast<T: Copy>(v: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
v as *const T as *const u8,
std::mem::size_of::<T>(),
)
}
}
const BLUR_WGSL: &str = r#"
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
var corners = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let xy = corners[vi];
var out: VsOut;
out.pos = vec4<f32>(xy, 0.0, 1.0);
out.uv = vec2<f32>((xy.x + 1.0) * 0.5, (1.0 - xy.y) * 0.5);
return out;
}
struct BlurParams {
direction: vec2<f32>,
pixel_size: vec2<f32>,
sigma: f32,
radius: f32,
_pad: vec2<f32>,
};
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var src_samp: sampler;
@group(0) @binding(2) var<uniform> params: BlurParams;
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {
let dir = params.direction * params.pixel_size;
let r = i32(params.radius);
let two_sigma_sq = 2.0 * params.sigma * params.sigma;
var acc = vec4<f32>(0.0);
var weight_sum = 0.0;
for (var i = -r; i <= r; i = i + 1) {
let fi = f32(i);
let w = exp(-(fi * fi) / two_sigma_sq);
acc = acc + textureSample(src_tex, src_samp, in.uv + dir * fi) * w;
weight_sum = weight_sum + w;
}
return acc / weight_sum;
}
"#;
const COLOR_WGSL: &str = r#"
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
var corners = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let xy = corners[vi];
var out: VsOut;
out.pos = vec4<f32>(xy, 0.0, 1.0);
out.uv = vec2<f32>((xy.x + 1.0) * 0.5, (1.0 - xy.y) * 0.5);
return out;
}
struct ColorParams {
r: vec4<f32>,
g: vec4<f32>,
b: vec4<f32>,
a: vec4<f32>,
bias: vec4<f32>,
};
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var src_samp: sampler;
@group(0) @binding(2) var<uniform> params: ColorParams;
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {
let c = textureSample(src_tex, src_samp, in.uv);
var o: vec4<f32>;
o.r = dot(params.r, c) + params.bias.r;
o.g = dot(params.g, c) + params.bias.g;
o.b = dot(params.b, c) + params.bias.b;
o.a = dot(params.a, c) + params.bias.a;
return clamp(o, vec4<f32>(0.0), vec4<f32>(1.0));
}
"#;
impl Surface for WinitSurface {
fn size(&self) -> (u32, u32) {
(self.config.width, self.config.height)
}
fn resize(&mut self, width: u32, height: u32) {
self.config.width = width.max(1);
self.config.height = height.max(1);
self.surface.configure(&self.device, &self.config);
let (tex, view) = create_intermediate(&self.device, self.config.width, self.config.height);
self.intermediate = tex;
self.intermediate_view = view;
let (otex, oview) =
create_intermediate(&self.device, self.config.width, self.config.height);
self.overlay = otex;
self.overlay_view = oview;
}
fn acquire(&mut self) -> Result<Frame, SurfaceError> {
let texture = self.surface.get_current_texture().map_err(|e| match e {
wgpu::SurfaceError::Lost => SurfaceError::Lost,
wgpu::SurfaceError::Outdated => SurfaceError::Outdated,
wgpu::SurfaceError::OutOfMemory => SurfaceError::OutOfMemory,
wgpu::SurfaceError::Timeout => SurfaceError::Timeout,
other => SurfaceError::Other(format!("{other:?}")),
})?;
let surface_view = texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
Ok(Frame {
surface_texture: texture,
surface_view,
intermediate_view: self.intermediate_view.clone(),
overlay_view: self.overlay_view.clone(),
width: self.config.width,
height: self.config.height,
})
}
fn present(&mut self, frame: Frame, hal: &Hal) {
let mut encoder = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("llimphi-blit"),
});
self.blitter.copy(
&hal.device,
&mut encoder,
&frame.intermediate_view,
&frame.surface_view,
);
hal.queue.submit(std::iter::once(encoder.finish()));
frame.surface_texture.present();
}
}