Skip to content

Commit 48488d3

Browse files
authored
Merge pull request #3 from hgaiser/av1-support-rebased
Add AV1 support v2
2 parents dd0e71f + f1dbf7e commit 48488d3

19 files changed

Lines changed: 2568 additions & 356 deletions

‎Cargo.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ default = []
1414
dmabuf = []
1515

1616
[dependencies]
17-
ash = { git = "https://github.com/ash-rs/ash", branch = "master", features = ["loaded"] }
17+
ash = { git = "https://github.com/ash-rs/ash", rev = "55dd56906bbb5760e9e9e6c56f45be67f67e0649", features = ["loaded"] }
1818
thiserror = "1.0"
1919
tracing = "0.1"
2020
shaderc = "0.8"

‎README.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# PixelForge
66

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

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

1616
- **Hardware-accelerated** video encoding using Vulkan Video extensions.
17-
- **Multiple codec support**: H.264/AVC, H.265/HEVC.
17+
- **Multiple codec support**: H.264/AVC, H.265/HEVC, AV1.
1818
- **GPU-native API**: Encode directly from Vulkan images (`vk::Image`).
1919
- **Flexible configuration**: Rate control (CBR, VBR, CQP), quality levels, GOP settings.
2020
- **Utility helpers**: [`InputImage`] for easy YUV data upload to GPU.
@@ -28,6 +28,12 @@ A Vulkan-based video encoding library for Rust, supporting H.264 and H.265 codec
2828
|-------|--------|
2929
| H.264/AVC | ✓ |
3030
| H.265/HEVC | ✓ |
31+
| AV1 | ✓ (experimental) |
32+
33+
> ⚠️ **AV1 Warning**: AV1 encoding is experimental. On NVIDIA GPUs, P-frames cannot
34+
> reference other P-frames, causing all P-frames to reference the I-frame instead. This
35+
> leads to progressively larger frame sizes over time. Consider using H.264 or HEVC
36+
> until this is resolved.
3137
3238
## Requirements
3339

@@ -67,7 +73,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
6773
.app_name("My App")
6874
.build()?;
6975

70-
for codec in [Codec::H264, Codec::H265] {
76+
for codec in [Codec::H264, Codec::H265, Codec::AV1] {
7177
println!("{:?}: encode={}",
7278
codec,
7379
context.supports_encode(codec)
@@ -130,13 +136,15 @@ cargo run --example encode_h264
130136
131137
# H.265 encoding example
132138
cargo run --example encode_h265
139+
140+
# AV1 encoding example
141+
cargo run --example encode_av1
133142
```
134143

135144
## TODO's
136145

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

141149
## Contributing
142150

‎examples/encode_av1.rs‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Example: AV1 Video Encoding
2+
//!
3+
//! Demonstrates AV1 video encoding using PixelForge with Vulkan Video.
4+
//! Loads raw YUV420 frames from `testdata/test_frames.yuv`.
5+
6+
use pixelforge::{
7+
Codec, EncodeBitDepth, EncodeConfig, Encoder, InputImage, PixelFormat, RateControlMode,
8+
VideoContextBuilder,
9+
};
10+
use std::fs::File;
11+
use std::io::{Read, Write};
12+
use std::path::Path;
13+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
14+
15+
const TEST_FRAMES_PATH: &str = "testdata/test_frames.yuv";
16+
const WIDTH: u32 = 320;
17+
const HEIGHT: u32 = 240;
18+
19+
fn main() -> Result<(), Box<dyn std::error::Error>> {
20+
// Initialize tracing with RUST_LOG support.
21+
tracing_subscriber::registry()
22+
.with(
23+
tracing_subscriber::fmt::layer().with_filter(
24+
tracing_subscriber::EnvFilter::try_from_default_env()
25+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
26+
),
27+
)
28+
.init();
29+
30+
println!("PixelForge AV1 Encode Example\n");
31+
32+
// Load test frames.
33+
let test_path = Path::new(TEST_FRAMES_PATH);
34+
if !test_path.exists() {
35+
eprintln!("Test frames not found at '{TEST_FRAMES_PATH}'");
36+
eprintln!("Generate with: ffmpeg -f lavfi -i testsrc=duration=0.5:size=320x240:rate=30 -pix_fmt yuv420p -f rawvideo testdata/test_frames.yuv");
37+
return Ok(());
38+
}
39+
40+
let mut yuv_data = Vec::new();
41+
File::open(test_path)?.read_to_end(&mut yuv_data)?;
42+
43+
let frame_size = (WIDTH * HEIGHT * 3 / 2) as usize;
44+
let num_frames = yuv_data.len() / frame_size;
45+
println!(
46+
"Input: {num_frames} frames, {WIDTH}x{HEIGHT} YUV420, {} bytes",
47+
yuv_data.len()
48+
);
49+
50+
// Create video context.
51+
let context = VideoContextBuilder::new()
52+
.app_name("AV1 Encode Example")
53+
.enable_validation(cfg!(debug_assertions))
54+
.require_encode(Codec::AV1)
55+
.build()?;
56+
57+
if !context.supports_encode(Codec::AV1) {
58+
eprintln!("AV1 encode not supported");
59+
return Ok(());
60+
}
61+
62+
// Configure encoder.
63+
let config = EncodeConfig::av1(WIDTH, HEIGHT)
64+
.with_rate_control(RateControlMode::Cqp)
65+
.with_quality_level(26)
66+
.with_frame_rate(30, 1)
67+
.with_gop_size(30)
68+
.with_b_frames(0);
69+
70+
println!(
71+
"Config: {:?}, QP={}, GOP={}, B-frames={}\n",
72+
config.rate_control_mode, config.quality_level, config.gop_size, config.b_frame_count
73+
);
74+
75+
// Create input image for uploading frames.
76+
let mut input_image = InputImage::new(
77+
context.clone(),
78+
Codec::AV1,
79+
WIDTH,
80+
HEIGHT,
81+
EncodeBitDepth::Eight,
82+
PixelFormat::Yuv420,
83+
)?;
84+
let mut encoder = Encoder::new(context, config)?;
85+
let mut output = File::create("output.av1")?;
86+
let mut total_bytes = 0;
87+
88+
// Encode frames.
89+
for i in 0..num_frames {
90+
let frame = &yuv_data[i * frame_size..(i + 1) * frame_size];
91+
92+
// Upload YUV420 data to the input image.
93+
input_image.upload_yuv420(frame)?;
94+
95+
// Encode the image (passing InputImage's image, which triggers
96+
// an internal copy to the encoder's input image with proper
97+
// layout transitions).
98+
for packet in encoder.encode(input_image.image())? {
99+
total_bytes += packet.data.len();
100+
output.write_all(&packet.data)?;
101+
println!(
102+
" pts={:<2} dts={:<2}: {:>5} bytes, {:?}{}",
103+
packet.pts,
104+
packet.dts,
105+
packet.data.len(),
106+
packet.frame_type,
107+
if packet.is_key_frame { " [KEY]" } else { "" }
108+
);
109+
}
110+
}
111+
112+
// Flush remaining frames.
113+
for packet in encoder.flush()? {
114+
total_bytes += packet.data.len();
115+
output.write_all(&packet.data)?;
116+
println!(
117+
" pts={:<2} dts={:<2}: {:>5} bytes, {:?} (flushed)",
118+
packet.pts,
119+
packet.dts,
120+
packet.data.len(),
121+
packet.frame_type
122+
);
123+
}
124+
125+
let ratio = (num_frames * frame_size) as f64 / total_bytes as f64;
126+
println!("\nEncoded {num_frames} frames, {total_bytes} bytes, {ratio:.1}:1 compression");
127+
println!("Output: output.av1");
128+
129+
Ok(())
130+
}

‎examples/encode_h264.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
2020
// Initialize tracing.
2121
tracing_subscriber::registry()
2222
.with(
23-
tracing_subscriber::fmt::layer()
24-
.with_filter(tracing_subscriber::filter::LevelFilter::INFO),
23+
tracing_subscriber::fmt::layer().with_filter(
24+
tracing_subscriber::EnvFilter::try_from_default_env()
25+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
26+
),
2527
)
2628
.init();
2729

‎examples/encode_h265.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
2020
// Initialize tracing.
2121
tracing_subscriber::registry()
2222
.with(
23-
tracing_subscriber::fmt::layer()
24-
.with_filter(tracing_subscriber::filter::LevelFilter::INFO),
23+
tracing_subscriber::fmt::layer().with_filter(
24+
tracing_subscriber::EnvFilter::try_from_default_env()
25+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
26+
),
2527
)
2628
.init();
2729

‎examples/query_capabilities.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@
55
66
use ash::vk;
77
use pixelforge::{Codec, VideoContextBuilder};
8-
use std::ffi::CStr;
8+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
99

1010
fn main() -> Result<(), Box<dyn std::error::Error>> {
11+
// Initialize tracing.
12+
tracing_subscriber::registry()
13+
.with(
14+
tracing_subscriber::fmt::layer().with_filter(
15+
tracing_subscriber::EnvFilter::try_from_default_env()
16+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
17+
),
18+
)
19+
.init();
20+
1121
println!("PixelForge Codec Capabilities Example");
1222
println!("======================================\n");
1323

0 commit comments

Comments
 (0)