24 KiB
MediaCodec AV1 을 이용하여 Vulkan 1.1 의 텍스처로 디코딩하는 함수를 작성하고자 한다. webm 동영상 파일로부터 frame packet 을 파싱이 완료된 상태부터 MediaCodec 에 frame packet 을 제공하고, 그 결과로 Vulkan texture 를 반환받으려고 한다. 이를 위한 전체 아키텍처 설계서 문서를 상세하게 작성해줘. 작성된 설계서는 md 로 디테일한 구현 예시도 함께 작성해줘.
✦ 네, 알겠습니다. MediaCodec AV1 디코딩 결과를 Vulkan 1.1 텍스처로 직접 연결하는 고성능 아키텍처 설계 문서를 상세한 구현 예시와 함께 Markdown 형식으로 작성해 드리겠습니다.
이 설계는 CPU-GPU 간의 불필요한 메모리 복사를 제거하는 '제로-카피(Zero-Copy)'를 목표로 하며, 안드로이드의 AHardwareBuffer와 Vulkan 확장 기능을 핵심으로 사용합니다.
MediaCodec과 Vulkan을 이용한 AV1 디코딩 아키텍처 설계
- 개요
본 문서는 안드로이드 MediaCodec을 사용하여 WebM 컨테이너로부터 파싱된 AV1 비디오 프레임을 디코딩하고, 그 결과를 Vulkan 1.1 텍스처(VkImage)로 직접 변환하는 시스템의 아키텍처를 정의합니다.
핵심 목표는 디코딩된 프레임을 CPU 메모리로 복사하지 않고 GPU 메모리 내에서 처리하는 제로-카피(Zero-Copy) 파이프라인을 구축하여 최고의 렌더링 성능을 달성하는 것입니다. 이를 위해 MediaCodec의 출력으로 AHardwareBuffer를 사용하고, 이 버퍼를 Vulkan에서 직접 참조(Import)하여 텍스처로 사용하는 방법을 제안합니다.
- 핵심 목표
- 고성능 제로-카피: 디코딩된 YUV 프레임을 CPU로 다운로드 후 다시 GPU로 업로드하는 과정을 생략하여 성능을 극대화합니다.
- 효율적인 색상 변환: 하드웨어 디코더가 출력하는 YUV(NV12) 포맷을 Vulkan 셰이더에서 실시간으로 RGB로 변환하여 처리합니다.
- 캡슐화 및 재사용성: 디코딩 및 Vulkan 텍스처 변환 로직을 하나의 클래스로 캡슐화하여 애플리케이션의 다른 부분과 분리하고 재사용성을 높입니다.
- 현대적 API 활용: Android NDK의 AMediaCodec과 Vulkan의 외부 메모리 확장(VK_KHR_android_external_memory_android_hardware_buffer)을 사용합니다.
- 시스템 아키텍처
전체 시스템은 다음과 같은 구성 요소로 이루어집니다.
-
WebM Parser (외부): .webm 파일에서 AV1 비디오 프레임 데이터(Frame Packet)를 추출합니다. (설계 범위 밖)
-
JNI Bridge: Java/Kotlin의 MediaCodec 설정 및 ImageReader 콜백과 네이티브 C++ 코드를 연결합니다.
-
Video Decoder Module (C++):
VulkanAV1Decoder클래스: 디코딩 프로세스 전체를 관리하는 핵심 클래스입니다.MediaCodec(via JNI): 실제 AV1 디코딩을 수행하는 안드로이드 컴포넌트. 하드웨어 가속을 사용합니다.ImageReader(via JNI): MediaCodec의 디코딩 출력 Surface를 제공하고, 출력된 프레임을 AHardwareBuffer 형태로 가져오는 역할을 합니다.
-
Vulkan Renderer (C++):
- VulkanAV1Decoder로부터 AHardwareBuffer를 받아 Vulkan VkImage로 임포트합니다.
- YCbCr to RGB 변환을 수행하는 픽셀 셰이더를 이용해 최종 이미지를 화면에 렌더링합니다.
-
주요 기술 및 API
4.1. Android
MediaCodec(Java/Kotlin & NDK): 비디오 디코딩을 위한 핵심 API. MIMETYPE_VIDEO_AV1으로 설정하여 AV1 코덱을 사용합니다.ImageReader(Java/Kotlin): MediaCodec의 출력 Surface를 생성하고, 디코딩된 프레임 버퍼에 접근할 수 있게 해주는 클래스. ImageFormat.PRIVATE과 USAGE_GPU_SAMPLED_IMAGE 플래그를 사용하여 AHardwareBuffer를 생성하도록 설정합니다.AHardwareBuffer(NDK): 여러 프로세스나 API(ex: MediaCodec, Vulkan, OpenGL)간에 그래픽 버퍼를 공유하기 위한 표준 인터페이스. 제로-카피의 핵심 요소입니다.
4.2. Vulkan
VK_KHR_android_external_memory_android_hardware_buffer: AHardwareBuffer를 VkDeviceMemory로 임포트할 수 있게 해주는 필수 Vulkan 확장입니다.VK_KHR_sampler_ycbcr_conversion: MediaCodec이 주로 출력하는 NV12와 같은 YCbCr 포맷의 이미지를 셰이더에서 직접 샘플링하고 RGB로 자동 변환할 수 있게 해주는 확장입니다.VkExternalMemoryImageCreateInfo: 외부 메모리를 바인딩할 VkImage를 생성할 때 사용됩니다.VkImportAndroidHardwareBufferInfoANDROID: AHardwareBuffer를 VkDeviceMemory로 임포트할 때 버퍼 정보를 전달하는 구조체입니다.
- 상세 설계 및 구현 예시
5.1. VulkanAV1Decoder 클래스 설계 (C++)
디코딩 로직을 캡슐화할 C++ 클래스입니다.
1 // VulkanAV1Decoder.h
2
3 #include <vulkan/vulkan.h>
4 #include <android/hardware_buffer.h>
5 #include <android/native_window.h>
6
7 // 디코딩된 Vulkan 텍스처 정보를 담는 구조체
8 struct DecodedVulkanTexture {
9 VkImage image = VK_NULL_HANDLE;
10 VkDeviceMemory memory = VK_NULL_HANDLE; 11 VkImageView imageView = VK_NULL_HANDLE; 12 VkSampler sampler = VK_NULL_HANDLE; 13 VkSamplerYcbcrConversion ycbcrConversion = VK_NULL_HANDLE; 14 15 // 리소스 해제를 위한 정보 16 VkDevice logicalDevice = VK_NULL_HANDLE; 17 18 void cleanup(); 19 }; 20 21 class VulkanAV1Decoder { 22 public: 23 VulkanAV1Decoder(VkDevice device, VkPhysicalDevice physicalDevice); 24 ~VulkanAV1Decoder(); 25 26 // 초기화: MediaCodec, ImageReader 등을 JNI를 통해 설정 27 bool initialize(JNIEnv* env, int width, int height); 28 29 // 디코딩: WebM에서 파싱된 프레임 데이터를 MediaCodec에 전달 30 void feedFrame(JNIEnv* env, uint8_t* frameData, size_t dataSize); 31 32 // 결과 확인: 디코딩된 최신 프레임이 있다면 AHardwareBuffer 핸들을 반환 33 AHardwareBuffer* acquireLatestFrame(JNIEnv* env); 34 35 // AHardwareBuffer를 Vulkan 텍스처로 임포트 36 bool importHardwareBufferToVulkan(AHardwareBuffer* buffer, DecodedVulkanTexture& outTexture); 37 38 // 리소스 해제 39 void cleanup(JNIEnv* env); 40 41 private: 42 // Vulkan 관련 객체 43 VkDevice m_vkDevice; 44 VkPhysicalDevice m_vkPhysicalDevice; 45 46 // JNI 관련 객체 (Java 측 Decoder 객체) 47 jobject m_jDecoderInstance; 48 jclass m_jDecoderClass; 49 50 // JNI 메서드 ID 51 jmethodID m_jFeedFrameMethod; 52 jmethodID m_jAcquireLatestFrameMethod; 53 jmethodID m_jCleanupMethod; 54 55 // Vulkan 확장 함수 포인터 56 PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID; 57 // ... 기타 필요한 함수 포인터 58 };
5.2. Java/Kotlin 측 Decoder 설정
C++에서 호출할 MediaCodec과 ImageReader를 설정하는 Java 또는 Kotlin 클래스입니다.
1 // VideoDecoder.kt
2 import android.media.*
3 import android.os.Handler
4 import android.os.HandlerThread
5 import android.view.Surface
6 import java.util.concurrent.ConcurrentLinkedQueue
7
8 class VideoDecoder(
9 private val width: Int,
10 private val height: Int 11 ) { 12 private lateinit var mediaCodec: MediaCodec 13 private lateinit var imageReader: ImageReader 14 private val handlerThread = HandlerThread("VideoDecoderThread") 15 private val handler: Handler 16 17 // C++에서 접근할 HardwareBuffer 큐 18 private val hardwareBufferQueue = ConcurrentLinkedQueue() 19 20 init { 21 handlerThread.start() 22 handler = Handler(handlerThread.looper) 23 } 24 25 fun initialize(): Surface { 26 // 1. ImageReader 생성: MediaCodec의 출력을 받을 Surface 제공자 27 imageReader = ImageReader.newInstance( 28 width, height, 29 ImageFormat.PRIVATE, // AHardwareBuffer를 사용하기 위한 포맷 30 2 // 버퍼 2개 31 ).apply { 32 // GPU에서 샘플링 가능한 버퍼로 사용 설정 33 this.usage = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE 34 } 35 36 // 2. ImageReader에 리스너 설정 37 imageReader.setOnImageAvailableListener({ reader -> 38 val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener 39 val hardwareBuffer = image.hardwareBuffer ?: return@setOnImageAvailableListener 40 41 // 이전 버퍼는 닫고 새로운 버퍼를 큐에 추가 42 hardwareBufferQueue.poll()?.close() 43 hardwareBufferQueue.add(hardwareBuffer) 44 45 image.close() 46 }, handler) 47 48 // 3. MediaFormat 설정 49 val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AV1, width, height) 50 // YUV 4:2:0 8-bit 포맷을 기대 51 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) 52 53 // 4. MediaCodec 생성 및 설정 54 mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AV1) 55 mediaCodec.configure(format, imageReader.surface, null, 0) 56 mediaCodec.start() 57 58 return imageReader.surface 59 } 60 61 // C++에서 호출할 함수 (JNI) 62 fun feedFrame(frameData: ByteArray) { 63 val inputBufferIndex = mediaCodec.dequeueInputBuffer(-1) 64 if (inputBufferIndex >= 0) { 65 val inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex) 66 inputBuffer?.clear() 67 inputBuffer?.put(frameData) 68 mediaCodec.queueInputBuffer(inputBufferIndex, 0, frameData.size, System.nanoTime() / 1000, 0) 69 } 70 } 71 72 // C++에서 호출할 함수 (JNI) 73 fun acquireLatestFrame(): HardwareBuffer? { 74 // 큐에서 가장 최신 버퍼를 가져옴 (소유권 이전) 75 return hardwareBufferQueue.poll() 76 } 77 78 fun cleanup() { 79 handlerThread.quitSafely() 80 mediaCodec.stop() 81 mediaCodec.release() 82 imageReader.close() 83 hardwareBufferQueue.forEach { it.close() } 84 } 85 }
5.3. JNI 브릿지 구현 (C++)
Java/Kotlin과 C++를 연결하는 부분입니다.
1 // jni_bridge.cpp
2
3 // ... JNIEnv*, jobject, jclass 등 포함
4
5 extern "C" JNIEXPORT jlong JNICALL
6 Java_com_example_vav2player_VulkanBridge_nativeCreateDecoder(JNIEnv* env, jobject /* this */, jlong vkDevice, jlong
vkPhysicalDevice) {
7 // VulkanAV1Decoder 인스턴스 생성
8 VulkanAV1Decoder* decoder = new VulkanAV1Decoder(
9 reinterpret_cast<VkDevice>(vkDevice),
10 reinterpret_cast(vkPhysicalDevice) 11 ); 12 return reinterpret_cast(decoder); 13 } 14 15 extern "C" JNIEXPORT void JNICALL 16 Java_com_example_vav2player_VulkanBridge_nativeInitializeDecoder(JNIEnv* env, jobject /* this /, jlong decoderPtr, jint width, jint height) { 17 VulkanAV1Decoder decoder = reinterpret_cast<VulkanAV1Decoder*>(decoderPtr); 18 decoder->initialize(env, width, height); 19 } 20 21 // ... feedFrame, acquireLatestFrame 등의 JNI 함수 구현 ... 22 23 // acquireLatestFrame 예시 24 extern "C" JNIEXPORT jlong JNICALL 25 Java_com_example_vav2player_VulkanBridge_nativeAcquireLatestHardwareBuffer(JNIEnv* env, jobject, jlong decoderPtr) { 26 VulkanAV1Decoder* decoder = reinterpret_cast<VulkanAV1Decoder*>(decoderPtr); 27 // Java의 acquireLatestFrame() 호출 후 HardwareBuffer 객체(jobject)를 받음 28 jobject hardwareBufferJObject = decoder->acquireLatestFrameAsJObject(env); 29 if (hardwareBufferJObject == nullptr) { 30 return 0; 31 } 32 // AHardwareBuffer로 변환 33 AHardwareBuffer ahb = AHardwareBuffer_fromHardwareBuffer(env, hardwareBufferJObject); 34 // Java 객체 로컬 참조 해제 35 env->DeleteLocalRef(hardwareBufferJObject); 36 return reinterpret_cast(ahb); 37 }
5.4. AHardwareBuffer를 Vulkan 텍스처로 임포트 (C++)
VulkanAV1Decoder::importHardwareBufferToVulkan 함수의 상세 구현입니다.
1 // VulkanAV1Decoder.cpp
2
3 bool VulkanAV1Decoder::importHardwareBufferToVulkan(AHardwareBuffer* buffer, DecodedVulkanTexture& outTexture) {
4 // 1. 필요한 Vulkan 확장 함수 포인터 가져오기 (초기화 시 1회만 수행)
5 if (!vkGetAndroidHardwareBufferPropertiesANDROID) {
6 vkGetAndroidHardwareBufferPropertiesANDROID = (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)vkGetDeviceProcAddr
(m_vkDevice, "vkGetAndroidHardwareBufferPropertiesANDROID");
7 if (!vkGetAndroidHardwareBufferPropertiesANDROID) {
8 // 오류 처리: 확장 함수를 찾을 수 없음
9 return false;
10 }
11 }
12
13 // 2. AHardwareBuffer 속성 가져오기
14 VkAndroidHardwareBufferPropertiesANDROID bufferProperties = { .sType =
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID };
15 VkAndroidHardwareBufferFormatPropertiesANDROID formatProperties = { .sType =
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID };
16 bufferProperties.pNext = &formatProperties;
17
18 VkResult result = vkGetAndroidHardwareBufferPropertiesANDROID(m_vkDevice, buffer, &bufferProperties);
19 if (result != VK_SUCCESS) {
20 // 오류 처리
21 return false;
22 }
23
24 // 3. 적합한 메모리 타입 인덱스 찾기
25 VkPhysicalDeviceMemoryProperties memProperties;
26 vkGetPhysicalDeviceMemoryProperties(m_vkPhysicalDevice, &memProperties);
27 uint32_t memoryTypeIndex = 0;
28 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
29 if ((bufferProperties.memoryTypeBits & (1 << i)) &&
30 (memProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
31 memoryTypeIndex = i;
32 break;
33 }
34 }
35
36 // 4. Vulkan 메모리 할당 정보 설정 (AHB 임포트)
37 VkImportAndroidHardwareBufferInfoANDROID hardwareBufferInfo = { .sType =
VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID };
38 hardwareBufferInfo.buffer = buffer;
39
40 VkMemoryAllocateInfo memoryAllocateInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
41 memoryAllocateInfo.pNext = &hardwareBufferInfo;
42 memoryAllocateInfo.allocationSize = bufferProperties.allocationSize;
43 memoryAllocateInfo.memoryTypeIndex = memoryTypeIndex;
44
45 result = vkAllocateMemory(m_vkDevice, &memoryAllocateInfo, nullptr, &outTexture.memory);
46 if (result != VK_SUCCESS) {
47 // 오류 처리
48 return false;
49 }
50
51 // 5. YCbCr 변환 객체 및 외부 메모리용 VkImage 생성
52 VkExternalFormatANDROID externalFormat = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID };
53 externalFormat.externalFormat = formatProperties.externalFormat;
54
55 VkExternalMemoryImageCreateInfo externalImageCreateInfo = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO };
56 externalImageCreateInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
57
58 VkImageCreateInfo imageCreateInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
59 imageCreateInfo.pNext = &externalImageCreateInfo;
60 externalImageCreateInfo.pNext = &externalFormat; // 체인 연결
61 imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
62 imageCreateInfo.extent = { /* width */, /* height */, 1 };
63 imageCreateInfo.mipLevels = 1;
64 imageCreateInfo.arrayLayers = 1;
65 imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
66 imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
67 imageCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
68 imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
69 imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
70 // format은 VK_FORMAT_UNDEFINED로 설정해야 함 (externalFormat 사용 시)
71 imageCreateInfo.format = VK_FORMAT_UNDEFINED;
72
73 result = vkCreateImage(m_vkDevice, &imageCreateInfo, nullptr, &outTexture.image);
74 if (result != VK_SUCCESS) { /* ... */ }
75
76 // 6. 이미지에 메모리 바인딩
77 result = vkBindImageMemory(m_vkDevice, outTexture.image, outTexture.memory, 0);
78 if (result != VK_SUCCESS) { /* ... */ }
79
80 // 7. Sampler YCbCr Conversion 생성
81 VkSamplerYcbcrConversionCreateInfo ycbcrConversionCreateInfo = { .sType =
VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO };
82 ycbcrConversionCreateInfo.pNext = &externalFormat; // externalFormat을 다시 사용
83 ycbcrConversionCreateInfo.format = formatProperties.format; // vkGetAndroidHardwareBufferPropertiesANDROID에서 얻은 포맷
84 ycbcrConversionCreateInfo.ycbcrModel = formatProperties.suggestedYcbcrModel;
85 ycbcrConversionCreateInfo.ycbcrRange = formatProperties.suggestedYcbcrRange;
86 ycbcrConversionCreateInfo.components = formatProperties.samplerYcbcrConversionComponents;
87 which
88 ycbcrConversionCreateInfo.xChromaOffset = formatProperties.suggestedXChromaOffset;
89 ycbcrConversionCreateInfo.yChromaOffset = formatProperties.suggestedYChromaOffset;
90 ycbcrConversionCreateInfo.chromaFilter = VK_FILTER_LINEAR;
91 ycbcrConversionCreateInfo.forceExplicitReconstruction = VK_FALSE;
92
93 result = vkCreateSamplerYcbcrConversion(m_vkDevice, &ycbcrConversionCreateInfo, nullptr, &outTexture.ycbcrConversion);
94 if (result != VK_SUCCESS) { /* ... */ }
95
96 // 8. ImageView 및 Sampler 생성
97 VkSamplerYcbcrConversionInfo samplerConversionInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO };
98 samplerConversionInfo.conversion = outTexture.ycbcrConversion;
99
100 VkImageViewCreateInfo imageViewCreateInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; 101 imageViewCreateInfo.pNext = &samplerConversionInfo; 102 // ... (image, format, subresourceRange 등 설정) 103 result = vkCreateImageView(m_vkDevice, &imageViewCreateInfo, nullptr, &outTexture.imageView); 104 105 VkSamplerCreateInfo samplerCreateInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; 106 samplerCreateInfo.pNext = &samplerConversionInfo; 107 // ... (magFilter, minFilter, addressMode 등 설정) 108 result = vkCreateSampler(m_vkDevice, &samplerCreateInfo, nullptr, &outTexture.sampler); 109 110 outTexture.logicalDevice = m_vkDevice; 111 return true; 112 }
5.5. 렌더링 (GLSL 프래그먼트 셰이더)
VK_KHR_sampler_ycbcr_conversion 확장을 사용하면 YCbCr 텍스처를 일반 sampler2D처럼 사용할 수 있습니다. Vulkan이 셰이더 내에서 texture() 함수 호출 시 자동으로 RGB 변환을 수행합니다.
1 #version 450
2
3 // YCbCr 샘플러를 일반 2D 샘플러로 선언
4 layout(binding = 1) uniform sampler2D ycbcrSampler;
5
6 layout(location = 0) in vec2 inTexCoord;
7 layout(location = 0) out vec4 outColor;
8
9 void main() {
10 // texture() 함수가 YCbCr -> RGB 변환을 자동으로 수행 11 outColor = texture(ycbcrSampler, inTexCoord); 12 }
- 결론
본 설계는 ImageReader와 AHardwareBuffer를 중간 다리로 사용하여 MediaCodec과 Vulkan을 효율적으로 연동하는 현대적인 접근 방식을 제시합니다. 이 아키텍처를 통해 AV1 비디오를 디코딩하고 Vulkan 씬에 렌더링하는 과정에서 CPU 부하와 메모리 대역폭 사용을 최소화하여, 고해상도 비디오 재생 애플리케이션에서도 부드러운 사용자 경험을 제공할 수 있습니다.
초기 설정은 다소 복잡하지만, 한번 구축되면 높은 성능과 확장성을 보장하는 견고한 기반이 될 것입니다.
● 설계 문서와 현재 구현 비교 분석 완료
IMPLEMENTATION_COMPARISON_ANALYSIS.md 문서를 작성했습니다.
🔴 핵심 발견사항 (CRITICAL)
현재 구현이 설계 문서와 가장 중요한 부분에서 차이가 있습니다:
설계 문서 (올바른 방법):
- VK_KHR_sampler_ycbcr_conversion 확장 사용
- VkSamplerYcbcrConversion 객체로 NV12 → RGB 자동 변환
- 단일 ImageView + YCbCr Sampler 사용
- 쉐이더에서 texture() 호출 시 하드웨어가 자동으로 YUV→RGB 변환
현재 구현 (잘못된 방법):
- ❌ VkSamplerYcbcrConversion 사용 안 함
- ❌ Y 평면과 UV 평면을 별도의 ImageView로 분리
- ❌ 일반 VkSampler 사용
- ❌ 쉐이더에서 수동으로 YUV→RGB 변환
💥 VK_ERROR_DEVICE_LOST 원인 분석
추정 원인:
- 수동으로 NV12를 두 개의 plane view로 분리하는 방식은 메모리 레이아웃 가정이 잘못될 수 있음
- VkImageView 생성 시 VK_IMAGE_ASPECT_PLANE_0_BIT, VK_IMAGE_ASPECT_PLANE_1_BIT 사용이 AHardwareBuffer와 호환되지 않을 수 있음
- ImageView 재사용 시 descriptor set 손상 또는 invalid handle 참조
증거:
- 프레임 1-5: 정상 작동 (imageIndex 0, 1, 2, 3 순차 사용)
- 프레임 6: imageIndex=0 재사용 시 DEVICE_LOST
- 패턴: 스왑체인 이미지 재사용 시 실패
✅ 해결 방법 (우선순위 1)
vulkan_renderer.cpp:2493-2597 수정 필요:
// 1. VkSamplerYcbcrConversion 생성 (AHardwareBuffer format properties 사용) VkSamplerYcbcrConversionCreateInfo ycbcrInfo = {}; ycbcrInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO; ycbcrInfo.format = ahb_format_props.format; ycbcrInfo.ycbcrModel = ahb_format_props.suggestedYcbcrModel; ycbcrInfo.ycbcrRange = ahb_format_props.suggestedYcbcrRange; ycbcrInfo.components = ahb_format_props.samplerYcbcrConversionComponents; ycbcrInfo.xChromaOffset = ahb_format_props.suggestedXChromaOffset; ycbcrInfo.yChromaOffset = ahb_format_props.suggestedYChromaOffset; ycbcrInfo.chromaFilter = VK_FILTER_LINEAR;
VkSamplerYcbcrConversion ycbcrConversion; vkCreateSamplerYcbcrConversion(m_device, &ycbcrInfo, nullptr, &ycbcrConversion);
// 2. 단일 ImageView 생성 (YCbCr conversion 연결) VkSamplerYcbcrConversionInfo conversionInfo = {}; conversionInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO; conversionInfo.conversion = ycbcrConversion;
VkImageViewCreateInfo viewInfo = {}; viewInfo.pNext = &conversionInfo; // CRITICAL: YCbCr conversion 연결 viewInfo.format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM; // NV12 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; // 전체 이미지
// 3. Sampler도 YCbCr conversion 연결 VkSamplerCreateInfo samplerInfo = {}; samplerInfo.pNext = &conversionInfo;
// 4. 쉐이더는 단일 sampler2D 사용 (자동 변환)
📊 기타 발견사항
장점:
- ✅ NDK AMediaCodec 직접 사용 (설계보다 우수)
- ✅ Thread-safe BufferProcessor
- ✅ Async MediaCodec 지원
단점:
- ❌ ImageReader listener 없음 (polling 방식)
- ❌ ImageView 메모리 누수 (파괴 시 크래시)
- ❌ Fence 동기화 복잡도 과다
🎯 권장 조치
- 즉시 수정: VK_KHR_sampler_ycbcr_conversion 구현
- 예상 결과: DEVICE_LOST 에러 해결, 연속 재생 가능
자세한 내용은 IMPLEMENTATION_COMPARISON_ANALYSIS.md 문서를 참조하세요.