summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-04-20 09:01:27 +0000
committers-ol <s+removethis@s-ol.nu>2026-05-14 14:46:13 +0000
commit15a5bc845648371619ffbdbc7a740d68c3c26872 (patch)
treecad36fafa0d143add0fcf0ff1a2c911e37463ff6 /src
parenttsv integration, split binaries (diff)
downloadwgsl-view-15a5bc845648371619ffbdbc7a740d68c3c26872.tar.gz
wgsl-view-15a5bc845648371619ffbdbc7a740d68c3c26872.zip
add tsv-video-buffer
Diffstat (limited to 'src')
-rw-r--r--src/bin/tsv_video_buffer.rs410
-rw-r--r--src/bin/tsv_video_stream.rs2
-rw-r--r--src/bin/wgsl_render.rs60
-rw-r--r--src/gpu.rs95
-rw-r--r--src/hap.rs162
-rw-r--r--src/lib.rs1
-rw-r--r--src/uniform.rs79
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);
- }
-}
diff --git a/src/gpu.rs b/src/gpu.rs
index e6529f9..993e524 100644
--- a/src/gpu.rs
+++ b/src/gpu.rs
@@ -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)
+}
diff --git a/src/lib.rs b/src/lib.rs
index 22a55d8..57a1042 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+ );
}
}
}