20 KiB
20 KiB
Intel VPL AV1 디코더 설계 문서
📋 개요
Intel Video Processing Library (VPL)를 사용하여 VavCore에 AV1 디코더를 추가하는 설계 문서입니다.
목적: Intel QuickSync 하드웨어 가속을 활용한 고성능 AV1 디코딩 대상 플랫폼: Intel GPU 지원 시스템 (소프트웨어 fallback 포함) 통합 위치: VavCore 디코더 팩토리 시스템
🏗️ 라이브러리 분석
Intel VPL 구조
- 소스 위치:
D:\Project\video-av1\oss\libvpl - 주요 헤더:
api/vpl/mfxvideo.h- 비디오 디코딩 APIapi/vpl/mfxstructures.h- 데이터 구조체- 참고 문서: https://intel.github.io/libvpl/latest/API_ref/VPL_func_vid_decode.html
핵심 API 워크플로우
- MFXLoad() - VPL 라이브러리 로더 생성
- MFXCreateConfig() - 디코더 구성 설정 (HW/SW, 코덱 타입)
- MFXCreateSession() - 디코딩 세션 생성
- MFXVideoDECODE_DecodeHeader() - 비트스트림 헤더 파싱
- MFXVideoDECODE_QueryIOSurf() - 필요한 표면 개수 계산
- MFXVideoDECODE_Init() - 디코더 초기화
- MFXVideoDECODE_DecodeFrameAsync() - 비동기 프레임 디코딩
- MFXVideoCORE_SyncOperation() - 디코딩 완료 대기
🎯 클래스 설계
VPLAV1Decoder 클래스 구조
namespace VavCore {
class VPLAV1Decoder : public IVideoDecoder {
public:
VPLAV1Decoder();
~VPLAV1Decoder() override;
// Prevent copying
VPLAV1Decoder(const VPLAV1Decoder&) = delete;
VPLAV1Decoder& operator=(const VPLAV1Decoder&) = delete;
// IVideoDecoder 인터페이스 구현
bool Initialize(const VideoMetadata& metadata) override;
void Cleanup() override;
bool IsInitialized() const override;
bool DecodeFrame(const VideoPacket& input_packet, VideoFrame& output_frame) override;
bool DecodeFrame(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame) override;
bool Reset() override;
bool Flush() override;
// IVideoDecoder 인터페이스 - 추가 메서드
std::string GetCodecName() const override { return "AV1 (Intel VPL)"; }
VideoCodecType GetCodecType() const override { return VideoCodecType::AV1; }
std::string GetVersion() const override;
DecoderStats GetStats() const override;
void ResetStats() override;
// VPL 전용 메서드
bool IsVPLAvailable() const;
bool InitializeVPL();
protected:
// Protected members for inheritance (AdaptiveVPLDecoder)
mfxSession m_session = nullptr;
mfxVideoParam m_videoParams = {};
uint32_t m_width = 0;
uint32_t m_height = 0;
uint32_t m_maxWidth = 4096;
uint32_t m_maxHeight = 4096;
// Protected helper methods
void LogVPLError(mfxStatus status, const std::string& operation) const;
private:
// VPL 객체
mfxLoader m_loader = nullptr;
mfxConfig m_config = nullptr;
// 표면 관리
std::vector<mfxFrameSurface1> m_surfaces;
mfxU16 m_numSurfaces = 0;
// 비트스트림 버퍼
std::unique_ptr<mfxU8[]> m_bitstreamBuffer;
mfxU32 m_bitstreamBufferSize = 2 * 1024 * 1024; // 2MB 기본값
// 통계
uint64_t m_framesDecoded = 0;
uint64_t m_decodeErrors = 0;
double m_avgDecodeTime = 0.0;
uint64_t m_bytesProcessed = 0;
// 상태
bool m_initialized = false;
bool m_headerParsed = false;
// 헬퍼 메서드
bool CheckVPLCapability();
bool CreateSession();
bool SetupDecoder();
bool AllocateSurfaces();
void CleanupVPL();
// 프레임 변환
bool ConvertVPLSurface(mfxFrameSurface1* surface, VideoFrame& output_frame);
mfxFrameSurface1* GetFreeSurface();
// 비트스트림 처리
bool PrepareDataForDecode(const uint8_t* packet_data, size_t packet_size, mfxBitstream& bitstream);
// 에러 처리
void LogError(const std::string& message) const;
};
} // namespace VavCore
⚙️ 주요 구현 단계
1. 초기화 단계 (Initialize)
bool VPLAV1Decoder::Initialize(const VideoMetadata& metadata) {
if (m_initialized) {
LogError("Decoder already initialized");
return false;
}
// Store video dimensions
m_width = metadata.width;
m_height = metadata.height;
// Initialize VPL
if (!InitializeVPL()) {
LogError("Failed to initialize VPL");
return false;
}
// Create VPL session
if (!CreateSession()) {
LogError("Failed to create VPL session");
CleanupVPL();
return false;
}
// Setup decoder parameters
if (!SetupDecoder()) {
LogError("Failed to setup decoder parameters");
CleanupVPL();
return false;
}
m_initialized = true;
return true;
}
// InitializeVPL 구현
bool VPLAV1Decoder::InitializeVPL() {
// Create loader
m_loader = MFXLoad();
if (!m_loader) {
LogError("Failed to create VPL loader");
return false;
}
// Create config
m_config = MFXCreateConfig(m_loader);
if (!m_config) {
LogError("Failed to create VPL config");
return false;
}
// Set AV1 codec filter
mfxVariant codecId;
codecId.Type = MFX_VARIANT_TYPE_U32;
codecId.Data.U32 = MFX_CODEC_AV1;
mfxStatus status = MFXSetConfigFilterProperty(m_config,
(const mfxU8*)"mfxImplDescription.mfxDecoderDescription.decoder.CodecID", codecId);
if (status != MFX_ERR_NONE) {
LogVPLError(status, "SetConfigFilterProperty");
return false;
}
return CheckVPLCapability();
}
2. 프레임 디코딩 (DecodeFrame)
bool VPLAV1Decoder::DecodeFrame(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame) {
if (!m_initialized) {
LogError("Decoder not initialized");
++m_decodeErrors;
return false;
}
if (!packet_data || packet_size == 0) {
LogError("Invalid packet data");
++m_decodeErrors;
return false;
}
auto start_time = high_resolution_clock::now();
try {
// Prepare bitstream for decoding
mfxBitstream bitstream = {};
if (!PrepareDataForDecode(packet_data, packet_size, bitstream)) {
LogError("Failed to prepare data for decode");
++m_decodeErrors;
return false;
}
// Parse header if not done yet
if (!m_headerParsed) {
mfxStatus status = MFXVideoDECODE_DecodeHeader(m_session, &bitstream, &m_videoParams);
if (status != MFX_ERR_NONE) {
LogVPLError(status, "DecodeHeader");
++m_decodeErrors;
return false;
}
// Allocate surfaces after header parsing
if (!AllocateSurfaces()) {
LogError("Failed to allocate surfaces");
++m_decodeErrors;
return false;
}
m_headerParsed = true;
}
// Get free surface for decoding
mfxFrameSurface1* work_surface = GetFreeSurface();
if (!work_surface) {
LogError("No free surface available");
++m_decodeErrors;
return false;
}
// Perform async decoding
mfxFrameSurface1* output_surface = nullptr;
mfxSyncPoint sync_point = nullptr;
mfxStatus status = MFXVideoDECODE_DecodeFrameAsync(
m_session, &bitstream, work_surface, &output_surface, &sync_point);
if (status == MFX_ERR_MORE_DATA) {
// Need more data - not an error for AV1 stream
return false;
}
if (status != MFX_ERR_NONE && status != MFX_WRN_VIDEO_PARAM_CHANGED) {
LogVPLError(status, "DecodeFrameAsync");
++m_decodeErrors;
return false;
}
// Wait for decoding to complete
if (sync_point) {
status = MFXVideoCORE_SyncOperation(m_session, sync_point, MFX_INFINITE);
if (status != MFX_ERR_NONE) {
LogVPLError(status, "SyncOperation");
++m_decodeErrors;
return false;
}
}
// Convert output surface to VideoFrame
if (output_surface) {
if (!ConvertVPLSurface(output_surface, output_frame)) {
LogError("Failed to convert VPL surface");
++m_decodeErrors;
return false;
}
// Update statistics
++m_framesDecoded;
m_bytesProcessed += packet_size;
auto end_time = high_resolution_clock::now();
auto decode_time = duration_cast<microseconds>(end_time - start_time).count() / 1000.0;
m_avgDecodeTime = (m_avgDecodeTime * (m_framesDecoded - 1) + decode_time) / m_framesDecoded;
return true;
}
return false;
} catch (const std::exception& e) {
LogError("Exception in DecodeFrame: " + string(e.what()));
++m_decodeErrors;
return false;
}
}
3. 표면 관리 (Surface Management)
bool VPLAV1Decoder::AllocateSurfaces() {
if (!m_session) {
LogError("Session not created");
return false;
}
// Query required number of surfaces
mfxFrameAllocRequest allocRequest = {};
mfxStatus status = MFXVideoDECODE_QueryIOSurf(m_session, &m_videoParams, &allocRequest);
if (status != MFX_ERR_NONE) {
LogVPLError(status, "QueryIOSurf");
return false;
}
// Allocate surfaces (suggested number + buffer)
m_numSurfaces = allocRequest.NumFrameSuggested + 4; // Extra buffer
m_surfaces.resize(m_numSurfaces);
for (mfxU16 i = 0; i < m_numSurfaces; i++) {
memset(&m_surfaces[i], 0, sizeof(mfxFrameSurface1));
// Copy frame info from decoder parameters
m_surfaces[i].Info = m_videoParams.mfx.FrameInfo;
// For simplicity, we use system memory allocation
// In production, you might want to use video memory
if (!AllocateSystemMemoryForSurface(&m_surfaces[i])) {
LogError("Failed to allocate system memory for surface " + to_string(i));
return false;
}
}
return true;
}
mfxFrameSurface1* VPLAV1Decoder::GetFreeSurface() {
// Find a free surface (not locked by VPL)
for (auto& surface : m_surfaces) {
if (surface.Data.Locked == 0) {
return &surface;
}
}
// All surfaces are busy - this might indicate a pipeline bottleneck
LogError("All surfaces are locked - decoder pipeline bottleneck");
return nullptr;
}
bool VPLAV1Decoder::AllocateSystemMemoryForSurface(mfxFrameSurface1* surface) {
mfxFrameInfo& info = surface->Info;
// Calculate required buffer size for YUV420 format
mfxU32 surfaceSize = info.Width * info.Height * 3 / 2; // YUV420P
// For NV12, we might need different calculations
if (info.FourCC == MFX_FOURCC_NV12) {
surfaceSize = info.Width * info.Height + (info.Width * info.Height) / 2;
}
// Allocate buffer
mfxU8* buffer = new (std::nothrow) mfxU8[surfaceSize];
if (!buffer) {
return false;
}
// Set up surface data pointers
surface->Data.Y = buffer;
surface->Data.U = buffer + info.Width * info.Height;
surface->Data.V = surface->Data.U + (info.Width * info.Height) / 4;
surface->Data.Pitch = info.Width;
// For NV12, UV is interleaved
if (info.FourCC == MFX_FOURCC_NV12) {
surface->Data.UV = surface->Data.U;
surface->Data.V = surface->Data.UV + 1;
}
return true;
}
4. 데이터 변환 (VPL → VideoFrame)
bool VPLAV1Decoder::ConvertVPLSurface(mfxFrameSurface1* surface, VideoFrame& output_frame) {
if (!surface || !surface->Data.Y) {
LogError("Invalid surface for conversion");
return false;
}
mfxFrameInfo& info = surface->Info;
// Set output frame metadata
output_frame.width = info.CropW ? info.CropW : info.Width;
output_frame.height = info.CropH ? info.CropH : info.Height;
output_frame.timestamp = 0; // VPL doesn't provide timestamp directly
// Determine and set color space
switch (info.FourCC) {
case MFX_FOURCC_NV12:
output_frame.color_space = ColorSpace::NV12;
break;
case MFX_FOURCC_I420:
case MFX_FOURCC_IYUV:
output_frame.color_space = ColorSpace::YUV420P;
break;
case MFX_FOURCC_YUY2:
output_frame.color_space = ColorSpace::YUV422P;
break;
default:
LogError("Unsupported pixel format: " + to_string(info.FourCC));
return false;
}
// Calculate plane sizes for the new VideoFrame structure
output_frame.y_size = output_frame.width * output_frame.height;
output_frame.u_size = output_frame.y_size / 4;
output_frame.v_size = output_frame.y_size / 4;
// Allocate memory for each plane
output_frame.y_plane = std::make_unique<uint8_t[]>(output_frame.y_size);
output_frame.u_plane = std::make_unique<uint8_t[]>(output_frame.u_size);
output_frame.v_plane = std::make_unique<uint8_t[]>(output_frame.v_size);
try {
if (info.FourCC == MFX_FOURCC_NV12) {
// NV12: Y plane + interleaved UV
return ConvertNV12ToYUV420P(surface, output_frame);
} else {
// I420/IYUV: Separate Y, U, V planes
return ConvertI420ToYUV420P(surface, output_frame);
}
} catch (const std::exception& e) {
LogError("Exception during surface conversion: " + string(e.what()));
return false;
}
}
bool VPLAV1Decoder::ConvertNV12ToYUV420P(mfxFrameSurface1* surface, VideoFrame& output_frame) {
mfxFrameData& src = surface->Data;
mfxFrameInfo& info = surface->Info;
// Copy Y plane
uint8_t* dst_y = output_frame.y_plane.get();
uint8_t* src_y = src.Y;
for (uint32_t y = 0; y < output_frame.height; y++) {
memcpy(dst_y, src_y, output_frame.width);
dst_y += output_frame.width;
src_y += src.Pitch;
}
// Convert interleaved UV to separate U and V planes
uint8_t* dst_u = output_frame.u_plane.get();
uint8_t* dst_v = output_frame.v_plane.get();
uint8_t* src_uv = src.UV;
uint32_t uv_width = output_frame.width / 2;
uint32_t uv_height = output_frame.height / 2;
for (uint32_t y = 0; y < uv_height; y++) {
for (uint32_t x = 0; x < uv_width; x++) {
dst_u[x] = src_uv[x * 2]; // U (even indices)
dst_v[x] = src_uv[x * 2 + 1]; // V (odd indices)
}
dst_u += uv_width;
dst_v += uv_width;
src_uv += src.Pitch;
}
return true;
}
bool VPLAV1Decoder::ConvertI420ToYUV420P(mfxFrameSurface1* surface, VideoFrame& output_frame) {
mfxFrameData& src = surface->Data;
// Copy Y plane
uint8_t* dst_y = output_frame.y_plane.get();
uint8_t* src_y = src.Y;
for (uint32_t y = 0; y < output_frame.height; y++) {
memcpy(dst_y, src_y, output_frame.width);
dst_y += output_frame.width;
src_y += src.Pitch;
}
// Copy U plane
uint8_t* dst_u = output_frame.u_plane.get();
uint8_t* src_u = src.U;
uint32_t uv_width = output_frame.width / 2;
uint32_t uv_height = output_frame.height / 2;
for (uint32_t y = 0; y < uv_height; y++) {
memcpy(dst_u, src_u, uv_width);
dst_u += uv_width;
src_u += src.Pitch / 2;
}
// Copy V plane
uint8_t* dst_v = output_frame.v_plane.get();
uint8_t* src_v = src.V;
for (uint32_t y = 0; y < uv_height; y++) {
memcpy(dst_v, src_v, uv_width);
dst_v += uv_width;
src_v += src.Pitch / 2;
}
return true;
}
🔧 VideoDecoderFactory 통합
팩토리 클래스 수정
// VideoDecoderFactory.h
enum class DecoderType {
AUTO = 0,
ADAPTIVE_NVDEC,
ADAPTIVE_DAV1D,
NVDEC,
DAV1D,
MEDIA_FOUNDATION,
VPL, // Intel VPL 추가
AMF // AMD AMF 추가
};
// VideoDecoderFactory.cpp 주요 부분
std::unique_ptr<IVideoDecoder> VideoDecoderFactory::CreateDecoder(DecoderType type) {
switch (type) {
case DecoderType::AUTO:
// 우선순위: Adaptive NVDEC > VPL > AMF > Adaptive dav1d > NVDEC > dav1d > MediaFoundation
if (s_nvdec_available) {
OutputDebugStringA("[VideoDecoderFactory] Auto mode: trying Adaptive NVDEC AV1 decoder first\n");
auto decoder = std::make_unique<AdaptiveNVDECDecoder>();
if (decoder) {
return decoder;
}
}
if (s_vpl_available) {
OutputDebugStringA("[VideoDecoderFactory] Auto mode: trying Intel VPL AV1 decoder\n");
auto decoder = std::make_unique<VPLAV1Decoder>();
if (decoder) {
return decoder;
}
}
if (s_amf_available) {
OutputDebugStringA("[VideoDecoderFactory] Auto mode: trying AMD AMF AV1 decoder\n");
auto decoder = std::make_unique<AMFAV1Decoder>();
if (decoder) {
return decoder;
}
}
// Continue with other decoders...
break;
case DecoderType::VPL:
if (s_vpl_available) {
OutputDebugStringA("[VideoDecoderFactory] Creating Intel VPL AV1 decoder\n");
return std::make_unique<VPLAV1Decoder>();
}
break;
// ... 기존 케이스들
}
return nullptr;
}
// 가용성 검사 구현
bool VideoDecoderFactory::CheckVPLAvailability() {
try {
// Try creating a temporary VPL decoder instance
auto temp_decoder = std::make_unique<VPLAV1Decoder>();
if (temp_decoder && temp_decoder->IsVPLAvailable()) {
OutputDebugStringA("[VideoDecoderFactory] Intel VPL AV1 decoder available\n");
return true;
}
} catch (...) {
// VPL not available or initialization failed
}
OutputDebugStringA("[VideoDecoderFactory] Intel VPL AV1 decoder not available\n");
return false;
}
📊 구현 특징 및 장점
주요 장점
- ✅ Intel QuickSync 하드웨어 가속: Intel GPU 전용 최적화
- ✅ 표준화된 API: 업계 표준 VPL 라이브러리 사용
- ✅ 비동기 처리: 높은 처리량과 성능
- ✅ 자동 Fallback: 하드웨어 → 소프트웨어 자동 전환
- ✅ 메모리 효율성: VPL 내부 표면 관리 활용
성능 특성
- 대상 플랫폼: Intel GPU (8세대 이후 권장)
- 예상 성능: 4K AV1 실시간 디코딩 (60fps+)
- 메모리 사용: 하드웨어 가속 시 GPU 메모리 활용
호환성
- Intel GPU: 최고 성능
- Non-Intel GPU: 소프트웨어 구현 fallback
- AMD/NVIDIA: 기존 디코더 우선 사용 (AUTO 모드)
🚧 구현 고려사항
에러 처리
- VPL 특정 에러 코드 매핑
- 하드웨어 실패 시 graceful fallback
- 메모리 부족 상황 처리
성능 최적화
- 표면 풀링으로 할당 오버헤드 최소화
- 비동기 처리로 CPU-GPU 병렬 실행
- Zero-copy 메모리 전송 (가능한 경우)
테스트 계획
- Intel GPU 환경에서 성능 벤치마크
- 다양한 AV1 비트스트림 호환성 테스트
- 메모리 누수 및 안정성 검증
📋 구현 우선순위
Phase 1: 기본 구현
- VPLAV1Decoder 클래스 기본 구조
- Initialize/DecodeFrame 핵심 로직
- VideoDecoderFactory 통합
Phase 2: 최적화
- 표면 관리 최적화
- 에러 처리 강화
- 성능 모니터링 추가
Phase 3: 고급 기능
- 적응형 품질 조정 지원
- 멀티스레드 디코딩
- GPU 메모리 최적화
작성일: 2025-09-26 Intel VPL 버전: 2.x API 기준