implement GPU texture copy and add build script with launch command

This commit is contained in:
2026-01-21 09:41:42 +01:00
parent fb58c2d959
commit 9cd16d98e4
5 changed files with 341 additions and 112 deletions

View File

@@ -56,6 +56,11 @@ void AMosisPhoneActor::BeginPlay()
{ {
// Default plane is 100x100 units, so bounds are -50 to 50 in X and Y // 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); 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 // 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.X = (LocalLocation.X - MinX) / (MaxX - MinX);
OutUV.Y = (LocalLocation.Y - MinY) / (MaxY - MinY); 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; return true;
} }

View File

@@ -171,8 +171,8 @@ void UMosisPhoneComponent::OnBufferAvailable()
UE_LOG(LogMosisPhone, Log, TEXT("OnBufferAvailable: Created phone texture")); UE_LOG(LogMosisPhone, Log, TEXT("OnBufferAvailable: Created phone texture"));
} }
// Import the hardware buffer // Import the hardware buffer for GPU-accelerated updates
PhoneTexture->UpdateFromHardwareBuffer(Buffer); PhoneTexture->ImportHardwareBuffer(Buffer);
// Update material if set // Update material if set
if (PhoneMaterial && PhoneTexture) if (PhoneMaterial && PhoneTexture)
@@ -185,20 +185,19 @@ void UMosisPhoneComponent::OnBufferAvailable()
void UMosisPhoneComponent::OnFrameAvailable() void UMosisPhoneComponent::OnFrameAvailable()
{ {
#if PLATFORM_ANDROID #if PLATFORM_ANDROID
if (!PhoneTexture) if (!PhoneTexture || !PhoneTexture->HasImportedBuffer())
{ {
return; return;
} }
// Notify texture that a new frame is available // Perform GPU-to-GPU copy from imported hardware buffer
// This schedules the render thread copy PhoneTexture->CopyFromImportedBuffer();
PhoneTexture->NotifyFrameAvailable();
// Log occasionally to avoid spam // Log occasionally to avoid spam
static int32 FrameCount = 0; static int32 FrameCount = 0;
if (++FrameCount % 60 == 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 #endif
} }

View File

@@ -2,21 +2,40 @@
#include "MosisPhoneTexture.h" #include "MosisPhoneTexture.h"
#include "RenderingThread.h" #include "RenderingThread.h"
#include "RHICommandList.h"
#if PLATFORM_ANDROID
#include "IVulkanDynamicRHI.h"
#endif
DEFINE_LOG_CATEGORY_STATIC(LogMosisPhoneTexture, Log, All); DEFINE_LOG_CATEGORY_STATIC(LogMosisPhoneTexture, Log, All);
UMosisPhoneTexture::UMosisPhoneTexture() UMosisPhoneTexture::UMosisPhoneTexture()
{ {
// UTexture2DDynamic defaults are fine
SRGB = true; SRGB = true;
Filter = TF_Bilinear; 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) void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight)
{ {
UE_LOG(LogMosisPhoneTexture, Log, TEXT("Initialize: %dx%d"), InWidth, 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); Init(InWidth, InHeight, PF_R8G8B8A8, false);
bIsReady = true; bIsReady = true;
@@ -24,131 +43,118 @@ void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight)
#if PLATFORM_ANDROID #if PLATFORM_ANDROID
void UMosisPhoneTexture::UpdateFromHardwareBuffer(AHardwareBuffer* Buffer) void UMosisPhoneTexture::ImportHardwareBuffer(AHardwareBuffer* Buffer)
{ {
if (!Buffer) if (!Buffer)
{ {
UE_LOG(LogMosisPhoneTexture, Warning, TEXT("UpdateFromHardwareBuffer: null buffer")); UE_LOG(LogMosisPhoneTexture, Warning, TEXT("ImportHardwareBuffer: null buffer"));
return; return;
} }
CurrentBuffer = Buffer;
// Get buffer dimensions // Get buffer dimensions
AHardwareBuffer_Desc Desc{}; AHardwareBuffer_Desc Desc{};
AHardwareBuffer_describe(Buffer, &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 // Verify buffer has GPU sampled image usage (required for Vulkan import)
void* LockedData = nullptr; if ((Desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) == 0)
int32 Result = AHardwareBuffer_lock(Buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &LockedData);
if (Result != 0 || !LockedData)
{ {
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; return;
} }
uint8* PixelData = static_cast<uint8*>(LockedData); // Check if RHI is Vulkan
if (GDynamicRHI->GetInterfaceType() != ERHIInterfaceType::Vulkan)
// Copy data to a buffer for update (accounting for stride)
uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width;
TArray<uint8> TextureData;
TextureData.SetNumUninitialized(Desc.width * Desc.height * 4);
for (uint32 y = 0; y < Desc.height; y++)
{ {
uint32 SrcRow = y; UE_LOG(LogMosisPhoneTexture, Error,
uint32 DstRow = Desc.height - 1 - y; // Flip Y for Vulkan TEXT("ImportHardwareBuffer: Vulkan RHI required for hardware buffer import"));
FMemory::Memcpy( return;
TextureData.GetData() + DstRow * Desc.width * 4,
PixelData + SrcRow * StridePixels * 4,
Desc.width * 4
);
} }
AHardwareBuffer_unlock(Buffer, nullptr); // Release old imported texture if any
if (ImportedTextureRHI.IsValid())
// Update the texture using UTexture2DDynamic's update regions
FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(GetResource());
if (TextureResource)
{ {
ENQUEUE_RENDER_COMMAND(UpdateMosisTexture)( ImportedTextureRHI.SafeRelease();
[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()
);
}
);
} }
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; return;
} }
// Get buffer dimensions // Get our destination texture resource
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<uint8*>(LockedData);
// Copy data to a buffer for update (accounting for stride)
uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width;
TArray<uint8> 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
FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(GetResource()); FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(GetResource());
if (TextureResource) if (!TextureResource)
{ {
ENQUEUE_RENDER_COMMAND(UpdateMosisTextureFrame)( return;
[TextureResource, Data = MoveTemp(TextureData), Width = Desc.width, Height = Desc.height](FRHICommandListImmediate& RHICmdList) }
FTextureRHIRef DestTextureRHI = TextureResource->GetTexture2DRHI();
if (!DestTextureRHI.IsValid())
{ {
FUpdateTextureRegion2D Region(0, 0, 0, 0, Width, Height); return;
RHIUpdateTexture2D( }
TextureResource->GetTexture2DRHI(),
0, // Capture references for the render thread lambda
Region, FTextureRHIRef SrcTexture = ImportedTextureRHI;
Width * 4, FTextureRHIRef DstTexture = DestTextureRHI;
Data.GetData() 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 #endif // PLATFORM_ANDROID

View File

@@ -14,6 +14,7 @@
* UMosisPhoneTexture - Dynamic texture that displays the Mosis phone screen. * UMosisPhoneTexture - Dynamic texture that displays the Mosis phone screen.
* *
* This texture is updated from an AHardwareBuffer received from MosisService. * 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. * Inherits from UTexture2DDynamic for proper material integration.
*/ */
UCLASS() UCLASS()
@@ -23,26 +24,27 @@ class UMosisPhoneTexture : public UTexture2DDynamic
public: public:
UMosisPhoneTexture(); UMosisPhoneTexture();
virtual ~UMosisPhoneTexture();
/** Initialize the texture with dimensions */ /** Initialize the texture with dimensions */
void Initialize(uint32 InWidth, uint32 InHeight); void Initialize(uint32 InWidth, uint32 InHeight);
#if PLATFORM_ANDROID #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 * @param Buffer The AHardwareBuffer from MosisService
*/ */
void UpdateFromHardwareBuffer(AHardwareBuffer* Buffer); void ImportHardwareBuffer(AHardwareBuffer* Buffer);
/** /**
* Notify that a new frame is available. * Perform GPU-to-GPU copy from imported buffer to this texture.
* Copies buffer data to the texture. * Called each frame when new content is available.
*/ */
void NotifyFrameAvailable(); void CopyFromImportedBuffer();
/** Store the current hardware buffer reference */ /** Check if the imported buffer is valid */
void SetHardwareBuffer(AHardwareBuffer* Buffer) { CurrentBuffer = Buffer; } bool HasImportedBuffer() const { return ImportedTextureRHI.IsValid(); }
AHardwareBuffer* GetHardwareBuffer() const { return CurrentBuffer; }
#endif #endif
/** Check if the texture is ready for rendering */ /** Check if the texture is ready for rendering */
@@ -50,10 +52,17 @@ public:
private: private:
#if PLATFORM_ANDROID #if PLATFORM_ANDROID
/** Current hardware buffer */ /** RHI texture imported from AHardwareBuffer (zero-copy Vulkan import) */
FTextureRHIRef ImportedTextureRHI;
/** Current hardware buffer reference */
AHardwareBuffer* CurrentBuffer = nullptr; AHardwareBuffer* CurrentBuffer = nullptr;
#endif #endif
/** True when texture has been initialized */ /** True when texture has been initialized */
bool bIsReady = false; bool bIsReady = false;
/** Texture dimensions */
uint32 TextureWidth = 0;
uint32 TextureHeight = 0;
}; };

207
build-unreal.bat Normal file
View File

@@ -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