This commit is contained in:
2025-10-13 22:55:54 +09:00
parent 146a861a2e
commit a41983ff65
11 changed files with 832 additions and 132 deletions

View File

@@ -165,3 +165,62 @@
● Perfect! All three fixes are confirmed in place:
1. ✅ Root LinearLayout has NO black background (line 2-8)
2. ✅ setZOrderMediaOverlay(true) in VulkanVideoView.java (line 106)
3. ✅ Proper NV12 YUV-to-RGB shader restored
Summary of Findings
Duration Metadata Issue (NOT a playback problem)
The WebM file's segment info reports duration = 0.00 seconds because:
- The file was encoded without duration metadata (common for streaming formats)
- Lines 696-700 in WebMFileReader.cpp show GetDuration() returns 0
However, this does NOT affect playback:
- ✅ 267 frames successfully decoded (confirmed in logs)
- ✅ Frame timing working correctly (~30-40ms intervals for 30fps)
- ✅ At 30fps, 267 frames = ~8.9 seconds of actual video
Frame Timing Analysis
From time.log timestamps:
Frame 1: 13:46:29.426
Frame 2: 13:46:29.460 (+34ms)
Frame 3: 13:46:29.482 (+22ms)
Frame 4: 13:46:29.513 (+31ms)
...
Average interval: ~33ms ≈ 30fps ✅
The playback loop in vavcore_vulkan_bridge.cpp:718-748 correctly implements:
sleepTime = m_frameDurationUs - frameProcessTime;
if (sleepTime.count() > 0) {
std::this_thread::sleep_for(sleepTime);
}
Root Cause of "Video Not Visible"
The video rendering was working all along, but was hidden by black background overlay.
Fixes applied:
1. Removed android:background="@color/background_dark" from root LinearLayout
2. Added setZOrderMediaOverlay(true) to VulkanVideoView (places SurfaceView at correct z-order)
3. Restored proper NV12 YUV-to-RGB conversion shader with BT.709 color matrix
Logs Confirm Successful Rendering
VulkanRenderer: Descriptor sets updated with NV12 Y and UV planes
VulkanRenderer: vkQueuePresentKHR returned: 0
VulkanRenderer: Frame presented successfully to screen
The video should now be visible on screen. The app needs to be rebuilt and reinstalled to test the fixes.
Next Steps for Verification

388
vav2/notes/gemini-anal1.txt Normal file
View File

@@ -0,0 +1,388 @@
✦ 알겠습니다. Vav2Player에서 화면이 검게 나오는 문제를 진단해 보겠습니다. 이 문제는 비디오 처리 파이프라인의 여러 단계에서 발생할 수 있습니다.
단계별로 나누어 원인을 좁혀가겠습니다.
진단 계획:
1. `VavCore` 디코딩 확인: VavCore가 비디오 파일을 성공적으로 열고, 디코더를 선택하고, 비디오 프레임을 디코딩하여 Vav2Player로 전달하는지
확인합니다.
2. `Vav2Player` 렌더링 확인: Vav2Player가 VavCore로부터 받은 프레임 데이터를 D3D12 텍스처로 올바르게 변환하고, 셰이더를 통해 색상 변환(YUV to
RGB)을 수행한 후 화면에 정상적으로 렌더링하는지 확인합니다.
예상 원인:
* 디코딩 실패: 적절한 디코더를 찾지 못하거나, 파일이 손상되었거나, 디코딩 과정에서 오류가 발생하여 유효한 프레임이 생성되지 않음.
* 데이터 전달 오류: 디코딩된 프레임 데이터가 VavCore에서 Vav2Player로 전달되지 않거나, 데이터 포인터가 유효하지 않음.
* 렌더링 파이프라인 문제:
* D3D12 리소스(텍스처) 생성 실패.
* CPU-GPU 데이터 복사 실패.
* YUV to RGB 변환 픽셀 셰이더 오류 (예: 항상 검은색을 출력).
* 렌더링 상태 또는 파이프라인 설정 오류.
첫 번째 단계: `VavCore`와 `Vav2Player`의 연동 지점 확인
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ x SearchText {"path":"D:\\Project\\video-av1\\vav2","pattern":"IVideoDecoder"} │
│ │
│ Path validation failed: Attempted path "D:\Project\video-av1\vav2" resolves outside the allowed workspace directories: │
│ D:\Project\video-av1\vav2\platforms\android │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
먼저 Vav2Player 코드에서 VavCore의 핵심 인터페이스인 IVideoDecoder 또는 VideoDecoderFactory를 사용하는 부분을 찾아, 디코딩 요청 및 결과 수신
과정에 로그를 추가하여 문제를 확인해 보겠습니다.
관련 파일을 찾기 위해 프로젝트 전체에서 IVideoDecoder를 검색하겠습니다.
✦ 경로 문제로 이전 명령이 실패했습니다. 현재 작업 디렉토리(D:\Project\video-av1\vav2\platforms\android)를 기준으로 다시 시도하겠습니다.
사용자께서 언급하신 vav2player는 WinUI3 애플리케이션이 아닌, 현재 경로에 있는 Android 애플리케이션(applications\vav2player)으로 보입니다.
따라서 Android 환경에 맞춰 진단을 진행하겠습니다.
Android 앱의 렌더링 파이프라인을 이해하기 위해, 관련 설계 문서를 먼저 읽어보겠습니다. 이 문서는 검은 화면 문제의 원인을 파악하는 데 중요한
단서가 될 수 있습니다.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile \Android_GPU_Surface_Pipeline_Design.md │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 디자인 문서를 잘 읽어보았습니다. 문서에 따르면, 이 앱은 MediaCodec 디코더가 GPU 메모리(Vulkan Image)에 직접 프레임을 출력하고, Vulkan 렌더러가
이 메모리를 복사 없이 바로 화면에 그리는 'Zero-Copy' 파이프라인을 목표로 합니다.
핵심 진단 포인트:
이 방식은 매우 효율적이지만, 한 단계라도 실패하면 화면이 검게 나올 수밖에 없는 구조입니다. CPU를 이용한 예비 경로(Fallback)가 의도적으로
배제되었기 때문입니다.
문서에 명시된 몇 가지 잠재적인 실패 지점이 있습니다.
1. 하드웨어/드라이버 미지원: 기기가 MediaCodec에서 Vulkan 표면으로 직접 디코딩하는 것을 지원하지 않을 수 있습니다.
2. 초기화 실패: Vulkan 디바이스를 VavCore에 등록하는 과정(vavcore_set_vulkan_device)이 실패했을 수 있습니다.
3. 잘못된 프레임 타입: VavCore가 디코딩은 했지만, 예상된 Vulkan 표면 타입이 아닌 다른 포맷(예: CPU 메모리 버퍼)으로 프레임을 반환했을 수
있습니다.
4. Vulkan 렌더링 오류: RenderVulkanImage 함수에서 전달받은 Vulkan 이미지를 화면에 그리는 데 실패했을 수 있습니다.
다음 단계: 로그 확인을 위한 소스 코드 탐색
가장 확실한 방법은 문서에 언급된 오류 로그가 실제 로그캣(logcat)에 출력되는지 확인하는 것입니다. 그전에, 해당 로직을 담고 있는 핵심 소스 파일인
vavcore_vulkan_bridge.cpp가 어디에 있는지 찾아보겠습니다. 이 파일의 내용을 보면 현재 구현 상태를 더 정확히 파악할 수 있습니다.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ FindFiles '**/vavcore_vulkan_bridge.cpp' within . │
│ │
│ Found 1 matching file(s) │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 좋습니다. vavcore_vulkan_bridge.cpp 파일의 위치를 찾았습니다. 이제 이 파일의 내용을 읽어서 디자인 문서에 기술된 대로 오류 처리 및 로깅 로직이
실제로 구현되어 있는지 확인하겠습니다.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...ons\main\cpp\vavcore_vulkan_bridge.cpp │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ vavcore_vulkan_bridge.cpp 파일의 내용을 확인했습니다. 소스 코드는 이전에 검토한 디자인 문서와 정확히 일치하며, 파이프라인의 각 단계마다 상세한
로그를 기록하고 있습니다.
이제 검은 화면의 원인은 다음 중 하나일 가능성이 매우 높으며, 이는 Android의 logcat을 통해 확인할 수 있습니다.
가장 유력한 시나리오:
1. `GPU SURFACE NOT SUPPORTED` 오류: LoadVideoFile 함수에서 이 로그가 발견된다면, 현재 기기의 하드웨어나 드라이버가 VavCore가 요구하는
제로-카피(zero-copy) 디코딩을 지원하지 않는다는 의미입니다. 이것이 가장 가능성 있는 원인입니다.
2. `UNSUPPORTED SURFACE TYPE` 오류: ConvertVavCoreFrameToVulkan 함수에서 이 로그가 나온다면, 디코더는 생성되었지만 예상과 다른 종류의 비디오
프레임(예: CPU 메모리)을 반환하고 있다는 뜻입니다.
3. `Failed to render GPU surface frame` 오류: ProcessNextFrame 함수에서 이 로그가 보인다면, 디코딩은 성공했지만 Vulkan 렌더러가 프레임을
화면에 그리는 과정에서 실패했다는 의미입니다.
4. `Failed to register Vulkan device` 오류: InitializeVulkanRenderer 함수에서 이 로그가 보인다면, 초기화 단계에서 Vulkan과 VavCore 연동에
실패한 것입니다.
사용자께 요청:
이제 실제 기기에서 어떤 오류가 발생하는지 확인해야 합니다.
1. Android 기기를 PC에 연결하고, vav2player 앱을 실행하여 검은 화면이 나타나도록 하십시오.
2. 그 상태에서 터미널 또는 명령 프롬프트를 열고 아래 명령어를 실행하여 로그를 캡처해 주십시오. 이 명령어는 이번 문제와 관련된 세 가지 주요
태그(VavCoreVulkanBridge, VulkanRenderer, VavCore)의 로그만 필터링합니다.
1 adb logcat -s VavCoreVulkanBridge VulkanRenderer VavCore
3. 위 명령어를 실행한 후 출력되는 로그를 복사하여 여기에 붙여넣어 주십시오. 로그를 분석하여 정확한 원인을 진단하겠습니다.
> D:\Project\video-av1\time.log 파일로 테스트 로그를 기록해두었다. 로그를 분석해보고 원인을 찾아봐줘.
✦ 알겠습니다. D:\Project\video-av1\time.log 에 저장된 로그를 분석해달라고 하셨군요.
하지만 보안상의 이유로 제 작업 공간은 D:\Project\video-av1\vav2\platforms\android 디렉토리로 제한되어 있어, 상위 폴더에 있는 파일을 직접
읽을 수 없습니다.
번거로우시겠지만, D:\Project\video-av1\time.log 파일의 전체 내용을 복사하여 여기에 붙여넣어 주시겠어요? 로그 내용을 받으면 즉시 분석하여
문제의 원인을 찾아드리겠습니다.
> D:\Project\video-av1\vav2\platforms\android\time.log 로 옮겨두었다
✦ 알겠습니다. 파일을 작업 공간으로 옮겨주셔서 감사합니다. 이제 로그 파일을 읽고 분석하여 원인을 찾아보겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile time.log │
│ │
│ Read lines 1-2000 of 5603 from time.log │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 로그 파일을 성공적으로 읽고 분석했습니다.
로그 분석 결과:
놀랍게도, 로그 자체는 오류 없이 전체 파이프라인이 성공적으로 실행되고 있음을 보여줍니다.
1. 디코더 초기화 성공: c2.qti.av1.decoder (Qualcomm AV1 하드웨어 디코더)가 성공적으로 로드되고, Vulkan 표면(GPU 메모리) 출력을 지원하는 것으로
확인됩니다.
1 I/VavCoreVulkanBridge: GPU SURFACE SUPPORT VERIFIED
2 I/VavCoreVulkanBridge: Zero-copy MediaCodec → Vulkan pipeline ENABLED
2. 프레임 디코딩 성공: vavcore_decode_to_surface가 호출되어 프레임을 성공적으로 디코딩하고, surface_type: 7 (VULKAN_IMAGE)을 반환합니다. 이는
디코더가 GPU 메모리에 직접 프레임을 쓰고 있음을 의미합니다.
1 I/VavCore-MediaCodec: DecodeToSurface [Vulkan]: Frame 1 decoded via ImageReader → VkImage
3. Vulkan 렌더링 성공: 디코딩된 VkImage가 VulkanRenderer로 전달되고, 렌더러는 vkQueuePresentKHR 호출에 성공하며 프레임을 화면에 표시했다고
로그를 남깁니다.
1 I/VulkanRenderer: Frame presented successfully to screen
2 I/VulkanRenderer: RenderVulkanImage completed successfully
이 로그대로라면 비디오가 정상적으로 보여야 합니다. 하지만 화면이 검게 나온다는 것은, API 호출은 성공했지만 처리 과정의 데이터에 근본적인
문제가 있음을 시사합니다.
---
결정적 단서 및 원인 진단
로그에서 결정적인 단서를 찾았습니다.
1 I/VavCore-SurfaceManager: AHardwareBuffer Vulkan properties:
2 I/VavCore-SurfaceManager: format: 0
3 I/VavCore-SurfaceManager: CRITICAL FIX: Overriding VK_FORMAT_UNDEFINED to VK_FORMAT_G8_B8R8_2PLANE_420_UNORM (NV12)
이것이 문제의 핵심 원인입니다.
1. MediaCodec 디코더가 AHardwareBuffer(하드웨어 버퍼)에 프레임을 출력했습니다.
2. VavCore가 이 하드웨어 버퍼의 Vulkan 속성을 질의(vkGetAndroidHardwareBufferPropertiesANDROID)했을 때, Vulkan 드라이버가 이미지의 픽셀 포맷을
알려주지 않고 format: 0 (VK_FORMAT_UNDEFINED)을 반환했습니다.
3. 코드에서는 이 문제를 해결하기 위해 포맷을 VK_FORMAT_G8_B8R8_2PLANE_420_UNORM (일반적인 NV12 포맷)으로 강제로 가정하고 있습니다. (CRITICAL
FIX 로그)
결론:
Vulkan 드라이버가 픽셀 포맷 정보를 제대로 제공하지 않고 있으며, 코드에서는 이를 NV12 포맷일 것이라고 "추측"하고 있습니다. 하지만 실제
디코더가 출력한 포맷이 NV12가 아니거나, 드라이버와의 호환성 문제로 인해 데이터에 접근할 수 없는 상태일 가능성이 매우 높습니다.
결과적으로 Vulkan 렌더러는 유효하지 않거나 잘못된 형식의 데이터를 가지고 YUV->RGB 색상 변환을 시도하게 되고, 그 결과는 검은색 화면으로
나타나는 것입니다. 모든 API 호출이 성공했음에도 불구하고 내용물 없는 '빈 껍데기'만 렌더링되고 있는 상황입니다.
다음 단계:
이 문제는 VavCore의 VavCore-SurfaceManager가 MediaCodec의 출력 버퍼(AHardwareBuffer)를 처리하는 방식과 Vulkan 드라이버의 특정 동작 방식
사이의 호환성 문제입니다.
이 문제를 해결하려면 AHardwareBuffer의 실제 포맷을 알아내거나, ImageReader를 설정할 때 특정 포맷을 강제해야 합니다. 관련 코드를 좀 더 자세히
살펴보겠습니다. VavCore-SurfaceManager 관련 소스 파일을 찾아보겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ FindFiles '**/VavCore-SurfaceManager.*' within . │
│ │
│ No files found │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ VavCore-SurfaceManager라는 이름의 파일을 직접 찾는 데는 실패했습니다. 로그 태그와 실제 파일명이 다를 수 있습니다.
대신, 로그에 기록된 "VavCore-SurfaceManager"라는 문자열이 포함된 소스 파일을 직접 검색하여 이 로직이 어느 파일에 구현되어 있는지
찾아보겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ SearchText 'VavCore-SurfaceManager' in **/*.{cpp,h} within ./ │
│ │
│ No matches found │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 검색 결과가 나오지 않는 것이 이상하네요. 로그에는 분명히 VavCore-SurfaceManager 태그가 존재하는데 소스 코드에서 해당 문자열을 찾을 수 없다는
것은, 이 로직이 현재 우리가 보고 있는 vav2player 애플리케이션 소스 코드 바깥에 존재할 가능성을 시사합니다.
가장 유력한 시나리오는 VavCore가 별도의 라이브러리로 빌드되고, vav2player는 이 라이브러리를 가져다 사용만 하는 구조라는 것입니다. 즉, 문제의
핵심인 VavCore-SurfaceManager의 소스 코드는 이 디렉토리에 없고, 미리 빌드된 libVavCore.so 파일 안에 포함되어 있을 것입니다.
가설 변경 및 새로운 접근:
문제의 원인이 VK_FORMAT_UNDEFINED를 NV12로 강제 오버라이드하는 부분이라는 점은 거의 확실합니다. VavCore 라이브러리의 소스 코드를 직접 수정할
수 없는 현 상황에서, 우리는 VavCore 라이브러리가 올바른 포맷을 인지하도록 다른 방법을 찾아야 합니다.
VavCore가 MediaCodec의 출력 포맷을 결정하는 데 영향을 줄 수 있는 부분은 vav2player 애플리케이션에서 MediaCodec을 설정하는 부분일 것입니다.
vavcore_vulkan_bridge.cpp 로그를 다시 살펴보면, MediaCodec을 설정하고 ImageReader에 연결하는 과정이 보입니다. 이 ImageReader를 생성할 때
출력 이미지 포맷을 명시적으로 지정할 수 있습니다. 만약 현재 ImageReader가 특정 포맷 지정 없이 생성되고 있다면, 드라이버가 기본 포맷(그리고
VK_FORMAT_UNDEFINED를 반환하는)을 사용하게 될 수 있습니다.
다음 단계:
vavcore_vulkan_bridge.cpp 또는 관련 JNI 코드에서 ImageReader를 생성하거나 MediaCodec을 설정하는 부분을 찾아, 이미지 포맷을 어떻게 설정하고
있는지 확인해야 합니다.
vavcore_vulkan_bridge.cpp 파일에서 ImageReader 또는 MediaCodec.configure와 관련된 부분을 찾아 분석해 보겠습니다. 다시 한번
vavcore_vulkan_bridge.cpp 파일을 읽고 해당 부분을 집중적으로 확인하겠습니다.
╭────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...ons\main\cpp\vavcore_vulkan_bridge.cpp │
╰────────────────────────────────────────────────────────╯
✦ vavcore_vulkan_bridge.cpp 파일을 다시 검토했지만, 이 파일은 VavCore 라이브러리의 공개 API를 호출하는 역할만 할 뿐, ImageReader의 포맷을 직접
설정하는 부분은 포함하고 있지 않습니다. ImageReader 생성 및 설정은 libVavCore.so 라이브러리 내부에 캡슐화되어 있는 것으로 보입니다.
따라서 VavCore 라이브러리 소스 코드 없이는 ImageReader가 어떤 포맷으로 생성되는지 직접 수정하기는 어렵습니다.
새로운 가설: 셰이더 문제
문제의 원인이 VK_FORMAT_UNDEFINED를 NV12로 강제 변환하는 부분이라는 것은 거의 확실합니다. 그렇다면, 반대로 생각해볼 수 있습니다.
VulkanRenderer의 픽셀 셰이더가 NV12 포맷을 올바르게 처리하고 있는지 확인하는 것입니다.
만약 셰이더 코드에 버그가 있거나, NV12가 아닌 다른 포맷(예: RGBA)을 기대하고 있다면, NV12 데이터가 입력되었을 때 화면이 검게 나올 수
있습니다.
다음 단계: 렌더러 및 셰이더 코드 확인
VulkanRenderer의 소스 코드와 YUV를 RGB로 변환하는 GLSL 셰이더 코드를 찾아서 분석해 보겠습니다. 이를 통해 렌더러가 어떤 색상 포맷을 기대하고
있는지, 그리고 색상 변환 로직이 올바른지 확인할 수 있습니다.
먼저 VulkanRenderer의 소스 파일을 찾아보겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ FindFiles '**/vulkan_renderer.*' within . │
│ │
│ Found 2 matching file(s) │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 좋습니다. vulkan_renderer 소스 파일들을 찾았습니다.
먼저 헤더 파일(vulkan_renderer.h)을 읽어서 RenderVulkanImage 함수의 인터페이스와 클래스의 전반적인 구조를 파악하겠습니다.
╭────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...ons\app\src\main\cpp\vulkan_renderer.h │
╰────────────────────────────────────────────────────────╯
✦ 헤더 파일을 잘 분석했습니다. RenderVulkanImage라는 이름의 제로-카피(zero-copy) 렌더링 함수와, 별도의 Y, U, V 평면을 처리하는 CPU 기반 렌더링
함수들이 모두 선언되어 있군요.
이제 가장 중요한 vulkan_renderer.cpp 구현 파일을 읽어서, 셰이더 코드가 어떻게 작성되어 있고 RenderVulkanImage 함수가 외부 VkImage를 어떻게
처리하는지 직접 확인해 보겠습니다. 특히 YUV를 RGB로 변환하는 셰이더 코드와 vkCreateImageView 함수 호출 부분을 집중적으로 분석하겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...tions\src\main\cpp\vulkan_renderer.cpp │
│ │
│ Read lines 1-2000 of 2577 from applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ vulkan_renderer.cpp 파일의 첫 부분을 분석한 결과, 흥미로운 점을 발견했습니다.
1. `#include "yuv_shaders.h"`: 셰이더 코드가 별도의 yuv_shaders.h 파일에 포함되어 있습니다. 이 파일을 분석하면 색상 변환 로직을 정확히 파악할
수 있습니다.
2. `RenderVulkanImage` 함수의 부재: 로그에서는 분명히 RenderVulkanImage 함수가 호출되고 있지만, 제가 읽은 파일의 첫 2000줄에는 해당 함수의
구현이 없습니다. 파일의 뒷부분에 있을 것으로 보입니다.
계속해서 vulkan_renderer.cpp 파일의 나머지 부분을 읽어서 RenderVulkanImage 함수의 실제 구현과, 비디오 렌더링에 사용되는 그래픽스 파이프라인
및 디스크립터 셋(descriptor set)이 어떻게 설정되는지 확인하겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...tions\src\main\cpp\vulkan_renderer.cpp │
│ │
│ Read lines 2001-2577 of 2577 from applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ vulkan_renderer.cpp 파일의 나머지 부분을 모두 분석했으며, 결정적인 단서를 찾았습니다.
분석 결과
1. `RenderVulkanImage` 함수 구현 확인
파일 끝에서 RenderVulkanImage 함수의 구현을 찾았습니다. 이 함수는 입력받은 VkImage를 NV12 포맷(2-plane YUV)으로 간주하고, Y 평면과 UV
평면에 대해 각각 별도의 VkImageView를 생성합니다.
* Y 평면 (Luminance): VK_FORMAT_R8_UNORM 포맷의 뷰 생성
* UV 평면 (Chrominance): VK_FORMAT_R8G8_UNORM 포맷의 뷰 생성
이것은 NV12 포맷을 처리하는 표준적인 접근 방식이며, 이 부분의 코드는 올바르게 작성된 것으로 보입니다.
2. 치명적인 모순 발견
하지만 심각한 문제점을 발견했습니다. RenderVulkanImage 함수는 Y 평면과 UV 평면, 이렇게 2개의 텍스처를 셰이더에 전달하기 위해 디스크립터
셋(descriptor set)의 바인딩 0번과 1번을 업데이트합니다.
그러나 파이프라인을 생성하는 CreateDescriptorSetLayout 함수는 다음과 같이 바인딩을 전혀 만들지 않도록 코딩되어 있습니다.
1 // vulkan_renderer.cpp 내의 CreateDescriptorSetLayout 함수
2 bool VulkanVideoRenderer::CreateDescriptorSetLayout() {
3 LOGI("Creating descriptor set layout (no bindings - Hello Triangle style)...");
4
5 // No bindings for hardcoded triangle shader
6 VkDescriptorSetLayoutCreateInfo layoutInfo = {};
7 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
8 layoutInfo.bindingCount = 0; // <-- 바인딩 개수가 0입니다!
9 layoutInfo.pBindings = nullptr;
10 // ...
11 }
결론: 문제의 원인
이것이 바로 검은 화면의 원인입니다.
Vulkan 렌더링 파이프라인이 텍스처를 받을 준비가 전혀 되어 있지 않은 상태(바인딩 0개)에서, 렌더링 루프는 디코딩된 비디오 텍스처를
파이프라인에 전달하려고 시도(바인딩 2개 업데이트)하고 있습니다.
Vulkan의 유효성 검사 계층(validation layer)이 비활성화되어 있다면 이 API 호출이 오류를 뿜지 않고 조용히 실패할 수 있습니다. 결과적으로
셰이더는 샘플링할 텍스처를 전혀 받지 못하므로, 최종적으로 검은색만 출력하게 됩니다.
다음 단계: 셰이더 확인 및 수정 제안
이제 마지막 확인 단계로, 셰이더 코드가 실제로 2개의 텍스처(Y, UV)를 필요로 하는지 yuv_shaders.h 파일을 읽어 확인하겠습니다. 이 확인이
끝나면, CreateDescriptorSetLayout 함수를 수정하여 문제를 해결하는 코드를 제안해 드릴 수 있습니다.
yuv_shaders.h 파일을 읽겠습니다.
╭────────────────────────────────────────────────────────╮
│ ✓ ReadFile ...cations\app\src\main\cpp\yuv_shaders.h │
╰────────────────────────────────────────────────────────╯
✦ yuv_shaders.h 파일을 분석한 결과, 마지막 퍼즐 조각이 맞춰졌고, 동시에 또 다른 모순점을 발견했습니다.
최종 분석 및 원인 확정
1. 셰이더의 요구사항: 주석으로 포함된 GLSL 프래그먼트 셰이더 코드는 3개의 분리된 텍스처(yTexture, uTexture, vTexture)를 바인딩 0, 1, 2번에서
받을 것으로 기대하고 있습니다. 이는 Y, U, V 평면이 완전히 분리된 YUV420p 포맷을 위한 코드입니다.
2. 드러난 모순점 종합:
* 파이프라인 vs 렌더러: CreateDescriptorSetLayout 함수는 0개의 텍스처 바인딩을 설정합니다. 하지만 RenderVulkanImage 함수는 2개의
바인딩(Y, UV)을 사용하려고 합니다.
* 렌더러 vs 셰이더: RenderVulkanImage 함수는 2-plane NV12 포맷(Y 평면 1개, UV 평면 1개)에 맞춰 2개의 텍스처 뷰를 준비합니다. 하지만
프래그먼트 셰이더는 3-plane YUV420p 포맷을 위한 3개의 텍스처를 기대합니다.
결론: 현재 코드는 렌더링 파이프라인의 여러 단계에 걸쳐 서로 맞지 않는 약속을 하고 있습니다. 파이프라인은 텍스처를 받을 준비가 안 됐고, 설령
받는다고 해도 렌더러가 준비한 데이터와 셰이더가 기대하는 데이터의 형식이 다릅니다. 이로 인해 GPU는 아무것도 그리지 못하고 검은 화면만
출력하게 됩니다.
---
해결 방안
이 문제를 해결하려면 이 세 가지 구성요소(파이프라인, 렌더러, 셰이더)가 모두 NV12 포맷을 사용하도록 통일해야 합니다. RenderVulkanImage 함수가
이미 NV12를 기준으로 작성되어 있으므로, 나머지 두 부분을 여기에 맞추는 것이 가장 효율적입니다.
수정 계획:
1. `CreateDescriptorSetLayout` 수정: 2개의 텍스처(Y, UV)를 받을 수 있도록 바인딩 2개를 설정하는 코드로 교체합니다.
2. 프래그먼트 셰이더 교체: 3개의 텍스처(Y, U, V) 대신 2개의 텍스처(Y, UV)를 입력받아 색상 변환을 수행하는 올바른 NV12용 GLSL 셰이더 코드로
교체합니다.
먼저, 셰이더 코드가 들어있는 파일을 찾아야 합니다. yuv_shaders.h에 선언된 vertex_shader_spirv 변수가 정의된 파일을 검색하겠습니다.
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ SearchText 'vertex_shader_spirv' in **/*.cpp within ./ │
│ │
│ Found 4 matches │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 좋습니다. 셰이더 코드가 yuv_shaders.cpp 파일에 정의되어 있는 것을 확인했습니다.
분석 결과, 문제는 세 부분에 걸쳐 있습니다.
1. 파이프라인 설정(`CreateDescriptorSetLayout`): 텍스처를 받을 준비(바인딩)가 전혀 되어있지 않습니다.
2. 렌더러-셰이더 바인딩 불일치: RenderVulkanImage 함수는 Y, UV 텍스처를 바인딩 0, 1번에 전달하려 하지만, 셰이더는 Y, U, V 텍스처를 바인딩 0,
1, 2번에서 기대합니다.
3. 셰이더 포맷 불일치: 렌더러는 2-plane NV12 포맷을 준비하지만, 셰이더는 3-plane YUV420p 포맷을 기대합니다.
이 문제들을 한 번에 해결하기 위해, NV12 포맷을 기준으로 코드를 통일하겠습니다.
수정 작업 계획:
1. `vulkan_renderer.cpp` 수정:
* CreateDescriptorSetLayout 함수를 3개의 바인딩(UBO, Y-sampler, UV-sampler)을 갖도록 수정합니다.
* CreateGraphicsPipeline 함수가 텍스처 좌표를 사용하는 정점(vertex) 속성을 올바르게 사용하도록 수정합니다.
* RenderVulkanImage 함수가 Y와 UV 텍스처를 올바른 바인딩 번호(1번, 2번)에 전달하도록 수정합니다.
2. `yuv_shaders.cpp` 수정:
* 프래그먼트 셰이더 코드를 2개의 텍스처(Y, UV)를 입력받아 NV12 포맷을 올바르게 처리하는 코드로 교체합니다.
먼저 1단계인 vulkan_renderer.cpp 파일 수정을 시작하겠습니다. 여러 부분을 수정해야 하므로, 가장 핵심적인 CreateDescriptorSetLayout 함수부터
교체하겠습니다.

View File

@@ -325,20 +325,27 @@ bool VavCoreVulkanBridge::ProcessNextFrame() {
frameData.vkImage, frameData.width, frameData.height);
// Render external VkImage directly (zero-copy GPU pipeline)
LOGI("Calling RenderVulkanImage()...");
bool renderSuccess = m_vulkanRenderer->RenderVulkanImage(
reinterpret_cast<VkImage>(frameData.vkImage),
frameData.width,
frameData.height
);
LOGI("RenderVulkanImage() returned: %s", renderSuccess ? "true" : "false");
if (renderSuccess) {
LOGI("Render successful - updating counters and position");
m_renderedFrameCount++;
m_currentPositionUs = frameData.timestampUs;
m_frameNumber = frameData.frameNumber;
// Call frame ready callback
if (m_frameReadyCallback) {
LOGI("Calling frame ready callback...");
m_frameReadyCallback(frameData);
LOGI("Frame ready callback completed");
} else {
LOGI("No frame ready callback registered");
}
} else {
LOGE("Failed to render GPU surface frame");
@@ -346,9 +353,12 @@ bool VavCoreVulkanBridge::ProcessNextFrame() {
}
// Free frame
LOGI("Calling vavcore_free_frame()...");
vavcore_free_frame(&frame);
LOGI("vavcore_free_frame() completed");
m_decodedFrameCount++;
LOGI("ProcessNextFrame() about to return: %s", renderSuccess ? "true" : "false");
return renderSuccess;
}
@@ -718,11 +728,17 @@ void VavCoreVulkanBridge::StopContinuousPlayback() {
void VavCoreVulkanBridge::PlaybackThreadMain() {
LOGI("Playback thread started");
int frameCount = 0;
while (ShouldContinuePlayback()) {
frameCount++;
LOGI("=== Playback Loop Iteration #%d START ===", frameCount);
auto frameStart = std::chrono::steady_clock::now();
// Process next frame
LOGI("ProcessNextFrame() calling...");
bool success = ProcessNextFrame();
LOGI("ProcessNextFrame() returned: %s", success ? "true" : "false");
if (!success) {
LOGI("End of video or decode error, stopping playback");
// Set state to stopped and break the loop
@@ -733,26 +749,41 @@ void VavCoreVulkanBridge::PlaybackThreadMain() {
// Calculate frame timing
auto frameEnd = std::chrono::steady_clock::now();
auto frameProcessTime = std::chrono::duration_cast<std::chrono::microseconds>(frameEnd - frameStart);
LOGI("Frame processing time: %lld us", (long long)frameProcessTime.count());
// Sleep for remaining frame duration to maintain proper playback rate
auto sleepTime = m_frameDurationUs - frameProcessTime;
if (sleepTime.count() > 0) {
LOGI("Sleeping for %lld us to maintain playback rate", (long long)sleepTime.count());
std::this_thread::sleep_for(sleepTime);
} else {
LOGI("No sleep needed - frame processing took longer than target duration");
}
// Update frame timing statistics
m_lastFrameTime = std::chrono::steady_clock::now();
LOGI("=== Playback Loop Iteration #%d END ===", frameCount);
}
LOGI("Playback thread ended");
LOGI("Playback thread ended - final frame count: %d", frameCount);
m_shouldContinuePlayback.store(false);
}
bool VavCoreVulkanBridge::ShouldContinuePlayback() const {
LOGI("ShouldContinuePlayback() - acquiring mutex...");
std::lock_guard<std::mutex> lock(m_stateMutex);
return m_shouldContinuePlayback.load() &&
m_playbackState == PlaybackState::PLAYING &&
m_fileLoaded;
bool shouldContinue = m_shouldContinuePlayback.load();
bool isPlaying = (m_playbackState == PlaybackState::PLAYING);
bool fileLoaded = m_fileLoaded;
LOGI("ShouldContinuePlayback(): shouldContinue=%s, isPlaying=%s, fileLoaded=%s",
shouldContinue ? "true" : "false",
isPlaying ? "true" : "false",
fileLoaded ? "true" : "false");
bool result = shouldContinue && isPlaying && fileLoaded;
LOGI("ShouldContinuePlayback() returning: %s", result ? "true" : "false");
return result;
}
PlaybackState VavCoreVulkanBridge::GetPlaybackState() const {

View File

@@ -676,9 +676,19 @@ bool VulkanVideoRenderer::CreateSwapchain() {
VkExtent2D extent = ChooseSwapExtent(capabilities);
// Determine number of images
uint32_t imageCount = capabilities.minImageCount + 1;
// Force triple buffering (3 images) to avoid DEVICE_LOST errors with more images
uint32_t imageCount = 3;
// Ensure we meet minimum requirements
if (imageCount < capabilities.minImageCount) {
imageCount = capabilities.minImageCount;
LOGW("minImageCount (%u) is higher than desired 3, using minImageCount", capabilities.minImageCount);
}
// Ensure we don't exceed maximum
if (capabilities.maxImageCount > 0 && imageCount > capabilities.maxImageCount) {
imageCount = capabilities.maxImageCount;
LOGW("maxImageCount (%u) is lower than desired 3, using maxImageCount", capabilities.maxImageCount);
}
// Create swapchain
@@ -721,6 +731,10 @@ bool VulkanVideoRenderer::CreateSwapchain() {
m_swapchainImages.resize(imageCount);
vkGetSwapchainImagesKHR(m_device, m_swapchain, &imageCount, m_swapchainImages.data());
// Initialize images in flight tracking (one fence per swapchain image)
// Initially set to VK_NULL_HANDLE (no fence associated yet)
m_imagesInFlight.resize(imageCount, VK_NULL_HANDLE);
m_swapchainImageFormat = surfaceFormat.format;
m_swapchainExtent = extent;
@@ -746,14 +760,10 @@ VkSurfaceFormatKHR VulkanVideoRenderer::ChooseSwapSurfaceFormat(const std::vecto
}
VkPresentModeKHR VulkanVideoRenderer::ChooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availableModes) {
// Prefer mailbox mode for low latency
for (const auto& availableMode : availableModes) {
if (availableMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availableMode;
}
}
// NOTE: Disabled MAILBOX mode as it creates too many swapchain images (5-6),
// which causes DEVICE_LOST errors. FIFO mode (V-Sync) should be sufficient.
// FIFO is guaranteed to be available
// FIFO is guaranteed to be available and uses standard double/triple buffering
return VK_PRESENT_MODE_FIFO_KHR;
}
@@ -868,15 +878,35 @@ bool VulkanVideoRenderer::CreateRenderPass() {
}
bool VulkanVideoRenderer::CreateDescriptorSetLayout() {
LOGI("Creating descriptor set layout (no bindings - Hello Triangle style)...");
LOGI("Creating descriptor set layout for YUV video rendering...");
std::array<VkDescriptorSetLayoutBinding, 3> bindings = {};
// Binding 0: Uniform Buffer (Vertex Shader)
bindings[0].binding = 0;
bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
bindings[0].descriptorCount = 1;
bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
bindings[0].pImmutableSamplers = nullptr;
// Binding 1: Y-plane Sampler (Fragment Shader)
bindings[1].binding = 1;
bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
bindings[1].descriptorCount = 1;
bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings[1].pImmutableSamplers = nullptr;
// Binding 2: UV-plane Sampler (Fragment Shader)
bindings[2].binding = 2;
bindings[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
bindings[2].descriptorCount = 1;
bindings[2].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings[2].pImmutableSamplers = nullptr;
// No bindings for hardcoded triangle shader
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 0;
layoutInfo.pBindings = nullptr;
LOGI("Descriptor layout: No bindings (hardcoded triangle shader)");
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();
VkResult result = vkCreateDescriptorSetLayout(m_device, &layoutInfo, nullptr, &m_descriptorSetLayout);
if (result != VK_SUCCESS) {
@@ -884,12 +914,12 @@ bool VulkanVideoRenderer::CreateDescriptorSetLayout() {
return false;
}
LOGI("Descriptor set layout created successfully");
LOGI("Descriptor set layout created successfully with 3 bindings.");
return true;
}
bool VulkanVideoRenderer::CreateGraphicsPipeline() {
LOGI("Creating graphics pipeline...");
LOGI("Creating graphics pipeline for YUV video rendering...");
// Create shader modules
VkShaderModule vertShaderModule = CreateShaderModule(Shaders::vertex_shader_spirv);
@@ -916,19 +946,16 @@ bool VulkanVideoRenderer::CreateGraphicsPipeline() {
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};
// Vertex input - Hello Triangle style (no vertex input, uses gl_VertexIndex)
LOGI("Setting up vertex input for Hello Triangle (no vertex input attributes)...");
// Vertex input
auto bindingDescription = GetVertexBindingDescription();
auto attributeDescriptions = GetVertexAttributeDescriptions();
VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
LOGI("Vertex input: %u bindings, %u attributes (Hello Triangle style)",
vertexInputInfo.vertexBindingDescriptionCount,
vertexInputInfo.vertexAttributeDescriptionCount);
vertexInputInfo.vertexBindingDescriptionCount = 1;
vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size());
vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data();
// Input assembly
VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
@@ -936,25 +963,11 @@ bool VulkanVideoRenderer::CreateGraphicsPipeline() {
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
// Viewport
VkViewport viewport = {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(m_swapchainExtent.width);
viewport.height = static_cast<float>(m_swapchainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
VkRect2D scissor = {};
scissor.offset = {0, 0};
scissor.extent = m_swapchainExtent;
// Viewport and scissor
VkPipelineViewportStateCreateInfo viewportState = {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
// Rasterizer
VkPipelineRasterizationStateCreateInfo rasterizer = {};
@@ -963,8 +976,8 @@ bool VulkanVideoRenderer::CreateGraphicsPipeline() {
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.cullMode = VK_CULL_MODE_NONE; // No culling for 2D quad
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;
// Multisampling
@@ -981,23 +994,22 @@ bool VulkanVideoRenderer::CreateGraphicsPipeline() {
VkPipelineColorBlendStateCreateInfo colorBlending = {};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
// Pipeline layout (Sascha Willems style - simple descriptor set only)
// Dynamic states
std::vector<VkDynamicState> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicState = {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();
// Pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &m_descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 0; // No push constants
pipelineLayoutInfo.pPushConstantRanges = nullptr;
LOGI("Pipeline layout: Simple descriptor set only (Sascha Willems style)");
pipelineLayoutInfo.pushConstantRangeCount = 0;
VkResult result = vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout);
if (result != VK_SUCCESS) {
@@ -1018,42 +1030,23 @@ bool VulkanVideoRenderer::CreateGraphicsPipeline() {
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = &dynamicState;
pipelineInfo.layout = m_pipelineLayout;
pipelineInfo.renderPass = m_renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
LOGI("=== CREATING GRAPHICS PIPELINE DEBUG ===");
LOGI("About to call vkCreateGraphicsPipelines...");
LOGI("Device: %p, Pipeline cache: VK_NULL_HANDLE", m_device);
LOGI("Pipeline layout: %p, Render pass: %p", m_pipelineLayout, m_renderPass);
LOGI("Shader stages: vertex=%p, fragment=%p", vertShaderModule, fragShaderModule);
result = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_graphicsPipeline);
LOGI("vkCreateGraphicsPipelines returned: %d", result);
if (result != VK_SUCCESS) {
LOGE("=== GRAPHICS PIPELINE CREATION FAILED ===");
LOGE("Error code: %d", result);
if (result == -13) {
LOGE("VK_ERROR_TOO_MANY_OBJECTS: Possible causes:");
LOGE(" - Too many pipeline objects already created");
LOGE(" - Device limits exceeded");
LOGE(" - Resource exhaustion");
}
LOGE("=== END PIPELINE FAILURE DEBUG ===");
}
// Cleanup shader modules
vkDestroyShaderModule(m_device, fragShaderModule, nullptr);
vkDestroyShaderModule(m_device, vertShaderModule, nullptr);
if (result == VK_SUCCESS) {
LOGI("Graphics pipeline created successfully");
return true;
if (result != VK_SUCCESS) {
LOGE("Failed to create graphics pipeline: %d", result);
return false;
}
return false;
LOGI("Graphics pipeline for YUV video created successfully.");
return true;
}
VkShaderModule VulkanVideoRenderer::CreateShaderModule(const std::vector<uint32_t>& code) {
@@ -2040,13 +2033,12 @@ bool VulkanVideoRenderer::RenderFrame(const uint8_t* yPlane, const uint8_t* uPla
}
bool VulkanVideoRenderer::BeginFrame(uint32_t& imageIndex) {
// Wait for previous frame to finish
vkWaitForFences(m_device, 1, &m_inFlightFences[m_currentFrame], VK_TRUE, UINT64_MAX);
// NOTE: Fence wait/reset is now done in RenderVulkanImage() BEFORE descriptor set update
// to ensure descriptor sets are not updated while GPU is still using them
// WORKAROUND: Adreno GPU timestamp issue
// Ensure queue is completely idle before acquiring next image
// This prevents "next client ts must be greater than current ts" errors
vkQueueWaitIdle(m_graphicsQueue);
// NOTE: Removed vkQueueWaitIdle() Adreno workaround as it causes VK_ERROR_DEVICE_LOST
// at frame 5 (imageIndex 3). The fence synchronization should be sufficient.
// If timestamp errors reappear, we'll need a different solution.
// Acquire next swapchain image
VkResult result = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX,
@@ -2064,8 +2056,12 @@ bool VulkanVideoRenderer::BeginFrame(uint32_t& imageIndex) {
return false;
}
// Reset fence for this frame
vkResetFences(m_device, 1, &m_inFlightFences[m_currentFrame]);
// NOTE: images-in-flight tracking disabled due to fence reuse conflicts when
// MAX_FRAMES_IN_FLIGHT < swapchain image count. The fence wait in RenderVulkanImage()
// before descriptor updates provides sufficient synchronization.
// NOTE: Fence reset is already done in RenderVulkanImage() at the start
// DO NOT reset here again as it's already in unsignaled state and ready for vkQueueSubmit
LOGI("BeginFrame succeeded: imageIndex=%u, currentFrame=%zu", imageIndex, m_currentFrame);
return true;
@@ -2101,6 +2097,11 @@ bool VulkanVideoRenderer::EndFrame(uint32_t imageIndex) {
return false;
}
// Mark this swapchain image as in use by this frame's fence
// This allows BeginFrame() to wait for the fence when this image is reused
m_imagesInFlight[imageIndex] = m_inFlightFences[m_currentFrame];
LOGI("Marked swapchain image %u as in use by frame %u fence", imageIndex, m_currentFrame);
// Present frame
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
@@ -2120,6 +2121,7 @@ bool VulkanVideoRenderer::EndFrame(uint32_t imageIndex) {
LOGW("Swapchain out of date or suboptimal, recreating (result=%d, resized=%d)", result, m_framebufferResized);
m_framebufferResized = false;
RecreateSwapchain();
LOGI("Swapchain recreation complete, continuing with rendering");
} else if (result != VK_SUCCESS) {
LOGE("Failed to present swapchain image: %d", result);
return false;
@@ -2128,10 +2130,13 @@ bool VulkanVideoRenderer::EndFrame(uint32_t imageIndex) {
}
// Collect timestamp query results from previous frame
LOGI("Collecting timestamp query results...");
CollectTimestampResults();
LOGI("Timestamp query results collected");
// Move to next frame
m_currentFrame = (m_currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
LOGI("RenderVulkanImage completed successfully - returning true");
return true;
}
@@ -2279,6 +2284,26 @@ void VulkanVideoRenderer::RecreateSwapchain() {
// Wait for device to be idle
vkDeviceWaitIdle(m_device);
// Recreate timestamp query pool to avoid hanging in CollectTimestampResults()
// (vkResetQueryPool is Vulkan 1.2+, not available on Android Vulkan 1.1)
if (m_timestampQueryPool != VK_NULL_HANDLE) {
LOGI("Recreating timestamp query pool after swapchain recreation...");
vkDestroyQueryPool(m_device, m_timestampQueryPool, nullptr);
VkQueryPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
poolInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
poolInfo.queryCount = MAX_FRAMES_IN_FLIGHT * TIMESTAMPS_PER_FRAME;
VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &m_timestampQueryPool);
if (result == VK_SUCCESS) {
LOGI("Timestamp query pool recreated successfully");
} else {
LOGW("Failed to recreate timestamp query pool: %d", result);
m_timestampQueryPool = VK_NULL_HANDLE;
}
}
// Cleanup old swapchain
CleanupSwapchain();
@@ -2334,8 +2359,8 @@ void VulkanVideoRenderer::WriteTimestampStart(VkCommandBuffer commandBuffer) {
// Query index for render start: frame_index * 2
uint32_t queryIndex = static_cast<uint32_t>(m_currentFrame * TIMESTAMPS_PER_FRAME);
// Reset query before writing
vkCmdResetQueryPool(commandBuffer, m_timestampQueryPool, queryIndex, 1);
// NOTE: Removed vkCmdResetQueryPool as it may cause DEVICE_LOST errors
// Query pool is recreated on swapchain recreation, which should be sufficient
// Write timestamp at top of pipe (all commands completed)
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
@@ -2350,8 +2375,8 @@ void VulkanVideoRenderer::WriteTimestampEnd(VkCommandBuffer commandBuffer) {
// Query index for render end: frame_index * 2 + 1
uint32_t queryIndex = static_cast<uint32_t>(m_currentFrame * TIMESTAMPS_PER_FRAME + 1);
// Reset query before writing
vkCmdResetQueryPool(commandBuffer, m_timestampQueryPool, queryIndex, 1);
// NOTE: Removed vkCmdResetQueryPool as it may cause DEVICE_LOST errors
// Query pool is recreated on swapchain recreation, which should be sufficient
// Write timestamp at bottom of pipe (all rendering completed)
vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
@@ -2367,7 +2392,7 @@ void VulkanVideoRenderer::CollectTimestampResults() {
uint32_t startQueryIndex = static_cast<uint32_t>(m_currentFrame * TIMESTAMPS_PER_FRAME);
uint32_t endQueryIndex = startQueryIndex + 1;
// Get timestamp results (blocking wait for results to be available)
// Get timestamp results (non-blocking - skip if not ready yet)
uint64_t timestamps[2] = {0, 0};
VkResult result = vkGetQueryPoolResults(
m_device,
@@ -2377,7 +2402,7 @@ void VulkanVideoRenderer::CollectTimestampResults() {
sizeof(timestamps),
timestamps,
sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT
VK_QUERY_RESULT_64_BIT // Removed WAIT_BIT to avoid hanging
);
if (result == VK_SUCCESS && timestamps[0] != 0 && timestamps[1] != 0) {
@@ -2437,6 +2462,21 @@ bool VulkanVideoRenderer::RenderVulkanImage(VkImage sourceImage, uint32_t width,
LOGI("RenderVulkanImage: Rendering external VkImage (%p) size=%ux%u",
(void*)sourceImage, width, height);
// CRITICAL FIX: Wait for previous frame using this currentFrame index to finish
// BEFORE updating descriptor sets. Without this, we update descriptor sets
// while GPU is still using them, causing DEVICE_LOST!
LOGI("Waiting for frame %u fence before updating descriptor sets...", m_currentFrame);
vkWaitForFences(m_device, 1, &m_inFlightFences[m_currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(m_device, 1, &m_inFlightFences[m_currentFrame]);
LOGI("Frame %u fence signaled and reset", m_currentFrame);
// Begin frame to get imageIndex (moved before descriptor updates)
uint32_t imageIndex;
if (!BeginFrame(imageIndex)) {
LOGW("Failed to begin frame, likely due to swapchain recreation. Skipping this frame.");
return true; // Allow playback loop to continue
}
// Update video dimensions if changed
if (width != m_videoWidth || height != m_videoHeight) {
m_videoWidth = width;
@@ -2512,19 +2552,19 @@ bool VulkanVideoRenderer::RenderVulkanImage(VkImage sourceImage, uint32_t width,
VkWriteDescriptorSet descriptorWrites[2] = {};
// Binding 0: Y plane
// Binding 1: Y plane
descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[0].dstSet = m_descriptorSets[m_currentFrame];
descriptorWrites[0].dstBinding = 0;
descriptorWrites[0].dstBinding = 1;
descriptorWrites[0].dstArrayElement = 0;
descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[0].descriptorCount = 1;
descriptorWrites[0].pImageInfo = &yImageInfo;
// Binding 1: UV plane (will be split in shader)
// Binding 2: UV plane
descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[1].dstSet = m_descriptorSets[m_currentFrame];
descriptorWrites[1].dstBinding = 1;
descriptorWrites[1].dstBinding = 2;
descriptorWrites[1].dstArrayElement = 0;
descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[1].descriptorCount = 1;
@@ -2534,14 +2574,8 @@ bool VulkanVideoRenderer::RenderVulkanImage(VkImage sourceImage, uint32_t width,
LOGI("Descriptor sets updated with NV12 Y and UV planes");
// Begin frame rendering
uint32_t imageIndex;
if (!BeginFrame(imageIndex)) {
LOGE("Failed to begin frame");
vkDestroyImageView(m_device, yPlaneView, nullptr);
vkDestroyImageView(m_device, uvPlaneView, nullptr);
return false;
}
// NOTE: BeginFrame() was already called before descriptor update to ensure
// proper synchronization. imageIndex is available from earlier call.
// Record and submit command buffer (uses existing pipeline)
if (!RecordCommandBuffer(imageIndex)) {
@@ -2562,13 +2596,14 @@ bool VulkanVideoRenderer::RenderVulkanImage(VkImage sourceImage, uint32_t width,
// Update performance metrics
UpdatePerformanceMetrics();
// Cleanup NV12 plane image views
// TODO: These views should be destroyed AFTER GPU finishes using them
// Currently we're destroying them immediately after vkQueueSubmit
// This works only because the GPU might still be using cached descriptor data
// A proper fix would be to cache these views and destroy them after fence wait
vkDestroyImageView(m_device, yPlaneView, nullptr);
vkDestroyImageView(m_device, uvPlaneView, nullptr);
// TODO: Properly manage ImageView lifecycle with frame-in-flight tracking
// For now, we're leaking ImageViews to avoid DEVICE_LOST errors
// The correct solution is to store ImageViews per frame-in-flight and destroy them
// after the fence signals that the GPU is done using them
//
// NOTE: Calling vkQueueWaitIdle() here causes timing issues on Adreno GPUs,
// leading to VK_ERROR_DEVICE_LOST on the 5th frame
LOGW("ImageViews not destroyed - memory leak! Need to implement proper lifecycle management");
LOGI("RenderVulkanImage completed successfully");
return true;

View File

@@ -172,6 +172,7 @@ private:
std::vector<VkSemaphore> m_imageAvailableSemaphores;
std::vector<VkSemaphore> m_renderFinishedSemaphores;
std::vector<VkFence> m_inFlightFences;
std::vector<VkFence> m_imagesInFlight; // Track which fence is used by each swapchain image
static const int MAX_FRAMES_IN_FLIGHT = 2;
size_t m_currentFrame = 0;

View File

@@ -59,9 +59,8 @@ const std::vector<uint32_t> vertex_shader_spirv = {
// #version 450
// layout(location = 0) in vec2 fragTexCoord;
// layout(location = 0) out vec4 outColor;
// layout(binding = 0) uniform sampler2D yTexture; // Y plane (single channel)
// layout(binding = 1) uniform sampler2D uvTexture; // UV plane (dual channel, interleaved)
// layout(binding = 2) uniform sampler2D vTexture; // Unused for NV12
// layout(binding = 1) uniform sampler2D yTexture; // Y plane (single channel)
// layout(binding = 2) uniform sampler2D uvTexture; // UV plane (dual channel, interleaved)
// void main() {
// float y = texture(yTexture, fragTexCoord).r;
// vec2 uv = texture(uvTexture, fragTexCoord).rg;
@@ -86,9 +85,9 @@ const std::vector<uint32_t> fragment_shader_spirv = {
0x00000065, 0x00030005, 0x0000001e, 0x00000075, 0x00030005, 0x00000023, 0x00000076, 0x00030005,
0x00000028, 0x00000072, 0x00030005, 0x0000002e, 0x00000067, 0x00030005, 0x00000038, 0x00000062,
0x00050005, 0x0000003f, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00050005, 0x00000045, 0x78655476,
0x65727574, 0x00000000, 0x00040047, 0x0000000c, 0x00000021, 0x00000000, 0x00040047, 0x0000000c,
0x65727574, 0x00000000, 0x00040047, 0x0000000c, 0x00000021, 0x00000001, 0x00040047, 0x0000000c,
0x00000022, 0x00000000, 0x00040047, 0x00000010, 0x0000001e, 0x00000000, 0x00040047, 0x00000019,
0x00000021, 0x00000001, 0x00040047, 0x00000019, 0x00000022, 0x00000000, 0x00040047, 0x0000003f,
0x00000021, 0x00000002, 0x00040047, 0x00000019, 0x00000022, 0x00000000, 0x00040047, 0x0000003f,
0x0000001e, 0x00000000, 0x00040047, 0x00000045, 0x00000021, 0x00000002, 0x00040047, 0x00000045,
0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00090019, 0x00000009,

View File

@@ -60,6 +60,8 @@ set(VAVCORE_TEST_SOURCES
${VAVCORE_ROOT}/src/Decoder/MediaCodecSurfaceManager.cpp
${VAVCORE_ROOT}/src/Decoder/AV1Decoder.cpp
${VAVCORE_ROOT}/src/FileIO/WebMFileReader.cpp
${VAVCORE_ROOT}/src/Common/ImageUtils.cpp
${VAVCORE_ROOT}/src/Common/VavCoreLogger.cpp
${VAVCORE_ROOT}/src/VavCore.cpp
)
@@ -75,6 +77,7 @@ find_library(mediandk-lib mediandk)
find_library(android-lib android)
find_library(glesv3-lib GLESv3)
find_library(egl-lib EGL)
find_library(vulkan-lib vulkan)
if(NOT log-lib)
message(FATAL_ERROR "Android log library not found")
@@ -93,6 +96,7 @@ target_link_libraries(VavCoreUnitTests
${android-lib}
${glesv3-lib}
${egl-lib}
${vulkan-lib}
)
# Import dav1d library

View File

@@ -248,4 +248,168 @@ TEST_F(MediaCodecAV1DecoderTest, HardwareAccelerationDetection) {
} else {
SUCCEED() << "Decoder reports software decoding (may be emulator)";
}
}
// Test 13: Async mode support and initialization
TEST_F(MediaCodecAV1DecoderTest, AsyncModeSupport) {
LOGI("Test: AsyncModeSupport");
auto codecs = decoder->GetAvailableCodecs();
if (codecs.empty()) {
GTEST_SKIP() << "No AV1 codecs available for async mode test";
}
// Initialize decoder first
VideoMetadata metadata;
metadata.width = 1920;
metadata.height = 1080;
metadata.frame_rate = 30.0;
metadata.codec_type = VideoCodecType::AV1;
bool initSuccess = decoder->Initialize(metadata);
if (!initSuccess) {
GTEST_SKIP() << "Cannot test async mode without successful initialization";
}
// Check async mode support
bool supportsAsync = decoder->SupportsAsyncMode();
LOGI("Async mode supported: %s", supportsAsync ? "YES" : "NO");
// On API 29+, async mode should be supported
EXPECT_TRUE(supportsAsync) << "Async mode should be supported on API 29+";
SUCCEED() << "Async mode support verified";
}
// Test 14: Async mode enable/disable cycle
TEST_F(MediaCodecAV1DecoderTest, AsyncModeEnableDisableCycle) {
LOGI("Test: AsyncModeEnableDisableCycle");
auto codecs = decoder->GetAvailableCodecs();
if (codecs.empty()) {
GTEST_SKIP() << "No AV1 codecs available for async mode cycle test";
}
// Initialize decoder
VideoMetadata metadata;
metadata.width = 1920;
metadata.height = 1080;
metadata.frame_rate = 30.0;
metadata.codec_type = VideoCodecType::AV1;
bool initSuccess = decoder->Initialize(metadata);
if (!initSuccess) {
GTEST_SKIP() << "Cannot test async mode cycle without successful initialization";
}
if (!decoder->SupportsAsyncMode()) {
GTEST_SKIP() << "Device doesn't support async mode";
}
// Async mode should be enabled during initialization
// Try to disable and re-enable
bool disableSuccess = decoder->EnableAsyncMode(false);
LOGI("Async mode disable: %s", disableSuccess ? "SUCCESS" : "FAILED");
bool enableSuccess = decoder->EnableAsyncMode(true);
LOGI("Async mode enable: %s", enableSuccess ? "SUCCESS" : "FAILED");
EXPECT_TRUE(enableSuccess) << "Should be able to re-enable async mode";
SUCCEED() << "Async mode enable/disable cycle completed";
}
// Test 15: SetVulkanDevice and MediaCodec reconfiguration
TEST_F(MediaCodecAV1DecoderTest, VulkanDeviceReconfiguration) {
LOGI("Test: VulkanDeviceReconfiguration");
auto codecs = decoder->GetAvailableCodecs();
if (codecs.empty()) {
GTEST_SKIP() << "No AV1 codecs available for Vulkan reconfiguration test";
}
// Initialize decoder
VideoMetadata metadata;
metadata.width = 1920;
metadata.height = 1080;
metadata.frame_rate = 30.0;
metadata.codec_type = VideoCodecType::AV1;
bool initSuccess = decoder->Initialize(metadata);
if (!initSuccess) {
GTEST_SKIP() << "Cannot test Vulkan reconfiguration without successful initialization";
}
// Create dummy Vulkan handles (null pointers for testing)
// In real scenario, these would be actual Vulkan objects
void* vk_device = reinterpret_cast<void*>(0x1234);
void* vk_instance = reinterpret_cast<void*>(0x5678);
void* vk_physical_device = reinterpret_cast<void*>(0x9ABC);
// SetVulkanDevice should trigger MediaCodec reconfiguration
// This will internally call:
// 1. CleanupAsyncMode()
// 2. AMediaCodec_stop()
// 3. AMediaCodec_configure() with ImageReader surface
// 4. InitializeAsyncMode() + EnableAsyncMode()
// 5. AMediaCodec_start()
bool vulkanSuccess = decoder->SetVulkanDevice(vk_device, vk_instance, vk_physical_device);
LOGI("SetVulkanDevice result: %s", vulkanSuccess ? "SUCCESS" : "FAILED");
// Note: This may fail on emulators or devices without proper Vulkan support
// The important thing is that it doesn't crash
if (vulkanSuccess) {
SUCCEED() << "Vulkan device set successfully - MediaCodec reconfigured";
} else {
LOGI("Vulkan device setup failed (expected on emulator)");
SUCCEED() << "Vulkan reconfiguration handled gracefully";
}
}
// Test 16: Async callbacks persistence after MediaCodec reconfiguration
TEST_F(MediaCodecAV1DecoderTest, AsyncCallbacksPersistenceAfterReconfiguration) {
LOGI("Test: AsyncCallbacksPersistenceAfterReconfiguration");
auto codecs = decoder->GetAvailableCodecs();
if (codecs.empty()) {
GTEST_SKIP() << "No AV1 codecs available";
}
// Initialize decoder
VideoMetadata metadata;
metadata.width = 1920;
metadata.height = 1080;
metadata.frame_rate = 30.0;
metadata.codec_type = VideoCodecType::AV1;
bool initSuccess = decoder->Initialize(metadata);
if (!initSuccess) {
GTEST_SKIP() << "Cannot test without successful initialization";
}
if (!decoder->SupportsAsyncMode()) {
GTEST_SKIP() << "Device doesn't support async mode";
}
// Verify async mode is active
bool asyncActive1 = decoder->SupportsAsyncMode();
LOGI("Async mode active before reconfiguration: %s", asyncActive1 ? "YES" : "NO");
// Simulate reconfiguration by calling Reset which internally may reconfigure
decoder->Reset();
// Re-initialize
initSuccess = decoder->Initialize(metadata);
if (!initSuccess) {
GTEST_SKIP() << "Cannot test without re-initialization";
}
// Verify async mode is still active after reconfiguration
bool asyncActive2 = decoder->SupportsAsyncMode();
LOGI("Async mode active after reconfiguration: %s", asyncActive2 ? "YES" : "NO");
EXPECT_TRUE(asyncActive2) << "Async mode should persist after reconfiguration";
SUCCEED() << "Async callbacks persistence verified";
}

View File

@@ -596,7 +596,10 @@ bool MediaCodecAV1Decoder::SetVulkanDevice(void* vk_device, void* vk_instance, v
// We must stop, reconfigure, and restart MediaCodec with the ImageReader surface
LogInfo("Reconfiguring MediaCodec with ImageReader surface");
// Step 1: Stop MediaCodec
// Step 1: Cleanup async mode before stopping MediaCodec
CleanupAsyncMode();
// Step 2: Stop MediaCodec
media_status_t status = AMediaCodec_stop(m_codec);
if (status != AMEDIA_OK) {
LogError("Failed to stop MediaCodec for reconfiguration: " + std::to_string(status));
@@ -604,7 +607,7 @@ bool MediaCodecAV1Decoder::SetVulkanDevice(void* vk_device, void* vk_instance, v
}
LogInfo("MediaCodec stopped for reconfiguration");
// Step 2: Reconfigure MediaCodec with ImageReader surface
// Step 3: Reconfigure MediaCodec with ImageReader surface
status = AMediaCodec_configure(
m_codec,
m_format,
@@ -618,7 +621,19 @@ bool MediaCodecAV1Decoder::SetVulkanDevice(void* vk_device, void* vk_instance, v
}
LogInfo("MediaCodec reconfigured with ImageReader surface");
// Step 3: Restart MediaCodec
// Step 4: Setup async callbacks AFTER configure but BEFORE start
LogInfo("Setting up async callbacks for reconfigured MediaCodec");
if (InitializeAsyncMode()) {
if (EnableAsyncMode(true)) {
LogInfo("Async callbacks registered successfully after reconfiguration");
} else {
LogWarning("Failed to activate async mode - continuing with sync mode");
}
} else {
LogWarning("Failed to initialize async mode - continuing with sync mode");
}
// Step 5: Start MediaCodec
status = AMediaCodec_start(m_codec);
if (status != AMEDIA_OK) {
LogError("Failed to restart MediaCodec after reconfiguration: " + std::to_string(status));
@@ -626,7 +641,7 @@ bool MediaCodecAV1Decoder::SetVulkanDevice(void* vk_device, void* vk_instance, v
}
LogInfo("MediaCodec restarted successfully with ImageReader output");
// Step 4: Re-prime the decoder after reconfiguration
// Step 6: Re-prime the decoder after reconfiguration
ResetPriming();
if (m_buffer_processor->PrimeDecoder()) {
m_is_primed = true;

View File

@@ -109,6 +109,11 @@ public:
// Component access (for async handler)
MediaCodecSurfaceManager* GetSurfaceManager() const { return m_surface_manager.get(); }
// Asynchronous MediaCodec support (public for testing)
bool SupportsAsyncMode() const;
bool EnableAsyncMode(bool enable);
bool IsAsyncModeEnabled() const { return m_async_handler->IsAsyncModeEnabled(); }
private:
// Initialization - Step-by-step helpers (refactored for clarity)
bool ValidateInitializationParams(const VideoMetadata& metadata);
@@ -127,10 +132,7 @@ private:
std::vector<std::string> GetEnhancedCodecList();
bool TryAlternativeCodecConfiguration(const std::string& codec_name);
// Asynchronous MediaCodec support (always enabled on API 29+)
bool SupportsAsyncMode() const;
bool EnableAsyncMode(bool enable);
bool IsAsyncModeEnabled() const { return m_async_handler->IsAsyncModeEnabled(); }
// Async decoding (internal)
bool DecodeFrameAsync(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame);
// Processing

View File

@@ -255,7 +255,7 @@ bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(int32_t output_index, AMedi
}
// Fill VideoFrame metadata
output_frame.timestamp_us = buffer_info->presentationTimeUs;
output_frame.timestamp_ns = buffer_info->presentationTimeUs * 1000; // Convert microseconds to nanoseconds
output_frame.is_keyframe = false; // NDK 26 limitation - WebM provides keyframe info
output_frame.surface_type = VAVCORE_SURFACE_ANDROID_HARDWARE_BUFFER;
@@ -283,8 +283,10 @@ bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(int32_t output_index, AMedi
return false;
}
// Step 3: Store AHardwareBuffer in VideoFrame
output_frame.ahardware_buffer = ahb;
// Step 3: Store AHardwareBuffer in VideoFrame's surface data
// Use android_native.native_window to store the AHardwareBuffer pointer
output_frame.surface_data.android_native.native_window = static_cast<void*>(ahb);
output_frame.surface_data.android_native.format = 0; // Not used for AHardwareBuffer
// Get video dimensions
uint32_t width, height;