use std::collections::HashMap;
use wgpu::util::DeviceExt;
use crate::resources::SkinWeights;
use crate::resources::ViewportGpuResources;
use crate::resources::mesh_store::MeshId;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct PackedSkinVertex {
pub weights: [f32; 4],
pub joints_01: u32,
pub joints_23: u32,
pub _pad: [u32; 2],
}
pub(crate) struct InstancePalette {
pub buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub joint_capacity: u32,
}
pub(crate) struct MeshSkinning {
pub weights_buffer: wgpu::Buffer,
pub instances: HashMap<u32, InstancePalette>,
}
pub(crate) struct SkinningState {
pub bind_group_layout: wgpu::BindGroupLayout,
pub meshes: HashMap<MeshId, MeshSkinning>,
}
impl SkinningState {
pub fn new(device: &wgpu::Device) -> Self {
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("skin_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
Self {
bind_group_layout,
meshes: HashMap::new(),
}
}
pub(crate) fn pack(weights: &SkinWeights) -> Vec<PackedSkinVertex> {
weights
.joint_indices
.iter()
.zip(weights.joint_weights.iter())
.map(|(j, w)| {
let j0 = j[0] as u32;
let j1 = j[1] as u32;
let j2 = j[2] as u32;
let j3 = j[3] as u32;
PackedSkinVertex {
weights: *w,
joints_01: j0 | (j1 << 16),
joints_23: j2 | (j3 << 16),
_pad: [0, 0],
}
})
.collect()
}
}
impl ViewportGpuResources {
pub fn supports_gpu_skinning(&self) -> bool {
true
}
pub fn set_skin_weights(
&mut self,
device: &wgpu::Device,
mesh_id: MeshId,
weights: &SkinWeights,
) {
let packed = SkinningState::pack(weights);
self.install_skin_weights(device, mesh_id, &packed);
}
pub fn begin_upload_skin_weights(
&mut self,
device: &wgpu::Device,
mesh_id: MeshId,
weights: SkinWeights,
) -> crate::resources::JobId {
let device_for_apply = device.clone();
let mut runner = self.jobs.lock().expect("upload job runner poisoned");
runner.submit_cpu(move |progress| {
progress.set(0.2);
let packed = SkinningState::pack(&weights);
progress.set(0.9);
Ok(crate::resources::upload_jobs::JobProduct::with_apply(
Box::new(move |resources: &mut ViewportGpuResources| {
resources.install_skin_weights(&device_for_apply, mesh_id, &packed);
}),
))
})
}
fn install_skin_weights(
&mut self,
device: &wgpu::Device,
mesh_id: MeshId,
packed: &[PackedSkinVertex],
) {
let weights_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("skin_weights_buffer"),
contents: bytemuck::cast_slice(packed),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
self.skinning.meshes.insert(
mesh_id,
MeshSkinning {
weights_buffer,
instances: HashMap::new(),
},
);
}
pub fn set_skin_palette(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_id: MeshId,
instance_id: u32,
palette: &[glam::Mat4],
) -> bool {
let bgl = &self.skinning.bind_group_layout;
let mesh = match self.skinning.meshes.get_mut(&mesh_id) {
Some(m) => m,
None => return false,
};
let joints_needed = palette.len() as u32;
let needs_realloc = match mesh.instances.get(&instance_id) {
Some(inst) => inst.joint_capacity < joints_needed,
None => true,
};
if needs_realloc {
let capacity = joints_needed.max(1);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("skin_palette_buffer"),
size: (capacity as u64) * std::mem::size_of::<[[f32; 4]; 4]>() as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("skin_bind_group"),
layout: bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: mesh.weights_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: buffer.as_entire_binding(),
},
],
});
mesh.instances.insert(
instance_id,
InstancePalette {
buffer,
bind_group,
joint_capacity: capacity,
},
);
}
let inst = mesh.instances.get(&instance_id).unwrap();
let bytes: Vec<[[f32; 4]; 4]> = palette.iter().map(|m| m.to_cols_array_2d()).collect();
queue.write_buffer(&inst.buffer, 0, bytemuck::cast_slice(&bytes));
true
}
pub fn is_skinned_mesh(&self, mesh_id: MeshId) -> bool {
self.skinning.meshes.contains_key(&mesh_id)
}
pub(crate) fn skin_instance_bind_group(
&self,
mesh_id: MeshId,
instance_id: u32,
) -> Option<&wgpu::BindGroup> {
self.skinning
.meshes
.get(&mesh_id)?
.instances
.get(&instance_id)
.map(|p| &p.bind_group)
}
}
#[cfg(test)]
mod async_skin_tests {
use crate::ViewportGpuResources;
use crate::geometry::primitives;
use crate::resources::{SkinWeights, UploadStatus};
fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
fn unit_weights(vertex_count: usize) -> SkinWeights {
SkinWeights {
joint_indices: vec![[0u8; 4]; vertex_count],
joint_weights: vec![[1.0, 0.0, 0.0, 0.0]; vertex_count],
}
}
#[test]
fn sync_set_skin_weights_marks_mesh_skinnable() {
let Some((device, _queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources =
ViewportGpuResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
let weights = unit_weights(plane.positions.len());
resources.set_skin_weights(&device, mesh_id, &weights);
assert!(resources.is_skinned_mesh(mesh_id));
}
#[test]
fn begin_upload_skin_weights_completes() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources =
ViewportGpuResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
let weights = unit_weights(plane.positions.len());
assert!(!resources.is_skinned_mesh(mesh_id));
let id = resources.begin_upload_skin_weights(&device, mesh_id, weights);
for _ in 0..200 {
resources.process_uploads(&device, &queue);
match resources.upload_status(id) {
UploadStatus::Ready => break,
UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
UploadStatus::Pending { .. } => {
std::thread::sleep(std::time::Duration::from_millis(5));
}
UploadStatus::Unknown => panic!("job disappeared"),
}
}
assert!(
resources.is_skinned_mesh(mesh_id),
"skin weights did not install"
);
}
}