progress
This commit is contained in:
@@ -1,20 +1,89 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.XR;
|
||||||
|
|
||||||
public class PhoneInteraction : MonoBehaviour
|
public class PhoneInteraction : MonoBehaviour
|
||||||
{
|
{
|
||||||
public GameObject Controller;
|
public GameObject Controller;
|
||||||
public GameObject Plane;
|
public GameObject Plane;
|
||||||
|
|
||||||
|
[Header("Input")]
|
||||||
|
[Tooltip("The XR input device to read trigger from. Leave at None for automatic detection.")]
|
||||||
|
public XRNode inputDevice = XRNode.RightHand;
|
||||||
|
|
||||||
|
[Tooltip("Trigger threshold to register as pressed")]
|
||||||
|
[Range(0.1f, 0.9f)]
|
||||||
|
public float triggerThreshold = 0.5f;
|
||||||
|
|
||||||
|
private bool wasTouching = false;
|
||||||
|
private bool wasTriggerPressed = false;
|
||||||
|
private Vector2 lastNormalized;
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
var t = Controller.GetComponent<Transform>();
|
var t = Controller.GetComponent<Transform>();
|
||||||
if (Physics.Raycast(t.position, t.forward, out RaycastHit hit) && hit.collider == Plane.GetComponent<Collider>())
|
|
||||||
|
// Check if ray hits the phone plane
|
||||||
|
bool isTouching = Physics.Raycast(t.position, t.forward, out RaycastHit hit) &&
|
||||||
|
hit.collider == Plane.GetComponent<Collider>();
|
||||||
|
|
||||||
|
if (isTouching)
|
||||||
{
|
{
|
||||||
|
// Calculate normalized UV coordinates
|
||||||
var local = Plane.transform.InverseTransformPoint(hit.point);
|
var local = Plane.transform.InverseTransformPoint(hit.point);
|
||||||
var normalized = new Vector2((local.x + 5) / 10, (local.z + 5) / 10);
|
var normalized = new Vector2((local.x + 5) / 10, (local.z + 5) / 10);
|
||||||
KotlinBridge.SendTouchMove(normalized.x, 1 - normalized.y);
|
lastNormalized = new Vector2(normalized.x, 1 - normalized.y);
|
||||||
//Debug.Log("MOVE " + normalized);
|
|
||||||
//Debug.DrawLine(t.position, hit.point, Color.green);
|
// Check trigger input
|
||||||
|
bool triggerPressed = IsTriggerPressed();
|
||||||
|
|
||||||
|
if (triggerPressed)
|
||||||
|
{
|
||||||
|
if (!wasTriggerPressed)
|
||||||
|
{
|
||||||
|
// Trigger just pressed - send touch down
|
||||||
|
KotlinBridge.SendTouchDown(lastNormalized.x, lastNormalized.y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Trigger held - send touch move
|
||||||
|
KotlinBridge.SendTouchMove(lastNormalized.x, lastNormalized.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (wasTriggerPressed)
|
||||||
|
{
|
||||||
|
// Trigger just released - send touch up
|
||||||
|
KotlinBridge.SendTouchUp(lastNormalized.x, lastNormalized.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasTriggerPressed = triggerPressed;
|
||||||
}
|
}
|
||||||
|
else if (wasTriggerPressed)
|
||||||
|
{
|
||||||
|
// Lost contact while touching - send touch up at last position
|
||||||
|
KotlinBridge.SendTouchUp(lastNormalized.x, lastNormalized.y);
|
||||||
|
wasTriggerPressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasTouching = isTouching;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTriggerPressed()
|
||||||
|
{
|
||||||
|
// Try to get trigger value from XR input
|
||||||
|
InputDevice device = InputDevices.GetDeviceAtXRNode(inputDevice);
|
||||||
|
if (device.isValid)
|
||||||
|
{
|
||||||
|
if (device.TryGetFeatureValue(CommonUsages.trigger, out float triggerValue))
|
||||||
|
{
|
||||||
|
return triggerValue > triggerThreshold;
|
||||||
|
}
|
||||||
|
if (device.TryGetFeatureValue(CommonUsages.triggerButton, out bool triggerButton))
|
||||||
|
{
|
||||||
|
return triggerButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Unity's legacy input (for testing in editor)
|
||||||
|
return Input.GetMouseButton(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.22.1)
|
|||||||
project("MyNativePlugin")
|
project("MyNativePlugin")
|
||||||
|
|
||||||
find_library(log-lib log)
|
find_library(log-lib log)
|
||||||
|
find_library(vulkan-lib vulkan)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -35,6 +36,8 @@ add_custom_command(
|
|||||||
|
|
||||||
add_library(my_native_lib SHARED
|
add_library(my_native_lib SHARED
|
||||||
my_native_code.cpp
|
my_native_code.cpp
|
||||||
|
opengl_backend.cpp
|
||||||
|
vulkan_backend.cpp
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisService.cpp
|
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisService.cpp
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisListener.cpp
|
${CMAKE_CURRENT_BINARY_DIR}/com/omixlab/mosis/IMosisListener.cpp
|
||||||
${SHARED_SRC_DIR}/logger.cpp
|
${SHARED_SRC_DIR}/logger.cpp
|
||||||
@@ -43,11 +46,27 @@ add_library(my_native_lib SHARED
|
|||||||
${SHARED_SRC_DIR}/glad/src/egl.c
|
${SHARED_SRC_DIR}/glad/src/egl.c
|
||||||
${SHARED_SRC_DIR}/glad/src/gles2.c
|
${SHARED_SRC_DIR}/glad/src/gles2.c
|
||||||
)
|
)
|
||||||
target_link_libraries(my_native_lib ${log-lib} binder_ndk EGL GLESv2 nativewindow)
|
|
||||||
|
target_link_libraries(my_native_lib
|
||||||
|
${log-lib}
|
||||||
|
${vulkan-lib}
|
||||||
|
binder_ndk
|
||||||
|
EGL
|
||||||
|
GLESv2
|
||||||
|
nativewindow
|
||||||
|
)
|
||||||
|
|
||||||
target_include_directories(my_native_lib PUBLIC
|
target_include_directories(my_native_lib PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${SHARED_SRC_DIR}
|
${SHARED_SRC_DIR}
|
||||||
${SHARED_SRC_DIR}/glad/include
|
${SHARED_SRC_DIR}/glad/include
|
||||||
${CMAKE_CURRENT_BINARY_DIR}
|
${CMAKE_CURRENT_BINARY_DIR}
|
||||||
${BINDER_DIR}
|
${BINDER_DIR}
|
||||||
${PLUGIN_API}
|
${PLUGIN_API}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Vulkan support definitions
|
||||||
|
target_compile_definitions(my_native_lib PRIVATE
|
||||||
|
VK_USE_PLATFORM_ANDROID_KHR
|
||||||
|
MOSIS_VULKAN_SUPPORT=1
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,90 +5,29 @@
|
|||||||
#include <aidl/com/omixlab/mosis/IMosisService.h>
|
#include <aidl/com/omixlab/mosis/IMosisService.h>
|
||||||
#include <android/binder_ibinder_jni.h>
|
#include <android/binder_ibinder_jni.h>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <glad/gles2.h>
|
|
||||||
#include <glad/egl.h>
|
|
||||||
#include <IUnityGraphics.h>
|
#include <IUnityGraphics.h>
|
||||||
#include <external_texture.h>
|
|
||||||
#include <render_target.h>
|
#include "texture_backend.h"
|
||||||
|
#include "opengl_backend.h"
|
||||||
|
#include "vulkan_backend.h"
|
||||||
|
|
||||||
using namespace aidl::com::omixlab::mosis;
|
using namespace aidl::com::omixlab::mosis;
|
||||||
using namespace aidl::android::hardware;
|
using namespace aidl::android::hardware;
|
||||||
|
|
||||||
|
// Global state
|
||||||
std::shared_ptr<IMosisService> g_service;
|
std::shared_ptr<IMosisService> g_service;
|
||||||
std::shared_ptr<class ServiceContext> g_context;
|
std::shared_ptr<class ServiceContext> g_context;
|
||||||
|
std::unique_ptr<ITextureBackend> g_backend;
|
||||||
|
IUnityInterfaces* g_unityInterfaces = nullptr;
|
||||||
|
UnityGfxRenderer g_rendererType = kUnityGfxRendererNull;
|
||||||
|
|
||||||
class TextureBlitter
|
// Callback function pointers
|
||||||
{
|
|
||||||
GLuint source_texture = 0;
|
|
||||||
GLuint source_fb = 0;
|
|
||||||
GLuint dest_texture = 0;
|
|
||||||
GLuint dest_fb = 0;
|
|
||||||
GLint width;
|
|
||||||
GLint height;
|
|
||||||
public:
|
|
||||||
bool create(AHardwareBuffer* hardwareBuffer)
|
|
||||||
{
|
|
||||||
AHardwareBuffer_Desc desc{};
|
|
||||||
AHardwareBuffer_describe(hardwareBuffer, &desc);
|
|
||||||
width = desc.width;
|
|
||||||
height = desc.height;
|
|
||||||
EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(hardwareBuffer);
|
|
||||||
EGLImageKHR eglImage = eglCreateImageKHR(eglGetCurrentDisplay(), EGL_NO_CONTEXT,
|
|
||||||
EGL_NATIVE_BUFFER_ANDROID, clientBuffer, nullptr);
|
|
||||||
if (eglImage == EGL_NO_IMAGE_KHR)
|
|
||||||
{
|
|
||||||
Logger::Log("Failed to create EGL image");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
glGenTextures(1, &source_texture);
|
|
||||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, source_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, eglImage);
|
|
||||||
eglDestroyImageKHR(eglGetCurrentDisplay(), eglImage);
|
|
||||||
|
|
||||||
glGenFramebuffers(1, &source_fb);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, source_fb);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
||||||
GL_TEXTURE_EXTERNAL_OES, source_texture, 0);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
return false;
|
|
||||||
glGenTextures(1, &dest_texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, dest_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, desc.width, desc.height,
|
|
||||||
0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
glGenFramebuffers(1, &dest_fb);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, dest_fb);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
||||||
GL_TEXTURE_2D, dest_texture, 0);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
return false;
|
|
||||||
blit();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
[[nodiscard]] GLuint dest_texture_id() const
|
|
||||||
{
|
|
||||||
return dest_texture;
|
|
||||||
}
|
|
||||||
void blit()
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, source_fb);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dest_fb);
|
|
||||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef void (*OnMessageCallback)(const char*);
|
typedef void (*OnMessageCallback)(const char*);
|
||||||
typedef void (*OnServiceInitializedCallback)(bool success);
|
typedef void (*OnServiceInitializedCallback)(bool success);
|
||||||
typedef void (*OnFrameAvailableCallback)();
|
typedef void (*OnFrameAvailableCallback)();
|
||||||
typedef void (*OnBufferReadyCallback)();
|
typedef void (*OnBufferReadyCallback)();
|
||||||
typedef void (*OnTextureReadyCallback)(GLuint gl_texture);
|
typedef void (*OnTextureReadyCallback)(void* nativeTexturePtr, int width, int height, bool isVulkan);
|
||||||
|
|
||||||
struct NativeCallbacks
|
struct NativeCallbacks
|
||||||
{
|
{
|
||||||
OnMessageCallback OnMessage;
|
OnMessageCallback OnMessage;
|
||||||
@@ -102,49 +41,156 @@ NativeCallbacks g_callbacks{};
|
|||||||
class ServiceContext : public BnMosisListener
|
class ServiceContext : public BnMosisListener
|
||||||
{
|
{
|
||||||
AHardwareBuffer* m_hwbuffer = nullptr;
|
AHardwareBuffer* m_hwbuffer = nullptr;
|
||||||
std::unique_ptr<TextureBlitter> m_texture;
|
|
||||||
public:
|
public:
|
||||||
ndk::ScopedAStatus onServiceInitialized(bool in_success) override
|
ndk::ScopedAStatus onServiceInitialized(bool in_success) override
|
||||||
{
|
{
|
||||||
Logger::Log("onServiceInitialized");
|
Logger::Log("onServiceInitialized");
|
||||||
g_callbacks.OnMessage("onServiceInitialized");
|
if (g_callbacks.OnMessage)
|
||||||
g_callbacks.OnServiceInitialized(in_success);
|
g_callbacks.OnMessage("onServiceInitialized");
|
||||||
|
if (g_callbacks.OnServiceInitialized)
|
||||||
|
g_callbacks.OnServiceInitialized(in_success);
|
||||||
return ndk::ScopedAStatus::ok();
|
return ndk::ScopedAStatus::ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
ndk::ScopedAStatus onFrameAvailable() override
|
ndk::ScopedAStatus onFrameAvailable() override
|
||||||
{
|
{
|
||||||
Logger::Log("onFrameAvailable");
|
Logger::Log("onFrameAvailable");
|
||||||
g_callbacks.OnMessage("onFrameAvailable");
|
if (g_callbacks.OnMessage)
|
||||||
g_callbacks.OnFrameAvailable();
|
g_callbacks.OnMessage("onFrameAvailable");
|
||||||
|
if (g_callbacks.OnFrameAvailable)
|
||||||
|
g_callbacks.OnFrameAvailable();
|
||||||
return ndk::ScopedAStatus::ok();
|
return ndk::ScopedAStatus::ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
ndk::ScopedAStatus onBufferAvailable(const HardwareBuffer &in_buffer) override
|
ndk::ScopedAStatus onBufferAvailable(const HardwareBuffer &in_buffer) override
|
||||||
{
|
{
|
||||||
Logger::Log("onBufferAvailable");
|
Logger::Log("onBufferAvailable");
|
||||||
g_callbacks.OnMessage("onBufferAvailable");
|
if (g_callbacks.OnMessage)
|
||||||
|
g_callbacks.OnMessage("onBufferAvailable");
|
||||||
m_hwbuffer = in_buffer.get();
|
m_hwbuffer = in_buffer.get();
|
||||||
AHardwareBuffer_acquire(m_hwbuffer);
|
AHardwareBuffer_acquire(m_hwbuffer);
|
||||||
g_callbacks.OnBufferReady();
|
if (g_callbacks.OnBufferReady)
|
||||||
|
g_callbacks.OnBufferReady();
|
||||||
return ndk::ScopedAStatus::ok();
|
return ndk::ScopedAStatus::ok();
|
||||||
}
|
}
|
||||||
[[nodiscard]] GLuint create_texture()
|
|
||||||
|
bool CreateTexture()
|
||||||
{
|
{
|
||||||
m_texture = std::make_unique<TextureBlitter>();
|
if (!m_hwbuffer)
|
||||||
m_texture->create(m_hwbuffer);
|
{
|
||||||
return m_texture->dest_texture_id();
|
Logger::Log("CreateTexture: No hardware buffer available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_backend)
|
||||||
|
{
|
||||||
|
Logger::Log("CreateTexture: No backend available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_backend->Create(m_hwbuffer))
|
||||||
|
{
|
||||||
|
Logger::Log("CreateTexture: Backend Create failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify C# with texture info
|
||||||
|
if (g_callbacks.OnTextureReady)
|
||||||
|
{
|
||||||
|
g_callbacks.OnTextureReady(
|
||||||
|
g_backend->GetNativeTexturePtr(),
|
||||||
|
g_backend->GetWidth(),
|
||||||
|
g_backend->GetHeight(),
|
||||||
|
g_backend->IsVulkan()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
void update_texture()
|
|
||||||
|
void UpdateTexture()
|
||||||
{
|
{
|
||||||
if (m_texture)
|
if (g_backend)
|
||||||
m_texture->blit();
|
{
|
||||||
|
g_backend->Update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Unity Plugin Interface
|
||||||
|
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API
|
||||||
|
UnityPluginLoad(IUnityInterfaces* unityInterfaces)
|
||||||
|
{
|
||||||
|
g_unityInterfaces = unityInterfaces;
|
||||||
|
|
||||||
|
IUnityGraphics* graphics = unityInterfaces->Get<IUnityGraphics>();
|
||||||
|
if (!graphics)
|
||||||
|
{
|
||||||
|
Logger::Log("UnityPluginLoad: IUnityGraphics not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_rendererType = graphics->GetRenderer();
|
||||||
|
Logger::Log(std::format("UnityPluginLoad: Renderer type = {}", static_cast<int>(g_rendererType)));
|
||||||
|
|
||||||
|
// Try to create Vulkan backend first
|
||||||
|
if (g_rendererType == kUnityGfxRendererVulkan)
|
||||||
|
{
|
||||||
|
auto vulkanBackend = std::make_unique<VulkanTextureBackend>();
|
||||||
|
if (vulkanBackend->Initialize(unityInterfaces))
|
||||||
|
{
|
||||||
|
g_backend = std::move(vulkanBackend);
|
||||||
|
Logger::Log("UnityPluginLoad: Using Vulkan backend");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logger::Log("UnityPluginLoad: Vulkan initialization failed, falling back to OpenGL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to OpenGL for GLES renderers
|
||||||
|
if (g_rendererType == kUnityGfxRendererOpenGLES20 ||
|
||||||
|
g_rendererType == kUnityGfxRendererOpenGLES30)
|
||||||
|
{
|
||||||
|
g_backend = std::make_unique<OpenGLTextureBackend>();
|
||||||
|
Logger::Log("UnityPluginLoad: Using OpenGL backend");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::Log(std::format("UnityPluginLoad: Unsupported renderer type {}", static_cast<int>(g_rendererType)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API
|
||||||
|
UnityPluginUnload()
|
||||||
|
{
|
||||||
|
Logger::Log("UnityPluginUnload");
|
||||||
|
|
||||||
|
// Clean up backend
|
||||||
|
if (g_backend)
|
||||||
|
{
|
||||||
|
g_backend->Destroy();
|
||||||
|
g_backend.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_unityInterfaces = nullptr;
|
||||||
|
g_rendererType = kUnityGfxRendererNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native callback setters
|
||||||
extern "C" void SetNativeCallbacks(const NativeCallbacks& ptr)
|
extern "C" void SetNativeCallbacks(const NativeCallbacks& ptr)
|
||||||
{
|
{
|
||||||
g_callbacks = ptr;
|
g_callbacks = ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Touch input forwarding
|
||||||
|
extern "C" void SendTouchDown(float x, float y)
|
||||||
|
{
|
||||||
|
if (g_service)
|
||||||
|
{
|
||||||
|
g_service->onTouchDown(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" void SendTouchMove(float x, float y)
|
extern "C" void SendTouchMove(float x, float y)
|
||||||
{
|
{
|
||||||
if (g_service)
|
if (g_service)
|
||||||
@@ -153,40 +199,60 @@ extern "C" void SendTouchMove(float x, float y)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" UnityRenderingEvent InitGLAD()
|
extern "C" void SendTouchUp(float x, float y)
|
||||||
{
|
{
|
||||||
return [](int eventId){
|
if (g_service)
|
||||||
gladLoaderLoadEGL(EGL_NO_DISPLAY);
|
{
|
||||||
int egl_version = gladLoadEGL(eglGetCurrentDisplay(), eglGetProcAddress);
|
g_service->onTouchUp(x, y);
|
||||||
if (egl_version == 0)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unity render thread callbacks
|
||||||
|
static void UNITY_INTERFACE_API OnInitBackendRenderThread(int eventId)
|
||||||
|
{
|
||||||
|
Logger::Log("OnInitBackendRenderThread");
|
||||||
|
|
||||||
|
// For OpenGL, we need to initialize GLAD on the render thread
|
||||||
|
if (!g_backend->IsVulkan())
|
||||||
|
{
|
||||||
|
if (!OpenGLTextureBackend::InitializeGLAD())
|
||||||
{
|
{
|
||||||
Logger::Log("Failed to load EGL");
|
Logger::Log("OnInitBackendRenderThread: Failed to initialize GLAD");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int gl_version = gladLoaderLoadGLES2();
|
}
|
||||||
if (gl_version == 0)
|
|
||||||
{
|
if (g_context)
|
||||||
Logger::Log("Failed to load GL");
|
{
|
||||||
return;
|
g_context->CreateTexture();
|
||||||
}
|
}
|
||||||
if (g_context)
|
}
|
||||||
{
|
|
||||||
GLuint texture = g_context->create_texture();
|
static void UNITY_INTERFACE_API OnUpdateTextureRenderThread(int eventId)
|
||||||
g_callbacks.OnTextureReady(texture);
|
{
|
||||||
}
|
if (g_context)
|
||||||
};
|
{
|
||||||
|
g_context->UpdateTexture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" UnityRenderingEvent InitBackend()
|
||||||
|
{
|
||||||
|
return OnInitBackendRenderThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" UnityRenderingEvent UpdateTexture()
|
extern "C" UnityRenderingEvent UpdateTexture()
|
||||||
{
|
{
|
||||||
return [](int eventId){
|
return OnUpdateTextureRenderThread;
|
||||||
if (g_context)
|
|
||||||
{
|
|
||||||
g_context->update_texture();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility - redirect to new names
|
||||||
|
extern "C" UnityRenderingEvent InitGLAD()
|
||||||
|
{
|
||||||
|
return InitBackend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// JNI entry point from Kotlin
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_omixlab_mosis_unity_MyKotlinPlugin_serviceConnected(JNIEnv *env, jobject thiz,
|
Java_com_omixlab_mosis_unity_MyKotlinPlugin_serviceConnected(JNIEnv *env, jobject thiz,
|
||||||
@@ -196,6 +262,7 @@ Java_com_omixlab_mosis_unity_MyKotlinPlugin_serviceConnected(JNIEnv *env, jobjec
|
|||||||
const ndk::SpAIBinder spBinder(pBinder);
|
const ndk::SpAIBinder spBinder(pBinder);
|
||||||
g_service = IMosisService::fromBinder(spBinder);
|
g_service = IMosisService::fromBinder(spBinder);
|
||||||
Logger::Log("Service Connected");
|
Logger::Log("Service Connected");
|
||||||
|
|
||||||
g_context = ndk::SharedRefBase::make<ServiceContext>();
|
g_context = ndk::SharedRefBase::make<ServiceContext>();
|
||||||
bool result{};
|
bool result{};
|
||||||
g_service->initOS(g_context, &result);
|
g_service->initOS(g_context, &result);
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#include "opengl_backend.h"
|
||||||
|
#include <logger.h>
|
||||||
|
|
||||||
|
bool OpenGLTextureBackend::InitializeGLAD()
|
||||||
|
{
|
||||||
|
gladLoaderLoadEGL(EGL_NO_DISPLAY);
|
||||||
|
int egl_version = gladLoadEGL(eglGetCurrentDisplay(), eglGetProcAddress);
|
||||||
|
if (egl_version == 0)
|
||||||
|
{
|
||||||
|
Logger::Log("OpenGLTextureBackend: Failed to load EGL");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gl_version = gladLoaderLoadGLES2();
|
||||||
|
if (gl_version == 0)
|
||||||
|
{
|
||||||
|
Logger::Log("OpenGLTextureBackend: Failed to load GLES2");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::Log("OpenGLTextureBackend: GLAD initialized successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLTextureBackend::Create(AHardwareBuffer* buffer)
|
||||||
|
{
|
||||||
|
if (m_Created)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get buffer dimensions
|
||||||
|
AHardwareBuffer_Desc desc{};
|
||||||
|
AHardwareBuffer_describe(buffer, &desc);
|
||||||
|
m_Width = static_cast<GLint>(desc.width);
|
||||||
|
m_Height = static_cast<GLint>(desc.height);
|
||||||
|
|
||||||
|
// Create EGL image from hardware buffer
|
||||||
|
EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(buffer);
|
||||||
|
EGLImageKHR eglImage = eglCreateImageKHR(
|
||||||
|
eglGetCurrentDisplay(),
|
||||||
|
EGL_NO_CONTEXT,
|
||||||
|
EGL_NATIVE_BUFFER_ANDROID,
|
||||||
|
clientBuffer,
|
||||||
|
nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eglImage == EGL_NO_IMAGE_KHR)
|
||||||
|
{
|
||||||
|
Logger::Log("OpenGLTextureBackend: Failed to create EGL image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create source texture (external OES format from hardware buffer)
|
||||||
|
glGenTextures(1, &m_SourceTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_SourceTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, eglImage);
|
||||||
|
eglDestroyImageKHR(eglGetCurrentDisplay(), eglImage);
|
||||||
|
|
||||||
|
// Create source framebuffer
|
||||||
|
glGenFramebuffers(1, &m_SourceFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_SourceFramebuffer);
|
||||||
|
glFramebufferTexture2D(
|
||||||
|
GL_FRAMEBUFFER,
|
||||||
|
GL_COLOR_ATTACHMENT0,
|
||||||
|
GL_TEXTURE_EXTERNAL_OES,
|
||||||
|
m_SourceTexture,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
Logger::Log("OpenGLTextureBackend: Source framebuffer incomplete");
|
||||||
|
Destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination texture (standard 2D texture for Unity)
|
||||||
|
glGenTextures(1, &m_DestTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, m_DestTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGB,
|
||||||
|
m_Width,
|
||||||
|
m_Height,
|
||||||
|
0,
|
||||||
|
GL_RGB,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create destination framebuffer
|
||||||
|
glGenFramebuffers(1, &m_DestFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, m_DestFramebuffer);
|
||||||
|
glFramebufferTexture2D(
|
||||||
|
GL_FRAMEBUFFER,
|
||||||
|
GL_COLOR_ATTACHMENT0,
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
m_DestTexture,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
Logger::Log("OpenGLTextureBackend: Dest framebuffer incomplete");
|
||||||
|
Destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Created = true;
|
||||||
|
|
||||||
|
// Initial blit
|
||||||
|
Blit();
|
||||||
|
|
||||||
|
Logger::Log("OpenGLTextureBackend: Created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLTextureBackend::Update()
|
||||||
|
{
|
||||||
|
if (m_Created)
|
||||||
|
{
|
||||||
|
Blit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLTextureBackend::Blit()
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_SourceFramebuffer);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_DestFramebuffer);
|
||||||
|
glBlitFramebuffer(
|
||||||
|
0, 0, m_Width, m_Height,
|
||||||
|
0, 0, m_Width, m_Height,
|
||||||
|
GL_COLOR_BUFFER_BIT,
|
||||||
|
GL_NEAREST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLTextureBackend::Destroy()
|
||||||
|
{
|
||||||
|
if (m_DestFramebuffer)
|
||||||
|
{
|
||||||
|
glDeleteFramebuffers(1, &m_DestFramebuffer);
|
||||||
|
m_DestFramebuffer = 0;
|
||||||
|
}
|
||||||
|
if (m_SourceFramebuffer)
|
||||||
|
{
|
||||||
|
glDeleteFramebuffers(1, &m_SourceFramebuffer);
|
||||||
|
m_SourceFramebuffer = 0;
|
||||||
|
}
|
||||||
|
if (m_DestTexture)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &m_DestTexture);
|
||||||
|
m_DestTexture = 0;
|
||||||
|
}
|
||||||
|
if (m_SourceTexture)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &m_SourceTexture);
|
||||||
|
m_SourceTexture = 0;
|
||||||
|
}
|
||||||
|
m_Width = 0;
|
||||||
|
m_Height = 0;
|
||||||
|
m_Created = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* OpenGLTextureBackend::GetNativeTexturePtr()
|
||||||
|
{
|
||||||
|
return reinterpret_cast<void*>(static_cast<uintptr_t>(m_DestTexture));
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "texture_backend.h"
|
||||||
|
#include <glad/gles2.h>
|
||||||
|
#include <glad/egl.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenGL ES implementation of ITextureBackend.
|
||||||
|
* Imports AHardwareBuffer via EGL image and blits to a standard GL_TEXTURE_2D.
|
||||||
|
*/
|
||||||
|
class OpenGLTextureBackend : public ITextureBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OpenGLTextureBackend() = default;
|
||||||
|
~OpenGLTextureBackend() override { Destroy(); }
|
||||||
|
|
||||||
|
bool Create(AHardwareBuffer* buffer) override;
|
||||||
|
void Update() override;
|
||||||
|
void Destroy() override;
|
||||||
|
void* GetNativeTexturePtr() override;
|
||||||
|
int GetWidth() const override { return m_Width; }
|
||||||
|
int GetHeight() const override { return m_Height; }
|
||||||
|
bool IsVulkan() const override { return false; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize GLAD for OpenGL ES.
|
||||||
|
* Must be called on the render thread before Create().
|
||||||
|
* @return true if initialization succeeded
|
||||||
|
*/
|
||||||
|
static bool InitializeGLAD();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Blit();
|
||||||
|
|
||||||
|
GLuint m_SourceTexture = 0; // GL_TEXTURE_EXTERNAL_OES from HardwareBuffer
|
||||||
|
GLuint m_SourceFramebuffer = 0;
|
||||||
|
GLuint m_DestTexture = 0; // GL_TEXTURE_2D for Unity
|
||||||
|
GLuint m_DestFramebuffer = 0;
|
||||||
|
GLint m_Width = 0;
|
||||||
|
GLint m_Height = 0;
|
||||||
|
bool m_Created = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <android/hardware_buffer.h>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract interface for texture backends.
|
||||||
|
* Allows swapping between OpenGL and Vulkan implementations at runtime.
|
||||||
|
*/
|
||||||
|
class ITextureBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~ITextureBackend() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create texture resources from a hardware buffer.
|
||||||
|
* @param buffer The AHardwareBuffer from the Mosis service
|
||||||
|
* @return true if creation succeeded
|
||||||
|
*/
|
||||||
|
virtual bool Create(AHardwareBuffer* buffer) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the texture (blit from source to destination).
|
||||||
|
* Called each frame when a new frame is available.
|
||||||
|
*/
|
||||||
|
virtual void Update() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all texture resources.
|
||||||
|
*/
|
||||||
|
virtual void Destroy() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the native texture pointer for Unity.
|
||||||
|
* For OpenGL: GLuint cast to void*
|
||||||
|
* For Vulkan: VkImage cast to void*
|
||||||
|
*/
|
||||||
|
virtual void* GetNativeTexturePtr() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get texture dimensions.
|
||||||
|
*/
|
||||||
|
virtual int GetWidth() const = 0;
|
||||||
|
virtual int GetHeight() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the backend is Vulkan-based.
|
||||||
|
* Unity needs this to know how to interpret the native pointer.
|
||||||
|
*/
|
||||||
|
virtual bool IsVulkan() const = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
#include "vulkan_backend.h"
|
||||||
|
#include <logger.h>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
bool VulkanTextureBackend::Initialize(IUnityInterfaces* unityInterfaces)
|
||||||
|
{
|
||||||
|
if (m_Initialized)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_UnityVulkan = unityInterfaces->Get<IUnityGraphicsVulkan>();
|
||||||
|
if (!m_UnityVulkan)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: IUnityGraphicsVulkan not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Unity's Vulkan instance
|
||||||
|
UnityVulkanInstance vkInstance = m_UnityVulkan->Instance();
|
||||||
|
m_Instance = vkInstance.instance;
|
||||||
|
m_PhysicalDevice = vkInstance.physicalDevice;
|
||||||
|
m_Device = vkInstance.device;
|
||||||
|
m_QueueFamilyIndex = vkInstance.queueFamilyIndex;
|
||||||
|
|
||||||
|
// Get a queue from Unity
|
||||||
|
vkGetDeviceQueue(m_Device, m_QueueFamilyIndex, 0, &m_Queue);
|
||||||
|
|
||||||
|
// Load extension function
|
||||||
|
vkGetAndroidHardwareBufferPropertiesANDROID =
|
||||||
|
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(
|
||||||
|
vkGetInstanceProcAddr(m_Instance, "vkGetAndroidHardwareBufferPropertiesANDROID")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!vkGetAndroidHardwareBufferPropertiesANDROID)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: vkGetAndroidHardwareBufferPropertiesANDROID not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create command pool
|
||||||
|
VkCommandPoolCreateInfo poolInfo{};
|
||||||
|
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||||
|
poolInfo.queueFamilyIndex = m_QueueFamilyIndex;
|
||||||
|
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||||
|
|
||||||
|
if (vkCreateCommandPool(m_Device, &poolInfo, nullptr, &m_CommandPool) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to create command pool");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate command buffer
|
||||||
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||||
|
allocInfo.commandPool = m_CommandPool;
|
||||||
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
|
allocInfo.commandBufferCount = 1;
|
||||||
|
|
||||||
|
if (vkAllocateCommandBuffers(m_Device, &allocInfo, &m_CommandBuffer) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to allocate command buffer");
|
||||||
|
vkDestroyCommandPool(m_Device, m_CommandPool, nullptr);
|
||||||
|
m_CommandPool = VK_NULL_HANDLE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Initialized = true;
|
||||||
|
Logger::Log("VulkanTextureBackend: Initialized successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VulkanTextureBackend::Create(AHardwareBuffer* buffer)
|
||||||
|
{
|
||||||
|
if (!m_Initialized)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Not initialized");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Created)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get buffer dimensions
|
||||||
|
AHardwareBuffer_Desc desc{};
|
||||||
|
AHardwareBuffer_describe(buffer, &desc);
|
||||||
|
m_Width = static_cast<int>(desc.width);
|
||||||
|
m_Height = static_cast<int>(desc.height);
|
||||||
|
|
||||||
|
Logger::Log(std::format("VulkanTextureBackend: Creating {}x{} texture", m_Width, m_Height));
|
||||||
|
|
||||||
|
if (!ImportHardwareBuffer(buffer))
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to import hardware buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CreateLocalImage())
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to create local image");
|
||||||
|
DestroyImportedResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Created = true;
|
||||||
|
|
||||||
|
// Initial copy
|
||||||
|
CopyImage();
|
||||||
|
|
||||||
|
Logger::Log("VulkanTextureBackend: Created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VulkanTextureBackend::ImportHardwareBuffer(AHardwareBuffer* buffer)
|
||||||
|
{
|
||||||
|
// Query hardware buffer properties
|
||||||
|
VkAndroidHardwareBufferFormatPropertiesANDROID formatProps{};
|
||||||
|
formatProps.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID;
|
||||||
|
|
||||||
|
VkAndroidHardwareBufferPropertiesANDROID props{};
|
||||||
|
props.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID;
|
||||||
|
props.pNext = &formatProps;
|
||||||
|
|
||||||
|
if (vkGetAndroidHardwareBufferPropertiesANDROID(m_Device, buffer, &props) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to get hardware buffer properties");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Format = formatProps.format;
|
||||||
|
Logger::Log(std::format("VulkanTextureBackend: Buffer format: {}", static_cast<int>(m_Format)));
|
||||||
|
|
||||||
|
// Create VkImage with external memory
|
||||||
|
VkExternalMemoryImageCreateInfo extMemImageInfo{};
|
||||||
|
extMemImageInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
|
||||||
|
extMemImageInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
|
||||||
|
|
||||||
|
VkImageCreateInfo imageInfo{};
|
||||||
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
imageInfo.pNext = &extMemImageInfo;
|
||||||
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
imageInfo.format = m_Format;
|
||||||
|
imageInfo.extent = {static_cast<uint32_t>(m_Width), static_cast<uint32_t>(m_Height), 1};
|
||||||
|
imageInfo.mipLevels = 1;
|
||||||
|
imageInfo.arrayLayers = 1;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||||
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
|
||||||
|
if (vkCreateImage(m_Device, &imageInfo, nullptr, &m_ImportedImage) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to create imported image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import memory from hardware buffer
|
||||||
|
VkImportAndroidHardwareBufferInfoANDROID importInfo{};
|
||||||
|
importInfo.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID;
|
||||||
|
importInfo.buffer = buffer;
|
||||||
|
|
||||||
|
VkMemoryDedicatedAllocateInfo dedicatedInfo{};
|
||||||
|
dedicatedInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO;
|
||||||
|
dedicatedInfo.pNext = &importInfo;
|
||||||
|
dedicatedInfo.image = m_ImportedImage;
|
||||||
|
|
||||||
|
VkMemoryAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||||
|
allocInfo.pNext = &dedicatedInfo;
|
||||||
|
allocInfo.allocationSize = props.allocationSize;
|
||||||
|
allocInfo.memoryTypeIndex = FindMemoryType(props.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
|
|
||||||
|
if (vkAllocateMemory(m_Device, &allocInfo, nullptr, &m_ImportedMemory) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to allocate imported memory");
|
||||||
|
vkDestroyImage(m_Device, m_ImportedImage, nullptr);
|
||||||
|
m_ImportedImage = VK_NULL_HANDLE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vkBindImageMemory(m_Device, m_ImportedImage, m_ImportedMemory, 0) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to bind imported memory");
|
||||||
|
DestroyImportedResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::Log("VulkanTextureBackend: Hardware buffer imported");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VulkanTextureBackend::CreateLocalImage()
|
||||||
|
{
|
||||||
|
// Create local VkImage for safe rendering
|
||||||
|
VkImageCreateInfo imageInfo{};
|
||||||
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
imageInfo.format = m_Format;
|
||||||
|
imageInfo.extent = {static_cast<uint32_t>(m_Width), static_cast<uint32_t>(m_Height), 1};
|
||||||
|
imageInfo.mipLevels = 1;
|
||||||
|
imageInfo.arrayLayers = 1;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||||
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
|
||||||
|
if (vkCreateImage(m_Device, &imageInfo, nullptr, &m_LocalImage) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to create local image");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate memory for local image
|
||||||
|
VkMemoryRequirements memReqs;
|
||||||
|
vkGetImageMemoryRequirements(m_Device, m_LocalImage, &memReqs);
|
||||||
|
|
||||||
|
VkMemoryAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||||
|
allocInfo.allocationSize = memReqs.size;
|
||||||
|
allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
|
|
||||||
|
if (vkAllocateMemory(m_Device, &allocInfo, nullptr, &m_LocalMemory) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to allocate local memory");
|
||||||
|
vkDestroyImage(m_Device, m_LocalImage, nullptr);
|
||||||
|
m_LocalImage = VK_NULL_HANDLE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vkBindImageMemory(m_Device, m_LocalImage, m_LocalMemory, 0) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to bind local memory");
|
||||||
|
DestroyLocalResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create image view
|
||||||
|
VkImageViewCreateInfo viewInfo{};
|
||||||
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
|
viewInfo.image = m_LocalImage;
|
||||||
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
viewInfo.format = m_Format;
|
||||||
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||||
|
viewInfo.subresourceRange.levelCount = 1;
|
||||||
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
||||||
|
viewInfo.subresourceRange.layerCount = 1;
|
||||||
|
|
||||||
|
if (vkCreateImageView(m_Device, &viewInfo, nullptr, &m_LocalImageView) != VK_SUCCESS)
|
||||||
|
{
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to create image view");
|
||||||
|
DestroyLocalResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::Log("VulkanTextureBackend: Local image created");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::Update()
|
||||||
|
{
|
||||||
|
if (m_Created)
|
||||||
|
{
|
||||||
|
CopyImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::CopyImage()
|
||||||
|
{
|
||||||
|
// Begin command buffer
|
||||||
|
VkCommandBufferBeginInfo beginInfo{};
|
||||||
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
|
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||||
|
|
||||||
|
vkResetCommandBuffer(m_CommandBuffer, 0);
|
||||||
|
vkBeginCommandBuffer(m_CommandBuffer, &beginInfo);
|
||||||
|
|
||||||
|
// Transition imported image to transfer src
|
||||||
|
TransitionImageLayout(m_ImportedImage,
|
||||||
|
VK_IMAGE_LAYOUT_UNDEFINED,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||||
|
|
||||||
|
// Transition local image to transfer dst
|
||||||
|
TransitionImageLayout(m_LocalImage,
|
||||||
|
VK_IMAGE_LAYOUT_UNDEFINED,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||||
|
|
||||||
|
// Copy image
|
||||||
|
VkImageCopy copyRegion{};
|
||||||
|
copyRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
copyRegion.srcSubresource.layerCount = 1;
|
||||||
|
copyRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
copyRegion.dstSubresource.layerCount = 1;
|
||||||
|
copyRegion.extent = {static_cast<uint32_t>(m_Width), static_cast<uint32_t>(m_Height), 1};
|
||||||
|
|
||||||
|
vkCmdCopyImage(
|
||||||
|
m_CommandBuffer,
|
||||||
|
m_ImportedImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
|
m_LocalImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
1, ©Region
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transition local image to shader read
|
||||||
|
TransitionImageLayout(m_LocalImage,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
|
|
||||||
|
vkEndCommandBuffer(m_CommandBuffer);
|
||||||
|
|
||||||
|
// Submit and wait (CPU sync for simplicity)
|
||||||
|
VkSubmitInfo submitInfo{};
|
||||||
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &m_CommandBuffer;
|
||||||
|
|
||||||
|
vkQueueSubmit(m_Queue, 1, &submitInfo, VK_NULL_HANDLE);
|
||||||
|
vkQueueWaitIdle(m_Queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::TransitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout)
|
||||||
|
{
|
||||||
|
VkImageMemoryBarrier barrier{};
|
||||||
|
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||||
|
barrier.oldLayout = oldLayout;
|
||||||
|
barrier.newLayout = newLayout;
|
||||||
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.image = image;
|
||||||
|
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
barrier.subresourceRange.baseMipLevel = 0;
|
||||||
|
barrier.subresourceRange.levelCount = 1;
|
||||||
|
barrier.subresourceRange.baseArrayLayer = 0;
|
||||||
|
barrier.subresourceRange.layerCount = 1;
|
||||||
|
|
||||||
|
VkPipelineStageFlags srcStage;
|
||||||
|
VkPipelineStageFlags dstStage;
|
||||||
|
|
||||||
|
if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED)
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = 0;
|
||||||
|
srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
}
|
||||||
|
else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||||
|
srcStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = 0;
|
||||||
|
srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
||||||
|
{
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||||
|
dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
}
|
||||||
|
else if (newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
||||||
|
{
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||||
|
dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
}
|
||||||
|
else if (newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||||
|
{
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
barrier.dstAccessMask = 0;
|
||||||
|
dstStage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
vkCmdPipelineBarrier(
|
||||||
|
m_CommandBuffer,
|
||||||
|
srcStage, dstStage,
|
||||||
|
0,
|
||||||
|
0, nullptr,
|
||||||
|
0, nullptr,
|
||||||
|
1, &barrier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t VulkanTextureBackend::FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties)
|
||||||
|
{
|
||||||
|
VkPhysicalDeviceMemoryProperties memProps;
|
||||||
|
vkGetPhysicalDeviceMemoryProperties(m_PhysicalDevice, &memProps);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < memProps.memoryTypeCount; i++)
|
||||||
|
{
|
||||||
|
if ((typeFilter & (1 << i)) &&
|
||||||
|
(memProps.memoryTypes[i].propertyFlags & properties) == properties)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::Log("VulkanTextureBackend: Failed to find suitable memory type");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::Destroy()
|
||||||
|
{
|
||||||
|
if (m_Device && m_Created)
|
||||||
|
{
|
||||||
|
vkDeviceWaitIdle(m_Device);
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyLocalResources();
|
||||||
|
DestroyImportedResources();
|
||||||
|
|
||||||
|
m_Width = 0;
|
||||||
|
m_Height = 0;
|
||||||
|
m_Created = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::DestroyImportedResources()
|
||||||
|
{
|
||||||
|
if (m_Device)
|
||||||
|
{
|
||||||
|
if (m_ImportedMemory != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkFreeMemory(m_Device, m_ImportedMemory, nullptr);
|
||||||
|
m_ImportedMemory = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (m_ImportedImage != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkDestroyImage(m_Device, m_ImportedImage, nullptr);
|
||||||
|
m_ImportedImage = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VulkanTextureBackend::DestroyLocalResources()
|
||||||
|
{
|
||||||
|
if (m_Device)
|
||||||
|
{
|
||||||
|
if (m_LocalImageView != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkDestroyImageView(m_Device, m_LocalImageView, nullptr);
|
||||||
|
m_LocalImageView = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (m_LocalMemory != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkFreeMemory(m_Device, m_LocalMemory, nullptr);
|
||||||
|
m_LocalMemory = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (m_LocalImage != VK_NULL_HANDLE)
|
||||||
|
{
|
||||||
|
vkDestroyImage(m_Device, m_LocalImage, nullptr);
|
||||||
|
m_LocalImage = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void* VulkanTextureBackend::GetNativeTexturePtr()
|
||||||
|
{
|
||||||
|
// For Vulkan, Unity expects the VkImage
|
||||||
|
return reinterpret_cast<void*>(m_LocalImage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "texture_backend.h"
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include <vulkan/vulkan_android.h>
|
||||||
|
#include <IUnityGraphicsVulkan.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vulkan implementation of ITextureBackend.
|
||||||
|
* Imports AHardwareBuffer via VK_ANDROID_external_memory_android_hardware_buffer
|
||||||
|
* and copies to a local VkImage for safe rendering.
|
||||||
|
*/
|
||||||
|
class VulkanTextureBackend : public ITextureBackend
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VulkanTextureBackend() = default;
|
||||||
|
~VulkanTextureBackend() override { Destroy(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Vulkan backend with Unity's Vulkan interface.
|
||||||
|
* Must be called during UnityPluginLoad.
|
||||||
|
* @param unityInterfaces Unity's interface registry
|
||||||
|
* @return true if Vulkan is available and initialization succeeded
|
||||||
|
*/
|
||||||
|
bool Initialize(IUnityInterfaces* unityInterfaces);
|
||||||
|
|
||||||
|
bool Create(AHardwareBuffer* buffer) override;
|
||||||
|
void Update() override;
|
||||||
|
void Destroy() override;
|
||||||
|
void* GetNativeTexturePtr() override;
|
||||||
|
int GetWidth() const override { return m_Width; }
|
||||||
|
int GetHeight() const override { return m_Height; }
|
||||||
|
bool IsVulkan() const override { return true; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the VkImageView for Unity texture binding.
|
||||||
|
*/
|
||||||
|
VkImageView GetLocalImageView() const { return m_LocalImageView; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ImportHardwareBuffer(AHardwareBuffer* buffer);
|
||||||
|
bool CreateLocalImage();
|
||||||
|
void DestroyImportedResources();
|
||||||
|
void DestroyLocalResources();
|
||||||
|
uint32_t FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties);
|
||||||
|
void TransitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout);
|
||||||
|
void CopyImage();
|
||||||
|
|
||||||
|
// Unity Vulkan interface
|
||||||
|
IUnityGraphicsVulkan* m_UnityVulkan = nullptr;
|
||||||
|
|
||||||
|
// Vulkan device objects (from Unity)
|
||||||
|
VkInstance m_Instance = VK_NULL_HANDLE;
|
||||||
|
VkPhysicalDevice m_PhysicalDevice = VK_NULL_HANDLE;
|
||||||
|
VkDevice m_Device = VK_NULL_HANDLE;
|
||||||
|
VkQueue m_Queue = VK_NULL_HANDLE;
|
||||||
|
uint32_t m_QueueFamilyIndex = 0;
|
||||||
|
|
||||||
|
// Command pool for copy operations
|
||||||
|
VkCommandPool m_CommandPool = VK_NULL_HANDLE;
|
||||||
|
VkCommandBuffer m_CommandBuffer = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Imported hardware buffer resources
|
||||||
|
VkImage m_ImportedImage = VK_NULL_HANDLE;
|
||||||
|
VkDeviceMemory m_ImportedMemory = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Local copy resources (for safe rendering)
|
||||||
|
VkImage m_LocalImage = VK_NULL_HANDLE;
|
||||||
|
VkDeviceMemory m_LocalMemory = VK_NULL_HANDLE;
|
||||||
|
VkImageView m_LocalImageView = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
// Texture format info
|
||||||
|
VkFormat m_Format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
int m_Width = 0;
|
||||||
|
int m_Height = 0;
|
||||||
|
|
||||||
|
bool m_Initialized = false;
|
||||||
|
bool m_Created = false;
|
||||||
|
|
||||||
|
// Extension function pointers
|
||||||
|
PFN_vkGetAndroidHardwareBufferPropertiesANDROID vkGetAndroidHardwareBufferPropertiesANDROID = nullptr;
|
||||||
|
};
|
||||||
@@ -1 +1,221 @@
|
|||||||
Use this file to describe your package's features.
|
# MosisSDK for Unity
|
||||||
|
|
||||||
|
Unity package providing integration with MosisService for virtual smartphone display in VR applications.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package connects to the MosisService Android app via AIDL Binder IPC to:
|
||||||
|
- Receive rendered phone screen frames via AHardwareBuffer
|
||||||
|
- Forward VR controller touch input to the virtual phone
|
||||||
|
- Support both OpenGL ES and Vulkan graphics backends
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Unity 6000.3.2f1 or later
|
||||||
|
- Android target platform
|
||||||
|
- MosisService app installed on device
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Open Window > Package Manager
|
||||||
|
2. Click + > Add package from disk
|
||||||
|
3. Navigate to `Packages/com.omarator.mosissdk/package.json`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Packages/com.omarator.mosissdk/
|
||||||
|
├── package.json
|
||||||
|
├── README.md
|
||||||
|
├── Runtime/
|
||||||
|
│ ├── KotlinBridge.cs # C# to native bridge
|
||||||
|
│ └── com.omarator.mosissdk.asmdef
|
||||||
|
├── Plugins/
|
||||||
|
│ └── Android/
|
||||||
|
│ ├── MosisSDK.aar # Kotlin service connection
|
||||||
|
│ ├── libs/
|
||||||
|
│ │ └── arm64-v8a/
|
||||||
|
│ │ └── libmy_native_lib.so
|
||||||
|
│ └── cpp/ # Native source
|
||||||
|
│ ├── CMakeLists.txt
|
||||||
|
│ ├── my_native_code.cpp
|
||||||
|
│ ├── texture_backend.h
|
||||||
|
│ ├── opengl_backend.h/cpp
|
||||||
|
│ └── vulkan_backend.h/cpp
|
||||||
|
└── Assets/
|
||||||
|
└── PhoneInteraction.cs # VR controller input
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Unity Build
|
||||||
|
|
||||||
|
1. File > Build Settings
|
||||||
|
2. Switch Platform to Android
|
||||||
|
3. Player Settings > Other Settings:
|
||||||
|
- Graphics APIs: Vulkan, OpenGLES3 (in order of preference)
|
||||||
|
- Minimum API Level: 29
|
||||||
|
4. Build or Build and Run
|
||||||
|
|
||||||
|
### Native Plugin Build
|
||||||
|
|
||||||
|
To rebuild the native library manually:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
cd Packages/com.omarator.mosissdk/Plugins/Android/cpp
|
||||||
|
|
||||||
|
:: Configure with Android NDK
|
||||||
|
cmake -B build ^
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=%ANDROID_NDK_HOME%/build/cmake/android.toolchain.cmake ^
|
||||||
|
-DANDROID_ABI=arm64-v8a ^
|
||||||
|
-DANDROID_PLATFORM=android-29
|
||||||
|
|
||||||
|
:: Build
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
:: Copy output
|
||||||
|
copy build\libmy_native_lib.so ..\libs\arm64-v8a\
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
1. Add `KotlinBridge` component to a GameObject in your scene
|
||||||
|
2. Add `PhoneInteraction` component for VR input handling
|
||||||
|
3. Assign VR controller and phone plane references
|
||||||
|
|
||||||
|
### KotlinBridge
|
||||||
|
|
||||||
|
The `KotlinBridge` component manages the connection to MosisService:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class KotlinBridge : MonoBehaviour
|
||||||
|
{
|
||||||
|
public Material phoneMaterial; // Material to apply phone texture
|
||||||
|
|
||||||
|
// Called automatically when texture is ready
|
||||||
|
// Texture is applied to phoneMaterial's _MainTex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PhoneInteraction
|
||||||
|
|
||||||
|
Handles VR controller raycasting and touch input:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PhoneInteraction : MonoBehaviour
|
||||||
|
{
|
||||||
|
public GameObject Controller; // VR controller
|
||||||
|
public GameObject Plane; // Phone display plane
|
||||||
|
public XRNode inputDevice; // Controller hand
|
||||||
|
public float triggerThreshold; // Trigger press threshold
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Touch API
|
||||||
|
|
||||||
|
Send touch events from custom scripts:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Touch down at normalized UV (0-1)
|
||||||
|
KotlinBridge.SendTouchDown(0.5f, 0.5f);
|
||||||
|
|
||||||
|
// Touch move
|
||||||
|
KotlinBridge.SendTouchMove(0.6f, 0.5f);
|
||||||
|
|
||||||
|
// Touch up
|
||||||
|
KotlinBridge.SendTouchUp(0.6f, 0.5f);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graphics Backend
|
||||||
|
|
||||||
|
The native plugin automatically selects the appropriate backend:
|
||||||
|
|
||||||
|
| Graphics API | Backend | Status |
|
||||||
|
|--------------|---------|--------|
|
||||||
|
| Vulkan | VulkanTextureBackend | Preferred |
|
||||||
|
| OpenGL ES 3.0 | OpenGLTextureBackend | Fallback |
|
||||||
|
| OpenGL ES 2.0 | OpenGLTextureBackend | Fallback |
|
||||||
|
|
||||||
|
Backend selection happens in `UnityPluginLoad()` based on the active renderer.
|
||||||
|
|
||||||
|
### Vulkan Import
|
||||||
|
|
||||||
|
When using Vulkan, the plugin imports AHardwareBuffer directly as a VkImage:
|
||||||
|
|
||||||
|
1. Query buffer properties via `vkGetAndroidHardwareBufferPropertiesANDROID`
|
||||||
|
2. Create VkImage with external memory handle type
|
||||||
|
3. Import memory with `VkImportAndroidHardwareBufferInfoANDROID`
|
||||||
|
4. Copy to local image each frame for safe rendering
|
||||||
|
|
||||||
|
### OpenGL Import
|
||||||
|
|
||||||
|
When using OpenGL ES, the plugin uses EGL image extension:
|
||||||
|
|
||||||
|
1. Create EGLImage from AHardwareBuffer via `eglCreateImageKHR`
|
||||||
|
2. Bind to OpenGL texture via `glEGLImageTargetTexture2DOES`
|
||||||
|
3. Blit to local texture each frame
|
||||||
|
|
||||||
|
## Device Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MosisService first
|
||||||
|
adb install -r MosisService-debug.apk
|
||||||
|
|
||||||
|
# Install Unity app
|
||||||
|
adb install -r MosisVR.apk
|
||||||
|
|
||||||
|
# Launch service
|
||||||
|
adb shell am start -n com.omixlab.mosis/.MainActivity
|
||||||
|
|
||||||
|
# Launch Unity app
|
||||||
|
adb shell am start -n com.omixlab.mosisvr/com.unity3d.player.UnityPlayerActivity
|
||||||
|
|
||||||
|
# Monitor logs
|
||||||
|
adb logcat -s Unity MosisSDK Vulkan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Black Screen / No Texture
|
||||||
|
|
||||||
|
- Verify MosisService is running (`adb shell ps | grep mosis`)
|
||||||
|
- Check logcat for connection errors
|
||||||
|
- Ensure both apps have same user ID or are debuggable
|
||||||
|
|
||||||
|
### Vulkan Errors
|
||||||
|
|
||||||
|
- Device must support `VK_ANDROID_external_memory_android_hardware_buffer`
|
||||||
|
- Check `adb logcat -s Vulkan` for extension availability
|
||||||
|
- Falls back to OpenGL if Vulkan init fails
|
||||||
|
|
||||||
|
### Touch Not Working
|
||||||
|
|
||||||
|
- Verify `PhoneInteraction` references are set
|
||||||
|
- Check XR input device selection matches controller
|
||||||
|
- Test with mouse click fallback in editor
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Files Modified in Vulkan Implementation
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `my_native_code.cpp` | Backend abstraction, `UnityPluginLoad` detection |
|
||||||
|
| `CMakeLists.txt` | Added Vulkan library, new source files |
|
||||||
|
| `KotlinBridge.cs` | Added `SendTouchDown`/`SendTouchUp`, dynamic resolution |
|
||||||
|
| `PhoneInteraction.cs` | Added VR trigger-based touch handling |
|
||||||
|
|
||||||
|
### New Files Added
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `texture_backend.h` | ITextureBackend abstract interface |
|
||||||
|
| `opengl_backend.h/cpp` | OpenGL ES implementation |
|
||||||
|
| `vulkan_backend.h/cpp` | Vulkan implementation |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **1.0.0** - Initial release with OpenGL support
|
||||||
|
- **1.1.0** - Added Vulkan backend, touch down/up events, dynamic resolution
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||||||
public class KotlinBridge : MonoBehaviour
|
public class KotlinBridge : MonoBehaviour
|
||||||
{
|
{
|
||||||
static KotlinBridge Instance;
|
static KotlinBridge Instance;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
struct NativeCallbacks
|
struct NativeCallbacks
|
||||||
{
|
{
|
||||||
@@ -15,24 +16,40 @@ public class KotlinBridge : MonoBehaviour
|
|||||||
public IntPtr OnBufferReady;
|
public IntPtr OnBufferReady;
|
||||||
public IntPtr OnTextureReady;
|
public IntPtr OnTextureReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate void OnMessageDelegate(string message);
|
delegate void OnMessageDelegate(string message);
|
||||||
delegate void OnServiceInitializedDelegate(bool success);
|
delegate void OnServiceInitializedDelegate(bool success);
|
||||||
delegate void OnFrameAvailableDelegate();
|
delegate void OnFrameAvailableDelegate();
|
||||||
delegate void OnBufferReadyDelegate();
|
delegate void OnBufferReadyDelegate();
|
||||||
delegate void OnTextureReadyDelegate(uint gl_texture);
|
delegate void OnTextureReadyDelegate(IntPtr nativeTexturePtr, int width, int height, bool isVulkan);
|
||||||
|
|
||||||
[DllImport("my_native_lib")]
|
[DllImport("my_native_lib")]
|
||||||
static extern void SetNativeCallbacks(ref NativeCallbacks callbacks);
|
static extern void SetNativeCallbacks(ref NativeCallbacks callbacks);
|
||||||
|
|
||||||
|
[DllImport("my_native_lib")]
|
||||||
|
public static extern void SendTouchDown(float u, float v);
|
||||||
|
|
||||||
[DllImport("my_native_lib")]
|
[DllImport("my_native_lib")]
|
||||||
public static extern void SendTouchMove(float u, float v);
|
public static extern void SendTouchMove(float u, float v);
|
||||||
|
|
||||||
[DllImport("my_native_lib")]
|
[DllImport("my_native_lib")]
|
||||||
static extern IntPtr InitGLAD();
|
public static extern void SendTouchUp(float u, float v);
|
||||||
|
|
||||||
|
[DllImport("my_native_lib")]
|
||||||
|
static extern IntPtr InitBackend();
|
||||||
|
|
||||||
[DllImport("my_native_lib")]
|
[DllImport("my_native_lib")]
|
||||||
static extern IntPtr UpdateTexture();
|
static extern IntPtr UpdateTexture();
|
||||||
|
|
||||||
|
// Legacy compatibility
|
||||||
|
[DllImport("my_native_lib")]
|
||||||
|
static extern IntPtr InitGLAD();
|
||||||
|
|
||||||
[AOT.MonoPInvokeCallback(typeof(OnMessageDelegate))]
|
[AOT.MonoPInvokeCallback(typeof(OnMessageDelegate))]
|
||||||
static void OnMessage(string message)
|
static void OnMessage(string message)
|
||||||
{
|
{
|
||||||
//Debug.Log("High-speed callback: " + message);
|
// High-speed callback, disabled for performance
|
||||||
|
// Debug.Log("Native callback: " + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AOT.MonoPInvokeCallback(typeof(OnServiceInitializedDelegate))]
|
[AOT.MonoPInvokeCallback(typeof(OnServiceInitializedDelegate))]
|
||||||
@@ -41,7 +58,7 @@ public class KotlinBridge : MonoBehaviour
|
|||||||
Debug.Log("Service Initialized: " + success);
|
Debug.Log("Service Initialized: " + success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AOT.MonoPInvokeCallback(typeof(OnServiceInitializedDelegate))]
|
[AOT.MonoPInvokeCallback(typeof(OnFrameAvailableDelegate))]
|
||||||
static void OnFrameAvailable()
|
static void OnFrameAvailable()
|
||||||
{
|
{
|
||||||
UnityMainThreadDispatcher.Enqueue(() => {
|
UnityMainThreadDispatcher.Enqueue(() => {
|
||||||
@@ -54,24 +71,59 @@ public class KotlinBridge : MonoBehaviour
|
|||||||
{
|
{
|
||||||
Debug.Log("Buffer Ready");
|
Debug.Log("Buffer Ready");
|
||||||
UnityMainThreadDispatcher.Enqueue(() => {
|
UnityMainThreadDispatcher.Enqueue(() => {
|
||||||
GL.IssuePluginEvent(InitGLAD(), 1);
|
GL.IssuePluginEvent(InitBackend(), 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[AOT.MonoPInvokeCallback(typeof(OnTextureReadyDelegate))]
|
[AOT.MonoPInvokeCallback(typeof(OnTextureReadyDelegate))]
|
||||||
static void OnTextureReady(uint gl_texture)
|
static void OnTextureReady(IntPtr nativeTexturePtr, int width, int height, bool isVulkan)
|
||||||
{
|
{
|
||||||
Debug.Log("Texture Ready: " + gl_texture);
|
Debug.Log($"Texture Ready: ptr={nativeTexturePtr}, size={width}x{height}, vulkan={isVulkan}");
|
||||||
|
|
||||||
UnityMainThreadDispatcher.Enqueue(() => {
|
UnityMainThreadDispatcher.Enqueue(() => {
|
||||||
var renderer = Instance.GetComponent<Renderer>();
|
var renderer = Instance.GetComponent<Renderer>();
|
||||||
renderer.materials[2].mainTexture = Texture2D.CreateExternalTexture(1024, 1024,
|
if (renderer == null)
|
||||||
TextureFormat.RGBA32, false, false, (IntPtr)gl_texture);
|
{
|
||||||
|
Debug.LogError("KotlinBridge: No Renderer component found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create external texture with actual dimensions from the service
|
||||||
|
// Unity handles both OpenGL and Vulkan native textures via CreateExternalTexture
|
||||||
|
Texture2D texture = Texture2D.CreateExternalTexture(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
TextureFormat.RGBA32,
|
||||||
|
false, // mipChain
|
||||||
|
false, // linear
|
||||||
|
nativeTexturePtr
|
||||||
|
);
|
||||||
|
|
||||||
|
if (texture == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("KotlinBridge: Failed to create external texture");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set texture on material (index 2 is the phone screen material)
|
||||||
|
if (renderer.materials.Length > 2)
|
||||||
|
{
|
||||||
|
renderer.materials[2].mainTexture = texture;
|
||||||
|
Debug.Log($"KotlinBridge: Texture set on material[2], {width}x{height}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"KotlinBridge: Not enough materials (have {renderer.materials.Length}, need 3)");
|
||||||
|
// Fall back to first material if not enough
|
||||||
|
renderer.material.mainTexture = texture;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
|
|
||||||
NativeCallbacks callbacks = new NativeCallbacks();
|
NativeCallbacks callbacks = new NativeCallbacks();
|
||||||
callbacks.OnMessage = Marshal.GetFunctionPointerForDelegate(new OnMessageDelegate(OnMessage));
|
callbacks.OnMessage = Marshal.GetFunctionPointerForDelegate(new OnMessageDelegate(OnMessage));
|
||||||
callbacks.OnServiceInitialized = Marshal.GetFunctionPointerForDelegate(new OnServiceInitializedDelegate(OnServiceInitialized));
|
callbacks.OnServiceInitialized = Marshal.GetFunctionPointerForDelegate(new OnServiceInitializedDelegate(OnServiceInitialized));
|
||||||
|
|||||||
Reference in New Issue
Block a user