From 6ba381f5d4f1a6f32ec877b83279560eed71756d Mon Sep 17 00:00:00 2001 From: hecomi Date: Tue, 1 Nov 2016 02:15:25 +0900 Subject: [PATCH] fetch cursor texture. --- .../Examples/Scenes/Toggle Monitors.unity | Bin 19768 -> 21448 bytes .../Plugins/uDesktopDuplication.dll | Bin 20992 -> 135168 bytes Assets/uDesktopDuplication/Scripts/Cursor.cs | 94 +++- Assets/uDesktopDuplication/Scripts/Lib.cs | 26 ++ Assets/uDesktopDuplication/Scripts/Manager.cs | 7 + Assets/uDesktopDuplication/Scripts/Monitor.cs | 20 + .../Shaders/uDD_Common.cginc | 11 +- .../Shaders/uDD_Cursor.shader | 79 ++++ .../Shaders/uDD_Cursor.shader.meta | 9 + Plugins/uDesktopDuplication/' | 405 ++++++++++++++++++ .../uDesktopDuplication/main.cpp | 324 +++++++++++--- 11 files changed, 905 insertions(+), 70 deletions(-) create mode 100644 Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader create mode 100644 Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader.meta create mode 100644 Plugins/uDesktopDuplication/' diff --git a/Assets/uDesktopDuplication/Examples/Scenes/Toggle Monitors.unity b/Assets/uDesktopDuplication/Examples/Scenes/Toggle Monitors.unity index 8ba08577b9f3ab3dc6e4ea87f9e55bc3e179d2c3..9b4363c2962328baa8d8710a81caef62dc6f116c 100644 GIT binary patch delta 2057 zcmZuwdrXs86hB{E3IZZ15hxVeU_jnQK^X{r3AK!8L?#If>9CV-9c2Xs9A!R8$DSh7r^JLmiEXPEOQ=ljn6{m!}P zanHRnDonpCWB}N10PLRzfK8~+;U9|gi}DMfFNC#FC?sq_;9(#msK{Ey?bxo$xwp^V zxp%DQ(kG#WTZ?(ixub=t{e4Tj7H(%vB@AfLx+G9%Q{-Gwf^R$S1e8P+u#+wT;RcQ; zNjMwYnDvya^A)GJ=O9S`7BP5y@-0hC__u4WnXvjh0|CEw^scAmQouz3-%i9-C}OmAcrnK*XNb^Y7TKFThn(*H?GCpSjzTYl zf^O=7g%;IhWsZ{c;WQPCiJ$GGwpF7%DYJCMpOjfH;#8rW84W@W9(({XY92LG zs!+n86~IS0jwA?XvXO8HfJ1bMB?ps4ynyNv6iO%#g_hhZIibxWCgId7IU$;E3PkO& zdfaZ;A*oqaVzU}Ldb?fiwGQl{%i$K=#&ztVy}QZlI(*paMcwMb={*Yn>u}JleNC%K z0rpcxw`J+Uq!vTLAO<|a;|dT85w^^^SZw=yxOa;paVwm=qF`f2P2YAUz$DU?w(S1r zGu?Y|3-^)Ri+_BnAuj>A7C2Xe7lS<)AC_12MtjcnbB><4mtXxN=Vp-`M85=4LVaSs zOPuTo1K5XzNfcfS=f;tv*Ps`L*TT7ZBKKojTY)GXDc zQFMi~Qj?~cM&|KT>SpZCbV?U;G!^Slsps50a+LK$v0UsKt_IE-Hn0y&n$im!0NzJ< zz5D%tDq2O;NB|n3Mp2~X7dhvPWNVnfPHpY?x|r(RGv zTywo2iGO;8vQBBrI+@CP1oN9U8F}aHE;s#lZSRNujh7Z&2U?|~u2z;dh1c0^!^y2L zetD+c$xfR`<5CjKcOJWYvU(_<3c&&__vx zqYPhdIpZIY&LoT$;gkoh6@IOHX$#AIZ{mA-Txf=9$+IQTs62=Ap3kffWI!Hpa;Ctw zo=#Vf$IGIsDu=Bq=CkES6TDSBBUDxlLJ zEq;wS$nZ<%BUH@1CQ@0}Tz-Un(jMo@lF}FkrE#@5hHP?IRK?fj6HqtGI#unLX07U_<}axt2(*XyDgz}&Z;HBHRd&IP1a0a#}( z_xH`;v!DGox@WR^|CT48$WL8vf{H9^%?wjN`GG`bF5&>}B} zX2}#D@lf?qB0mKHdgOdtaFA7&GV{4`+sv}^+jwGZ+%!sw2~#hW$BlxFwQsF;t-P|n z*3miLzWIiS=p4QI{e|Y!zvpkh^UED^`|M^|Ucq2O*EP9u>tmG`EIAhz$0`%i=x2?p ztjcGC7hpU*)|tAoX2OcJ!kOGoV0HTa_ zW!&#(T3^k0objdxqPTE&;3hf%l1!9|@Xb(!e8m0{%JR=w(eV#(Ara5@WzsbTsW78 zGr@$=nocITFBb05gG7CfV1W@6YIh6-ora^YuxCdd4z3!kTfP+zikV82YraA!vd50=WE?)KkD9}JPKUoy9TA81ym3Mw>o@X6qktG?U@Dp-h;6#8 zmKN+WDJZ|_FYg{1ImtJUW>A4!7~Ib{JnGS!z55fVh^oL&Vpc^`vcGH@d1c=~Nc++& zUm6HK*XASQa&lP(e%a#B7s^jzq9|konE9dPCg5+^hR^r#JOSA$%-L?#m)YXHEt|r6%9hRAva2g(AsG@R-%f_tevB8P2qpDe!HFinOLhpKyqOHi z#|J-v0(J1Ah;~XS!rD-6rD)y>ev#AGifi5B2-`%9vYZEEAPy U4*F}ChI)le?QN~CI`E_RFN4OZlK=n! diff --git a/Assets/uDesktopDuplication/Plugins/uDesktopDuplication.dll b/Assets/uDesktopDuplication/Plugins/uDesktopDuplication.dll index f3b16755ab84028382eec9dbe4adfd0fbcefc334..21a564d039b3a4db8976a32eb22f4cdae97f93fd 100644 GIT binary patch literal 135168 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 TxmR&lTqtv8EReB4Zwven;Kp%F literal 20992 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 + uddTexture_ = GetComponent(); + } + + void Start() + { + var clone = new GameObject(name + " Cursor"); + clone.transform.SetParent(transform); + clone.transform.localPosition = Vector3.zero; + clone.transform.localRotation = Quaternion.identity; + clone.transform.localScale = Vector3.one; + + var filter = clone.AddComponent(); + filter.mesh = GetComponent().sharedMesh; + + var renderer = clone.AddComponent(); + var shader = Shader.Find("uDesktopDuplication/Cursor"); + cursorMaterial_ = new Material(shader); + renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + renderer.receiveShadows = false; + renderer.motionVectors = false; + renderer.material = cursorMaterial_; } void Update() { - var monitor = texture_.monitor; if (monitor.isPointerVisible) { - var x = (1f * monitor.pointerX / monitor.width - 0.5f) * modelScale.x; - var y = (0.5f - 1f * monitor.pointerY / monitor.height) * modelScale.y; - var iy = texture_.invertY ? +1 : -1; - var localPos = transform.right * x + iy * transform.up * y; - var worldPos = transform.TransformPoint(localPos); - worldPos += cursor.right * offset.x * cursor.localScale.x; - worldPos += -cursor.up * offset.y * cursor.localScale.y; - worldPos += -cursor.forward * 0.001f; - cursor.position = worldPos; + UpdatePosition(); + UpdateTexture(); + UpdateMaterial(); } } + + void UpdatePosition() + { + var x = (1f * monitor.pointerX / monitor.width - 0.5f) * modelScale.x; + var y = (1f * monitor.pointerY / monitor.height - 0.5f) * modelScale.y; + var iy = uddTexture_.invertY ? -1 : +1; + var localPos = transform.right * x + iy * transform.up * y; + worldPosition = transform.TransformPoint(localPos); + } + + void UpdateTexture() + { + pointerTexture_ = new Texture2D(monitor.pointerShapeWidth, monitor.pointerShapeHeight, TextureFormat.BGRA32, false); + pointerTexture_.wrapMode = TextureWrapMode.Clamp; + monitor.UpdatePointerTexture(pointerTexture_.GetNativeTexturePtr()); + cursorMaterial_.SetTexture("_MainTex", pointerTexture_); + } + + void UpdateMaterial() + { + if (uddTexture_.invertX) { + cursorMaterial_.EnableKeyword("INVERT_X"); + } else { + cursorMaterial_.DisableKeyword("INVERT_X"); + } + + if (uddTexture_.invertY) { + cursorMaterial_.EnableKeyword("INVERT_Y"); + } else { + cursorMaterial_.DisableKeyword("INVERT_Y"); + } + + if (monitor.isVertical) { + cursorMaterial_.EnableKeyword("VERTICAL"); + } else { + cursorMaterial_.DisableKeyword("VERTICAL"); + } + + var x = (float)monitor.pointerX / monitor.width; + var y = (float)monitor.pointerY / monitor.height; + var w = (float)monitor.pointerShapeWidth / monitor.width; + var h = (float)monitor.pointerShapeHeight / monitor.height; + cursorMaterial_.SetVector("_PositionScale", new Vector4(x, y, w, h)); + } } } \ No newline at end of file diff --git a/Assets/uDesktopDuplication/Scripts/Lib.cs b/Assets/uDesktopDuplication/Scripts/Lib.cs index 9a5564c..aff5155 100644 --- a/Assets/uDesktopDuplication/Scripts/Lib.cs +++ b/Assets/uDesktopDuplication/Scripts/Lib.cs @@ -5,6 +5,13 @@ using System.Runtime.InteropServices; namespace uDesktopDuplication { +public enum PointerShapeType +{ + MonoChrome = 1, + Color = 2, + MaskedColor = 4, +} + public static class Lib { [DllImport("uDesktopDuplication")] @@ -28,9 +35,21 @@ public static class Lib [DllImport("uDesktopDuplication")] public static extern int GetPointerY(int id); [DllImport("uDesktopDuplication")] + public static extern int GetPointerShapeWidth(int id); + [DllImport("uDesktopDuplication")] + public static extern int GetPointerShapeHeight(int id); + [DllImport("uDesktopDuplication")] + public static extern int GetPointerShapePitch(int id); + [DllImport("uDesktopDuplication")] + public static extern PointerShapeType GetPointerShapeType(int id); + [DllImport("uDesktopDuplication")] + public static extern void UpdatePointerTexture(int id, System.IntPtr ptr); + [DllImport("uDesktopDuplication")] public static extern int SetTexturePtr(int id, IntPtr ptr); [DllImport("uDesktopDuplication")] public static extern int GetErrorCode(); + [DllImport("uDesktopDuplication")] + public static extern void GetErrorMessage(StringBuilder buf, int len); public static string GetName(int id) { @@ -38,6 +57,13 @@ public static class Lib GetName(id, buf, buf.Capacity); return buf.ToString(); } + + public static string GetErrorMessage() + { + var buf = new StringBuilder(64); + GetErrorMessage(buf, buf.Capacity); + return buf.ToString(); + } } } \ No newline at end of file diff --git a/Assets/uDesktopDuplication/Scripts/Manager.cs b/Assets/uDesktopDuplication/Scripts/Manager.cs index 0220dde..2db896e 100644 --- a/Assets/uDesktopDuplication/Scripts/Manager.cs +++ b/Assets/uDesktopDuplication/Scripts/Manager.cs @@ -40,6 +40,9 @@ public class Manager : MonoBehaviour [SerializeField, Tooltip("Set Desktop Duplication API timeout (milliseconds).")] int timeout = 0; + [SerializeField, Tooltip("Output logs given by the plugin.")] + bool outputDebugLog = false; + private Coroutine renderCoroutine_ = null; void Awake() @@ -78,6 +81,10 @@ public class Manager : MonoBehaviour } monitor.shouldBeUpdated = false; } + if (outputDebugLog && Lib.GetErrorCode() != 0) + { + Debug.Log(Lib.GetErrorMessage()); + } } } } diff --git a/Assets/uDesktopDuplication/Scripts/Monitor.cs b/Assets/uDesktopDuplication/Scripts/Monitor.cs index ad34e3e..bb7c3ae 100644 --- a/Assets/uDesktopDuplication/Scripts/Monitor.cs +++ b/Assets/uDesktopDuplication/Scripts/Monitor.cs @@ -66,6 +66,21 @@ public class Monitor get { return Lib.GetPointerY(id); } } + public int pointerShapeWidth + { + get { return Lib.GetPointerShapeWidth(id); } + } + + public int pointerShapeHeight + { + get { return Lib.GetPointerShapeHeight(id); } + } + + public PointerShapeType pointerShapeType + { + get { return Lib.GetPointerShapeType(id); } + } + public bool shouldBeUpdated { get; @@ -91,6 +106,11 @@ public class Monitor { GL.IssuePluginEvent(Lib.GetRenderEventFunc(), id); } + + public void UpdatePointerTexture(System.IntPtr ptr) + { + Lib.UpdatePointerTexture(id, ptr); + } } } \ No newline at end of file diff --git a/Assets/uDesktopDuplication/Shaders/uDD_Common.cginc b/Assets/uDesktopDuplication/Shaders/uDD_Common.cginc index dcf294c..84b073f 100644 --- a/Assets/uDesktopDuplication/Shaders/uDD_Common.cginc +++ b/Assets/uDesktopDuplication/Shaders/uDD_Common.cginc @@ -18,13 +18,18 @@ inline void uddInvertUV(inout float2 uv) #endif } +inline void uddToLinearIfNeeded(inout float3 rgb) +{ + if (!IsGammaSpace()) { + rgb = GammaToLinearSpace(rgb); + } +} + inline fixed4 uddGetTexture(sampler2D tex, float2 uv) { uddInvertUV(uv); fixed4 c = tex2D(tex, uv); - if (!IsGammaSpace()) { - c.rgb = GammaToLinearSpace(c.rgb); - } + uddToLinearIfNeeded(c.rgb); return c; } diff --git a/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader b/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader new file mode 100644 index 0000000..7cfa6c7 --- /dev/null +++ b/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader @@ -0,0 +1,79 @@ +Shader "uDesktopDuplication/Cursor" +{ + +Properties +{ + _MainTex ("Texture", 2D) = "white" {} +} + +SubShader +{ + +Tags { "RenderType"="Opaque" } + +ZWrite Off +Offset -1, -1 + +CGINCLUDE + +#include "UnityCG.cginc" +#include "./uDD_Common.cginc" + +half4 _PositionScale; +#define _PointerX _PositionScale.x +#define _PointerY _PositionScale.y +#define _PointerWidth _PositionScale.z +#define _PointerHeight _PositionScale.w + +struct appdata +{ + float4 vertex : POSITION; + float2 uv : TEXCOORD0; +}; + +struct v2f +{ + float4 vertex : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +sampler2D _MainTex; + +v2f vert(appdata v) +{ + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = v.uv; + return o; +} + +fixed4 frag(v2f i) : SV_Target +{ + uddInvertUV(i.uv); + i.uv.x = (i.uv.x - _PointerX) / _PointerWidth; + i.uv.y = (i.uv.y - _PointerY) / _PointerHeight; + fixed4 color = tex2D(_MainTex, i.uv); + color.a *= step(0, i.uv.x) * step(0, i.uv.y) * step(i.uv.x, 1) * step(i.uv.y, 1); + clip(color.a - 0.01); + uddToLinearIfNeeded(color.rgb); + return color; +} + +ENDCG + +Pass +{ + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma multi_compile ___ INVERT_X + #pragma multi_compile ___ INVERT_Y + #pragma multi_compile ___ VERTICAL + ENDCG +} + +} + +Fallback "Unlit/Texture" + +} diff --git a/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader.meta b/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader.meta new file mode 100644 index 0000000..95a4abc --- /dev/null +++ b/Assets/uDesktopDuplication/Shaders/uDD_Cursor.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d224cadaa1bfa2d46a6224ce2a1443c7 +timeCreated: 1477847636 +licenseType: Pro +ShaderImporter: + defaultTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/uDesktopDuplication/' b/Plugins/uDesktopDuplication/' new file mode 100644 index 0000000..a4697dc --- /dev/null +++ b/Plugins/uDesktopDuplication/' @@ -0,0 +1,405 @@ +#include +#include +#include +#include + +#include "IUnityInterface.h" +#include "IUnityGraphics.h" +#include "IUnityGraphicsD3D11.h" + +#pragma comment(lib, "dxgi.lib") + + +namespace +{ + struct Monitor + { + IDXGIOutputDuplication* deskDupl = nullptr; + ID3D11Texture2D* texture = nullptr; + DXGI_OUTPUT_DESC outputDesc; + MONITORINFOEX monitorInfo; + + struct Pointer + { + bool isVisible = false; + int x = -1; + int y = -1; + BYTE* apiBuffer = nullptr; + UINT apiBufferSize = 0; + BYTE* bgra32Buffer = nullptr; + UINT bgra32BufferSize = 0; + DXGI_OUTDUPL_POINTER_SHAPE_INFO shapeInfo; + }; + Pointer pointer; + }; + + IUnityInterfaces* g_unity = nullptr; + int g_timeout = 10; + HRESULT g_errorCode = 0; + std::string g_errorMessage = ""; + std::vector g_monitors; +} + + +extern "C" +{ + void FinalizeDuplication() + { + for (auto& monitor : g_monitors) + { + monitor.deskDupl->Release(); + } + g_monitors.clear(); + } + + void InitializeDuplication() + { + FinalizeDuplication(); + + IDXGIFactory1* factory; + CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory)); + + // Check all display adapters. + IDXGIAdapter1* adapter; + for (int i = 0; (factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND); ++i) + { + // Search the main monitor from all outputs. + IDXGIOutput* output; + for (int j = 0; (adapter->EnumOutputs(j, &output) != DXGI_ERROR_NOT_FOUND); ++j) + { + Monitor monitor; + + output->GetDesc(&monitor.outputDesc); + monitor.monitorInfo.cbSize = sizeof(MONITORINFOEX); + GetMonitorInfo(monitor.outputDesc.Monitor, &monitor.monitorInfo); + + auto device = g_unity->Get()->GetDevice(); + auto output1 = reinterpret_cast(output); + output1->DuplicateOutput(device, &monitor.deskDupl); + output->Release(); + + g_monitors.push_back(monitor); + } + + adapter->Release(); + } + + factory->Release(); + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces) + { + g_unity = unityInterfaces; + InitializeDuplication(); + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginUnload() + { + g_unity = nullptr; + FinalizeDuplication(); + } + + bool IsValidMonitorId(int id) + { + return id >= 0 && id < g_monitors.size(); + } + + bool UpdateMouse(const DXGI_OUTDUPL_FRAME_INFO& frameInfo, Monitor& monitor) + { + auto& pointer = monitor.pointer; + pointer.isVisible = frameInfo.PointerPosition.Visible != 0; + pointer.x = frameInfo.PointerPosition.Position.x; + pointer.y = frameInfo.PointerPosition.Position.y; + if (frameInfo.PointerShapeBufferSize != 0) + { + if (frameInfo.PointerShapeBufferSize > pointer.apiBufferSize) + { + if (pointer.apiBuffer) delete[] pointer.apiBuffer; + pointer.apiBuffer = new BYTE[frameInfo.PointerShapeBufferSize]; + pointer.apiBufferSize = frameInfo.PointerShapeBufferSize; + } + if (pointer.apiBuffer) + { + UINT bufferSize; + monitor.deskDupl->GetFramePointerShape( + frameInfo.PointerShapeBufferSize, + reinterpret_cast(pointer.apiBuffer), + &bufferSize, + &pointer.shapeInfo); + } + + // Convert buffer give by API into BGRA32 + const auto bgraBufferSize = pointer.shapeInfo.Width * pointer.shapeInfo.Height * 4; + if (bgraBufferSize > pointer.bgra32BufferSize) + { + if (pointer.bgra32Buffer) delete[] pointer.bgra32Buffer; + pointer.bgra32Buffer = new BYTE[bgraBufferSize]; + pointer.bgra32BufferSize = bgraBufferSize; + } + if (pointer.bgra32Buffer) + { + const auto w = pointer.shapeInfo.Width; + const auto h = pointer.shapeInfo.Height; + const auto p = pointer.shapeInfo.Pitch; + + // If masked, copy the desktop image and merge it with masked image. + const bool isMasked = + pointer.shapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME || + pointer.shapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR; + if (isMasked) + { + HRESULT hr; + + D3D11_TEXTURE2D_DESC desc; + desc.Width = w; + desc.Height = h; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.MiscFlags = 0; + + ID3D11DeviceContext* context; + auto device = g_unity->Get()->GetDevice(); + device->GetImmediateContext(&context); + + ID3D11Texture2D* texture = nullptr; + hr = device->CreateTexture2D(&desc, nullptr, &texture); + if (FAILED(hr)) + { + return false; + } + + D3D11_BOX box; + box.left = pointer.x; + box.top = pointer.y; + box.right = pointer.x + w; + box.bottom = pointer.y + h; + context->CopySubresourceRegion(texture, 0, 0, 0, 0, monitor.texture, 0, &box); + + IDXGISurface* surface = nullptr; + hr = texture->QueryInterface(__uuidof(IDXGISurface), (void**)&surface); + texture->Release(); + if (FAILED(hr)) + { + return false; + } + + DXGI_MAPPED_RECT mappedSurface; + hr = surface->Map(&mappedSurface, DXGI_MAP_READ); + if (FAILED(hr)) + { + surface->Release(); + return false; + } + + // Finally, get the texture behind the mouse pointer. + const auto desktop32 = reinterpret_cast(mappedSurface.pBits); + const auto desktopPitch = mappedSurface.Pitch / sizeof(UINT); + + // Access RGBA values at the same time + auto output32 = reinterpret_cast(pointer.bgra32Buffer); + + if (pointer.shapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME) + { + for (UINT row = 0; row < h; ++row) + { + for (UINT col = 0; col < w; ++col) + { + const int i = row * w + col; + const UINT mask = 0b10000000 >> (i % 8); + const BYTE andMask = pointer.apiBuffer[col / 8 + row * p] & mask; + const BYTE xorMask = pointer.apiBuffer[col / 8 + (row + h / 2) * p] & mask; + const UINT andMask32 = andMask ? 0xFFFFFFFF : 0xFF000000; + const UINT xorMask32 = xorMask ? 0x00FFFFFF : 0x00000000; + output32[i] = (desktop32[row * desktopPitch + col] & andMask32) ^ xorMask32; + } + } + } + else // DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR + { + const auto buffer32 = reinterpret_cast(pointer.apiBuffer); + + for (UINT row = 0; row < h; ++row) + { + for (UINT col = 0; col < w; ++col) + { + const int i = row * w + col; + const int j = row * p / sizeof(UINT) + col; + + UINT MaskVal = 0xFF000000 & buffer32[j]; + + const UINT mask = 0xFF000000 & buffer[j]; + if (mask) { + output[i] = 0xFF000000 ^ buffer[j]; + } else { + output[i] = 0x00000000; + } + } + } + } + + hr = surface->Unmap(); + surface->Release(); + if (FAILED(hr)) + { + return false; + } + } + } + } + + return true; + } + + void UNITY_INTERFACE_API OnRenderEvent(int id) + { + if (!IsValidMonitorId(id)) return; + auto& monitor = g_monitors[id]; + + if (monitor.deskDupl == nullptr || monitor.texture == nullptr) return; + + IDXGIResource* resource = nullptr; + DXGI_OUTDUPL_FRAME_INFO frameInfo; + + g_errorCode = monitor.deskDupl->AcquireNextFrame(g_timeout, &frameInfo, &resource); + if (FAILED(g_errorCode)) + { + if (g_errorCode == DXGI_ERROR_ACCESS_LOST) + { + InitializeDuplication(); + g_errorMessage = "[IDXGIOutputDuplication::AcquireNextFrame()] Access lost."; + } + else + { + g_errorMessage = "[IDXGIOutputDuplication::AcquireNextFrame()] Unknown error."; + } + return; + } + + ID3D11Texture2D* texture; + resource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(&texture)); + + ID3D11DeviceContext* context; + auto device = g_unity->Get()->GetDevice(); + device->GetImmediateContext(&context); + context->CopyResource(monitor.texture, texture); + + if (!UpdateMouse(frameInfo, monitor)) + { + g_errorCode = -999; + g_errorMessage = "[UpdateMouse()] failed."; + } + + resource->Release(); + monitor.deskDupl->ReleaseFrame(); + } + + UNITY_INTERFACE_EXPORT UnityRenderingEvent UNITY_INTERFACE_API GetRenderEventFunc() + { + return OnRenderEvent; + } + + UNITY_INTERFACE_EXPORT size_t UNITY_INTERFACE_API GetMonitorCount() + { + return g_monitors.size(); + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetTimeout(int timeout) + { + g_timeout = timeout; + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API GetName(int id, char* buf, int len) + { + if (!IsValidMonitorId(id)) return; + strcpy_s(buf, len, g_monitors[id].monitorInfo.szDevice); + } + + UNITY_INTERFACE_EXPORT bool UNITY_INTERFACE_API IsPrimary(int id) + { + if (!IsValidMonitorId(id)) return false; + return g_monitors[id].monitorInfo.dwFlags == MONITORINFOF_PRIMARY; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetWidth(int id) + { + if (!IsValidMonitorId(id)) return -1; + const auto rect = g_monitors[id].monitorInfo.rcMonitor; + return rect.right - rect.left; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetHeight(int id) + { + if (!IsValidMonitorId(id)) return -1; + const auto rect = g_monitors[id].monitorInfo.rcMonitor; + return rect.bottom - rect.top; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API IsPointerVisible(int id) + { + if (!IsValidMonitorId(id)) return false; + return g_monitors[id].pointer.isVisible; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerX(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.x; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerY(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.y; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeWidth(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Width; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeHeight(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Height; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapePitch(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Pitch; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeType(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Type; + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UpdatePointerTexture(int id, ID3D11Texture2D* ptr) + { + if (!IsValidMonitorId(id)) return; + const auto& pointer = g_monitors[id].pointer; + auto device = g_unity->Get()->GetDevice(); + ID3D11DeviceContext* context; + device->GetImmediateContext(&context); + context->UpdateSubresource(ptr, 0, nullptr, pointer.bgra32Buffer, pointer.shapeInfo.Pitch, 0); + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetTexturePtr(int id, void* texture) + { + if (!IsValidMonitorId(id)) return; + g_monitors[id].texture = reinterpret_cast(texture); + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetErrorCode() + { + return static_cast(g_errorCode); + } +} \ No newline at end of file diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp b/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp index 6e7b858..511dcc0 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp @@ -14,22 +14,31 @@ namespace { struct Monitor { - IDXGIOutputDuplication* deskDupl = nullptr; - ID3D11Texture2D* texture = nullptr; + int id = -1; + IDXGIOutputDuplication* deskDupl = nullptr; + ID3D11Texture2D* texture = nullptr; DXGI_OUTPUT_DESC outputDesc; MONITORINFOEX monitorInfo; - std::string name = ""; - bool isPrimary = false; - bool isPointerVisible = false; - int pointerX = -1; - int pointerY = -1; - int width = -1; - int height = -1; + + struct Pointer + { + bool isVisible = false; + int x = -1; + int y = -1; + BYTE* apiBuffer = nullptr; + UINT apiBufferSize = 0; + BYTE* bgra32Buffer = nullptr; + UINT bgra32BufferSize = 0; + DXGI_OUTDUPL_POINTER_SHAPE_INFO shapeInfo; + }; + Pointer pointer; }; - IUnityInterfaces* g_unity = nullptr; - int g_timeout = 10; - HRESULT g_errorCode = 0; + IUnityInterfaces* g_unity = nullptr; + int g_mouseMonitor = 0; + int g_timeout = 10; + HRESULT g_errorCode = 0; + std::string g_errorMessage = ""; std::vector g_monitors; } @@ -53,6 +62,7 @@ extern "C" CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory)); // Check all display adapters. + int id = 0; IDXGIAdapter1* adapter; for (int i = 0; (factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND); ++i) { @@ -61,6 +71,7 @@ extern "C" for (int j = 0; (adapter->EnumOutputs(j, &output) != DXGI_ERROR_NOT_FOUND); ++j) { Monitor monitor; + monitor.id = id++; output->GetDesc(&monitor.outputDesc); monitor.monitorInfo.cbSize = sizeof(MONITORINFOEX); @@ -92,14 +103,192 @@ extern "C" FinalizeDuplication(); } - bool DoesMonitorExist(int id) + bool IsValidMonitorId(int id) { return id >= 0 && id < g_monitors.size(); } + bool UpdateMouse(const DXGI_OUTDUPL_FRAME_INFO& frameInfo, Monitor& monitor) + { + auto& pointer = monitor.pointer; + pointer.isVisible = frameInfo.PointerPosition.Visible != 0; + pointer.x = frameInfo.PointerPosition.Position.x; + pointer.y = frameInfo.PointerPosition.Position.y; + + // Pointer type + const auto pointerType = pointer.shapeInfo.Type; + const bool isMono = pointerType == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME; + const bool isColorMask = pointerType == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR; + + if (pointer.isVisible) + { + g_mouseMonitor = monitor.id; + } + + if (g_mouseMonitor != monitor.id) { + return true; + } + + // Increase the buffer size if needed + if (frameInfo.PointerShapeBufferSize > pointer.apiBufferSize) + { + if (pointer.apiBuffer) delete[] pointer.apiBuffer; + pointer.apiBuffer = new BYTE[frameInfo.PointerShapeBufferSize]; + pointer.apiBufferSize = frameInfo.PointerShapeBufferSize; + } + if (!pointer.apiBuffer) return true; + + // Get information about the mouse pointer if needed + if (frameInfo.PointerShapeBufferSize != 0) + { + UINT bufferSize; + monitor.deskDupl->GetFramePointerShape( + frameInfo.PointerShapeBufferSize, + reinterpret_cast(pointer.apiBuffer), + &bufferSize, + &pointer.shapeInfo); + } + + // Size + const auto w = pointer.shapeInfo.Width; + const auto h = pointer.shapeInfo.Height / (isMono ? 2 : 1); + const auto p = pointer.shapeInfo.Pitch; + + // Convert the buffer given by API into BGRA32 + const auto bgraBufferSize = w * h * 4; + if (bgraBufferSize > pointer.bgra32BufferSize) + { + if (pointer.bgra32Buffer) delete[] pointer.bgra32Buffer; + pointer.bgra32Buffer = new BYTE[bgraBufferSize]; + pointer.bgra32BufferSize = bgraBufferSize; + } + if (!pointer.bgra32Buffer) return true; + + // If masked, copy the desktop image and merge it with masked image. + if (isMono || isColorMask) + { + HRESULT hr; + + D3D11_TEXTURE2D_DESC desc; + desc.Width = w; + desc.Height = h; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.MiscFlags = 0; + + ID3D11DeviceContext* context; + auto device = g_unity->Get()->GetDevice(); + device->GetImmediateContext(&context); + + ID3D11Texture2D* texture = nullptr; + hr = device->CreateTexture2D(&desc, nullptr, &texture); + if (FAILED(hr)) + { + return false; + } + + D3D11_BOX box; + box.front = 0; + box.back = 1; + box.left = pointer.x; + box.top = pointer.y; + box.right = pointer.x + w; + box.bottom = pointer.y + h; + context->CopySubresourceRegion(texture, 0, 0, 0, 0, monitor.texture, 0, &box); + + IDXGISurface* surface = nullptr; + hr = texture->QueryInterface(__uuidof(IDXGISurface), (void**)&surface); + texture->Release(); + if (FAILED(hr)) + { + return false; + } + + DXGI_MAPPED_RECT mappedSurface; + hr = surface->Map(&mappedSurface, DXGI_MAP_READ); + if (FAILED(hr)) + { + surface->Release(); + return false; + } + + // Finally, get the texture behind the mouse pointer. + const auto desktop32 = reinterpret_cast(mappedSurface.pBits); + const UINT desktopPitch = mappedSurface.Pitch / sizeof(UINT); + + // Access RGBA values at the same time + auto output32 = reinterpret_cast(pointer.bgra32Buffer); + + if (isMono) + { + for (UINT row = 0; row < h; ++row) + { + BYTE mask = 0x80; + for (UINT col = 0; col < w; ++col) + { + const int i = row * w + col; + const BYTE andMask = pointer.apiBuffer[col / 8 + row * p] & mask; + const BYTE xorMask = pointer.apiBuffer[col / 8 + (row + h) * p] & mask; + const UINT andMask32 = andMask ? 0xFFFFFFFF : 0xFF000000; + const UINT xorMask32 = xorMask ? 0x00FFFFFF : 0x00000000; + output32[i] = (desktop32[row * desktopPitch + col] & andMask32) ^ xorMask32; + mask = (mask == 0x01) ? 0x80 : (mask >> 1); + } + } + } + else // DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR + { + const auto buffer32 = reinterpret_cast(pointer.apiBuffer); + + for (UINT row = 0; row < h; ++row) + { + for (UINT col = 0; col < w; ++col) + { + const int i = row * w + col; + const int j = row * p / sizeof(UINT) + col; + + UINT mask = 0xFF000000 & buffer32[j]; + if (mask) + { + output32[i] = (desktop32[row * desktopPitch + col] ^ buffer32[j]) | 0xFF000000; + } + else + { + output32[i] = buffer32[j] | 0xFF000000; + } + } + } + } + + hr = surface->Unmap(); + surface->Release(); + if (FAILED(hr)) + { + return false; + } + } + else // DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR + { + auto output32 = reinterpret_cast(pointer.bgra32Buffer); + const auto buffer32 = reinterpret_cast(pointer.apiBuffer); + for (UINT i = 0; i < w * h; ++i) + { + output32[i] = buffer32[i]; + } + } + + return true; + } + void UNITY_INTERFACE_API OnRenderEvent(int id) { - if (!DoesMonitorExist(id)) return; + if (!IsValidMonitorId(id)) return; auto& monitor = g_monitors[id]; if (monitor.deskDupl == nullptr || monitor.texture == nullptr) return; @@ -108,44 +297,35 @@ extern "C" DXGI_OUTDUPL_FRAME_INFO frameInfo; g_errorCode = monitor.deskDupl->AcquireNextFrame(g_timeout, &frameInfo, &resource); - switch (g_errorCode) + if (FAILED(g_errorCode)) { - case S_OK: + if (g_errorCode == DXGI_ERROR_ACCESS_LOST) { - break; - } - case DXGI_ERROR_ACCESS_LOST: - { - resource->Release(); InitializeDuplication(); - return; + g_errorMessage = "[IDXGIOutputDuplication::AcquireNextFrame()] Access lost."; } - default: + else { - return; + g_errorMessage = "[IDXGIOutputDuplication::AcquireNextFrame()] Maybe timeout."; } - } - - monitor.isPointerVisible = frameInfo.PointerPosition.Visible == TRUE; - monitor.pointerX = frameInfo.PointerPosition.Position.x; - monitor.pointerY = frameInfo.PointerPosition.Position.y; - - ID3D11Texture2D* texture; - g_errorCode = resource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(&texture)); - if (g_errorCode != S_OK) - { - resource->Release(); - monitor.deskDupl->ReleaseFrame(); return; } + ID3D11Texture2D* texture; + resource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(&texture)); + ID3D11DeviceContext* context; auto device = g_unity->Get()->GetDevice(); device->GetImmediateContext(&context); context->CopyResource(monitor.texture, texture); + if (!UpdateMouse(frameInfo, monitor)) + { + g_errorCode = -999; + g_errorMessage = "[UpdateMouse()] failed."; + } + resource->Release(); - texture->Release(); monitor.deskDupl->ReleaseFrame(); } @@ -166,56 +346,102 @@ extern "C" UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API GetName(int id, char* buf, int len) { - if (!DoesMonitorExist(id)) return; + if (!IsValidMonitorId(id)) return; strcpy_s(buf, len, g_monitors[id].monitorInfo.szDevice); } UNITY_INTERFACE_EXPORT bool UNITY_INTERFACE_API IsPrimary(int id) { - if (!DoesMonitorExist(id)) return false; + if (!IsValidMonitorId(id)) return false; return g_monitors[id].monitorInfo.dwFlags == MONITORINFOF_PRIMARY; } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetWidth(int id) { - if (!DoesMonitorExist(id)) return -1; + if (!IsValidMonitorId(id)) return -1; const auto rect = g_monitors[id].monitorInfo.rcMonitor; return rect.right - rect.left; } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetHeight(int id) { - if (!DoesMonitorExist(id)) return -1; + if (!IsValidMonitorId(id)) return -1; const auto rect = g_monitors[id].monitorInfo.rcMonitor; return rect.bottom - rect.top; } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API IsPointerVisible(int id) { - if (!DoesMonitorExist(id)) return false; - return g_monitors[id].isPointerVisible; + if (!IsValidMonitorId(id)) return false; + return g_monitors[id].pointer.isVisible; } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerX(int id) { - if (!DoesMonitorExist(id)) return -1; - return g_monitors[id].pointerX; + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.x; } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerY(int id) { - if (!DoesMonitorExist(id)) return -1; - return g_monitors[id].pointerY; + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.y; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeWidth(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Width; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeHeight(int id) + { + if (!IsValidMonitorId(id)) return -1; + const auto& info = g_monitors[id].pointer.shapeInfo; + return (info.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME) ? info.Height / 2 : info.Height; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapePitch(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Pitch; + } + + UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerShapeType(int id) + { + if (!IsValidMonitorId(id)) return -1; + return g_monitors[id].pointer.shapeInfo.Type; + } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UpdatePointerTexture(int id, ID3D11Texture2D* ptr) + { + if (!IsValidMonitorId(id)) return; + const auto& pointer = g_monitors[id].pointer; + if (!pointer.bgra32Buffer) return; + auto device = g_unity->Get()->GetDevice(); + ID3D11DeviceContext* context; + device->GetImmediateContext(&context); + context->UpdateSubresource(ptr, 0, nullptr, pointer.bgra32Buffer, pointer.shapeInfo.Width * 4, 0); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetTexturePtr(int id, void* texture) { - if (!DoesMonitorExist(id)) return; + if (!IsValidMonitorId(id)) return; g_monitors[id].texture = reinterpret_cast(texture); } UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetErrorCode() { - return static_cast(g_errorCode); + const auto code = g_errorCode; + g_errorCode = 0; + return static_cast(code); } + + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API GetErrorMessage(char* buf, int len) + { + const auto message = g_errorMessage; + g_errorMessage = ""; + strcpy_s(buf, len, message.c_str()); + } + } \ No newline at end of file