861 lines
32 KiB
C#
861 lines
32 KiB
C#
using Godot;
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
// VavCore 데이터 구조체들
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct VavCoreVideoFrame
|
|
{
|
|
// Legacy CPU fields (for backward compatibility)
|
|
public IntPtr y_plane; // uint8_t*
|
|
public IntPtr u_plane; // uint8_t*
|
|
public IntPtr v_plane; // uint8_t*
|
|
|
|
public int y_stride; // Y plane stride
|
|
public int u_stride; // U plane stride
|
|
public int v_stride; // V plane stride
|
|
|
|
// Frame metadata
|
|
public int width; // Frame width
|
|
public int height; // Frame height
|
|
|
|
public ulong timestamp_us; // Timestamp in microseconds
|
|
public ulong frame_number; // Frame sequence number
|
|
|
|
// Surface type and data (we'll use CPU mode for now)
|
|
public int surface_type; // VavCoreSurfaceType (0 = CPU)
|
|
|
|
// Union data - we'll only use the first 64 bytes for CPU data
|
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
|
public ulong[] surface_data; // Union as array of ulongs
|
|
}
|
|
|
|
public partial class VavCorePlayer : Control
|
|
{
|
|
// VavCore DLL Import - Use actual file system path
|
|
private const string VavCoreDll = "VavCore.dll";
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int vavcore_initialize();
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr vavcore_create_player();
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int vavcore_open_file(IntPtr player, string filePath);
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern bool vavcore_is_open(IntPtr player);
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void vavcore_destroy_player(IntPtr player);
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void vavcore_cleanup();
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int vavcore_decode_next_frame(IntPtr player, out VavCoreVideoFrame frame);
|
|
|
|
[DllImport(VavCoreDll, CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void vavcore_free_frame(ref VavCoreVideoFrame frame);
|
|
|
|
// VavCore Player 인스턴스
|
|
private IntPtr _vavCorePlayer = IntPtr.Zero;
|
|
private string _currentVideoPath = string.Empty;
|
|
private bool _isInitialized = false;
|
|
|
|
// Godot 노드들
|
|
private TextureRect _videoTexture;
|
|
private ShaderMaterial _yuvShaderMaterial;
|
|
|
|
// GPU 텍스처들
|
|
private ImageTexture _yTexture;
|
|
private ImageTexture _uTexture;
|
|
private ImageTexture _vTexture;
|
|
|
|
// 텍스처 캐싱 최적화
|
|
private int _cachedFrameWidth = 0;
|
|
private int _cachedFrameHeight = 0;
|
|
private bool _texturesInitialized = false;
|
|
|
|
// 재생 상태 관리
|
|
private bool _isPlaying = false;
|
|
private bool _isPaused = false;
|
|
private Timer _playbackTimer;
|
|
private double _targetFrameRate = 30.0; // 기본 30fps
|
|
|
|
public override void _Ready()
|
|
{
|
|
GD.Print("VavCorePlayer: Initializing...");
|
|
|
|
// VavCore 라이브러리 초기화
|
|
InitializeVavCore();
|
|
|
|
// 비디오 출력용 TextureRect 생성
|
|
SetupVideoTexture();
|
|
}
|
|
|
|
private void InitializeVavCore()
|
|
{
|
|
try
|
|
{
|
|
GD.Print("VavCorePlayer: Initializing VavCore library...");
|
|
|
|
// DLL 경로 확인
|
|
string dllPath = System.IO.Path.Combine(System.Environment.CurrentDirectory, "VavCore.dll");
|
|
GD.Print($"VavCorePlayer: Looking for DLL at: {dllPath}");
|
|
GD.Print($"VavCorePlayer: DLL exists: {System.IO.File.Exists(dllPath)}");
|
|
|
|
GD.Print("VavCorePlayer: Calling vavcore_initialize()...");
|
|
int initResult = vavcore_initialize();
|
|
GD.Print($"VavCorePlayer: vavcore_initialize() returned: {initResult}");
|
|
|
|
if (initResult == 0) // VAVCORE_SUCCESS = 0
|
|
{
|
|
_isInitialized = true;
|
|
GD.Print("VavCorePlayer: VavCore initialized successfully!");
|
|
|
|
// VavCore 플레이어 인스턴스 생성
|
|
GD.Print("VavCorePlayer: Creating VavCore player instance...");
|
|
_vavCorePlayer = vavcore_create_player();
|
|
GD.Print($"VavCorePlayer: vavcore_create_player() returned: {_vavCorePlayer}");
|
|
|
|
if (_vavCorePlayer != IntPtr.Zero)
|
|
{
|
|
GD.Print("VavCorePlayer: VavCore player created successfully!");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to create VavCore player!");
|
|
_isInitialized = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to initialize VavCore!");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Exception during initialization: {ex.Message}");
|
|
GD.PrintErr($"VavCorePlayer: Exception type: {ex.GetType().Name}");
|
|
GD.PrintErr($"VavCorePlayer: Stack trace: {ex.StackTrace}");
|
|
}
|
|
}
|
|
|
|
private void SetupVideoTexture()
|
|
{
|
|
// 비디오 출력용 TextureRect 노드 생성
|
|
_videoTexture = new TextureRect();
|
|
_videoTexture.Name = "VideoTexture";
|
|
|
|
// Fill the entire parent control
|
|
_videoTexture.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
|
|
_videoTexture.ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional;
|
|
_videoTexture.StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered;
|
|
|
|
// 가시성 강제 설정
|
|
_videoTexture.Visible = true;
|
|
_videoTexture.Modulate = Colors.White; // 완전 불투명
|
|
_videoTexture.ZIndex = 100; // 최상위에 표시
|
|
|
|
// 투명 배경으로 설정
|
|
_videoTexture.SelfModulate = Colors.White;
|
|
|
|
// GPU 셰이더 설정
|
|
SetupGPUShader();
|
|
|
|
AddChild(_videoTexture);
|
|
|
|
// 재생 타이머 설정
|
|
_playbackTimer = new Timer();
|
|
_playbackTimer.Name = "PlaybackTimer";
|
|
_playbackTimer.WaitTime = 1.0 / _targetFrameRate; // 30fps = ~0.033초
|
|
_playbackTimer.Timeout += OnPlaybackTimerTimeout;
|
|
AddChild(_playbackTimer);
|
|
|
|
// 디버그 정보 출력
|
|
GD.Print($"VavCorePlayer: Video texture setup complete");
|
|
GD.Print($"VavCorePlayer: TextureRect position: {_videoTexture.Position}");
|
|
GD.Print($"VavCorePlayer: TextureRect size: {_videoTexture.Size}");
|
|
GD.Print($"VavCorePlayer: TextureRect visible: {_videoTexture.Visible}");
|
|
GD.Print($"VavCorePlayer: GPU shader setup complete");
|
|
}
|
|
|
|
public bool LoadVideo(string videoPath)
|
|
{
|
|
if (!_isInitialized || _vavCorePlayer == IntPtr.Zero)
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Not initialized!");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
GD.Print($"VavCorePlayer: Loading video: {videoPath}");
|
|
|
|
// Godot 리소스 경로를 실제 파일 경로로 변환
|
|
string realPath = ProjectSettings.GlobalizePath(videoPath);
|
|
GD.Print($"VavCorePlayer: Real path: {realPath}");
|
|
|
|
int result = vavcore_open_file(_vavCorePlayer, realPath);
|
|
if (result == 0) // VAVCORE_SUCCESS = 0
|
|
{
|
|
_currentVideoPath = realPath; // 성공시 경로 저장
|
|
|
|
// 텍스처 캐시 초기화
|
|
_texturesInitialized = false;
|
|
_cachedFrameWidth = 0;
|
|
_cachedFrameHeight = 0;
|
|
|
|
GD.Print("VavCorePlayer: Video loaded successfully!");
|
|
|
|
// 비디오가 로드되었는지 확인
|
|
bool isOpen = vavcore_is_open(_vavCorePlayer);
|
|
GD.Print($"VavCorePlayer: Video is open: {isOpen}");
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to load video!");
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Exception during video loading: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool IsVideoLoaded()
|
|
{
|
|
if (!_isInitialized || _vavCorePlayer == IntPtr.Zero)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
return vavcore_is_open(_vavCorePlayer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Exception checking video status: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public override void _ExitTree()
|
|
{
|
|
GD.Print("VavCorePlayer: Cleaning up...");
|
|
|
|
// VavCore 리소스 정리
|
|
if (_vavCorePlayer != IntPtr.Zero)
|
|
{
|
|
vavcore_destroy_player(_vavCorePlayer);
|
|
_vavCorePlayer = IntPtr.Zero;
|
|
}
|
|
|
|
if (_isInitialized)
|
|
{
|
|
vavcore_cleanup();
|
|
_isInitialized = false;
|
|
}
|
|
|
|
GD.Print("VavCorePlayer: Cleanup complete");
|
|
}
|
|
|
|
// 재생 제어 메서드들
|
|
public void StartPlayback()
|
|
{
|
|
if (!IsVideoLoaded())
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Cannot start playback - no video loaded");
|
|
return;
|
|
}
|
|
|
|
_isPlaying = true;
|
|
_isPaused = false;
|
|
_playbackTimer.Start();
|
|
GD.Print("VavCorePlayer: Playback started");
|
|
}
|
|
|
|
public void PausePlayback()
|
|
{
|
|
_isPaused = true;
|
|
_playbackTimer.Stop();
|
|
GD.Print("VavCorePlayer: Playback paused");
|
|
}
|
|
|
|
public void StopPlayback()
|
|
{
|
|
_isPlaying = false;
|
|
_isPaused = false;
|
|
_playbackTimer.Stop();
|
|
|
|
// 처음부터 다시 재생하기 위해 비디오를 다시 로드
|
|
if (_vavCorePlayer != IntPtr.Zero && !string.IsNullOrEmpty(_currentVideoPath))
|
|
{
|
|
GD.Print("VavCorePlayer: Reloading video to reset position");
|
|
|
|
// 현재 파일 경로 저장
|
|
string currentPath = _currentVideoPath;
|
|
|
|
// 비디오 다시 로드 (내부적으로 seek to beginning과 동일한 효과)
|
|
int result = vavcore_open_file(_vavCorePlayer, currentPath);
|
|
if (result == 0)
|
|
{
|
|
GD.Print("VavCorePlayer: Video reset to beginning successfully");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Failed to reset video position: {result}");
|
|
}
|
|
}
|
|
|
|
GD.Print("VavCorePlayer: Playback stopped");
|
|
}
|
|
|
|
public bool IsPlaying()
|
|
{
|
|
return _isPlaying && !_isPaused;
|
|
}
|
|
|
|
// 타이머 콜백 - 매 프레임마다 호출됨
|
|
private void OnPlaybackTimerTimeout()
|
|
{
|
|
if (!_isPlaying || _isPaused || !IsVideoLoaded())
|
|
return;
|
|
|
|
try
|
|
{
|
|
DecodeAndDisplayNextFrame();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error during frame decode: {ex.Message}");
|
|
StopPlayback();
|
|
}
|
|
}
|
|
|
|
// 다음 프레임 디코딩 및 표시
|
|
private void DecodeAndDisplayNextFrame()
|
|
{
|
|
VavCoreVideoFrame frame = new VavCoreVideoFrame();
|
|
|
|
// 구조체 초기화
|
|
frame.surface_data = new ulong[16];
|
|
|
|
int result = vavcore_decode_next_frame(_vavCorePlayer, out frame);
|
|
|
|
GD.Print($"VavCorePlayer: Decode result: {result}");
|
|
|
|
if (result == 0) // VAVCORE_SUCCESS
|
|
{
|
|
GD.Print($"VavCorePlayer: Decoded frame {frame.frame_number} ({frame.width}x{frame.height})");
|
|
GD.Print($"VavCorePlayer: Y-plane: {frame.y_plane}, U-plane: {frame.u_plane}, V-plane: {frame.v_plane}");
|
|
GD.Print($"VavCorePlayer: Y-stride: {frame.y_stride}, U-stride: {frame.u_stride}, V-stride: {frame.v_stride}");
|
|
|
|
// YUV 데이터가 유효한지 확인
|
|
if (frame.y_plane != IntPtr.Zero && frame.width > 0 && frame.height > 0)
|
|
{
|
|
// GPU에서 YUV 데이터를 직접 처리하여 표시
|
|
DisplayFrameGPU(frame);
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Invalid frame data received");
|
|
}
|
|
|
|
// 프레임 메모리 해제
|
|
vavcore_free_frame(ref frame);
|
|
}
|
|
else if (result == 1) // VAVCORE_END_OF_STREAM
|
|
{
|
|
GD.Print("VavCorePlayer: End of video reached");
|
|
StopPlayback();
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Frame decode failed with error: {result}");
|
|
StopPlayback();
|
|
}
|
|
}
|
|
|
|
// 폴백 이미지 생성 (GPU 셰이더 사용 불가시)
|
|
private Image CreateFallbackImage(VavCoreVideoFrame frame)
|
|
{
|
|
// GPU 셰이더가 사용 가능한 경우 null 반환 (셰이더가 처리)
|
|
if (_yuvShaderMaterial != null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// GPU 셰이더 사용 불가시 CPU 방식으로 폴백
|
|
GD.PrintErr("VavCorePlayer: GPU shader not available, using CPU fallback");
|
|
|
|
// 간단한 그레이스케일 변환 (성능 최적화)
|
|
try
|
|
{
|
|
var image = Image.CreateEmpty(frame.width, frame.height, false, Image.Format.Rgb8);
|
|
|
|
unsafe
|
|
{
|
|
byte* yPtr = (byte*)frame.y_plane.ToPointer();
|
|
|
|
for (int y = 0; y < frame.height; y++)
|
|
{
|
|
for (int x = 0; x < frame.width; x++)
|
|
{
|
|
int yIndex = y * frame.y_stride + x;
|
|
byte yVal = yPtr[yIndex];
|
|
|
|
// Y 값만 사용하여 그레이스케일로 표시 (고속 처리)
|
|
float gray = yVal / 255.0f;
|
|
var color = new Color(gray, gray, gray, 1.0f);
|
|
image.SetPixel(x, y, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
return image;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Fallback image creation error: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// GPU 셰이더 설정
|
|
private void SetupGPUShader()
|
|
{
|
|
try
|
|
{
|
|
// YUV to RGB 셰이더 로드
|
|
var shader = GD.Load<Shader>("res://shaders/yuv_to_rgb.gdshader");
|
|
if (shader == null)
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to load YUV shader");
|
|
return;
|
|
}
|
|
|
|
// 셰이더 머티리얼 생성
|
|
_yuvShaderMaterial = new ShaderMaterial();
|
|
_yuvShaderMaterial.Shader = shader;
|
|
|
|
GD.Print("VavCorePlayer: GPU shader loaded successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error setting up GPU shader: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// GPU에서 YUV 프레임을 직접 처리하여 표시
|
|
private void DisplayFrameGPU(VavCoreVideoFrame frame)
|
|
{
|
|
try
|
|
{
|
|
// TextureRect 크기가 0이면 다시 설정
|
|
if (_videoTexture.Size.X <= 0 || _videoTexture.Size.Y <= 0)
|
|
{
|
|
GD.Print("VavCorePlayer: TextureRect size is 0, forcing resize...");
|
|
|
|
// 부모 컨테이너 크기 확인
|
|
var parentSize = GetParent<Control>().Size;
|
|
GD.Print($"VavCorePlayer: Parent size: {parentSize}");
|
|
|
|
// 강제로 크기 설정
|
|
_videoTexture.Size = parentSize;
|
|
_videoTexture.Position = Vector2.Zero;
|
|
|
|
GD.Print($"VavCorePlayer: TextureRect resized to: {_videoTexture.Size}");
|
|
}
|
|
|
|
// YUV 데이터를 GPU 텍스처로 변환
|
|
bool success = CreateYUVTextures(frame);
|
|
if (success && _yuvShaderMaterial != null)
|
|
{
|
|
// 셰이더에 YUV 텍스처 할당
|
|
_yuvShaderMaterial.SetShaderParameter("y_texture", _yTexture);
|
|
_yuvShaderMaterial.SetShaderParameter("u_texture", _uTexture);
|
|
_yuvShaderMaterial.SetShaderParameter("v_texture", _vTexture);
|
|
|
|
// 메쉬 기반 렌더링을 위한 더미 텍스처 생성
|
|
var dummyImage = Image.CreateEmpty(frame.width, frame.height, false, Image.Format.Rgb8);
|
|
var dummyTexture = ImageTexture.CreateFromImage(dummyImage);
|
|
|
|
// TextureRect에 셰이더 머티리얼 적용
|
|
_videoTexture.Texture = dummyTexture;
|
|
_videoTexture.Material = _yuvShaderMaterial;
|
|
|
|
GD.Print($"VavCorePlayer: GPU YUV frame displayed successfully ({frame.width}x{frame.height})");
|
|
GD.Print($"VavCorePlayer: TextureRect final size: {_videoTexture.Size}");
|
|
}
|
|
else
|
|
{
|
|
// GPU 처리 실패시 빨간색 에러 표시
|
|
var errorImage = Image.CreateEmpty(frame.width, frame.height, false, Image.Format.Rgb8);
|
|
for (int y = 0; y < frame.height; y++)
|
|
{
|
|
for (int x = 0; x < frame.width; x++)
|
|
{
|
|
errorImage.SetPixel(x, y, Colors.Red);
|
|
}
|
|
}
|
|
var errorTexture = ImageTexture.CreateFromImage(errorImage);
|
|
_videoTexture.Texture = errorTexture;
|
|
_videoTexture.Material = null; // 셰이더 비활성화
|
|
GD.PrintErr($"VavCorePlayer: GPU YUV processing failed, showing error image");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error displaying GPU frame: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// YUV 데이터로 GPU 텍스처 생성
|
|
private bool CreateYUVTextures(VavCoreVideoFrame frame)
|
|
{
|
|
if (frame.y_plane == IntPtr.Zero || frame.width <= 0 || frame.height <= 0)
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Invalid frame data for GPU texture creation");
|
|
return false;
|
|
}
|
|
|
|
if (frame.u_plane == IntPtr.Zero || frame.v_plane == IntPtr.Zero)
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Missing UV plane data for GPU texture");
|
|
return false;
|
|
}
|
|
|
|
GD.Print($"VavCorePlayer: Creating GPU YUV textures - Y stride: {frame.y_stride}, U stride: {frame.u_stride}, V stride: {frame.v_stride}");
|
|
|
|
// YUV420P 메모리 연속성 분석
|
|
long yAddr = (long)frame.y_plane;
|
|
long uAddr = (long)frame.u_plane;
|
|
long vAddr = (long)frame.v_plane;
|
|
|
|
long ySize = frame.width * frame.height;
|
|
long uSize = (frame.width / 2) * (frame.height / 2);
|
|
long vSize = (frame.width / 2) * (frame.height / 2);
|
|
|
|
GD.Print($"VavCorePlayer: Memory layout analysis:");
|
|
GD.Print($" Y plane: 0x{yAddr:X} (size: {ySize} bytes)");
|
|
GD.Print($" U plane: 0x{uAddr:X} (size: {uSize} bytes)");
|
|
GD.Print($" V plane: 0x{vAddr:X} (size: {vSize} bytes)");
|
|
GD.Print($" Y->U gap: {uAddr - (yAddr + ySize)} bytes");
|
|
GD.Print($" U->V gap: {vAddr - (uAddr + uSize)} bytes");
|
|
|
|
// 연속 메모리 공간인지 확인
|
|
bool isContiguous = (uAddr == yAddr + ySize) && (vAddr == uAddr + uSize);
|
|
bool hasOptimalStrides = frame.y_stride == frame.width && frame.u_stride == (frame.width / 2) && frame.v_stride == (frame.width / 2);
|
|
GD.Print($"VavCorePlayer: YUV planes contiguous: {isContiguous}");
|
|
GD.Print($"VavCorePlayer: Optimal strides: {hasOptimalStrides}");
|
|
|
|
if (isContiguous && hasOptimalStrides)
|
|
{
|
|
GD.Print("VavCorePlayer: Attempting single-block YUV420P copy!");
|
|
return CreateSingleBlockYUVTexture(frame);
|
|
}
|
|
|
|
try
|
|
{
|
|
// 텍스처 캐싱 최적화: 해상도가 바뀌었거나 첫 번째 프레임인 경우에만 재생성
|
|
bool needsResize = !_texturesInitialized ||
|
|
_cachedFrameWidth != frame.width ||
|
|
_cachedFrameHeight != frame.height;
|
|
|
|
if (needsResize)
|
|
{
|
|
GD.Print($"VavCorePlayer: CREATING new textures - {frame.width}x{frame.height}");
|
|
_cachedFrameWidth = frame.width;
|
|
_cachedFrameHeight = frame.height;
|
|
_texturesInitialized = true;
|
|
|
|
// Y 평면 텍스처 생성
|
|
var yImage = CreatePlaneImageBlockCopy(frame.y_plane, frame.width, frame.height, frame.y_stride);
|
|
if (yImage != null)
|
|
{
|
|
_yTexture = ImageTexture.CreateFromImage(yImage);
|
|
GD.Print($"VavCorePlayer: Y texture CREATED - {yImage.GetWidth()}x{yImage.GetHeight()}");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to create Y plane image");
|
|
return false;
|
|
}
|
|
|
|
// U 평면 텍스처 생성
|
|
int uvWidth = frame.width / 2;
|
|
int uvHeight = frame.height / 2;
|
|
var uImage = CreatePlaneImageBlockCopy(frame.u_plane, uvWidth, uvHeight, frame.u_stride);
|
|
if (uImage != null)
|
|
{
|
|
_uTexture = ImageTexture.CreateFromImage(uImage);
|
|
GD.Print($"VavCorePlayer: U texture CREATED - {uImage.GetWidth()}x{uImage.GetHeight()}");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to create U plane image");
|
|
return false;
|
|
}
|
|
|
|
// V 평면 텍스처 생성
|
|
var vImage = CreatePlaneImageBlockCopy(frame.v_plane, uvWidth, uvHeight, frame.v_stride);
|
|
if (vImage != null)
|
|
{
|
|
_vTexture = ImageTexture.CreateFromImage(vImage);
|
|
GD.Print($"VavCorePlayer: V texture CREATED - {vImage.GetWidth()}x{vImage.GetHeight()}");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to create V plane image");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 텍스처 업데이트만 수행 (훨씬 빠름!)
|
|
var yImage = CreatePlaneImageBlockCopy(frame.y_plane, frame.width, frame.height, frame.y_stride);
|
|
var uImage = CreatePlaneImageBlockCopy(frame.u_plane, frame.width / 2, frame.height / 2, frame.u_stride);
|
|
var vImage = CreatePlaneImageBlockCopy(frame.v_plane, frame.width / 2, frame.height / 2, frame.v_stride);
|
|
|
|
if (yImage != null && uImage != null && vImage != null)
|
|
{
|
|
_yTexture.Update(yImage);
|
|
_uTexture.Update(uImage);
|
|
_vTexture.Update(vImage);
|
|
// GD.Print("VavCorePlayer: Textures UPDATED efficiently");
|
|
}
|
|
else
|
|
{
|
|
GD.PrintErr("VavCorePlayer: Failed to update textures");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
GD.Print($"VavCorePlayer: GPU YUV textures created successfully (3-block method)");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: GPU texture creation error: {ex.Message}");
|
|
GD.PrintErr($"VavCorePlayer: Stack trace: {ex.StackTrace}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 단일 평면 데이터로 이미지 생성 (최적화된 버전)
|
|
private Image CreatePlaneImage(IntPtr planeData, int width, int height, int stride)
|
|
{
|
|
try
|
|
{
|
|
// R8 포맷 사용 (단일 체널, 8-bit)
|
|
var image = Image.CreateEmpty(width, height, false, Image.Format.R8);
|
|
|
|
GD.Print($"VavCorePlayer: Creating plane image - Size: {width}x{height}, Stride: {stride}, Format: R8");
|
|
|
|
unsafe
|
|
{
|
|
byte* srcPtr = (byte*)planeData.ToPointer();
|
|
|
|
// 직접 픽셀 데이터 복사 (고속 처리)
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int srcIndex = y * stride + x;
|
|
byte value = srcPtr[srcIndex];
|
|
|
|
// R8 포맷: 빨간 채널에만 값 설정
|
|
var color = new Color(value / 255.0f, 0.0f, 0.0f, 1.0f);
|
|
image.SetPixel(x, y, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
GD.Print($"VavCorePlayer: Plane image created successfully - Format: {image.GetFormat()}");
|
|
return image;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error creating plane image: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 블록 메모리 복사를 위한 최고속 평면 이미지 생성
|
|
private Image CreatePlaneImageBlockCopy(IntPtr planeData, int width, int height, int stride)
|
|
{
|
|
try
|
|
{
|
|
GD.Print($"VavCorePlayer: Block copy - Width: {width}, Height: {height}, Stride: {stride}");
|
|
|
|
// 케이스 1: 스트라이드가 폭과 같은 경우 - 전체 블록 복사
|
|
if (stride == width)
|
|
{
|
|
GD.Print("VavCorePlayer: Using full block copy (stride == width)");
|
|
|
|
int totalBytes = width * height;
|
|
var imageData = new byte[totalBytes];
|
|
|
|
unsafe
|
|
{
|
|
byte* srcPtr = (byte*)planeData.ToPointer();
|
|
fixed (byte* dstPtr = imageData)
|
|
{
|
|
// 전체 메모리 블록을 한 번에 복사 (최고속)
|
|
Buffer.MemoryCopy(srcPtr, dstPtr, totalBytes, totalBytes);
|
|
}
|
|
}
|
|
|
|
var image = Image.CreateFromData(width, height, false, Image.Format.R8, imageData);
|
|
GD.Print($"VavCorePlayer: Block copy completed - {totalBytes} bytes");
|
|
return image;
|
|
}
|
|
// 케이스 2: 스트라이드가 폭보다 큰 경우 - 라인별 복사 (하지만 memcpy 사용)
|
|
else
|
|
{
|
|
GD.Print($"VavCorePlayer: Using line-by-line memcpy (stride {stride} > width {width})");
|
|
|
|
var imageData = new byte[width * height];
|
|
|
|
unsafe
|
|
{
|
|
byte* srcPtr = (byte*)planeData.ToPointer();
|
|
fixed (byte* dstPtr = imageData)
|
|
{
|
|
// 라인별 고속 메모리 복사
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
byte* srcLine = srcPtr + (y * stride);
|
|
byte* dstLine = dstPtr + (y * width);
|
|
|
|
// 한 라인을 memcpy로 고속 복사
|
|
Buffer.MemoryCopy(srcLine, dstLine, width, width);
|
|
}
|
|
}
|
|
}
|
|
|
|
var image = Image.CreateFromData(width, height, false, Image.Format.R8, imageData);
|
|
GD.Print($"VavCorePlayer: Line memcpy completed - {height} lines of {width} bytes");
|
|
return image;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error in block copy: {ex.Message}");
|
|
// 폴백: 셰이더 기반 스트라이드 처리 시도
|
|
return CreatePlaneImageWithShaderStride(planeData, width, height, stride);
|
|
}
|
|
}
|
|
|
|
// 셰이더에서 스트라이드를 처리하는 방식 (실험적)
|
|
private Image CreatePlaneImageWithShaderStride(IntPtr planeData, int width, int height, int stride)
|
|
{
|
|
try
|
|
{
|
|
GD.Print($"VavCorePlayer: Shader stride mode - Creating {stride}x{height} texture for {width}x{height} content");
|
|
|
|
// 스트라이드 전체 데이터를 텍스처로 업로드
|
|
int totalBytes = stride * height;
|
|
var imageData = new byte[totalBytes];
|
|
|
|
unsafe
|
|
{
|
|
byte* srcPtr = (byte*)planeData.ToPointer();
|
|
fixed (byte* dstPtr = imageData)
|
|
{
|
|
// 전체 스트라이드 데이터를 블록 복사
|
|
Buffer.MemoryCopy(srcPtr, dstPtr, totalBytes, totalBytes);
|
|
}
|
|
}
|
|
|
|
// 스트라이드 크기로 텍스처 생성 (셰이더에서 UV 좌표 조정 필요)
|
|
var image = Image.CreateFromData(stride, height, false, Image.Format.R8, imageData);
|
|
GD.Print($"VavCorePlayer: Shader stride texture created - {stride}x{height} (actual content: {width}x{height})");
|
|
return image;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Error in shader stride mode: {ex.Message}");
|
|
// 최종 폴백: 기존 방식
|
|
return CreatePlaneImage(planeData, width, height, stride);
|
|
}
|
|
}
|
|
|
|
private bool CreateSingleBlockYUVTexture(VavCoreVideoFrame frame)
|
|
{
|
|
try
|
|
{
|
|
var startTime = DateTime.Now;
|
|
|
|
long ySize = frame.width * frame.height;
|
|
long uSize = (frame.width / 2) * (frame.height / 2);
|
|
long vSize = (frame.width / 2) * (frame.height / 2);
|
|
long totalSize = ySize + uSize + vSize;
|
|
|
|
GD.Print($"VavCorePlayer: TRUE single-block copy - Total size: {totalSize} bytes");
|
|
|
|
// 진정한 단일 블록 복사: 전체 YUV420P 데이터를 하나의 텍스처로
|
|
var yuvData = new byte[totalSize];
|
|
unsafe
|
|
{
|
|
byte* srcPtr = (byte*)frame.y_plane.ToPointer();
|
|
fixed (byte* dstPtr = yuvData)
|
|
{
|
|
Buffer.MemoryCopy(srcPtr, dstPtr, totalSize, totalSize);
|
|
}
|
|
}
|
|
|
|
var copyTime = DateTime.Now;
|
|
GD.Print($"VavCorePlayer: ONLY ONE Buffer.MemoryCopy completed in {(copyTime - startTime).TotalMilliseconds:F2}ms");
|
|
|
|
// 전체 YUV 데이터를 하나의 1D 텍스처로 생성 (셰이더에서 오프셋 계산)
|
|
var yuvImage = Image.CreateFromData((int)totalSize, 1, false, Image.Format.R8, yuvData);
|
|
|
|
// 텍스처 캐싱 최적화 적용
|
|
ImageTexture yuvTexture;
|
|
if (_material.GetShaderParameter("yuv_texture").AsGodotObject() == null)
|
|
{
|
|
yuvTexture = ImageTexture.CreateFromImage(yuvImage);
|
|
GD.Print("VavCorePlayer: Single YUV texture CREATED");
|
|
}
|
|
else
|
|
{
|
|
yuvTexture = _material.GetShaderParameter("yuv_texture").As<ImageTexture>();
|
|
yuvTexture.Update(yuvImage);
|
|
// GD.Print("VavCorePlayer: Single YUV texture UPDATED efficiently");
|
|
}
|
|
|
|
var textureTime = DateTime.Now;
|
|
GD.Print($"VavCorePlayer: Single YUV texture creation completed in {(textureTime - copyTime).TotalMilliseconds:F2}ms");
|
|
|
|
// 셰이더에 YUV 오프셋과 크기 정보 전달
|
|
_material.SetShaderParameter("yuv_texture", yuvTexture);
|
|
_material.SetShaderParameter("y_offset", 0);
|
|
_material.SetShaderParameter("u_offset", (int)ySize);
|
|
_material.SetShaderParameter("v_offset", (int)(ySize + uSize));
|
|
_material.SetShaderParameter("y_size", (int)ySize);
|
|
_material.SetShaderParameter("u_size", (int)uSize);
|
|
_material.SetShaderParameter("v_size", (int)vSize);
|
|
_material.SetShaderParameter("frame_width", frame.width);
|
|
_material.SetShaderParameter("frame_height", frame.height);
|
|
|
|
var finalTime = DateTime.Now;
|
|
GD.Print($"VavCorePlayer: TRUE single-block method total time: {(finalTime - startTime).TotalMilliseconds:F2}ms");
|
|
GD.Print("VavCorePlayer: TRUE single-block YUV420P copy SUCCESS - ZERO additional copies!");
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"VavCorePlayer: Single-block copy failed: {ex.Message}");
|
|
GD.PrintErr($"VavCorePlayer: Falling back to 3-block method");
|
|
return false;
|
|
}
|
|
}
|
|
} |