use std::borrow::Cow;
use openh264::formats::YUVSource;
use wgpu::
{
Adapter,
BindGroup,
BindGroupDescriptor,
BindGroupEntry,
BindGroupLayout,
Buffer,
BufferDescriptor,
BufferUsages,
CommandEncoderDescriptor,
ComputePassDescriptor,
ComputePipeline,
ComputePipelineDescriptor,
Device,
DeviceDescriptor,
Instance,
InstanceDescriptor,
MapMode,
PollType,
PowerPreference,
Queue,
RequestAdapterOptions,
ShaderModuleDescriptor,
ShaderSource,
};
const WORKGROUP: u32 = 64;
const SHADER: &str = include_str!("rgba_to_i420.wgsl");
pub struct I420Frame {
data: Vec<u8>,
width: usize,
height: usize,
}
struct Resources {
width: u32,
height: u32,
source: Buffer,
target: Buffer,
staging: Buffer,
bind_group: BindGroup,
invocations: u32,
output_bytes: u64,
}
pub struct GpuConverter
{
device: Device,
queue: Queue,
pipeline: ComputePipeline,
layout: BindGroupLayout,
adapter: String,
resources: Option<Resources>,
frame: I420Frame,
}
impl YUVSource for I420Frame
{
fn dimensions(&self) -> (usize, usize)
{
(self.width, self.height)
}
fn strides(&self) -> (usize, usize, usize)
{
(self.width, self.width / 2, self.width / 2)
}
fn y(&self) -> &[u8]
{
&self.data[..self.width * self.height]
}
fn u(&self) -> &[u8]
{
let base = self.width * self.height;
&self.data[base..base + base / 4]
}
fn v(&self) -> &[u8]
{
let base = self.width * self.height;
&self.data[base + base / 4..base + base / 2]
}
}
impl GpuConverter
{
pub fn supports(width: u32, height: u32) -> bool {
width % 8 == 0 && height % 2 == 0 && width > 0 && height > 0
}
pub fn new() -> Result<Self, String>
{
let instance = Instance::new(InstanceDescriptor::new_without_display_handle_from_env());
let adapter: Adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions
{
power_preference: PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
apply_limit_buckets: false,
})).map_err(|e| format!("no usable GPU adapter ({e})"))?;
let name = adapter.get_info().name;
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor
{
label: Some("why2 capture converter"),
..Default::default()
})).map_err(|e| format!("requesting a GPU device failed ({e})"))?;
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = device.create_shader_module(ShaderModuleDescriptor
{
label: Some("rgba -> i420"),
source: ShaderSource::Wgsl(Cow::Borrowed(SHADER)),
});
let pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor
{
label: Some("rgba -> i420"),
layout: None,
module: &module,
entry_point: Some("convert"),
compilation_options: Default::default(),
cache: None,
});
if let Some(error) = pollster::block_on(scope.pop())
{
return Err(format!("the conversion shader was rejected ({error})"));
}
let layout = pipeline.get_bind_group_layout(0);
Ok(Self
{
device,
queue,
pipeline,
layout,
adapter: name,
resources: None,
frame: I420Frame { data: Vec::new(), width: 0, height: 0 },
})
}
pub fn adapter(&self) -> &str
{
&self.adapter
}
fn prepare(&mut self, width: u32, height: u32)
{
if self.resources.as_ref().is_some_and(|r| r.width == width && r.height == height) { return; }
let pixels = width as u64 * height as u64;
let y_words = pixels / 4;
let c_words = pixels / 16;
let source_bytes = pixels * 4;
let output_bytes = (y_words + 2 * c_words) * 4;
let source = self.device.create_buffer(&BufferDescriptor
{
label: Some("rgba source"),
size: source_bytes,
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let target = self.device.create_buffer(&BufferDescriptor
{
label: Some("i420 target"),
size: output_bytes,
usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = self.device.create_buffer(&BufferDescriptor
{
label: Some("i420 readback"),
size: output_bytes,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut dimensions = Vec::with_capacity(16);
for value in [width, height, y_words as u32, c_words as u32]
{
dimensions.extend_from_slice(&value.to_le_bytes());
}
let uniform = self.device.create_buffer(&BufferDescriptor
{
label: Some("dimensions"),
size: 16,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&uniform, 0, &dimensions);
let bind_group = self.device.create_bind_group(&BindGroupDescriptor
{
label: Some("rgba -> i420"),
layout: &self.layout,
entries:
&[
BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() },
BindGroupEntry { binding: 1, resource: source.as_entire_binding() },
BindGroupEntry { binding: 2, resource: target.as_entire_binding() },
],
});
self.resources = Some(Resources
{
width,
height,
source,
target,
staging,
bind_group,
invocations: (y_words + c_words) as u32,
output_bytes,
});
self.frame = I420Frame
{
data: vec![0; output_bytes as usize],
width: width as usize,
height: height as usize,
};
}
pub fn convert(&mut self, width: u32, height: u32, rgba: &[u8]) -> Result<&I420Frame, String>
{
if !Self::supports(width, height)
{
return Err(format!("resolution {width}x{height} is not supported by the GPU converter"));
}
let expected = width as usize * height as usize * 4;
if rgba.len() < expected
{
return Err(format!("frame is {} bytes, expected {expected}", rgba.len()));
}
self.prepare(width, height);
let resources = self.resources.as_ref().expect("prepare always installs resources");
self.queue.write_buffer(&resources.source, 0, &rgba[..expected]);
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor
{
label: Some("rgba -> i420"),
});
{
let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor
{
label: Some("rgba -> i420"),
timestamp_writes: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &resources.bind_group, &[]);
pass.dispatch_workgroups(resources.invocations.div_ceil(WORKGROUP), 1, 1);
}
encoder.copy_buffer_to_buffer(&resources.target, 0, &resources.staging, 0, resources.output_bytes);
self.queue.submit(Some(encoder.finish()));
let slice = resources.staging.slice(..);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
slice.map_async(MapMode::Read, move |result| { ready_tx.send(result).ok(); });
self.device.poll(PollType::wait_indefinitely())
.map_err(|e| format!("waiting on the GPU failed ({e})"))?;
match ready_rx.recv()
{
Ok(Ok(())) => {},
Ok(Err(e)) => return Err(format!("mapping the readback buffer failed ({e})")),
Err(_) => return Err("the GPU never reported the readback".to_owned()),
}
{
let mapped = slice.get_mapped_range()
.map_err(|e| format!("reading back the converted frame failed ({e})"))?;
self.frame.data.copy_from_slice(&mapped);
}
resources.staging.unmap();
Ok(&self.frame)
}
}