> 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 4 #include 5 #include 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), 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(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(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 } 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 문서를 참조하세요.