diff options
| author | s-ol <s+removethis@s-ol.nu> | 2026-04-20 09:01:27 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2026-05-14 14:46:13 +0000 |
| commit | 15a5bc845648371619ffbdbc7a740d68c3c26872 (patch) | |
| tree | cad36fafa0d143add0fcf0ff1a2c911e37463ff6 /src | |
| parent | tsv integration, split binaries (diff) | |
| download | wgsl-view-15a5bc845648371619ffbdbc7a740d68c3c26872.tar.gz wgsl-view-15a5bc845648371619ffbdbc7a740d68c3c26872.zip | |
add tsv-video-buffer
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/tsv_video_buffer.rs | 410 | ||||
| -rw-r--r-- | src/bin/tsv_video_stream.rs | 2 | ||||
| -rw-r--r-- | src/bin/wgsl_render.rs | 60 | ||||
| -rw-r--r-- | src/gpu.rs | 95 | ||||
| -rw-r--r-- | src/hap.rs | 162 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/uniform.rs | 79 |
7 files changed, 744 insertions, 65 deletions
diff --git a/src/bin/tsv_video_buffer.rs b/src/bin/tsv_video_buffer.rs new file mode 100644 index 0000000..7fc94b8 --- /dev/null +++ b/src/bin/tsv_video_buffer.rs @@ -0,0 +1,410 @@ +use std::io::Read; +use std::process::{Command, Stdio}; + +use ash::vk; +use wgsl_view::{gpu, hap}; + +/// Options that ffmpeg supports but ffprobe does not. +const FFMPEG_ONLY_OPTIONS: &[&str] = &["-stream_loop", "-t"]; + +fn filter_probe_args(ff_args: &[String]) -> Vec<String> { + let mut out = Vec::new(); + let mut skip_next = false; + for arg in ff_args { + if skip_next { + skip_next = false; + continue; + } + if FFMPEG_ONLY_OPTIONS.contains(&arg.as_str()) { + skip_next = true; + continue; + } + out.push(arg.clone()); + } + out +} + +struct VideoInfo { + width: u32, + height: u32, + num_frames: u32, + codec_tag: String, +} + +fn probe_video(ff_args: &[String], max_frames: Option<u32>) -> VideoInfo { + let probe_args = filter_probe_args(ff_args); + let output = Command::new("ffprobe") + .args(["-v", "error", "-select_streams", "v:0"]) + .args([ + "-show_entries", + "stream=width,height,nb_frames,codec_tag_string", + ]) + .args(["-of", "csv=p=0"]) + .args(&probe_args) + .output() + .expect("failed to run ffprobe"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("ffprobe failed: {stderr}"); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let line = stdout.trim(); + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() < 4 { + panic!("unexpected ffprobe output: {line}"); + } + + let num_frames = parts[3].parse().unwrap_or_else(|_| { + max_frames.unwrap_or_else(|| { + panic!( + "could not determine frame count (nb_frames={}).\n\ + Use --frames N or use tsv-video-stream instead.", + parts[3] + ) + }) + }); + + VideoInfo { + codec_tag: parts[0].to_string(), + width: parts[1].parse().expect("parse width"), + height: parts[2].parse().expect("parse height"), + num_frames, + } +} + +fn is_hap_codec(codec_tag: &str) -> Option<hap::HapFormat> { + match codec_tag { + "Hap1" => Some(hap::HapFormat::Bc1), + "Hap5" => Some(hap::HapFormat::Bc3), + _ => None, + } +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = std::env::args().skip(1).collect(); + let sep = args.iter().position(|a| a == "--"); + + let (own_args, ff_args) = match sep { + Some(i) => (&args[..i], &args[i + 1..]), + None => { + eprintln!("usage: tsv-video-buffer [--name NAME] [--array] [--frames N] -- <ffmpeg input args>"); + std::process::exit(1); + } + }; + + let mut name = "tsv-video-buffer".to_string(); + let mut use_array = false; + let mut max_frames: Option<u32> = None; + + let mut i = 0; + while i < own_args.len() { + match own_args[i].as_str() { + "--name" => { + name = own_args[i + 1].clone(); + i += 2; + } + "--array" => { + use_array = true; + i += 1; + } + "--frames" => { + max_frames = Some(own_args[i + 1].parse().expect("parse --frames")); + i += 2; + } + other => panic!("unknown argument: {other}"), + } + } + + let mut info = probe_video(ff_args, max_frames); + if let Some(max) = max_frames { + info.num_frames = info.num_frames.min(max); + } + let hap_format = is_hap_codec(&info.codec_tag); + + let (texture_format, tsv_img_format) = match hap_format { + Some(hap::HapFormat::Bc1) => ( + wgpu::TextureFormat::Bc1RgbaUnorm, + gpu::ImgFormat::BC1_RGBA, + ), + Some(hap::HapFormat::Bc3) => ( + wgpu::TextureFormat::Bc3RgbaUnorm, + gpu::ImgFormat::BC3_RGBA, + ), + None => (wgpu::TextureFormat::Rgba8UnormSrgb, gpu::ImgFormat::R8G8B8A8), + }; + + log::info!( + "{}x{}, {} frames, codec_tag={}, format={:?}, tsv image: {}", + info.width, + info.height, + info.num_frames, + info.codec_tag, + texture_format, + name + ); + + // Request BC compression features if needed. + let features = if hap_format.is_some() { + wgpu::Features::TEXTURE_COMPRESSION_BC + | if use_array { + wgpu::Features::empty() + } else { + wgpu::Features::TEXTURE_COMPRESSION_BC_SLICED_3D + } + } else { + wgpu::Features::empty() + }; + + let (tsv_img_type, wgpu_dimension) = if use_array { + (gpu::ImgType::D2, wgpu::TextureDimension::D2) + } else { + (gpu::ImgType::D3, wgpu::TextureDimension::D3) + }; + + let instance = gpu::create_instance(); + let adapter = gpu::create_adapter(&instance, None); + let (device, queue) = gpu::create_device_with_features(&adapter, features); + + let mut client = gpu::create_tsv_client(&device); + client + .init_image( + &name, + info.width, + info.height, + info.num_frames, + tsv_img_format, + tsv_img_type, + true, + ) + .expect("init tsv image"); + + let fence = gpu::create_fence(&client); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("video_buffer"), + size: wgpu::Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: info.num_frames, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu_dimension, + format: texture_format, + usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + if let Some(hap_fmt) = hap_format { + upload_hap_frames(&device, &queue, &texture, ff_args, &info, hap_fmt); + } else { + upload_raw_frames(&device, &queue, &texture, ff_args, &info); + } + + // Submit all pending writes and wait for GPU + queue.submit([]); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); + + // Share the complete texture via TSV + let raw = unsafe { gpu::raw_image(&texture) }; + if let Err(e) = client.send_image( + &name, + raw, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + fence, + ) { + log::error!("send_image error: {e}"); + std::process::exit(1); + } + + log::info!("all {} frames uploaded and shared, waiting...", info.num_frames); + + // Keep the process alive so the shared image remains available + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} + +fn upload_hap_frames( + _device: &wgpu::Device, + queue: &wgpu::Queue, + texture: &wgpu::Texture, + ff_args: &[String], + info: &VideoInfo, + hap_fmt: hap::HapFormat, +) { + // For HAP, we need raw packet data (not decoded pixels). + // Use ffmpeg-next to read packets directly. + ffmpeg_next::init().expect("ffmpeg init"); + + let (_, input_url) = parse_ff_input_args(ff_args); + let mut ictx = ffmpeg_next::format::input(input_url).expect("open input"); + + let video_stream_index = ictx + .streams() + .best(ffmpeg_next::media::Type::Video) + .expect("no video stream") + .index(); + + let compressed_frame_size = hap_fmt.compressed_size(info.width, info.height); + let mut decode_buf = vec![0u8; compressed_frame_size + 1024 * 1024]; // extra margin + let mut frame_index: u32 = 0; + + let block_bytes = hap_fmt.block_bytes(); + let blocks_x = info.width.div_ceil(4); + + for (stream, packet) in ictx.packets() { + if stream.index() != video_stream_index { + continue; + } + + let data = packet.data().expect("packet data"); + let (_, decoded_len) = hap::decode(data, &mut decode_buf).expect("HAP decode"); + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: frame_index, + }, + aspect: wgpu::TextureAspect::All, + }, + &decode_buf[..decoded_len], + wgpu::TexelCopyBufferLayout { + offset: 0, + // BC: bytes_per_row = blocks_x * block_bytes + bytes_per_row: Some(blocks_x * block_bytes), + // rows_per_image = height in blocks + rows_per_image: Some(info.height.div_ceil(4)), + }, + wgpu::Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: 1, + }, + ); + + frame_index += 1; + if frame_index.is_multiple_of(10) || frame_index == info.num_frames { + log::info!("uploaded frame {}/{}", frame_index, info.num_frames); + } + if frame_index >= info.num_frames { + break; + } + } + + if frame_index < info.num_frames { + log::warn!( + "expected {} frames but only decoded {frame_index}", + info.num_frames + ); + } +} + +fn upload_raw_frames( + _device: &wgpu::Device, + queue: &wgpu::Queue, + texture: &wgpu::Texture, + ff_args: &[String], + info: &VideoInfo, +) { + let frame_size = (info.width * info.height * 4) as usize; + + let mut ffmpeg = Command::new("ffmpeg") + .args(["-v", "quiet"]) + .args(ff_args) + .args(["-f", "rawvideo", "-pix_fmt", "rgba", "pipe:1"]) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("failed to start ffmpeg"); + + let stdout = ffmpeg.stdout.take().unwrap(); + let mut reader = std::io::BufReader::new(stdout); + let mut frame_buf = vec![0u8; frame_size]; + let mut frame_index: u32 = 0; + + while reader.read_exact(&mut frame_buf).is_ok() { + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: frame_index, + }, + aspect: wgpu::TextureAspect::All, + }, + &frame_buf, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(info.width * 4), + rows_per_image: Some(info.height), + }, + wgpu::Extent3d { + width: info.width, + height: info.height, + depth_or_array_layers: 1, + }, + ); + + frame_index += 1; + if frame_index.is_multiple_of(10) || frame_index == info.num_frames { + log::info!("uploaded frame {}/{}", frame_index, info.num_frames); + } + if frame_index >= info.num_frames { + break; + } + } + + // Drop the reader/stdout to close the pipe — this signals ffmpeg to exit + // (important when --frames truncates before ffmpeg finishes) + drop(reader); + ffmpeg.wait().ok(); + + if frame_index < info.num_frames { + log::warn!( + "expected {} frames but only decoded {frame_index}", + info.num_frames + ); + } +} + +/// Parse ffmpeg-style input args to extract -f <format> and the input URL. +/// Returns (Option<format>, input_url). +fn parse_ff_input_args(ff_args: &[String]) -> (Option<String>, &str) { + let mut format = None; + let mut input_url = None; + let mut i = 0; + + while i < ff_args.len() { + match ff_args[i].as_str() { + "-f" => { + format = Some(ff_args[i + 1].clone()); + i += 2; + } + "-i" => { + input_url = Some(ff_args[i + 1].as_str()); + i += 2; + } + _ => { + // Last positional arg is input URL if no -i was given + if i == ff_args.len() - 1 && input_url.is_none() { + input_url = Some(ff_args[i].as_str()); + } + i += 1; + } + } + } + + (format, input_url.expect("no input file specified in ffmpeg args")) +} diff --git a/src/bin/tsv_video_stream.rs b/src/bin/tsv_video_stream.rs index d68f4b4..22b38f8 100644 --- a/src/bin/tsv_video_stream.rs +++ b/src/bin/tsv_video_stream.rs @@ -98,7 +98,7 @@ fn main() { let mut client = gpu::create_tsv_client(&device); client - .init_image(&name, width, height, gpu::TSV_FORMAT, true) + .init_image(&name, width, height, 1, gpu::TSV_FORMAT, gpu::ImgType::D2, true) .expect("init tsv image"); let fence = gpu::create_fence(&client); diff --git a/src/bin/wgsl_render.rs b/src/bin/wgsl_render.rs index 1860b7c..29b5d5d 100644 --- a/src/bin/wgsl_render.rs +++ b/src/bin/wgsl_render.rs @@ -33,11 +33,15 @@ fn main() { let instance = gpu::create_instance(); let adapter = gpu::create_adapter(&instance, None); - let (device, queue) = gpu::create_device(&adapter); + let (device, queue) = gpu::create_device_with_features( + &adapter, + wgpu::Features::TEXTURE_COMPRESSION_BC + | wgpu::Features::TEXTURE_COMPRESSION_BC_SLICED_3D, + ); let mut client = gpu::create_tsv_client(&device); client - .init_image(&name, width, height, gpu::TSV_FORMAT, true) + .init_image(&name, width, height, 1, gpu::TSV_FORMAT, gpu::ImgType::D2, true) .expect("init tsv image"); let fence = gpu::create_fence(&client); @@ -114,7 +118,7 @@ fn main() { }; if dirty { - refresh_textures(renderer.uniforms(), &device, &mut client, fence); + gpu::refresh_textures(renderer.uniforms(), &device, &mut client, fence); renderer.render(&device, &queue); device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); @@ -228,54 +232,4 @@ fn configure_sampler( Ok(()) } -/// Refresh all texture inputs from TSV shared images. -fn refresh_textures( - cache: &mut wgsl_view::uniform::UniformCache, - device: &wgpu::Device, - client: &mut texture_share_vk_client::VkClient, - fence: vk::Fence, -) { - let mut needs_rebind = false; - - for slot in cache.texture_slots_mut() { - let tsv_name = match slot.tsv_name() { - Some(n) => n.to_string(), - None => continue, - }; - - if !slot.tsv_registered() { - if let Err(e) = client.find_image(&tsv_name, true) { - log::debug!("tsv find '{tsv_name}': {e}"); - continue; - } - - match client.find_image_data(&tsv_name, true) { - Ok(Some((_lock, data))) => { - if slot.resize(device, data.width, data.height) { - needs_rebind = true; - } - } - _ => continue, - } - - slot.set_tsv_registered(); - } - let raw = unsafe { gpu::raw_image(slot.texture()) }; - match client.recv_image( - &tsv_name, - raw, - vk::ImageLayout::UNDEFINED, - vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, - fence, - ) { - Ok(Some(())) => log::trace!("texture '{}' updated from '{tsv_name}'", slot.name()), - Ok(None) => {} - Err(e) => log::warn!("recv_image '{tsv_name}': {e}"), - } - } - - if needs_rebind { - cache.rebuild_bind_group(device); - } -} @@ -38,8 +38,17 @@ pub fn create_adapter( /// Requests a wgpu device + queue. pub fn create_device(adapter: &wgpu::Adapter) -> (wgpu::Device, wgpu::Queue) { + create_device_with_features(adapter, wgpu::Features::empty()) +} + +/// Requests a wgpu device + queue with additional features. +pub fn create_device_with_features( + adapter: &wgpu::Adapter, + features: wgpu::Features, +) -> (wgpu::Device, wgpu::Queue) { pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { label: Some("device"), + required_features: features, ..Default::default() })) .expect("create device") @@ -116,6 +125,32 @@ pub fn create_tsv_client(device: &wgpu::Device) -> VkClient { pub const TSV_FORMAT: texture_share_vk_base::ipc::platform::img_data::ImgFormat = texture_share_vk_base::ipc::platform::img_data::ImgFormat::R8G8B8A8; +/// Re-export ImgFormat and ImgType for use in binaries. +pub use texture_share_vk_base::ipc::platform::img_data::{ImgFormat, ImgType}; + +/// Convert a TSV ImgFormat to a wgpu TextureFormat. +pub fn img_format_to_wgpu(fmt: ImgFormat) -> wgpu::TextureFormat { + match fmt { + ImgFormat::R8G8B8A8 => wgpu::TextureFormat::Rgba8UnormSrgb, + ImgFormat::R8G8B8 => wgpu::TextureFormat::Rgba8UnormSrgb, + ImgFormat::B8G8R8A8 => wgpu::TextureFormat::Bgra8UnormSrgb, + ImgFormat::B8G8R8 => wgpu::TextureFormat::Bgra8UnormSrgb, + ImgFormat::BC1_RGBA => wgpu::TextureFormat::Bc1RgbaUnorm, + ImgFormat::BC3_RGBA => wgpu::TextureFormat::Bc3RgbaUnorm, + ImgFormat::BC7_RGBA => wgpu::TextureFormat::Bc7RgbaUnorm, + ImgFormat::Undefined => wgpu::TextureFormat::Rgba8UnormSrgb, + } +} + +/// Returns the wgpu Features required for a TSV image format. +pub fn img_format_features(fmt: ImgFormat) -> wgpu::Features { + if fmt.is_compressed() { + wgpu::Features::TEXTURE_COMPRESSION_BC + } else { + wgpu::Features::empty() + } +} + /// Extracts the raw vk::Image handle from a wgpu Texture. /// /// # Safety @@ -139,3 +174,63 @@ pub fn destroy_fence(client: &VkClient, fence: vk::Fence) { let device = &client.get_vk_setup().device.device; unsafe { device.destroy_fence(fence, None) }; } + +/// Refresh all texture inputs from TSV shared images. +/// Handles format detection and texture recreation internally. +pub fn refresh_textures( + cache: &mut crate::uniform::UniformCache, + device: &wgpu::Device, + client: &mut VkClient, + fence: vk::Fence, +) { + let mut needs_rebind = false; + + for slot in cache.texture_slots_mut() { + let tsv_name = match slot.tsv_name() { + Some(n) => n.to_string(), + None => continue, + }; + + if !slot.tsv_registered() { + if let Err(e) = client.find_image(&tsv_name, true) { + log::debug!("tsv find '{tsv_name}': {e}"); + continue; + } + + match client.find_image_data(&tsv_name, true) { + Ok(Some((_lock, data))) => { + let format = img_format_to_wgpu(data.format); + if slot.resize( + device, + data.width, + data.height, + data.depth_or_array_layers, + format, + ) { + needs_rebind = true; + } + } + _ => continue, + } + + slot.set_tsv_registered(); + } + + let raw = unsafe { raw_image(slot.texture()) }; + match client.recv_image( + &tsv_name, + raw, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + fence, + ) { + Ok(Some(())) => log::trace!("texture '{}' updated from '{tsv_name}'", slot.name()), + Ok(None) => {} + Err(e) => log::warn!("recv_image '{tsv_name}': {e}"), + } + } + + if needs_rebind { + cache.rebuild_bind_group(device); + } +} diff --git a/src/hap.rs b/src/hap.rs new file mode 100644 index 0000000..2189d7b --- /dev/null +++ b/src/hap.rs @@ -0,0 +1,162 @@ +//! Safe Rust wrapper around the HAP codec C library. +//! +//! HAP is a video codec that stores GPU-compressed DXT/BC texture data +//! (optionally Snappy-compressed). This module decodes HAP frames into +//! raw BC block data suitable for direct GPU upload. + +#[allow(non_upper_case_globals)] +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +#[allow(dead_code)] +mod ffi { + include!(concat!(env!("OUT_DIR"), "/hap_bindings.rs")); +} + +use std::fmt; + +/// Compressed texture format produced by HAP decoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HapFormat { + /// DXT1 / BC1 — 4 bits per pixel, RGB with 1-bit alpha + Bc1, + /// DXT5 / BC3 — 8 bits per pixel, RGB with full alpha + Bc3, +} + +impl HapFormat { + /// Returns the corresponding wgpu texture format. + pub fn wgpu_format(self) -> wgpu::TextureFormat { + match self { + HapFormat::Bc1 => wgpu::TextureFormat::Bc1RgbaUnorm, + HapFormat::Bc3 => wgpu::TextureFormat::Bc3RgbaUnorm, + } + } + + /// Bytes per 4x4 block for this format. + pub fn block_bytes(self) -> u32 { + match self { + HapFormat::Bc1 => 8, + HapFormat::Bc3 => 16, + } + } + + /// Compute the byte size of a compressed image with the given pixel dimensions. + pub fn compressed_size(self, width: u32, height: u32) -> usize { + let blocks_x = width.div_ceil(4); + let blocks_y = height.div_ceil(4); + (blocks_x * blocks_y * self.block_bytes()) as usize + } +} + +impl fmt::Display for HapFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HapFormat::Bc1 => write!(f, "BC1/DXT1"), + HapFormat::Bc3 => write!(f, "BC3/DXT5"), + } + } +} + +/// Error type for HAP decoding operations. +#[derive(Debug)] +pub enum HapError { + BadArguments, + BufferTooSmall, + BadFrame, + InternalError, + UnsupportedFormat(u32), +} + +impl fmt::Display for HapError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HapError::BadArguments => write!(f, "HAP: bad arguments"), + HapError::BufferTooSmall => write!(f, "HAP: buffer too small"), + HapError::BadFrame => write!(f, "HAP: bad frame data"), + HapError::InternalError => write!(f, "HAP: internal error"), + HapError::UnsupportedFormat(fmt) => write!(f, "HAP: unsupported texture format 0x{fmt:04x}"), + } + } +} + +impl std::error::Error for HapError {} + +fn result_from_code(code: u32) -> Result<(), HapError> { + match code { + 0 => Ok(()), + 1 => Err(HapError::BadArguments), + 2 => Err(HapError::BufferTooSmall), + 3 => Err(HapError::BadFrame), + _ => Err(HapError::InternalError), + } +} + +fn format_from_raw(raw: u32) -> Result<HapFormat, HapError> { + match raw { + ffi::HapTextureFormat_HapTextureFormat_RGB_DXT1 => Ok(HapFormat::Bc1), + ffi::HapTextureFormat_HapTextureFormat_RGBA_DXT5 => Ok(HapFormat::Bc3), + other => Err(HapError::UnsupportedFormat(other)), + } +} + +/// Callback passed to HapDecode for multi-threaded chunk decoding. +/// We decode sequentially (sufficient for our use case). +unsafe extern "C" fn decode_callback( + function: ffi::HapDecodeWorkFunction, + p: *mut std::os::raw::c_void, + count: std::os::raw::c_uint, + _info: *mut std::os::raw::c_void, +) { + if let Some(func) = function { + for i in 0..count { + func(p, i); + } + } +} + +/// Decode a HAP video packet into compressed BC texture data. +/// +/// `packet_data` is the raw packet data from ffmpeg. +/// `output_buf` is a reusable buffer that will be resized as needed. +/// +/// Returns the format and the number of valid bytes written to `output_buf`. +pub fn decode( + packet_data: &[u8], + output_buf: &mut Vec<u8>, +) -> Result<(HapFormat, usize), HapError> { + let mut output_length: u64 = 0; + let mut raw_format: u32 = 0; + + let result = unsafe { + ffi::HapDecode( + packet_data.as_ptr() as *const std::os::raw::c_void, + packet_data.len() as u64, + 0, // texture index (always 0 for single-texture HAP) + Some(decode_callback), + std::ptr::null_mut(), + output_buf.as_mut_ptr() as *mut std::os::raw::c_void, + output_buf.len() as u64, + &mut output_length, + &mut raw_format, + ) + }; + + result_from_code(result)?; + let format = format_from_raw(raw_format)?; + Ok((format, output_length as usize)) +} + +/// Detect the texture format of a HAP frame without fully decoding it. +pub fn get_frame_format(packet_data: &[u8]) -> Result<HapFormat, HapError> { + let mut raw_format: u32 = 0; + let result = unsafe { + ffi::HapGetFrameTextureFormat( + packet_data.as_ptr() as *const std::os::raw::c_void, + packet_data.len() as u64, + 0, + &mut raw_format, + ) + }; + result_from_code(result)?; + format_from_raw(raw_format) +} @@ -1,4 +1,5 @@ pub mod gpu; +pub mod hap; pub mod osc; pub mod renderer; pub mod uniform; diff --git a/src/uniform.rs b/src/uniform.rs index 57a0a4b..44fb541 100644 --- a/src/uniform.rs +++ b/src/uniform.rs @@ -405,8 +405,10 @@ pub struct TextureSlot { tsv_registered: bool, texture: wgpu::Texture, view: wgpu::TextureView, + format: wgpu::TextureFormat, width: u32, height: u32, + depth_or_array_layers: u32, view_dimension: wgpu::TextureViewDimension, sample_type: wgpu::TextureSampleType, multisampled: bool, @@ -421,7 +423,10 @@ impl TextureSlot { sample_type: wgpu::TextureSampleType, multisampled: bool, ) -> Self { - let (texture, view) = create_input_texture(device, &name, 1, 1); + let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let dimension = view_dimension_to_texture_dimension(view_dimension); + let (texture, view) = + create_input_texture(device, &name, 1, 1, 1, dimension, view_dimension, format); Self { name, binding, @@ -429,8 +434,10 @@ impl TextureSlot { tsv_registered: false, texture, view, + format, width: 1, height: 1, + depth_or_array_layers: 1, view_dimension, sample_type, multisampled, @@ -472,41 +479,82 @@ impl TextureSlot { self.height } - /// Recreate the texture at new dimensions. Returns true if the size actually changed. - pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) -> bool { - if self.width == width && self.height == height { + /// Recreate the texture if size or format changed. Returns true if recreated. + pub fn resize( + &mut self, + device: &wgpu::Device, + width: u32, + height: u32, + depth_or_array_layers: u32, + format: wgpu::TextureFormat, + ) -> bool { + if self.width == width + && self.height == height + && self.depth_or_array_layers == depth_or_array_layers + && self.format == format + { return false; } - let (texture, view) = create_input_texture(device, &self.name, width, height); + let dimension = view_dimension_to_texture_dimension(self.view_dimension); + let (texture, view) = create_input_texture( + device, + &self.name, + width, + height, + depth_or_array_layers, + dimension, + self.view_dimension, + format, + ); self.texture = texture; self.view = view; + self.format = format; self.width = width; self.height = height; + self.depth_or_array_layers = depth_or_array_layers; true } } +fn view_dimension_to_texture_dimension( + view_dim: wgpu::TextureViewDimension, +) -> wgpu::TextureDimension { + match view_dim { + wgpu::TextureViewDimension::D1 => wgpu::TextureDimension::D1, + wgpu::TextureViewDimension::D3 => wgpu::TextureDimension::D3, + // D2, D2Array, Cube, CubeArray all use D2 textures + _ => wgpu::TextureDimension::D2, + } +} + fn create_input_texture( device: &wgpu::Device, label: &str, width: u32, height: u32, + depth_or_array_layers: u32, + dimension: wgpu::TextureDimension, + view_dimension: wgpu::TextureViewDimension, + format: wgpu::TextureFormat, ) -> (wgpu::Texture, wgpu::TextureView) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width, height, - depth_or_array_layers: 1, + depth_or_array_layers, }, mip_level_count: 1, sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + dimension, + format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - let view = texture.create_view(&Default::default()); + let view = texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(view_dimension), + ..Default::default() + }); (texture, view) } @@ -702,8 +750,17 @@ impl UniformCache { { slot.tsv_name = old.tsv_name.clone(); slot.tsv_registered = old.tsv_registered; - if old.width > 1 || old.height > 1 { - slot.resize(device, old.width, old.height); + if old.width > 1 + || old.height > 1 + || old.depth_or_array_layers > 1 + { + slot.resize( + device, + old.width, + old.height, + old.depth_or_array_layers, + old.format, + ); } } } |
