use teksilo_canvas::PixelRect;
use teksilo_canvas::live_image::ScalingFilter;
use teksilo_canvas::live_image::internal::{
LiveImageRead, LiveTextureBackend, TextureOutOfMemory, mip_bytes, mip_footprint,
mip_level_size, mip_levels,
};
use crate::device_health::DeviceHealth;
pub(crate) struct WgpuTexture {
pub(crate) texture: wgpu::Texture,
width: u32,
height: u32,
levels: u32,
level_views: Vec<wgpu::TextureView>,
full_view: Option<wgpu::TextureView>,
mip_groups: Vec<wgpu::BindGroup>,
groups: [Option<wgpu::BindGroup>; 3],
}
impl WgpuTexture {
pub(crate) fn bind_group(&self, filter: ScalingFilter) -> Option<&wgpu::BindGroup> {
self.groups[filter_index(filter)].as_ref()
}
pub(crate) fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub(crate) fn levels(&self) -> u32 {
self.levels
}
}
struct LiveMipPipelines {
layout: wgpu::BindGroupLayout,
straight: wgpu::RenderPipeline,
opaque: wgpu::RenderPipeline,
}
impl LiveMipPipelines {
fn new(device: &wgpu::Device) -> Self {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("live_mip"),
source: wgpu::ShaderSource::Wgsl(
concat!(
include_str!("shaders/fullscreen.wgsl"),
include_str!("shaders/live_mip.wgsl")
)
.into(),
),
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("live_mip_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("live_mip_pipeline_layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = |entry: &'static str| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(entry),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(entry),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
};
Self {
straight: pipeline("fs_straight"),
opaque: pipeline("fs_opaque"),
layout,
}
}
}
struct MipJob {
size: (u32, u32),
level_views: Vec<wgpu::TextureView>,
mip_groups: Vec<wgpu::BindGroup>,
region: Option<PixelRect>,
opaque: bool,
}
struct UploadJob {
staged: crate::live_staging::Staged,
bytes_per_row: u32,
texture: wgpu::Texture,
band: PixelRect,
}
struct CopyJob {
from: wgpu::Texture,
into: wgpu::Texture,
}
pub(crate) struct WgpuBackend {
device: wgpu::Device,
queue: wgpu::Queue,
layout: wgpu::BindGroupLayout,
linear: wgpu::Sampler,
nearest: wgpu::Sampler,
trilinear: wgpu::Sampler,
mip_pipelines: Option<LiveMipPipelines>,
mip_jobs: Vec<MipJob>,
copy_jobs: Vec<CopyJob>,
upload_jobs: Vec<UploadJob>,
staging: crate::live_staging::StagingPool,
health: DeviceHealth,
max_dimension: u32,
released: bool,
fail_next_create: bool,
refusal_logged: bool,
}
impl WgpuBackend {
pub(crate) fn new(
device: wgpu::Device,
queue: wgpu::Queue,
layout: wgpu::BindGroupLayout,
health: DeviceHealth,
) -> Self {
let sampler =
|label: &'static str, filter: wgpu::FilterMode, mips: wgpu::MipmapFilterMode| {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: filter,
min_filter: filter,
mipmap_filter: mips,
..Default::default()
})
};
let linear = sampler(
"live_linear",
wgpu::FilterMode::Linear,
wgpu::MipmapFilterMode::Nearest,
);
let nearest = sampler(
"live_nearest",
wgpu::FilterMode::Nearest,
wgpu::MipmapFilterMode::Nearest,
);
let trilinear = sampler(
"live_trilinear",
wgpu::FilterMode::Linear,
wgpu::MipmapFilterMode::Linear,
);
let max_dimension = device.limits().max_texture_dimension_2d;
Self {
device,
queue,
layout,
linear,
nearest,
trilinear,
mip_pipelines: None,
mip_jobs: Vec::new(),
copy_jobs: Vec::new(),
upload_jobs: Vec::new(),
staging: crate::live_staging::StagingPool::default(),
health,
max_dimension,
released: false,
fail_next_create: false,
refusal_logged: false,
}
}
pub(crate) fn take_released(&mut self) -> bool {
std::mem::take(&mut self.released)
}
pub(crate) fn set_max_dimension(&mut self, max: u32) {
self.max_dimension = max.min(self.device.limits().max_texture_dimension_2d);
}
pub(crate) fn fail_next_create(&mut self) {
self.fail_next_create = true;
}
pub(crate) fn health(&self) -> &DeviceHealth {
&self.health
}
pub(crate) fn after_submit(&mut self, idle: bool) {
let freed = if idle {
self.staging.release_all()
} else {
self.staging.after_submit()
};
if freed {
self.released = true;
}
}
pub(crate) fn staging_stats(&self) -> crate::live_staging::StagingStats {
self.staging.stats()
}
pub(crate) fn set_staging_idle_lifetime(&mut self, lifetime: std::time::Duration) {
self.staging.set_idle_lifetime(lifetime);
}
pub(crate) fn fail_next_staging(&mut self) {
self.staging.fail_next();
}
pub(crate) fn encode(&mut self, encoder: &mut wgpu::CommandEncoder) {
self.staging.finish_frame();
for job in self.upload_jobs.drain(..) {
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: &job.staged.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: job.staged.offset,
bytes_per_row: Some(job.bytes_per_row),
rows_per_image: None,
},
},
wgpu::TexelCopyTextureInfo {
texture: &job.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: job.band.x,
y: job.band.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width: job.band.width,
height: job.band.height,
depth_or_array_layers: 1,
},
);
}
for job in self.copy_jobs.drain(..) {
let size = job.into.size();
debug_assert_eq!(job.from.size(), size, "the live pass copies one size");
if job.from.size() != size {
continue;
}
encoder.copy_texture_to_texture(
job.from.as_image_copy(),
job.into.as_image_copy(),
wgpu::Extent3d {
width: size.width,
height: size.height,
depth_or_array_layers: 1,
},
);
}
let Some(pipelines) = self.mip_pipelines.as_ref() else {
return;
};
for job in self.mip_jobs.drain(..) {
let pipeline = if job.opaque {
&pipelines.opaque
} else {
&pipelines.straight
};
for k in 1..job.level_views.len() as u32 {
let (w, h) = mip_level_size(job.size.0, job.size.1, k);
let (load, scissor) = match job.region {
None => (wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), None),
Some(changed) => {
let f = mip_footprint(changed, k, (w, h));
(wgpu::LoadOp::Load, Some(even_scissor(f, (w, h))))
}
};
crate::fullscreen::run_fullscreen_pass(
encoder,
pipeline,
&job.mip_groups[k as usize - 1],
&[],
&job.level_views[k as usize],
w,
h,
load,
scissor,
"live_mip",
);
}
}
}
}
fn even_scissor(footprint: PixelRect, level: (u32, u32)) -> [u32; 4] {
let up = |edge: u32, side: u32| ((edge + 1) & !1).min(side);
let (x0, y0) = (footprint.x & !1, footprint.y & !1);
let x1 = up(footprint.x + footprint.width, level.0);
let y1 = up(footprint.y + footprint.height, level.1);
[x0, y0, x1 - x0, y1 - y0]
}
fn filter_index(filter: ScalingFilter) -> usize {
match filter {
ScalingFilter::Nearest => 1,
ScalingFilter::Trilinear => 2,
_ => 0,
}
}
impl LiveTextureBackend for WgpuBackend {
type Texture = WgpuTexture;
type Shared = wgpu::Texture;
fn max_dimension(&self) -> u32 {
self.max_dimension
}
fn create_texture(
&mut self,
width: u32,
height: u32,
mipped: bool,
label: &str,
) -> Result<WgpuTexture, TextureOutOfMemory> {
if std::mem::take(&mut self.fail_next_create) {
return Err(TextureOutOfMemory);
}
let levels = if mipped { mip_levels(width, height) } else { 1 };
let mut usage = wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC;
if mipped {
usage |= wgpu::TextureUsages::RENDER_ATTACHMENT;
}
let validation = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let memory = self.device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: levels,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage,
view_formats: &[],
});
let out_of_memory = pollster::block_on(memory.pop());
let invalid = pollster::block_on(validation.pop());
if out_of_memory.is_some() {
return Err(TextureOutOfMemory);
}
if let Some(error) = invalid {
if !std::mem::replace(&mut self.refusal_logged, true) {
eprintln!(
"teksilo-render: the device refused a {width}x{height} live texture: {error}"
);
}
return Err(TextureOutOfMemory);
}
let level_views: Vec<wgpu::TextureView> = (0..levels)
.map(|k| {
texture.create_view(&wgpu::TextureViewDescriptor {
base_mip_level: k,
mip_level_count: Some(1),
..Default::default()
})
})
.collect();
let (full_view, mip_groups) = if mipped {
let pipelines = self
.mip_pipelines
.get_or_insert_with(|| LiveMipPipelines::new(&self.device));
let groups = level_views[..levels as usize - 1]
.iter()
.map(|below| {
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("live_mip_bind_group"),
layout: &pipelines.layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(below),
}],
})
})
.collect();
(Some(texture.create_view(&Default::default())), groups)
} else {
(None, Vec::new())
};
Ok(WgpuTexture {
texture,
width,
height,
levels,
level_views,
full_view,
mip_groups,
groups: [None, None, None],
})
}
fn staged_row_bytes(&self, width: u32) -> u64 {
(u64::from(width) * 4).next_multiple_of(u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT))
}
fn write(&mut self, texture: &mut WgpuTexture, read: &LiveImageRead<'_>, band: PixelRect) {
let stride = read.stride();
let pitch = self.staged_row_bytes(band.width);
let row = band.width as usize * 4;
let pixels = read.pixels();
let staged = self
.staging
.stage(&self.device, pitch * u64::from(band.height), |dst| {
if row == stride && row as u64 == pitch {
let from = band.y as usize * stride;
let len = row * band.height as usize;
dst.slice(..len).copy_from_slice(&pixels[from..from + len]);
return;
}
for r in 0..band.height as usize {
let from = (band.y as usize + r) * stride + band.x as usize * 4;
let to = r * pitch as usize;
dst.slice(to..to + row)
.copy_from_slice(&pixels[from..from + row]);
}
});
if let Some(staged) = staged {
self.upload_jobs.push(UploadJob {
staged,
bytes_per_row: pitch as u32,
texture: texture.texture.clone(),
band,
});
return;
}
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: band.x,
y: band.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
read.pixels(),
wgpu::TexelCopyBufferLayout {
offset: band.y as u64 * stride as u64 + u64::from(band.x) * 4,
bytes_per_row: Some(stride as u32),
rows_per_image: None,
},
wgpu::Extent3d {
width: band.width,
height: band.height,
depth_or_array_layers: 1,
},
);
}
fn prepare_filter(&mut self, texture: &mut WgpuTexture, filter: ScalingFilter) {
let (view, sampler) = match (filter, &texture.full_view) {
(ScalingFilter::Nearest, _) => (&texture.level_views[0], &self.nearest),
(ScalingFilter::Trilinear, Some(full)) => (full, &self.trilinear),
_ => (&texture.level_views[0], &self.linear),
};
let group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("live_image_bind_group"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
texture.groups[filter_index(filter)] = Some(group);
}
fn rebuild_mips(&mut self, texture: &mut WgpuTexture, region: Option<PixelRect>, opaque: bool) {
if texture.levels < 2 {
return;
}
self.mip_jobs.push(MipJob {
size: (texture.width, texture.height),
level_views: texture.level_views.clone(),
mip_groups: texture.mip_groups.clone(),
region,
opaque,
});
}
fn texture_bytes(&self, texture: &WgpuTexture) -> u64 {
mip_bytes(texture.width, texture.height, texture.levels)
}
fn share(&self, texture: &WgpuTexture) -> wgpu::Texture {
texture.texture.clone()
}
fn copy(&mut self, from: &wgpu::Texture, into: &mut WgpuTexture) {
self.copy_jobs.push(CopyJob {
from: from.clone(),
into: into.texture.clone(),
});
}
fn device_lost(&self) -> bool {
self.health.is_lost()
}
fn released(&mut self) {
self.released = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_scissor_grows_to_even_edges_and_stays_in_the_level() {
assert_eq!(
even_scissor(PixelRect::new(50, 101, 149, 156), (360, 640)),
[50, 100, 150, 158]
);
assert_eq!(
even_scissor(PixelRect::new(4, 6, 10, 2), (64, 40)),
[4, 6, 10, 2]
);
assert_eq!(
even_scissor(PixelRect::new(3, 0, 2, 1), (5, 3)),
[2, 0, 3, 2]
);
assert_eq!(
even_scissor(PixelRect::new(0, 0, 1, 1), (1, 1)),
[0, 0, 1, 1]
);
}
}