This commit is contained in:
2025-10-13 23:01:32 +09:00
parent a41983ff65
commit 379983233a
23 changed files with 1295 additions and 461 deletions

View File

@@ -0,0 +1,507 @@
> 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 디코딩 아키텍처 설계
1. 개요
본 문서는 안드로이드 MediaCodec을 사용하여 WebM 컨테이너로부터 파싱된 AV1 비디오 프레임을 디코딩하고, 그 결과를 Vulkan 1.1 텍스처(VkImage)로
직접 변환하는 시스템의 아키텍처를 정의합니다.
핵심 목표는 디코딩된 프레임을 CPU 메모리로 복사하지 않고 GPU 메모리 내에서 처리하는 제로-카피(Zero-Copy) 파이프라인을 구축하여 최고의 렌더링
성능을 달성하는 것입니다. 이를 위해 MediaCodec의 출력으로 AHardwareBuffer를 사용하고, 이 버퍼를 Vulkan에서 직접 참조(Import)하여 텍스처로
사용하는 방법을 제안합니다.
2. 핵심 목표
* 고성능 제로-카피: 디코딩된 YUV 프레임을 CPU로 다운로드 후 다시 GPU로 업로드하는 과정을 생략하여 성능을 극대화합니다.
* 효율적인 색상 변환: 하드웨어 디코더가 출력하는 YUV(NV12) 포맷을 Vulkan 셰이더에서 실시간으로 RGB로 변환하여 처리합니다.
* 캡슐화 및 재사용성: 디코딩 및 Vulkan 텍스처 변환 로직을 하나의 클래스로 캡슐화하여 애플리케이션의 다른 부분과 분리하고 재사용성을 높입니다.
* 현대적 API 활용: Android NDK의 AMediaCodec과 Vulkan의 외부 메모리 확장(VK_KHR_android_external_memory_android_hardware_buffer)을
사용합니다.
3. 시스템 아키텍처
전체 시스템은 다음과 같은 구성 요소로 이루어집니다.
1. WebM Parser (외부): .webm 파일에서 AV1 비디오 프레임 데이터(Frame Packet)를 추출합니다. (설계 범위 밖)
2. JNI Bridge: Java/Kotlin의 MediaCodec 설정 및 ImageReader 콜백과 네이티브 C++ 코드를 연결합니다.
3. Video Decoder Module (C++):
* `VulkanAV1Decoder` 클래스: 디코딩 프로세스 전체를 관리하는 핵심 클래스입니다.
* `MediaCodec` (via JNI): 실제 AV1 디코딩을 수행하는 안드로이드 컴포넌트. 하드웨어 가속을 사용합니다.
* `ImageReader` (via JNI): MediaCodec의 디코딩 출력 Surface를 제공하고, 출력된 프레임을 AHardwareBuffer 형태로 가져오는 역할을 합니다.
4. Vulkan Renderer (C++):
* VulkanAV1Decoder로부터 AHardwareBuffer를 받아 Vulkan VkImage로 임포트합니다.
* YCbCr to RGB 변환을 수행하는 픽셀 셰이더를 이용해 최종 이미지를 화면에 렌더링합니다.
4. 주요 기술 및 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. 상세 설계 및 구현 예시
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<HardwareBuffer>()
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>(vkPhysicalDevice)
11 );
12 return reinterpret_cast<jlong>(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<jlong>(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 }
6. 결론
본 설계는 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 원인 분석
추정 원인:
1. 수동으로 NV12를 두 개의 plane view로 분리하는 방식은 메모리 레이아웃 가정이 잘못될 수 있음
2. VkImageView 생성 시 VK_IMAGE_ASPECT_PLANE_0_BIT, VK_IMAGE_ASPECT_PLANE_1_BIT 사용이 AHardwareBuffer와 호환되지 않을 수 있음
3. 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 동기화 복잡도 과다
🎯 권장 조치
1. 즉시 수정: VK_KHR_sampler_ycbcr_conversion 구현
2. 예상 결과: DEVICE_LOST 에러 해결, 연속 재생 가능
자세한 내용은 IMPLEMENTATION_COMPARISON_ANALYSIS.md 문서를 참조하세요.