This commit is contained in:
2025-10-17 03:41:25 +09:00
parent cb482bb514
commit 51473ee5fd
6 changed files with 172 additions and 36 deletions

View File

@@ -1566,3 +1566,104 @@ packet 을 입력받으면 비동기로 처리되어서 결과로 얻어진 Vulk
MediaCodec 출력 포맷 분석
Android MediaCodec의 출력은 디바이스마다 다릅니다:
1. NV12 (CbCr): Y plane + CbCr interleaved
- AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420
- Vulkan: VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR
2. NV21 (CrCb): Y plane + CrCb interleaved
- AHARDWAREBUFFER_FORMAT_Y8Cr8Cb8_420
- Vulkan: VK_FORMAT_G8_B8_R8_2PLANE_420_UNORM_KHR (R8과 B8 순서 바뀜)
보라/초록 색상이 나오는 것은 Cb와 Cr이 뒤바뀐 전형적인 증상입니다!
로그를 확인해서 실제 포맷을 체크하겠습니다:
● Bash(adb logcat -d | grep -E "AHardwareBuffer desc:|format=" | tail -20)
⎿ 10-16 23:23:31.142 16802 5211 I VavCore-AsyncHandler: ProcessAsyncOutputFrame: Vulkan image created (VkImage=12970367434181141296, YCbC
r=12970367429885862960, format=1000156003)
10-16 23:23:31.142 16802 5211 I VulkanRenderer: RenderVulkanImage: Rendering external VkImage (0xb400007620a96330) with YCbCr conversio
… +31 lines (ctrl+o to expand)
● 완벽합니다! 문제를 찾았습니다:
format=2141391878 (0x7FA00C06) - Vendor-specific format
Vulkan format=0 (VK_FORMAT_UNDEFINED)
문제: Vulkan이 포맷을 UNDEFINED로 반환하고 있어서 NV12 (CbCr)로 폴백했는데, 실제로는 NV21 (CrCb)일 가능성이 높습니다!
Component swizzle로 Cb와 Cr을 바꿔보겠습니다:
> Vulkan 의 포맷이 undefined 일 경우, CbCr, CrCb 어느쪽이 일반적인 포맷인가?
✦ 좋은 질문입니다. 이 질문은 안드로이드 하드웨어와 드라이버의 파편화 문제를 정확히 보여줍니다.
결론부터 말씀드리면, 현대 비디오 디코딩 환경에서는 CbCr (NV12)이 더 일반적이고 표준에 가깝습니다.
상세 설명
1. 용어 정리
* NV12 (CbCr 순서): Y 평면 1개와 Cb(U), Cr(V)가 픽셀마다 번갈아 저장된 CbCr 평면 1개로 구성된, 총 2개의 평면을 가집니다.
* Vulkan 포맷: VK_FORMAT_G8_B8R8_2PLANE_420_UNORM (여기서 B는 Cb, R은 Cr을 의미)
* NV21 (CrCb 순서): Y 평면 1개와 Cr(V), Cb(U)가 번갈아 저장된 CrCb 평면 1개로 구성됩니다.
* Vulkan 포맷: VK_FORMAT_G8_R8B8_2PLANE_420_UNORM (여기서 R은 Cr, B는 Cb를 의미)
2. 어느 쪽이 더 일반적인가?
* 과거: 안드로이드 초기 카메라 API에서는 NV21 (CrCb) 포맷이 기본값으로 많이 사용되어 안드로이드 개발자들에게 익숙한
포맷입니다.
* 현재: 하지만 PC 환경, 최신 그래픽스 API(Vulkan, DirectX), 그리고 대부분의 하드웨어 비디오 코덱(NVIDIA, Intel, AMD 등)의
표준 출력은 NV12 (CbCr) 입니다. 안드로이드 MediaCodec 역시 하드웨어에 따라 다르지만, 업계 표준을 따라 NV12로 출력하는
경우가 더 많습니다.
3. 'Undefined'의 의미
vkGetAndroidHardwareBufferPropertiesANDROID 함수가 VK_FORMAT_UNDEFINED를 반환하는 것은, 안드로이드 시스템(드라이버)이
Vulkan에게 하드웨어 버퍼의 정확한 포맷을 알려주지 못하는 상황입니다. 이것은 드라이버 버그일 가능성이 높습니다.
결론
Vulkan 포맷이 UNDEFINED로 나올 때, 가장 합리적인 추측은 업계 표준에 더 가까운 NV12 (CbCr), 즉
VK_FORMAT_G8_B8R8_2PLANE_420_UNORM로 가정하는 것입니다.
하지만 저희 프로젝트의 경우, 기존 코드에 1000156004 (VK_FORMAT_G8_R8B8_2PLANE_420_UNORM_KHR, 즉 CrCb) 포맷을 사용하는
워크어라운드가 있었던 것으로 보아, 현재 테스트하고 계신 특정 기기에서는 비표준인 CrCb 순서로 데이터를 출력하는 것으로 보입니다.
따라서 저희가 VK_FORMAT_G8_R8B8_2PLANE_420_UNORM_KHR로 수정한 것은, 일반적인 상황보다는 현재 기기의 특성에 맞춘 정확한
수정이라고 할 수 있습니다.
> Galaxy 씨리즈 단말기에서는 NV12, NV21 어느쪽이 일반적인가?
✦ 매우 중요한 포인트입니다. 이 문제 역시 간단하게 답변하기는 어렵지만, 일반적인 경향성은 존재합니다.
결론적으로, 어떤 AP(칩셋)가 탑재되었느냐에 따라 다릅니다.
* 퀄컴 스냅드래곤 (Qualcomm Snapdragon) 탑재 모델: NV21 (CrCb) 포맷을 사용하는 경향이 강합니다.
* 역사적으로 퀄컴 칩셋은 안드로이드 카메라 파이프라인 등에서 NV21을 기본으로 사용해왔고, 이 특성이 비디오 디코딩에서도
나타나는 경우가 많습니다. 안드로이드에서 YUV 색상 문제가 발생하면 스냅드래곤 탑재 기기인 경우가 많습니다.
* 삼성 엑시노스 (Samsung Exynos) 탑재 모델: 업계 표준인 NV12 (CbCr) 포맷을 따르는 경향이 더 강합니다.
* 엑시노스는 자체 설계한 비디오 하드웨어(Multi-Format Codec)를 사용하며, 일반적으로는 표준 규격을 잘 준수합니다.
갤럭시 시리즈의 복잡성
갤럭시 플래그십 모델(예: Galaxy S 시리즈)은 출시 지역에 따라 스냅드래곤과 엑시노스 칩셋을 혼용하여 탑재합니다. 예를 들어,
국내판은 엑시노스, 북미판은 스냅드래곤을 사용하는 식입니다.
따라서 동일한 "Galaxy S24" 모델이라도, 어느 지역 제품이냐에 따라 하드웨어 디코더가 출력하는 YUV 포맷의 순서가 다를 수 있습니다.
현재 프로젝트 상황에 대한 추론
지금 디버깅하고 계신 기기에서 VK_FORMAT_G8_R8B8_2PLANE_420_UNORM_KHR (CrCb) 포맷을 사용해야 색상이 정상적으로 나왔다는 것은,
해당 기기가 퀄컴 스냅드래곤 칩셋을 탑재한 갤럭시 모델일 가능성이 매우 높다는 것을 시사합니다.

View File

@@ -12,7 +12,7 @@ enum class DecodeResult {
SUCCESS, // Frame decoded successfully
PENDING, // Input queued but output not ready yet (priming/warming up)
END_OF_STREAM, // All buffered frames consumed (draining complete)
ERROR // Actual error occurred
FAILED // Actual error occurred (renamed from ERROR to avoid Windows.h macro conflict)
};
// Video codec types
@@ -39,20 +39,8 @@ enum class ColorSpace {
VULKAN_IMAGE // Vulkan image format
};
// YUV-to-RGB Matrix Coefficients
// Based on https://www.itu.int/rec/T-REC-H.273-201612-I/en
enum class VavMatrixCoefficients {
IDENTITY = 0,
BT709 = 1,
UNSPECIFIED = 2,
BT470M = 4,
BT470BG = 5,
BT601 = 6,
SMPTE240M = 7,
YCGCO = 8,
BT2020_NON_CONSTANT_LUMINANCE = 9,
BT2020_CONSTANT_LUMINANCE = 10,
};
// Note: VavMatrixCoefficients is defined in VavCore.h (C API)
// Use VAVCORE_MATRIX_COEFFICIENTS_* constants from VavCore.h
// Pixel format definitions
enum class PixelFormat {
@@ -78,7 +66,7 @@ struct VideoMetadata {
VideoCodecType codec_type = VideoCodecType::AV1;
std::string codec_name;
ColorSpace color_space = ColorSpace::YUV420P;
VavMatrixCoefficients matrix_coefficients = VavMatrixCoefficients::UNSPECIFIED;
VavMatrixCoefficients matrix_coefficients = VAVCORE_MATRIX_COEFFICIENTS_UNSPECIFIED;
// Pixel format information
uint32_t bit_depth = 8;
@@ -108,7 +96,7 @@ struct VideoFrame {
uint32_t height = 0;
PixelFormat format = PixelFormat::YUV420P; // Pixel format
ColorSpace color_space = ColorSpace::YUV420P;
VavMatrixCoefficients matrix_coefficients = VavMatrixCoefficients::UNSPECIFIED;
VavMatrixCoefficients matrix_coefficients = VAVCORE_MATRIX_COEFFICIENTS_UNSPECIFIED;
// YUV data (per plane)
std::unique_ptr<uint8_t[]> y_plane;
@@ -262,7 +250,7 @@ struct VideoFrame {
width = 0;
height = 0;
color_space = ColorSpace::YUV420P;
matrix_coefficients = VavMatrixCoefficients::UNSPECIFIED;
matrix_coefficients = VAVCORE_MATRIX_COEFFICIENTS_UNSPECIFIED;
y_plane.reset();
u_plane.reset();
v_plane.reset();

View File

@@ -325,7 +325,7 @@ bool MediaCodecAV1Decoder::DecodeToSurface(const uint8_t* packet_data, size_t pa
return true; // Not an error - just no frame yet
case DecodeResult::END_OF_STREAM:
return false; // All frames consumed
case DecodeResult::ERROR:
case DecodeResult::FAILED:
default:
return false;
}

View File

@@ -8,6 +8,8 @@
#include <android/looper.h> // For ALooper (required for AImageReader callbacks)
#include <vulkan/vulkan.h>
#include <vulkan/vulkan_android.h>
#include <sys/system_properties.h> // For __system_property_get
#include <algorithm> // For std::transform
#define LOG_TAG "VavCore-SurfaceManager"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
@@ -334,6 +336,25 @@ bool MediaCodecSurfaceManager::CreateVulkanImage(void* vk_device, void* vk_insta
LogInfo("Previous VkSamplerYcbcrConversion destroyed");
}
// Detect device type for NV12/NV21 workaround
bool is_nv21_device = (ahb_desc.format == 0x7FA00C06); // Qualcomm vendor format
// Also check CPU type using system property
char manufacturer[256] = {0};
__system_property_get("ro.product.manufacturer", manufacturer);
std::string manufacturer_str(manufacturer);
std::transform(manufacturer_str.begin(), manufacturer_str.end(), manufacturer_str.begin(), ::tolower);
bool is_qualcomm = (manufacturer_str.find("qualcomm") != std::string::npos) ||
(manufacturer_str.find("qcom") != std::string::npos) ||
(manufacturer_str.find("snapdragon") != std::string::npos);
bool is_samsung = (manufacturer_str.find("samsung") != std::string::npos);
LogInfo("Device manufacturer: " + manufacturer_str);
LogInfo(" is_qualcomm: " + std::string(is_qualcomm ? "true" : "false"));
LogInfo(" is_samsung: " + std::string(is_samsung ? "true" : "false"));
VkSamplerYcbcrConversionCreateInfo ycbcrConversionCreateInfo = {};
ycbcrConversionCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO;
ycbcrConversionCreateInfo.pNext = nullptr;
@@ -341,18 +362,34 @@ bool MediaCodecSurfaceManager::CreateVulkanImage(void* vk_device, void* vk_insta
// Use format properties from AHardwareBuffer
VkFormat vulkan_format = ahb_format_props.format;
if (vulkan_format == VK_FORMAT_UNDEFINED || vulkan_format == 0) {
// WORKAROUND: On some devices, vkGetAndroidHardwareBufferPropertiesANDROID returns UNDEFINED.
// The correct format for NV12 CbCr is VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR.
vulkan_format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR;
LogInfo("Vulkan format is UNDEFINED, falling back to VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR");
// WORKAROUND: CPU-specific format detection
// Qualcomm Snapdragon → NV21 (CrCb)
// Samsung Exynos → NV12 (CbCr)
if (is_qualcomm || is_nv21_device) {
vulkan_format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR; // NV12 (will use BT.601)
is_nv21_device = true;
LogInfo("Qualcomm device detected → Using NV12 format with BT.601 color matrix");
} else {
vulkan_format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM_KHR; // NV12
is_nv21_device = false;
LogInfo("Non-Qualcomm device → Using NV12 format with MediaCodec settings");
}
}
ycbcrConversionCreateInfo.format = vulkan_format;
// DIAGNOSTIC: Override with standard BT.709 settings
// MediaCodec suggested values are logged but not used for testing
ycbcrConversionCreateInfo.ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709; // BT.709
ycbcrConversionCreateInfo.ycbcrRange = VK_SAMPLER_YCBCR_RANGE_ITU_FULL; // Full range 0-255 (was ITU_NARROW)
// DIAGNOSTIC: Try BT.601 instead of BT.709 for NV21 devices
// Qualcomm devices may expect BT.601 color matrix
if (is_nv21_device) {
ycbcrConversionCreateInfo.ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_601; // BT.601
ycbcrConversionCreateInfo.ycbcrRange = VK_SAMPLER_YCBCR_RANGE_ITU_NARROW; // Limited range
LogInfo(" Using BT.601 + ITU_NARROW for NV21 device");
} else {
// Use MediaCodec suggested settings
ycbcrConversionCreateInfo.ycbcrModel = static_cast<VkSamplerYcbcrModelConversion>(ahb_format_props.suggestedYcbcrModel);
ycbcrConversionCreateInfo.ycbcrRange = static_cast<VkSamplerYcbcrRange>(ahb_format_props.suggestedYcbcrRange);
LogInfo(" Using MediaCodec suggested YCbCr model and range");
}
// Log color space settings
LogInfo("YCbCr conversion:");
@@ -367,12 +404,21 @@ bool MediaCodecSurfaceManager::CreateVulkanImage(void* vk_device, void* vk_insta
LogInfo(" b: " + std::to_string(ahb_format_props.samplerYcbcrConversionComponents.b));
LogInfo(" a: " + std::to_string(ahb_format_props.samplerYcbcrConversionComponents.a));
// DIAGNOSTIC: Override component mapping with IDENTITY
ycbcrConversionCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
LogInfo(" Using IDENTITY component mapping");
// Use already-detected is_nv21_device from above
if (is_nv21_device || ahb_format_props.samplerYcbcrConversionComponents.r == 0) {
// NV21 device: Need to swap Cb/Cr which appear as G/B in conversion output
// In YCbCr conversion, Cb maps to B-Y and Cr maps to R-Y
// For NV21 (CrCb), we get the order reversed
ycbcrConversionCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
ycbcrConversionCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
LogInfo(" Using IDENTITY components (will use BT.601 instead of BT.709)");
} else {
// Use MediaCodec suggested component mapping
ycbcrConversionCreateInfo.components = ahb_format_props.samplerYcbcrConversionComponents;
LogInfo(" Using MediaCodec suggested component mapping");
}
// DIAGNOSTIC: Override with MIDPOINT chroma offset (standard for JPEG/MPEG)
ycbcrConversionCreateInfo.xChromaOffset = VK_CHROMA_LOCATION_MIDPOINT;

View File

@@ -139,7 +139,7 @@ private:
// Decoder configuration
CUVIDPARSERPARAMS m_parserParams = {};
VavMatrixCoefficients m_matrixCoefficients = VavMatrixCoefficients::UNSPECIFIED;
VavMatrixCoefficients m_matrixCoefficients = VAVCORE_MATRIX_COEFFICIENTS_UNSPECIFIED;
// Codec private data (AV1 sequence header from WebM)
const uint8_t* m_codecPrivateData = nullptr;

View File

@@ -897,9 +897,10 @@ VAVCORE_API VavCoreResult vavcore_decode_to_surface(VavCorePlayer* player,
frame->surface_data.vulkan.vk_device_memory = videoFrame.surface_data.vulkan.vk_device_memory;
frame->surface_data.vulkan.memory_offset = videoFrame.surface_data.vulkan.memory_offset;
frame->surface_data.vulkan.vk_ycbcr_conversion = videoFrame.surface_data.vulkan.vk_ycbcr_conversion;
LOGF_DEBUG("[vavcore_decode_to_surface] Copied Vulkan surface data: VkImage=%p, VkMemory=%p, YCbCr=%p",
frame->surface_data.vulkan.vk_format = videoFrame.surface_data.vulkan.vk_format;
LOGF_DEBUG("[vavcore_decode_to_surface] Copied Vulkan surface data: VkImage=%p, VkMemory=%p, YCbCr=%p, VkFormat=%d",
frame->surface_data.vulkan.vk_image, frame->surface_data.vulkan.vk_device_memory,
frame->surface_data.vulkan.vk_ycbcr_conversion);
frame->surface_data.vulkan.vk_ycbcr_conversion, frame->surface_data.vulkan.vk_format);
break;
case VAVCORE_SURFACE_CPU:
default: