Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ output.*

# Generated at runtime by examples/verify_all.rs (ensure_test_data); dimension-keyed.
testdata/test_frames_*x*_*.yuv

# Stray raw decode outputs from manual testing
*.yuv
133 changes: 133 additions & 0 deletions examples/decode_adopted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! Decode on a caller-created device.
//!
//! Mirrors what an application with its own Vulkan device does: create the
//! instance and device itself, then hand them to pixelforge via
//! `build_from_existing_decode` so decoded images live on the app's device.
//!
//! Usage:
//! decode_adopted <input.264> [output.yuv]
//!
//! Output is NV12 in display order, comparable to:
//! ffmpeg -i input.264 -pix_fmt nv12 reference.yuv

use std::fs::File;
use std::io::Write;

use ash::vk;
use ash::vk::TaggedStructure;
use pixelforge::decoder::{DecodeConfig, Decoder, access_units};
use pixelforge::encoder::Codec;
use pixelforge::vulkan::VideoContextBuilder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let input_path = args
.next()
.expect("usage: decode_adopted <input.264> [output.yuv]");
let output_path = args.next();

// --- The application creates its own instance and device ---
let entry = unsafe { ash::Entry::load()? };
let app_name = c"decode-adopted";
let app_info = vk::ApplicationInfo::default()
.application_name(app_name)
.api_version(vk::API_VERSION_1_3);
let instance_info = vk::InstanceCreateInfo::default().application_info(&app_info);
let instance = unsafe { entry.create_instance(&instance_info, None)? };

// Ask pixelforge what a decode device needs, and pick a physical device that
// satisfies it — exactly the negotiation an app would do.
let builder = VideoContextBuilder::new().require_decode(Codec::H264);
let physical_devices = unsafe { instance.enumerate_physical_devices()? };
let (physical_device, reqs) = physical_devices
.iter()
.find_map(|&pd| {
builder
.decode_device_requirements(&entry, &instance, pd)
.ok()
.map(|r| (pd, r))
})
.expect("no physical device can decode H.264");

let name = unsafe {
std::ffi::CStr::from_ptr(
instance
.get_physical_device_properties(physical_device)
.device_name
.as_ptr(),
)
};
println!("Device: {}", name.to_string_lossy());
println!(
"pixelforge needs queue families {:?} and {} extensions",
reqs.queue_families,
reqs.extensions.len()
);

// The app merges pixelforge's requirements with its own. Here there is
// nothing else, so we use them directly.
let priorities = [1.0f32];
let queue_infos: Vec<vk::DeviceQueueCreateInfo> = reqs
.queue_families
.iter()
.map(|&f| {
vk::DeviceQueueCreateInfo::default()
.queue_family_index(f)
.queue_priorities(&priorities)
})
.collect();
let ext_ptrs: Vec<*const std::os::raw::c_char> =
reqs.extensions.iter().map(|e| e.as_ptr()).collect();

let mut sync2 = vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true);
let device_info = vk::DeviceCreateInfo::default()
.queue_create_infos(&queue_infos)
.enabled_extension_names(&ext_ptrs)
.push(&mut sync2);
let device = unsafe { instance.create_device(physical_device, &device_info, None)? };

// --- Hand the app's device to pixelforge ---
let context = builder.build_from_existing_decode(
entry.clone(),
instance.clone(),
physical_device,
device.clone(),
)?;

let mut decoder = Decoder::new(context, DecodeConfig::h264())?;
let stream = std::fs::read(&input_path)?;
let mut output = output_path.as_ref().map(File::create).transpose()?;
let mut count = 0usize;

let write = |decoder: &mut Decoder,
frame: &pixelforge::decoder::DecodedFrame,
output: &mut Option<File>,
count: &mut usize|
-> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = output.as_mut() {
let data = decoder.download(frame)?;
file.write_all(&data.y)?;
file.write_all(&data.uv)?;
}
*count += 1;
Ok(())
};

for (i, au) in access_units(&stream).enumerate() {
for frame in decoder.decode(au, i as u64)? {
write(&mut decoder, &frame, &mut output, &mut count)?;
}
}
for frame in decoder.flush()? {
write(&mut decoder, &frame, &mut output, &mut count)?;
}
println!("Decoded {} frames on the adopted device", count);

// The context borrowed the device; drop it before we destroy the device.
drop(decoder);
unsafe {
device.destroy_device(None);
instance.destroy_instance(None);
}
Ok(())
}
108 changes: 108 additions & 0 deletions examples/decode_h264.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Decode an H.264 Annex B stream to raw YUV.
//!
//! Usage:
//! cargo run --example decode_h264 -- input.264 [output.yuv]
//!
//! Produces the same layout as `ffmpeg -i input.264 -pix_fmt nv12 out.yuv`,
//! so the output can be compared directly:
//!
//! ffmpeg -i input.264 -pix_fmt nv12 reference.yuv
//! cmp reference.yuv output.yuv

use std::fs::File;
use std::io::Write;

use pixelforge::decoder::{DecodeConfig, Decoder, access_units};
use pixelforge::encoder::Codec;
use pixelforge::vulkan::VideoContextBuilder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();

let mut args = std::env::args().skip(1);
let input_path = args.next().unwrap_or_else(|| {
eprintln!("usage: decode_h264 <input.264> [output.yuv]");
std::process::exit(1);
});
let output_path = args.next();

// PIXELFORGE_VALIDATION=1 enables the Vulkan validation layers, which
// report the exact offending call for any API misuse.
let validation = std::env::var("PIXELFORGE_VALIDATION").is_ok();
let context = VideoContextBuilder::new()
.app_name("pixelforge-decode")
.require_decode(Codec::H264)
.enable_validation(validation)
.build()?;

println!(
"Device: {}",
unsafe { std::ffi::CStr::from_ptr(context.device_properties().device_name.as_ptr()) }
.to_string_lossy()
);

// Display order by default: frames arrive ready to present, no sorting.
// Set PIXELFORGE_DECODE_ORDER=1 for the low-latency decode-order path.
let config = if std::env::var("PIXELFORGE_DECODE_ORDER").is_ok() {
DecodeConfig::h264().with_decode_order()
} else {
DecodeConfig::h264()
};
let mut decoder = Decoder::new(context, config)?;
let stream = std::fs::read(&input_path)?;
let mut output = output_path.as_ref().map(File::create).transpose()?;

let start = std::time::Instant::now();
let mut frame_count = 0usize;
let mut last_info = None;

// Write each frame as it comes out. `flush` at the end drains the frames
// the reorder buffer was still holding.
let write = |decoder: &mut Decoder,
frame: &pixelforge::decoder::DecodedFrame,
output: &mut Option<File>|
-> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = output.as_mut() {
let data = decoder.download(frame)?;
file.write_all(&data.y)?;
file.write_all(&data.uv)?;
}
Ok(())
};

for au in access_units(&stream) {
let decoded = match decoder.decode(au, frame_count as u64) {
Ok(frames) => frames,
// Joining mid-stream (or after loss): skip until a keyframe. A live
// client would ask the sender for an IDR here.
Err(pixelforge::error::PixelForgeError::NeedsKeyframe(_)) => continue,
Err(e) => return Err(e.into()),
};
for frame in decoded {
write(&mut decoder, &frame, &mut output)?;
last_info = Some((frame.width, frame.height, frame.poc, frame.is_idr));
frame_count += 1;
}
}
for frame in decoder.flush()? {
write(&mut decoder, &frame, &mut output)?;
last_info = Some((frame.width, frame.height, frame.poc, frame.is_idr));
frame_count += 1;
}
let decode_time = start.elapsed();

println!(
"Decoded {} frames in {:.1?} ({:.1} fps)",
frame_count,
decode_time,
frame_count as f64 / decode_time.as_secs_f64()
);
if let Some((w, h, poc, idr)) = last_info {
println!("Last frame: {}x{} poc={} idr={}", w, h, poc, idr);
}
if let Some(path) = output_path {
println!("Wrote raw frames to {}", path);
}

Ok(())
}
142 changes: 142 additions & 0 deletions examples/roundtrip_h264.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! Encode with pixelforge, then decode the result with pixelforge.
//!
//! The decoder must handle anything this project's encoder can produce. That is
//! not covered by decoding third-party streams: the encoder makes its own
//! choices (notably explicit reference marking for B-frames), so it needs its
//! own round trip.
//!
//! Usage:
//! roundtrip_h264 <input.yuv> <width> <height> <b_frames> <gop> <out.h264> [out.yuv]
//!
//! The input is planar YUV420 (I420). The decoded output is NV12 in display
//! order, so it can be compared against ffmpeg decoding the same bitstream:
//!
//! ffmpeg -i out.h264 -pix_fmt nv12 reference.yuv
//! cmp reference.yuv out.yuv

use std::collections::VecDeque;
use std::fs::File;
use std::io::{Read, Write};

use pixelforge::decoder::{DecodeConfig, Decoder, access_units};
use pixelforge::{
Codec, EncodeBitDepth, EncodeConfig, Encoder, InputImage, PixelFormat, RateControlMode,
VideoContextBuilder,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();

let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() < 6 {
eprintln!(
"usage: roundtrip_h264 <input.yuv> <width> <height> <b_frames> <gop> <out.h264> [out.yuv]"
);
std::process::exit(1);
}
let input_path = &args[0];
let width: u32 = args[1].parse()?;
let height: u32 = args[2].parse()?;
let b_frames: u32 = args[3].parse()?;
let gop: u32 = args[4].parse()?;
let bitstream_path = &args[5];
let yuv_path = args.get(6);

let context = VideoContextBuilder::new()
.app_name("pixelforge-roundtrip")
.require_encode(Codec::H264)
.require_decode(Codec::H264)
.enable_validation(std::env::var("PIXELFORGE_VALIDATION").is_ok())
.build()?;

// --- Encode ---
let mut yuv = Vec::new();
File::open(input_path)?.read_to_end(&mut yuv)?;
let frame_size = (width * height * 3 / 2) as usize;
let frame_count = yuv.len() / frame_size;

let config = EncodeConfig::h264(width, height)
.with_rate_control(RateControlMode::Cqp)
.with_quality_level(26)
.with_frame_rate(30, 1)
.with_gop_size(gop)
.with_b_frames(b_frames);

let mut input_image = InputImage::new(
context.clone(),
Codec::H264,
width,
height,
EncodeBitDepth::Eight,
PixelFormat::Yuv420,
)?;
let mut encoder = Encoder::new(context.clone(), config)?;

let mut bitstream = Vec::new();
let mut pending: VecDeque<pixelforge::EncodeFuture> = VecDeque::new();
for i in 0..frame_count {
input_image.upload_yuv420(&yuv[i * frame_size..(i + 1) * frame_size])?;
pending.push_back(encoder.encode(input_image.image())?);
while pending.len() > 2 {
let packet = pollster::block_on(pending.pop_front().expect("non-empty"))?;
bitstream.extend_from_slice(&packet.data);
}
}
encoder.flush()?;
while let Some(future) = pending.pop_front() {
let packet = pollster::block_on(future)?;
bitstream.extend_from_slice(&packet.data);
}
File::create(bitstream_path)?.write_all(&bitstream)?;
println!(
"Encoded {} frames -> {} bytes (b_frames={}, gop={})",
frame_count,
bitstream.len(),
b_frames,
gop
);

// --- Decode it back ---
// Display order by default, so frames come out ready to write; `flush`
// drains whatever the reorder buffer still holds at end of stream.
let mut decoder = Decoder::new(context, DecodeConfig::h264())?;
let mut out = yuv_path.map(File::create).transpose()?;
let mut decoded_count = 0usize;

let write = |decoder: &mut Decoder,
frame: &pixelforge::decoder::DecodedFrame,
out: &mut Option<File>,
count: &mut usize|
-> Result<(), Box<dyn std::error::Error>> {
if let Some(file) = out.as_mut() {
let data = decoder.download(frame)?;
file.write_all(&data.y)?;
file.write_all(&data.uv)?;
}
*count += 1;
Ok(())
};

for au in access_units(&bitstream) {
for frame in decoder.decode(au, decoded_count as u64)? {
write(&mut decoder, &frame, &mut out, &mut decoded_count)?;
}
}
for frame in decoder.flush()? {
write(&mut decoder, &frame, &mut out, &mut decoded_count)?;
}
println!("Decoded {} frames", decoded_count);

if decoded_count != frame_count {
return Err(format!(
"round trip lost frames: encoded {} but decoded {}",
frame_count, decoded_count
)
.into());
}
if yuv_path.is_some() {
println!("Wrote decoded frames to {}", yuv_path.unwrap());
}

Ok(())
}
Loading
Loading