Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ default = []
dmabuf = []

[dependencies]
ash = { git = "https://github.com/ash-rs/ash", branch = "master", features = ["loaded"] }
ash = { git = "https://github.com/ash-rs/ash", rev = "55dd56906bbb5760e9e9e6c56f45be67f67e0649", features = ["loaded"] }
thiserror = "1.0"
tracing = "0.1"
shaderc = "0.8"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# PixelForge

A Vulkan-based video encoding library for Rust, supporting H.264 and H.265 codecs.
A Vulkan-based video encoding library for Rust, supporting H.264, H.265, and AV1 codecs.

> ⚠️ **Disclaimer**: This library was developed using AI ("vibe-coding") - partly to
> see if it could be done, partly because I have practically zero experience with Vulkan.
Expand All @@ -14,7 +14,7 @@ A Vulkan-based video encoding library for Rust, supporting H.264 and H.265 codec
## Features

- **Hardware-accelerated** video encoding using Vulkan Video extensions.
- **Multiple codec support**: H.264/AVC, H.265/HEVC.
- **Multiple codec support**: H.264/AVC, H.265/HEVC, AV1.
- **GPU-native API**: Encode directly from Vulkan images (`vk::Image`).
- **Flexible configuration**: Rate control (CBR, VBR, CQP), quality levels, GOP settings.
- **Utility helpers**: [`InputImage`] for easy YUV data upload to GPU.
Expand All @@ -28,6 +28,7 @@ A Vulkan-based video encoding library for Rust, supporting H.264 and H.265 codec
|-------|--------|
| H.264/AVC | ✓ |
| H.265/HEVC | ✓ |
| AV1 | ✓ |

## Requirements

Expand Down Expand Up @@ -67,7 +68,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.app_name("My App")
.build()?;

for codec in [Codec::H264, Codec::H265] {
for codec in [Codec::H264, Codec::H265, Codec::AV1] {
println!("{:?}: encode={}",
codec,
context.supports_encode(codec)
Expand Down Expand Up @@ -136,7 +137,6 @@ cargo run --example encode_h265

1. [] Decoding.
1. [] B-frames support.
1. [] AV1 support (depends on a new version of ash with more up-to-date Vulkan support).

## Contributing

Expand Down
129 changes: 129 additions & 0 deletions examples/encode_av1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Example: AV1 Video Encoding
//!
//! Demonstrates AV1 video encoding using PixelForge with Vulkan Video.
//! Loads raw YUV420 frames from `testdata/test_frames.yuv`.

use pixelforge::{
Codec, EncodeBitDepth, EncodeConfig, Encoder, InputImage, PixelFormat, RateControlMode,
VideoContextBuilder,
};
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};

const TEST_FRAMES_PATH: &str = "testdata/test_frames.yuv";
const WIDTH: u32 = 320;
const HEIGHT: u32 = 240;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing.
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_filter(tracing_subscriber::filter::LevelFilter::INFO),
)
.init();

println!("PixelForge AV1 Encode Example\n");

// Load test frames.
let test_path = Path::new(TEST_FRAMES_PATH);
if !test_path.exists() {
eprintln!("Test frames not found at '{TEST_FRAMES_PATH}'");
eprintln!("Generate with: ffmpeg -f lavfi -i testsrc=duration=0.5:size=320x240:rate=30 -pix_fmt yuv420p -f rawvideo testdata/test_frames.yuv");
return Ok(());
}

let mut yuv_data = Vec::new();
File::open(test_path)?.read_to_end(&mut yuv_data)?;

let frame_size = (WIDTH * HEIGHT * 3 / 2) as usize;
let num_frames = yuv_data.len() / frame_size;
println!(
"Input: {num_frames} frames, {WIDTH}x{HEIGHT} YUV420, {} bytes",
yuv_data.len()
);

// Create video context.
let context = VideoContextBuilder::new()
.app_name("AV1 Encode Example")
.enable_validation(cfg!(debug_assertions))
.require_encode(Codec::AV1)
.build()?;

if !context.supports_encode(Codec::AV1) {
eprintln!("AV1 encode not supported");
return Ok(());
}

// Configure encoder.
let config = EncodeConfig::av1(WIDTH, HEIGHT)
.with_rate_control(RateControlMode::Cqp)
.with_quality_level(26)
.with_frame_rate(30, 1)
.with_gop_size(30)
.with_b_frames(0);

println!(
"Config: {:?}, QP={}, GOP={}, B-frames={}\n",
config.rate_control_mode, config.quality_level, config.gop_size, config.b_frame_count
);

// Create input image for uploading frames.
let mut input_image = InputImage::new(
context.clone(),
Codec::AV1,
WIDTH,
HEIGHT,
EncodeBitDepth::Eight,
PixelFormat::Yuv420,
)?;
let mut encoder = Encoder::new(context, config)?;
let mut output = File::create("output.av1")?;
let mut total_bytes = 0;

// Encode frames.
for i in 0..num_frames {
let frame = &yuv_data[i * frame_size..(i + 1) * frame_size];

// Upload directly to encoder's input image to avoid cross-queue
// copy issues (InputImage uses the transfer queue, encoder uses the
// video encode queue which doesn't support transfer ops).
let encoder_image = encoder.input_image();
input_image.upload_yuv420_to(encoder_image, frame)?;

// Encode the image.
for packet in encoder.encode(encoder_image)? {
total_bytes += packet.data.len();
output.write_all(&packet.data)?;
println!(
" pts={:<2} dts={:<2}: {:>5} bytes, {:?}{}",
packet.pts,
packet.dts,
packet.data.len(),
packet.frame_type,
if packet.is_key_frame { " [KEY]" } else { "" }
);
}
}

// Flush remaining frames.
for packet in encoder.flush()? {
total_bytes += packet.data.len();
output.write_all(&packet.data)?;
println!(
" pts={:<2} dts={:<2}: {:>5} bytes, {:?} (flushed)",
packet.pts,
packet.dts,
packet.data.len(),
packet.frame_type
);
}

let ratio = (num_frames * frame_size) as f64 / total_bytes as f64;
println!("\nEncoded {num_frames} frames, {total_bytes} bytes, {ratio:.1}:1 compression");
println!("Output: output.av1");

Ok(())
}
1 change: 0 additions & 1 deletion examples/query_capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

use ash::vk;
use pixelforge::{Codec, VideoContextBuilder};
use std::ffi::CStr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("PixelForge Codec Capabilities Example");
Expand Down
63 changes: 31 additions & 32 deletions examples/verify_all.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Example: Verify all encoding combinations
//!
//! Verifies H.264/H.265, 8-bit/10-bit, YUV420/YUV444 combinations.
//! Verifies H.264/H.265/AV1, 8-bit/10-bit, YUV420/YUV444 combinations.
//! Runs PSNR analysis for each combination.

use pixelforge::{
Expand All @@ -13,8 +13,8 @@ use std::path::Path;
use std::process::Command;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};

const WIDTH: u32 = 320;
const HEIGHT: u32 = 240;
const WIDTH: u32 = 480;
const HEIGHT: u32 = 320;
const FRAMES: u32 = 30;

fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -26,9 +26,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)
.init();

// Ensure test data exists
ensure_test_data("yuv420p", "testdata/test_frames_yuv420p.yuv")?;
ensure_test_data("yuv444p", "testdata/test_frames_yuv444p.yuv")?;
// Ensure test data exists (dimensions encoded in filename to avoid stale data
// when switching between branches with different WIDTH/HEIGHT constants).
let yuv420_path = format!("testdata/test_frames_{}x{}_yuv420p.yuv", WIDTH, HEIGHT);
let yuv444_path = format!("testdata/test_frames_{}x{}_yuv444p.yuv", WIDTH, HEIGHT);
ensure_test_data("yuv420p", &yuv420_path)?;
ensure_test_data("yuv444p", &yuv444_path)?;

let combinations = [
(Codec::H264, EncodeBitDepth::Eight, PixelFormat::Yuv420),
Expand All @@ -39,6 +42,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
(Codec::H265, EncodeBitDepth::Eight, PixelFormat::Yuv444),
(Codec::H265, EncodeBitDepth::Ten, PixelFormat::Yuv420),
(Codec::H265, EncodeBitDepth::Ten, PixelFormat::Yuv444),
(Codec::AV1, EncodeBitDepth::Eight, PixelFormat::Yuv420),
(Codec::AV1, EncodeBitDepth::Eight, PixelFormat::Yuv444),
(Codec::AV1, EncodeBitDepth::Ten, PixelFormat::Yuv420),
(Codec::AV1, EncodeBitDepth::Ten, PixelFormat::Yuv444),
];

let context = VideoContextBuilder::new()
Expand Down Expand Up @@ -101,15 +108,18 @@ fn run_test(
depth: EncodeBitDepth,
format: PixelFormat,
) -> Result<f64, Box<dyn std::error::Error>> {
let output_filename = format!("output_{:?}_{:?}_{:?}.bin", codec, depth, format);
// AV1 uses .obu extension for raw OBU streams (with temporal delimiters).
// H.264/H.265 use .bin for raw Annex B bitstreams.
let output_ext = if codec == Codec::AV1 { "obu" } else { "bin" };
let output_filename = format!("output_{:?}_{:?}_{:?}.{}", codec, depth, format, output_ext);
let decoded_filename = format!("decoded_{:?}_{:?}_{:?}.yuv", codec, depth, format);

// 1. Encode
{
let config = match codec {
Codec::H264 => EncodeConfig::h264(WIDTH, HEIGHT),
Codec::H265 => EncodeConfig::h265(WIDTH, HEIGHT),
_ => return Err("Unsupported codec".into()),
Codec::AV1 => EncodeConfig::av1(WIDTH, HEIGHT),
}
.with_rate_control(RateControlMode::Cqp)
.with_quality_level(10)
Expand All @@ -125,13 +135,13 @@ fn run_test(
InputImage::new(context.clone(), codec, WIDTH, HEIGHT, depth, format)?;

let input_path = match format {
PixelFormat::Yuv420 => "testdata/test_frames_yuv420p.yuv",
PixelFormat::Yuv444 => "testdata/test_frames_yuv444p.yuv",
PixelFormat::Yuv420 => format!("testdata/test_frames_{}x{}_yuv420p.yuv", WIDTH, HEIGHT),
PixelFormat::Yuv444 => format!("testdata/test_frames_{}x{}_yuv444p.yuv", WIDTH, HEIGHT),
_ => return Err("Unsupported format".into()),
};

let mut yuv_data = Vec::new();
File::open(input_path)?.read_to_end(&mut yuv_data)?;
File::open(&input_path)?.read_to_end(&mut yuv_data)?;

let frame_size = match format {
PixelFormat::Yuv420 => (WIDTH * HEIGHT * 3 / 2) as usize,
Expand All @@ -149,31 +159,20 @@ fn run_test(
}
let frame = &yuv_data[start..end];

// Upload directly to encoder's input image to avoid cross-queue
// copy issues (InputImage uses the transfer queue, encoder uses the
// video encode queue which doesn't support transfer ops).
let encoder_image = encoder.input_image();
match format {
PixelFormat::Yuv420 => input_image.upload_yuv420(frame)?,
PixelFormat::Yuv444 => {
// Bypass InputImage's internal image and upload directly to encoder's image
// to avoid potential issues with vkCmdCopyImage between images.
let encoder_image = encoder.input_image();
input_image.upload_yuv444_to(encoder_image, frame)?;
}
PixelFormat::Yuv420 => input_image.upload_yuv420_to(encoder_image, frame)?,
PixelFormat::Yuv444 => input_image.upload_yuv444_to(encoder_image, frame)?,
_ => return Err("Unsupported format".into()),
}

// For YUV444, we uploaded directly to encoder image.
// For YUV420, we uploaded to input_image.image().
let src_image = match format {
PixelFormat::Yuv420 => input_image.image(),
PixelFormat::Yuv444 => encoder.input_image(),
_ => return Err("Unsupported format".into()),
};

for packet in encoder.encode(src_image)? {
for packet in encoder.encode(encoder_image)? {
output_file.write_all(&packet.data)?;
}
}
// Flush? The encoder doesn't seem to have a flush method in the example,
// but usually we just stop feeding frames.
}

// 2. Decode to raw YUV
Expand All @@ -191,8 +190,8 @@ fn run_test(

// Let's decode to the input format (8-bit).
let (input_pix_fmt, input_path) = match format {
PixelFormat::Yuv420 => ("yuv420p", "testdata/test_frames_yuv420p.yuv"),
PixelFormat::Yuv444 => ("yuv444p", "testdata/test_frames_yuv444p.yuv"),
PixelFormat::Yuv420 => ("yuv420p", format!("testdata/test_frames_{}x{}_yuv420p.yuv", WIDTH, HEIGHT)),
PixelFormat::Yuv444 => ("yuv444p", format!("testdata/test_frames_{}x{}_yuv444p.yuv", WIDTH, HEIGHT)),
_ => return Err("Unsupported format".into()),
};

Expand Down Expand Up @@ -233,7 +232,7 @@ fn run_test(
"-f",
"rawvideo",
"-i",
input_path,
&input_path,
"-s",
&format!("{}x{}", WIDTH, HEIGHT),
"-pix_fmt",
Expand Down
Loading
Loading