1use std::collections::HashSet;
5use std::ffi::CString;
6use std::path::Path;
7
8use alpha2_ffmpeg_bridge::{VulkanFrameInfo, VulkanFrameLease, pixel_format_name};
9use alpha2_media::DecodeError;
10use alpha2_native_gpu::probe_vulkan_submission;
11use rsmpeg::avcodec::{AVCodec, AVCodecContext};
12use rsmpeg::avutil::{AVFrame, AVHWDeviceContext};
13use rsmpeg::error::RsmpegError;
14use rsmpeg::ffi;
15
16use crate::convert::adapter_error;
17use crate::decode::{open_input, probe_context};
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct VulkanDecodeProbe {
22 pub decoder: String,
24 pub hardware_format: String,
26 pub software_format: String,
28 pub display_size: (i32, i32),
30 pub coded_size: (i32, i32),
32 pub hardware_frames: u64,
34 pub unique_images: usize,
36 pub reused_images: u64,
38 pub all_images_sampled: bool,
40 pub all_images_synchronized: bool,
42 pub seek_frames: u64,
44 pub device: u64,
46 pub physical_device: u64,
48 pub max_plane_images: u32,
50 pub interop_submissions: u64,
52 pub source_cpu_plane_copy_bytes: u64,
54 pub source_upload_bytes: u64,
56 pub source_download_bytes: u64,
58}
59
60#[allow(clippy::too_many_lines)]
72pub fn probe_vulkan_decode(
73 path: impl AsRef<Path>,
74 frame_limit: usize,
75) -> Result<VulkanDecodeProbe, DecodeError> {
76 if frame_limit < 2 {
77 return Err(DecodeError::Unsupported(
78 "Vulkan decode probe requires at least two frames".into(),
79 ));
80 }
81 let mut input = open_input(path.as_ref())?;
82 let selected = probe_context(&input, path.as_ref())?
83 .selected_video
84 .ok_or_else(|| DecodeError::Unsupported("source has no video stream".into()))?;
85 input.discard_all_but(selected);
86 let parameters = input
87 .streams()
88 .get(selected)
89 .ok_or(DecodeError::OutOfRange)?
90 .codecpar();
91 let preferred = AVCodec::find_decoder(parameters.codec_id)
92 .ok_or_else(|| DecodeError::Unsupported("selected video codec has no decoder".into()))?;
93 let (codec, config) = crate::decode::registered_video_decoders(parameters.codec_id)
96 .find_map(|codec| {
97 let config = (0..)
98 .map_while(|index| codec.hw_config(index))
99 .find(|config| {
100 config.device_type == ffi::AV_HWDEVICE_TYPE_VULKAN
101 && config.pix_fmt == ffi::AV_PIX_FMT_VULKAN
102 && crate::decode::has_hw_config_method(
103 config.methods,
104 ffi::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX,
105 )
106 })?;
107 Some((codec, config))
108 })
109 .ok_or_else(|| {
110 DecodeError::Unsupported(format!(
111 "decoder {} has no Vulkan HW_DEVICE_CTX configuration",
112 preferred.name().to_string_lossy()
113 ))
114 })?;
115 debug_assert_eq!(config.pix_fmt, ffi::AV_PIX_FMT_VULKAN);
116
117 let requested_device = CString::new(
121 alpha2_native_gpu::shared_device_index(
122 std::env::var("ALPHA2_VULKAN_DEVICE").ok().as_deref(),
123 )
124 .map_err(|error| DecodeError::Unsupported(error.to_string()))?
125 .to_string(),
126 )
127 .map_err(|_| DecodeError::Unsupported("a device index held a NUL byte".into()))?;
128 let device = AVHWDeviceContext::create(
129 ffi::AV_HWDEVICE_TYPE_VULKAN,
130 Some(&requested_device),
131 None,
132 0,
133 )
134 .map_err(adapter_error)?;
135 let mut decoder = AVCodecContext::new(&codec);
136 decoder.apply_codecpar(¶meters).map_err(adapter_error)?;
137 drop(parameters);
138 decoder.set_pkt_timebase(input.streams()[selected].time_base);
139 decoder.set_get_format(Some(alpha2_ffmpeg_bridge::vulkan_get_format_callback()));
140 decoder.set_hw_device_ctx(device);
141 decoder.open(None).map_err(adapter_error)?;
142
143 let mut demux_eof = false;
144 let mut flushed = false;
145 let mut images = HashSet::new();
146 let mut reuse = 0_u64;
147 let mut first_info = None;
148 let mut all_sampled = true;
149 let mut all_synchronized = true;
150 let mut max_plane_images = 0;
151 let mut frames = 0_u64;
152 while frames < frame_limit as u64 {
153 let Some(frame) = decode_next_raw(
154 &mut input,
155 &mut decoder,
156 selected,
157 &mut demux_eof,
158 &mut flushed,
159 )?
160 else {
161 break;
162 };
163 let lease = VulkanFrameLease::new(frame);
164 let info = validate_frame(&lease, first_info.as_ref())?;
165 probe_vulkan_submission(&lease).map_err(adapter_error)?;
166 let primary = info
167 .images
168 .iter()
169 .copied()
170 .find(|image| *image != 0)
171 .ok_or_else(|| DecodeError::Adapter("Vulkan frame has no image".into()))?;
172 if !images.insert(primary) {
173 reuse += 1;
174 }
175 all_sampled &= info.sampled;
176 all_synchronized &= synchronized(&info);
177 max_plane_images = max_plane_images.max(info.plane_count);
178 first_info.get_or_insert(info);
179 frames += 1;
180 }
181 let first_info = first_info.ok_or_else(|| {
182 DecodeError::Unsupported("decoder returned no Vulkan hardware frame".into())
183 })?;
184 if !all_sampled {
185 return Err(DecodeError::Unsupported(
186 "decoded Vulkan image is not sampleable".into(),
187 ));
188 }
189 if !all_synchronized {
190 return Err(DecodeError::Adapter(
191 "decoded Vulkan image has no ready timeline semaphore".into(),
192 ));
193 }
194
195 input
196 .seek(
197 i32::try_from(selected).map_err(|_| DecodeError::OutOfRange)?,
198 0,
199 ffi::AVSEEK_FLAG_BACKWARD.cast_signed(),
200 )
201 .map_err(adapter_error)?;
202 decoder.flush_buffers();
203 demux_eof = false;
204 flushed = false;
205 let seek_frame = decode_next_raw(
206 &mut input,
207 &mut decoder,
208 selected,
209 &mut demux_eof,
210 &mut flushed,
211 )?
212 .ok_or_else(|| DecodeError::Adapter("no Vulkan frame after seek".into()))?;
213 let seek_lease = VulkanFrameLease::new(seek_frame);
214 validate_frame(&seek_lease, Some(&first_info))?;
215 probe_vulkan_submission(&seek_lease).map_err(adapter_error)?;
216
217 Ok(VulkanDecodeProbe {
218 decoder: codec.name().to_string_lossy().into_owned(),
219 hardware_format: format_name(first_info.hardware_format),
220 software_format: format_name(first_info.software_format),
221 display_size: (first_info.width, first_info.height),
222 coded_size: (first_info.coded_width, first_info.coded_height),
223 hardware_frames: frames,
224 unique_images: images.len(),
225 reused_images: reuse,
226 all_images_sampled: all_sampled,
227 all_images_synchronized: all_synchronized,
228 seek_frames: 1,
229 device: first_info.device,
230 physical_device: first_info.physical_device,
231 max_plane_images,
232 interop_submissions: frames + 1,
233 source_cpu_plane_copy_bytes: 0,
234 source_upload_bytes: 0,
235 source_download_bytes: 0,
236 })
237}
238
239fn decode_next_raw(
240 input: &mut alpha2_ffmpeg_bridge::DemuxInput,
241 decoder: &mut AVCodecContext,
242 stream_index: usize,
243 demux_eof: &mut bool,
244 flushed: &mut bool,
245) -> Result<Option<AVFrame>, DecodeError> {
246 loop {
247 match decoder.receive_frame() {
248 Ok(frame) => return Ok(Some(frame)),
249 Err(RsmpegError::DecoderDrainError) => {}
250 Err(RsmpegError::DecoderFlushedError) => return Ok(None),
251 Err(error) => return Err(adapter_error(error)),
252 }
253 if *demux_eof {
254 if !*flushed {
255 decoder.send_packet(None).map_err(adapter_error)?;
256 *flushed = true;
257 continue;
258 }
259 return Ok(None);
260 }
261 let Some(packet) = input.read_packet().map_err(adapter_error)? else {
262 *demux_eof = true;
263 continue;
264 };
265 if usize::try_from(packet.stream_index).ok() == Some(stream_index) {
266 decoder.send_packet(Some(&packet)).map_err(adapter_error)?;
267 }
268 }
269}
270
271fn validate_frame(
272 frame: &VulkanFrameLease,
273 expected: Option<&VulkanFrameInfo>,
274) -> Result<VulkanFrameInfo, DecodeError> {
275 let info = frame.info().map_err(adapter_error)?;
276 if info.hardware_format != ffi::AV_PIX_FMT_VULKAN {
277 return Err(DecodeError::Adapter(format!(
278 "decoder returned {} instead of vulkan",
279 format_name(info.hardware_format)
280 )));
281 }
282 if let Some(expected) = expected {
283 if info.device != expected.device || info.physical_device != expected.physical_device {
284 return Err(DecodeError::Adapter(
285 "Vulkan decoder changed device while probing".into(),
286 ));
287 }
288 if info.software_format != expected.software_format {
289 return Err(DecodeError::Adapter(
290 "Vulkan decoder changed surface format while probing".into(),
291 ));
292 }
293 if info.coded_width != expected.coded_width || info.coded_height != expected.coded_height {
294 return Err(DecodeError::Adapter(
295 "Vulkan decoder changed surface extent while probing".into(),
296 ));
297 }
298 }
299 if info.coded_width < info.width || info.coded_height < info.height {
302 return Err(DecodeError::Adapter(
303 "Vulkan surface is smaller than the frame it carries".into(),
304 ));
305 }
306 Ok(info)
307}
308
309fn synchronized(info: &VulkanFrameInfo) -> bool {
310 info.images
311 .iter()
312 .enumerate()
313 .filter(|(_, image)| **image != 0)
314 .all(|(index, _)| info.semaphores[index] != 0)
315}
316
317fn format_name(format: ffi::AVPixelFormat) -> String {
318 pixel_format_name(format).unwrap_or_else(|| format!("unknown({format})"))
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn synchronization_ignores_unused_plane_slots() {
327 let info = VulkanFrameInfo {
328 hardware_format: ffi::AV_PIX_FMT_VULKAN,
329 software_format: ffi::AV_PIX_FMT_NV12,
330 width: 16,
331 height: 16,
332 coded_width: 16,
333 coded_height: 16,
334 plane_count: 1,
335 sampled: true,
336 instance: 1,
337 physical_device: 2,
338 device: 3,
339 images: [4, 0, 0, 0, 0, 0, 0, 0],
340 formats: [1; 8],
341 layouts: [0; 8],
342 access: [0; 8],
343 semaphores: [5, 0, 0, 0, 0, 0, 0, 0],
344 semaphore_values: [0; 8],
345 queue_families: [0; 8],
346 };
347 assert!(synchronized(&info));
348 }
349}