Skip to content
Merged
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
16 changes: 12 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,12 @@ A Vulkan-based video encoding library for Rust, supporting H.264 and H.265 codec
|-------|--------|
| H.264/AVC | ✓ |
| H.265/HEVC | ✓ |
| AV1 | ✓ (experimental) |

> ⚠️ **AV1 Warning**: AV1 encoding is experimental. On NVIDIA GPUs, P-frames cannot
> reference other P-frames, causing all P-frames to reference the I-frame instead. This
> leads to progressively larger frame sizes over time. Consider using H.264 or HEVC
> until this is resolved.

## Requirements

Expand Down Expand Up @@ -67,7 +73,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 @@ -130,13 +136,15 @@ cargo run --example encode_h264

# H.265 encoding example
cargo run --example encode_h265

# AV1 encoding example
cargo run --example encode_av1
```

## TODO's

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
130 changes: 130 additions & 0 deletions examples/encode_av1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! 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 with RUST_LOG support.
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
),
)
.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 YUV420 data to the input image.
input_image.upload_yuv420(frame)?;

// Encode the image (passing InputImage's image, which triggers
// an internal copy to the encoder's input image with proper
// layout transitions).
for packet in encoder.encode(input_image.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(())
}
6 changes: 4 additions & 2 deletions examples/encode_h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ 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),
tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
),
)
.init();

Expand Down
6 changes: 4 additions & 2 deletions examples/encode_h265.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ 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),
tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
),
)
.init();

Expand Down
12 changes: 11 additions & 1 deletion examples/query_capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@

use ash::vk;
use pixelforge::{Codec, VideoContextBuilder};
use std::ffi::CStr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing.
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
),
)
.init();

println!("PixelForge Codec Capabilities Example");
println!("======================================\n");

Expand Down
Loading