diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp index d13c339..edc4cd3 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp @@ -56,6 +56,11 @@ void AMosisPhoneActor::BeginPlay() { // Default plane is 100x100 units, so bounds are -50 to 50 in X and Y ScreenBoundsLocal = FVector4(-50.0f, -50.0f, 50.0f, 50.0f); + + // Flip the mesh on Y axis to correct texture orientation + // (OpenGL renders with origin at bottom-left, Vulkan/UE5 expects top-left) + FVector CurrentScale = PhoneMesh->GetRelativeScale3D(); + PhoneMesh->SetRelativeScale3D(FVector(CurrentScale.X, -CurrentScale.Y, CurrentScale.Z)); } // Create dynamic material for the screen @@ -143,5 +148,8 @@ bool AMosisPhoneActor::WorldToPhoneUV(FVector WorldLocation, FVector2D& OutUV) c OutUV.X = (LocalLocation.X - MinX) / (MaxX - MinX); OutUV.Y = (LocalLocation.Y - MinY) / (MaxY - MinY); + // Flip Y to match the flipped mesh scale (corrects for OpenGL vs Vulkan coordinate systems) + OutUV.Y = 1.0f - OutUV.Y; + return true; } diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneComponent.cpp b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneComponent.cpp index 9146f70..75d5598 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneComponent.cpp +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneComponent.cpp @@ -171,8 +171,8 @@ void UMosisPhoneComponent::OnBufferAvailable() UE_LOG(LogMosisPhone, Log, TEXT("OnBufferAvailable: Created phone texture")); } - // Import the hardware buffer - PhoneTexture->UpdateFromHardwareBuffer(Buffer); + // Import the hardware buffer for GPU-accelerated updates + PhoneTexture->ImportHardwareBuffer(Buffer); // Update material if set if (PhoneMaterial && PhoneTexture) @@ -185,20 +185,19 @@ void UMosisPhoneComponent::OnBufferAvailable() void UMosisPhoneComponent::OnFrameAvailable() { #if PLATFORM_ANDROID - if (!PhoneTexture) + if (!PhoneTexture || !PhoneTexture->HasImportedBuffer()) { return; } - // Notify texture that a new frame is available - // This schedules the render thread copy - PhoneTexture->NotifyFrameAvailable(); + // Perform GPU-to-GPU copy from imported hardware buffer + PhoneTexture->CopyFromImportedBuffer(); // Log occasionally to avoid spam static int32 FrameCount = 0; if (++FrameCount % 60 == 0) { - UE_LOG(LogMosisPhone, Log, TEXT("OnFrameAvailable: Frame %d"), FrameCount); + UE_LOG(LogMosisPhone, Log, TEXT("OnFrameAvailable: Frame %d (GPU copy)"), FrameCount); } #endif } diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.cpp b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.cpp index 15332db..d47e9d1 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.cpp +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.cpp @@ -2,21 +2,40 @@ #include "MosisPhoneTexture.h" #include "RenderingThread.h" +#include "RHICommandList.h" + +#if PLATFORM_ANDROID +#include "IVulkanDynamicRHI.h" +#endif DEFINE_LOG_CATEGORY_STATIC(LogMosisPhoneTexture, Log, All); UMosisPhoneTexture::UMosisPhoneTexture() { - // UTexture2DDynamic defaults are fine SRGB = true; Filter = TF_Bilinear; } +UMosisPhoneTexture::~UMosisPhoneTexture() +{ +#if PLATFORM_ANDROID + // Release imported texture RHI resource + if (ImportedTextureRHI.IsValid()) + { + ImportedTextureRHI.SafeRelease(); + } + CurrentBuffer = nullptr; +#endif +} + void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight) { UE_LOG(LogMosisPhoneTexture, Log, TEXT("Initialize: %dx%d"), InWidth, InHeight); - // Use UTexture2DDynamic's Init method + TextureWidth = InWidth; + TextureHeight = InHeight; + + // Initialize the UTexture2DDynamic base - this creates our destination texture Init(InWidth, InHeight, PF_R8G8B8A8, false); bIsReady = true; @@ -24,131 +43,118 @@ void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight) #if PLATFORM_ANDROID -void UMosisPhoneTexture::UpdateFromHardwareBuffer(AHardwareBuffer* Buffer) +void UMosisPhoneTexture::ImportHardwareBuffer(AHardwareBuffer* Buffer) { if (!Buffer) { - UE_LOG(LogMosisPhoneTexture, Warning, TEXT("UpdateFromHardwareBuffer: null buffer")); + UE_LOG(LogMosisPhoneTexture, Warning, TEXT("ImportHardwareBuffer: null buffer")); return; } - CurrentBuffer = Buffer; - // Get buffer dimensions AHardwareBuffer_Desc Desc{}; AHardwareBuffer_describe(Buffer, &Desc); - UE_LOG(LogMosisPhoneTexture, Log, TEXT("UpdateFromHardwareBuffer: %dx%d"), Desc.width, Desc.height); + UE_LOG(LogMosisPhoneTexture, Log, TEXT("ImportHardwareBuffer: %dx%d, format=%d, usage=0x%x"), + Desc.width, Desc.height, Desc.format, Desc.usage); - // Lock buffer and read data - void* LockedData = nullptr; - int32 Result = AHardwareBuffer_lock(Buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &LockedData); - - if (Result != 0 || !LockedData) + // Verify buffer has GPU sampled image usage (required for Vulkan import) + if ((Desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) == 0) { - UE_LOG(LogMosisPhoneTexture, Error, TEXT("UpdateFromHardwareBuffer: Failed to lock buffer, result=%d"), Result); + UE_LOG(LogMosisPhoneTexture, Error, + TEXT("ImportHardwareBuffer: Buffer missing AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE flag")); return; } - uint8* PixelData = static_cast(LockedData); - - // Copy data to a buffer for update (accounting for stride) - uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width; - TArray TextureData; - TextureData.SetNumUninitialized(Desc.width * Desc.height * 4); - - for (uint32 y = 0; y < Desc.height; y++) + // Check if RHI is Vulkan + if (GDynamicRHI->GetInterfaceType() != ERHIInterfaceType::Vulkan) { - uint32 SrcRow = y; - uint32 DstRow = Desc.height - 1 - y; // Flip Y for Vulkan - FMemory::Memcpy( - TextureData.GetData() + DstRow * Desc.width * 4, - PixelData + SrcRow * StridePixels * 4, - Desc.width * 4 - ); + UE_LOG(LogMosisPhoneTexture, Error, + TEXT("ImportHardwareBuffer: Vulkan RHI required for hardware buffer import")); + return; } - AHardwareBuffer_unlock(Buffer, nullptr); - - // Update the texture using UTexture2DDynamic's update regions - FTexture2DDynamicResource* TextureResource = static_cast(GetResource()); - if (TextureResource) + // Release old imported texture if any + if (ImportedTextureRHI.IsValid()) { - ENQUEUE_RENDER_COMMAND(UpdateMosisTexture)( - [TextureResource, Data = MoveTemp(TextureData), Width = Desc.width, Height = Desc.height](FRHICommandListImmediate& RHICmdList) - { - FUpdateTextureRegion2D Region(0, 0, 0, 0, Width, Height); - RHIUpdateTexture2D( - TextureResource->GetTexture2DRHI(), - 0, - Region, - Width * 4, - Data.GetData() - ); - } - ); + ImportedTextureRHI.SafeRelease(); } + CurrentBuffer = Buffer; + + // Import the hardware buffer using UE5's Vulkan RHI API + // This creates a VkImage backed by the AHardwareBuffer's shared memory (zero-copy) + IVulkanDynamicRHI* VulkanRHI = GetIVulkanDynamicRHI(); + ImportedTextureRHI = VulkanRHI->RHICreateTexture2DFromAndroidHardwareBuffer(Buffer); + + if (ImportedTextureRHI.IsValid()) + { + UE_LOG(LogMosisPhoneTexture, Log, + TEXT("ImportHardwareBuffer: Successfully imported as Vulkan texture")); + + // Initialize our destination texture if not done yet + if (!bIsReady) + { + Initialize(Desc.width, Desc.height); + } + } + else + { + UE_LOG(LogMosisPhoneTexture, Error, + TEXT("ImportHardwareBuffer: Failed to create Vulkan texture from hardware buffer")); + } } -void UMosisPhoneTexture::NotifyFrameAvailable() +void UMosisPhoneTexture::CopyFromImportedBuffer() { - if (!CurrentBuffer || !bIsReady) + if (!ImportedTextureRHI.IsValid() || !bIsReady) { return; } - // Get buffer dimensions - AHardwareBuffer_Desc Desc{}; - AHardwareBuffer_describe(CurrentBuffer, &Desc); - - // Lock buffer and read data - void* LockedData = nullptr; - int32 Result = AHardwareBuffer_lock(CurrentBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &LockedData); - - if (Result != 0 || !LockedData) - { - return; - } - - uint8* PixelData = static_cast(LockedData); - - // Copy data to a buffer for update (accounting for stride) - uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width; - TArray TextureData; - TextureData.SetNumUninitialized(Desc.width * Desc.height * 4); - - for (uint32 y = 0; y < Desc.height; y++) - { - uint32 SrcRow = y; - uint32 DstRow = Desc.height - 1 - y; // Flip Y for Vulkan - FMemory::Memcpy( - TextureData.GetData() + DstRow * Desc.width * 4, - PixelData + SrcRow * StridePixels * 4, - Desc.width * 4 - ); - } - - AHardwareBuffer_unlock(CurrentBuffer, nullptr); - - // Update the texture + // Get our destination texture resource FTexture2DDynamicResource* TextureResource = static_cast(GetResource()); - if (TextureResource) + if (!TextureResource) { - ENQUEUE_RENDER_COMMAND(UpdateMosisTextureFrame)( - [TextureResource, Data = MoveTemp(TextureData), Width = Desc.width, Height = Desc.height](FRHICommandListImmediate& RHICmdList) - { - FUpdateTextureRegion2D Region(0, 0, 0, 0, Width, Height); - RHIUpdateTexture2D( - TextureResource->GetTexture2DRHI(), - 0, - Region, - Width * 4, - Data.GetData() - ); - } - ); + return; } + + FTextureRHIRef DestTextureRHI = TextureResource->GetTexture2DRHI(); + if (!DestTextureRHI.IsValid()) + { + return; + } + + // Capture references for the render thread lambda + FTextureRHIRef SrcTexture = ImportedTextureRHI; + FTextureRHIRef DstTexture = DestTextureRHI; + uint32 Width = TextureWidth; + uint32 Height = TextureHeight; + + // Enqueue GPU-to-GPU copy on render thread + ENQUEUE_RENDER_COMMAND(CopyMosisTexture)( + [SrcTexture, DstTexture, Width, Height](FRHICommandListImmediate& RHICmdList) + { + // Transition source to copy source state + RHICmdList.Transition(FRHITransitionInfo(SrcTexture, ERHIAccess::Unknown, ERHIAccess::CopySrc)); + + // Transition destination to copy dest state + RHICmdList.Transition(FRHITransitionInfo(DstTexture, ERHIAccess::Unknown, ERHIAccess::CopyDest)); + + // Perform GPU copy + FRHICopyTextureInfo CopyInfo; + CopyInfo.Size.X = Width; + CopyInfo.Size.Y = Height; + CopyInfo.Size.Z = 1; + CopyInfo.NumMips = 1; + CopyInfo.NumSlices = 1; + + RHICmdList.CopyTexture(SrcTexture, DstTexture, CopyInfo); + + // Transition destination back to shader resource for rendering + RHICmdList.Transition(FRHITransitionInfo(DstTexture, ERHIAccess::CopyDest, ERHIAccess::SRVMask)); + } + ); } #endif // PLATFORM_ANDROID diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.h b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.h index bf2a009..ccba153 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.h +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneTexture.h @@ -14,6 +14,7 @@ * UMosisPhoneTexture - Dynamic texture that displays the Mosis phone screen. * * This texture is updated from an AHardwareBuffer received from MosisService. + * Uses GPU-to-GPU copy via Vulkan external memory import for optimal performance. * Inherits from UTexture2DDynamic for proper material integration. */ UCLASS() @@ -23,26 +24,27 @@ class UMosisPhoneTexture : public UTexture2DDynamic public: UMosisPhoneTexture(); + virtual ~UMosisPhoneTexture(); /** Initialize the texture with dimensions */ void Initialize(uint32 InWidth, uint32 InHeight); #if PLATFORM_ANDROID /** - * Update the texture from a hardware buffer. + * Import a hardware buffer for GPU-accelerated texture updates. + * Creates a Vulkan image from the AHardwareBuffer for zero-copy import. * @param Buffer The AHardwareBuffer from MosisService */ - void UpdateFromHardwareBuffer(AHardwareBuffer* Buffer); + void ImportHardwareBuffer(AHardwareBuffer* Buffer); /** - * Notify that a new frame is available. - * Copies buffer data to the texture. + * Perform GPU-to-GPU copy from imported buffer to this texture. + * Called each frame when new content is available. */ - void NotifyFrameAvailable(); + void CopyFromImportedBuffer(); - /** Store the current hardware buffer reference */ - void SetHardwareBuffer(AHardwareBuffer* Buffer) { CurrentBuffer = Buffer; } - AHardwareBuffer* GetHardwareBuffer() const { return CurrentBuffer; } + /** Check if the imported buffer is valid */ + bool HasImportedBuffer() const { return ImportedTextureRHI.IsValid(); } #endif /** Check if the texture is ready for rendering */ @@ -50,10 +52,17 @@ public: private: #if PLATFORM_ANDROID - /** Current hardware buffer */ + /** RHI texture imported from AHardwareBuffer (zero-copy Vulkan import) */ + FTextureRHIRef ImportedTextureRHI; + + /** Current hardware buffer reference */ AHardwareBuffer* CurrentBuffer = nullptr; #endif /** True when texture has been initialized */ bool bIsReady = false; + + /** Texture dimensions */ + uint32 TextureWidth = 0; + uint32 TextureHeight = 0; }; diff --git a/build-unreal.bat b/build-unreal.bat new file mode 100644 index 0000000..a295d99 --- /dev/null +++ b/build-unreal.bat @@ -0,0 +1,207 @@ +@echo off +setlocal enabledelayedexpansion + +:: MosisUnreal Build and Deploy Script +:: Usage: build-unreal.bat [build|install|deploy|launch|clean] +:: build - Build Android APK only +:: install - Install APK to device only +:: deploy - Build and install (default) +:: launch - Launch app (starts MosisService first) +:: clean - Clean build artifacts + +set ENGINE_PATH=D:\Epic\UE_5.5 +set PROJECT_PATH=%~dp0MosisUnreal.uproject +set UAT_PATH=%ENGINE_PATH%\Engine\Build\BatchFiles\RunUAT.bat +set CONFIG=Development + +:: Parse command line argument +set ACTION=%1 +if "%ACTION%"=="" set ACTION=deploy + +:: Validate engine path +if not exist "%UAT_PATH%" ( + echo ERROR: Unreal Engine not found at %ENGINE_PATH% + echo Please update ENGINE_PATH in this script. + exit /b 1 +) + +:: Execute action +if "%ACTION%"=="build" goto :build +if "%ACTION%"=="install" goto :install +if "%ACTION%"=="deploy" goto :deploy +if "%ACTION%"=="launch" goto :launch +if "%ACTION%"=="clean" goto :clean + +echo Unknown action: %ACTION% +echo Usage: build-unreal.bat [build^|install^|deploy^|launch^|clean] +exit /b 1 + +:build +echo. +echo ============================================ +echo Building MosisUnreal for Android... +echo ============================================ +echo. + +call "%UAT_PATH%" BuildCookRun ^ + -project="%PROJECT_PATH%" ^ + -platform=Android ^ + -clientconfig=%CONFIG% ^ + -build -cook -stage -pak -package ^ + -noP4 ^ + -utf8output + +if errorlevel 1 ( + echo. + echo ERROR: Build failed! + exit /b 1 +) + +echo. +echo Build completed successfully! +echo APK: %~dp0Binaries\Android\MosisUnreal-arm64.apk +goto :eof + +:install +echo. +echo ============================================ +echo Installing MosisUnreal to device... +echo ============================================ +echo. + +:: Check for connected device +adb devices | findstr /r /c:"device$" >nul +if errorlevel 1 ( + echo ERROR: No Android device connected! + echo Connect a device and enable USB debugging. + exit /b 1 +) + +:: Get device ID +for /f "tokens=1" %%d in ('adb devices ^| findstr /r /c:"device$"') do ( + set DEVICE=%%d + goto :found_device +) +:found_device +echo Device: %DEVICE% + +set APK_PATH=%~dp0Binaries\Android\MosisUnreal-arm64.apk +set OBB_PATH=%~dp0Binaries\Android\main.1.com.omixlab.MosisUnreal.obb + +:: Check APK exists +if not exist "%APK_PATH%" ( + echo ERROR: APK not found at %APK_PATH% + echo Run 'build-unreal.bat build' first. + exit /b 1 +) + +:: Install APK +echo Installing APK... +adb -s %DEVICE% install -r "%APK_PATH%" +if errorlevel 1 ( + echo ERROR: APK installation failed! + exit /b 1 +) + +:: Install OBB if exists +if exist "%OBB_PATH%" ( + echo. + echo Installing OBB... + + :: Create temp directory + adb -s %DEVICE% shell "mkdir -p /data/local/tmp/obb/com.omixlab.MosisUnreal" + + :: Push OBB (use forward slashes for adb) + set OBB_UNIX=%OBB_PATH:\=/% + adb -s %DEVICE% push "!OBB_UNIX!" /data/local/tmp/obb/com.omixlab.MosisUnreal/ + + :: Move to final location + adb -s %DEVICE% shell "rm -rf /sdcard/Android/obb/com.omixlab.MosisUnreal" + adb -s %DEVICE% shell "mv /data/local/tmp/obb/com.omixlab.MosisUnreal /sdcard/Android/obb/" + + echo OBB installed. +) else ( + echo No OBB file found, skipping. +) + +echo. +echo Installation completed! +echo. +echo To launch: adb -s %DEVICE% shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity +goto :eof + +:deploy +call :build +if errorlevel 1 exit /b 1 +call :install +if errorlevel 1 exit /b 1 +call :launch +goto :eof + +:launch +echo. +echo ============================================ +echo Launching MosisUnreal... +echo ============================================ +echo. + +:: Check for connected device +adb devices | findstr /r /c:"device$" >nul +if errorlevel 1 ( + echo ERROR: No Android device connected! + exit /b 1 +) + +:: Get device ID +for /f "tokens=1" %%d in ('adb devices ^| findstr /r /c:"device$"') do ( + set DEVICE=%%d + goto :found_device_launch +) +:found_device_launch +echo Device: %DEVICE% + +:: Stop any running instances +echo Stopping existing instances... +adb -s %DEVICE% shell am force-stop com.omixlab.MosisUnreal >nul 2>&1 +adb -s %DEVICE% shell am force-stop com.omixlab.mosis >nul 2>&1 +timeout /t 1 /nobreak >nul + +:: Start MosisService first +echo Starting MosisService... +adb -s %DEVICE% shell am start -n com.omixlab.mosis/.MainActivity +timeout /t 2 /nobreak >nul + +:: Start MosisUnreal +echo Starting MosisUnreal... +adb -s %DEVICE% shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity + +echo. +echo Launched! Monitoring logs (Ctrl+C to stop)... +echo. +adb -s %DEVICE% logcat -s MosisSDK MosisOS MosisTest UE +goto :eof + +:clean +echo. +echo ============================================ +echo Cleaning build artifacts... +echo ============================================ +echo. + +if exist "%~dp0Binaries" ( + echo Removing Binaries... + rmdir /s /q "%~dp0Binaries" +) + +if exist "%~dp0Intermediate\Build" ( + echo Removing Intermediate\Build... + rmdir /s /q "%~dp0Intermediate\Build" +) + +if exist "%~dp0Saved\StagedBuilds" ( + echo Removing Saved\StagedBuilds... + rmdir /s /q "%~dp0Saved\StagedBuilds" +) + +echo Clean completed! +goto :eof