From a41983ff65fe92263b30ef4b8ddfe49a9d1ccdfe Mon Sep 17 00:00:00 2001 From: ened Date: Mon, 13 Oct 2025 22:55:54 +0900 Subject: [PATCH] WIP --- todo29.txt | 59 +++ vav2/notes/gemini-anal1.txt | 388 ++++++++++++++++++ .../src/main/cpp/vavcore_vulkan_bridge.cpp | 39 +- .../app/src/main/cpp/vulkan_renderer.cpp | 259 +++++++----- .../app/src/main/cpp/vulkan_renderer.h | 1 + .../app/src/main/cpp/yuv_shaders.cpp | 9 +- .../android/tests/unit-tests/CMakeLists.txt | 4 + .../src/MediaCodecAV1DecoderTest.cpp | 164 ++++++++ .../src/Decoder/MediaCodecAV1Decoder.cpp | 23 +- .../src/Decoder/MediaCodecAV1Decoder.h | 10 +- .../src/Decoder/MediaCodecAsyncHandler.cpp | 8 +- 11 files changed, 832 insertions(+), 132 deletions(-) create mode 100644 vav2/notes/gemini-anal1.txt diff --git a/todo29.txt b/todo29.txt index 3084207..818ab68 100644 --- a/todo29.txt +++ b/todo29.txt @@ -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 + + + + diff --git a/vav2/notes/gemini-anal1.txt b/vav2/notes/gemini-anal1.txt new file mode 100644 index 0000000..46ffe60 --- /dev/null +++ b/vav2/notes/gemini-anal1.txt @@ -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 함수부터 + 교체하겠습니다. diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vavcore_vulkan_bridge.cpp b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vavcore_vulkan_bridge.cpp index 48693df..63bc088 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vavcore_vulkan_bridge.cpp +++ b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vavcore_vulkan_bridge.cpp @@ -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(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(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 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 { diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp index c9fdf1d..986c731 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp +++ b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.cpp @@ -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& 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 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(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(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(m_swapchainExtent.width); - viewport.height = static_cast(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 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(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& 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(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(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(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; diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.h b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.h index 7cbc28a..c912737 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.h +++ b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/vulkan_renderer.h @@ -172,6 +172,7 @@ private: std::vector m_imageAvailableSemaphores; std::vector m_renderFinishedSemaphores; std::vector m_inFlightFences; + std::vector m_imagesInFlight; // Track which fence is used by each swapchain image static const int MAX_FRAMES_IN_FLIGHT = 2; size_t m_currentFrame = 0; diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/yuv_shaders.cpp b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/yuv_shaders.cpp index cadf342..b939f00 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/cpp/yuv_shaders.cpp +++ b/vav2/platforms/android/applications/vav2player/app/src/main/cpp/yuv_shaders.cpp @@ -59,9 +59,8 @@ const std::vector 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 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, diff --git a/vav2/platforms/android/tests/unit-tests/CMakeLists.txt b/vav2/platforms/android/tests/unit-tests/CMakeLists.txt index c0faf27..f20c401 100644 --- a/vav2/platforms/android/tests/unit-tests/CMakeLists.txt +++ b/vav2/platforms/android/tests/unit-tests/CMakeLists.txt @@ -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 diff --git a/vav2/platforms/android/tests/unit-tests/src/MediaCodecAV1DecoderTest.cpp b/vav2/platforms/android/tests/unit-tests/src/MediaCodecAV1DecoderTest.cpp index f89d17d..5dfb789 100644 --- a/vav2/platforms/android/tests/unit-tests/src/MediaCodecAV1DecoderTest.cpp +++ b/vav2/platforms/android/tests/unit-tests/src/MediaCodecAV1DecoderTest.cpp @@ -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(0x1234); + void* vk_instance = reinterpret_cast(0x5678); + void* vk_physical_device = reinterpret_cast(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"; } \ No newline at end of file diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp index 36a6d1b..8ca47ee 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp @@ -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; diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h index dbd0a4a..ddf2fb2 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h @@ -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 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 diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp index 58d4320..eda0bd4 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp @@ -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(ahb); + output_frame.surface_data.android_native.format = 0; // Not used for AHardwareBuffer // Get video dimensions uint32_t width, height;