diff --git a/Assets/uDesktopDuplication/Examples/Scripts/DisplacementMapping.cs b/Assets/uDesktopDuplication/Examples/Scripts/DisplacementMapping.cs index bccdc27..8eebc34 100644 --- a/Assets/uDesktopDuplication/Examples/Scripts/DisplacementMapping.cs +++ b/Assets/uDesktopDuplication/Examples/Scripts/DisplacementMapping.cs @@ -45,6 +45,10 @@ public class DisplacementMapping : MonoBehaviour case TargetMonitor.Next: ++id; break; case TargetMonitor.Prev: --id; break; } + if (uDesktopDuplication.Manager.monitorCount == 0) + { + return; + } id = Mathf.Clamp(id, 0, uDesktopDuplication.Manager.monitorCount - 1); var monitor = uDesktopDuplication.Manager.GetMonitor(id); monitor.shouldBeUpdated = true; diff --git a/Assets/uDesktopDuplication/Plugins/x86_64/uDesktopDuplication.dll b/Assets/uDesktopDuplication/Plugins/x86_64/uDesktopDuplication.dll index c9cbd12..c7c4fee 100644 Binary files a/Assets/uDesktopDuplication/Plugins/x86_64/uDesktopDuplication.dll and b/Assets/uDesktopDuplication/Plugins/x86_64/uDesktopDuplication.dll differ diff --git a/Assets/uDesktopDuplication/Scripts/Lib.cs b/Assets/uDesktopDuplication/Scripts/Lib.cs index 8163559..eac0169 100644 --- a/Assets/uDesktopDuplication/Scripts/Lib.cs +++ b/Assets/uDesktopDuplication/Scripts/Lib.cs @@ -86,7 +86,7 @@ public static class Lib [DllImport("uDesktopDuplication")] public static extern void Finalize(); [DllImport("uDesktopDuplication")] - public static extern void Reinitialize(); + public static extern void Reinitialize(bool useThread); [DllImport("uDesktopDuplication")] public static extern void Update(); [DllImport("uDesktopDuplication")] diff --git a/Assets/uDesktopDuplication/Scripts/Manager.cs b/Assets/uDesktopDuplication/Scripts/Manager.cs index aee4e7b..624bc8a 100644 --- a/Assets/uDesktopDuplication/Scripts/Manager.cs +++ b/Assets/uDesktopDuplication/Scripts/Manager.cs @@ -57,6 +57,8 @@ public class Manager : MonoBehaviour [SerializeField] int desktopDuplicationApiTimeout = 0; [SerializeField] float retryReinitializationDuration = 1f; + [SerializeField] + bool useThread = false; private Coroutine renderCoroutine_ = null; private bool shouldReinitialize_ = false; @@ -143,7 +145,7 @@ public class Manager : MonoBehaviour public void Reinitialize() { Debug.Log("[uDD] Reinitialize"); - Lib.Reinitialize(); + Lib.Reinitialize(useThread); CreateMonitors(); if (onReinitialized != null) { onReinitialized(); diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.cpp b/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.cpp index 95fa7ee..9d92259 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.cpp +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.cpp @@ -11,16 +11,23 @@ using namespace Microsoft::WRL; Monitor::Monitor(int id) : id_(id) { + ZeroMemory(&monitorInfo_, sizeof(monitorInfo_)); } Monitor::~Monitor() { + m_stopLoop = true; + if (deskDupl_) { deskDupl_->Release(); deskDupl_ = nullptr; } + + if (m_desktopDuplicationThread.joinable()) { + m_desktopDuplicationThread.join(); + } } diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.h b/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.h index bb06519..a6dcd93 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.h +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/Monitor.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "Common.h" enum class MonitorState @@ -23,6 +24,8 @@ enum class MonitorState class Monitor { + class ThreadedDesktopDuplicator *m_threaded = nullptr; + public: using State = MonitorState; @@ -57,6 +60,19 @@ public: bool UseGetPixels() const; bool GetPixels(BYTE* output, int x, int y, int width, int height); +public: + void InitializeThreaded(const Microsoft::WRL::ComPtr &adapter + , bool isUnityDeviceAdapter + , const Microsoft::WRL::ComPtr &output); + void CopyTextureFromThread(); +private: + void DuplicateAndCopyLoop(); + void DuplicateAndMapLoop(); + std::shared_ptr m_pIsolated; + std::thread m_desktopDuplicationThread; + volatile bool m_stopLoop = false; + std::shared_ptr m_textureQueue; + private: void UpdateCursor(const DXGI_OUTDUPL_FRAME_INFO& frameInfo); void UpdateMetadata(const DXGI_OUTDUPL_FRAME_INFO& frameInfo); @@ -79,4 +95,4 @@ private: Buffer bufferForGetPixels_; UINT moveRectSize_ = 0; UINT dirtyRectSize_ = 0;; -}; \ No newline at end of file +}; diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.cpp b/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.cpp index 16439b6..fceec28 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.cpp +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.cpp @@ -17,9 +17,9 @@ using namespace Microsoft::WRL; -MonitorManager::MonitorManager() +MonitorManager::MonitorManager(LUID unityAdapterLuid) + : unityAdapterLuid_(unityAdapterLuid) { - Initialize(); } @@ -29,7 +29,7 @@ MonitorManager::~MonitorManager() } -void MonitorManager::Initialize() +void MonitorManager::Initialize(LUID unityAdapterLuid, bool useThread) { Finalize(); @@ -46,12 +46,23 @@ void MonitorManager::Initialize() ComPtr adapter; for (int i = 0; (factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND); ++i) { + DXGI_ADAPTER_DESC desc; + adapter->GetDesc(&desc); + // Search the main monitor from all outputs ComPtr output; for (int j = 0; (adapter->EnumOutputs(j, &output) != DXGI_ERROR_NOT_FOUND); ++j) { auto monitor = std::make_shared(id++); - monitor->Initialize(output.Get()); + if (useThread) { + monitor->InitializeThreaded(adapter + , desc.AdapterLuid.HighPart == unityAdapterLuid.HighPart + && desc.AdapterLuid.LowPart == unityAdapterLuid.LowPart + , output); + } + else { + monitor->Initialize(output.Get()); + } monitors_.push_back(monitor); } } @@ -70,8 +81,9 @@ void MonitorManager::RequireReinitilization() } -void MonitorManager::Reinitialize() +void MonitorManager::Reinitialize(bool useThread) { + useThread_ = useThread; Debug::Log("MonitorManager::Reinitialize()"); Initialize(); SendMessageToUnity(Message::Reinitialized); @@ -104,6 +116,9 @@ bool MonitorManager::HasMonitorCountChanged() const std::shared_ptr MonitorManager::GetMonitor(int id) const { + if(monitors_.empty()) { + const_cast(this)->Initialize(); + } if (id >= 0 && id < static_cast(monitors_.size())) { return monitors_[id]; @@ -122,7 +137,7 @@ void MonitorManager::Update() { if (isReinitializationRequired_) { - Reinitialize(); + Reinitialize(useThread_); isReinitializationRequired_ = false; } } @@ -142,6 +157,9 @@ int MonitorManager::GetTimeout() const int MonitorManager::GetMonitorCount() const { + if (monitors_.empty()) { + const_cast(this)->Initialize(); + } return static_cast(monitors_.size()); } @@ -171,4 +189,4 @@ int MonitorManager::GetTotalHeight() const const auto minTop = *std::min_element(tops.begin(), tops.end()); const auto maxBottom = *std::max_element(bottoms.begin(), bottoms.end()); return maxBottom - minTop; -} \ No newline at end of file +} diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.h b/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.h index 4c2f383..e4bc1c7 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.h +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/MonitorManager.h @@ -13,9 +13,9 @@ class Cursor; class MonitorManager { public: - explicit MonitorManager(); + explicit MonitorManager(LUID unityAdapterLuid_); ~MonitorManager(); - void Reinitialize(); + void Reinitialize(bool useThread); bool HasMonitorCountChanged() const; void RequireReinitilization(); void SetCursorMonitorId(int id) { cursorMonitorId_ = id; } @@ -24,7 +24,8 @@ public: std::shared_ptr GetCursor() const; private: - void Initialize(); + void Initialize(){ Initialize(unityAdapterLuid_, useThread_); } + void Initialize(LUID unityAdapterLuid, bool useThread); void Finalize(); // Setters from Unity @@ -38,12 +39,16 @@ public: int GetMonitorCount() const; int GetTotalWidth() const; int GetTotalHeight() const; + bool UseThread()const { return useThread_; } private: + LUID unityAdapterLuid_; + bool useThread_ = false; + int timeout_ = 10; bool enableTextureCopyFromGpuToCpu_ = false; std::vector> monitors_; std::shared_ptr cursor_ = std::make_shared(); int cursorMonitorId_ = -1; bool isReinitializationRequired_ = false; -}; \ No newline at end of file +}; diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/ThreadedDesktopDuplicator.cpp b/Plugins/uDesktopDuplication/uDesktopDuplication/ThreadedDesktopDuplicator.cpp new file mode 100644 index 0000000..a9a9082 --- /dev/null +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/ThreadedDesktopDuplicator.cpp @@ -0,0 +1,386 @@ +#include "Monitor.h" +#include "MonitorManager.h" +#include "Debug.h" +#include +#include +#pragma comment(lib, "d3d11.lib") + +using namespace Microsoft::WRL; + + +/// +/// Thraed safe self created ID3D11Device from specified adapter +/// +class IsolatedD3D11Device +{ + ComPtr m_device; + ComPtr m_copyTarget; + +public: + ComPtr GetDevice()const { return m_device; } + + bool CreateDevice(const ComPtr &adapter) + { + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT // D2D Compatible + //| D3D11_CREATE_DEVICE_SINGLETHREADED + //| D3D11_CREATE_DEVICE_VIDEO_SUPPORT // MediaFoundation + ; + + D3D_FEATURE_LEVEL FeatureLevelsRequested[6] = { + D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 }; + + UINT numLevelsRequested = sizeof(FeatureLevelsRequested) / sizeof(D3D_FEATURE_LEVEL); + D3D_FEATURE_LEVEL FeatureLevelsSupported; + + if (FAILED(D3D11CreateDevice(adapter.Get() + , adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, nullptr + , flags + , FeatureLevelsRequested, numLevelsRequested + , D3D11_SDK_VERSION + , &m_device + , &FeatureLevelsSupported + , nullptr + ))) { + return false; + } + return true; + } + + ComPtr CreateCopyTargetWithSharedFlag(const ComPtr &src) + { + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + if (m_copyTarget) { + D3D11_TEXTURE2D_DESC target_desc; + m_copyTarget->GetDesc(&target_desc); + if ( + target_desc.Format == desc.Format + && target_desc.Width == desc.Width + && target_desc.Height == desc.Height + ) { + // already created + return m_copyTarget; + } + } + + //desc.BindFlags |= D3D11_BIND_SHADER_RESOURCE; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; // for share texture with unity device + + if (FAILED(m_device->CreateTexture2D(&desc, NULL, &m_copyTarget))) + { + return nullptr; + } + return m_copyTarget; + } +}; + + +// RAII helper +class ReleaseGuard +{ + ComPtr m_dd; + + ReleaseGuard(const ReleaseGuard &)=delete; + ReleaseGuard& operator=(const ReleaseGuard &)=delete; +public: + ReleaseGuard(const ComPtr &dd) + : m_dd(dd) + {} + + ~ReleaseGuard() + { + m_dd->ReleaseFrame(); + } +}; + + +struct QueueItem +{ + ComPtr Texture; + DXGI_OUTDUPL_FRAME_INFO Info; + + QueueItem() + {} + + QueueItem(const ComPtr &texture + , const DXGI_OUTDUPL_FRAME_INFO &info + ) + : Texture(texture), Info(info) + {} +}; +class TextureQueue +{ + std::queue m_queue; + std::mutex m_mutex; + +public: + void Enqueue(const QueueItem &item) + { + std::lock_guard lk(m_mutex); + m_queue.push(item); + } + + QueueItem Dequeue() + { + std::lock_guard lk(m_mutex); + if (m_queue.empty()) { + return QueueItem(); + } + auto front = m_queue.front(); + m_queue.pop(); + return front; + } +}; + + +void Monitor::InitializeThreaded( + const ComPtr &adapter + , bool isUnityDeviceAdapter + , const ComPtr &output +) +{ + m_pIsolated = std::make_shared(); + m_textureQueue = std::make_shared(); + + if (!m_pIsolated->CreateDevice(adapter)) { + return; + } + + if (FAILED(output->GetDesc(&outputDesc_))) + { + Debug::Error("Monitor::Initialize() => IDXGIOutput::GetDesc() failed."); + return; + } + + monitorInfo_.cbSize = sizeof(MONITORINFOEX); + if (!GetMonitorInfo(outputDesc_.Monitor, &monitorInfo_)) + { + Debug::Error("Monitor::Initialize() => GetMonitorInfo() failed."); + return; + } + else + { + const auto rect = monitorInfo_.rcMonitor; + width_ = rect.right - rect.left; + height_ = rect.bottom - rect.top; + } + + if (FAILED(GetDpiForMonitor(outputDesc_.Monitor, MDT_RAW_DPI, &dpiX_, &dpiY_))) + { + Debug::Error("Monitor::Initialize() => GetDpiForMonitor() failed."); + // DPI is set as -1, so the application has to use the appropriate value. + } + + ComPtr output1; + if (FAILED(output.As(&output1))) { + return; + } + + // use self created device + auto hr = output1->DuplicateOutput(m_pIsolated->GetDevice().Get(), &deskDupl_); + switch (hr) + { + case S_OK: + { + state_ = State::Available; + const auto rot = static_cast(GetRotation()); + Debug::Log("Monitor::Initialize() => OK."); + Debug::Log(" ID : ", GetId()); + Debug::Log(" Size : (", GetWidth(), ", ", GetHeight(), ")"); + Debug::Log(" DPI : (", GetDpiX(), ", ", GetDpiY(), ")"); + Debug::Log(" Rot : ", + rot == DXGI_MODE_ROTATION_IDENTITY ? "Landscape" : + rot == DXGI_MODE_ROTATION_ROTATE90 ? "Portrait" : + rot == DXGI_MODE_ROTATION_ROTATE180 ? "Landscape (flipped)" : + rot == DXGI_MODE_ROTATION_ROTATE270 ? "Portrait (flipped)" : + "Unspecified"); + break; + } + case E_INVALIDARG: + { + state_ = State::InvalidArg; + Debug::Error("Monitor::Initialize() => Invalid arguments."); + break; + } + case E_ACCESSDENIED: + { + // For example, when the user presses Ctrl + Alt + Delete and the screen + // switches to admin screen, this error occurs. + state_ = State::AccessDenied; + Debug::Error("Monitor::Initialize() => Access denied."); + break; + } + case DXGI_ERROR_UNSUPPORTED: + { + // If the display adapter on the computer is running under the Microsoft Hybrid system, + // this error occurs. + state_ = State::Unsupported; + Debug::Error("Monitor::Initialize() => Unsupported display."); + break; + } + case DXGI_ERROR_NOT_CURRENTLY_AVAILABLE: + { + // When other application use Desktop Duplication API, this error occurs. + state_ = State::CurrentlyNotAvailable; + Debug::Error("Monitor::Initialize() => Currently not available."); + break; + } + case DXGI_ERROR_SESSION_DISCONNECTED: + { + state_ = State::SessionDisconnected; + Debug::Error("Monitor::Initialize() => Session disconnected."); + break; + } + default: + { + state_ = State::Unknown; + Debug::Error("Monitor::Render() => Unknown Error."); + break; + } + } + + // start desktop duplication thread + m_stopLoop = false; + if (isUnityDeviceAdapter) { + m_desktopDuplicationThread = std::thread(std::bind(&Monitor::DuplicateAndCopyLoop, this)); + } + else { + m_desktopDuplicationThread = std::thread(std::bind(&Monitor::DuplicateAndMapLoop, this)); + } +} + +void Monitor::CopyTextureFromThread() +{ + QueueItem item; + if (m_textureQueue) { + item=m_textureQueue->Dequeue(); + } + auto texture = item.Texture; + if (!texture) { + return; + } + auto &frameInfo = item.Info; + + // Get texture + if (unityTexture_) + { + D3D11_TEXTURE2D_DESC srcDesc, dstDesc; + texture->GetDesc(&srcDesc); + unityTexture_->GetDesc(&dstDesc); + if (srcDesc.Width != dstDesc.Width || + srcDesc.Height != dstDesc.Height) + { + Debug::Error("Monitor::Render() => Texture sizes are defferent."); + Debug::Error(" Source : (", srcDesc.Width, ", ", srcDesc.Height, ")"); + Debug::Error(" Dest : (", dstDesc.Width, ", ", dstDesc.Height, ")"); + //Debug::Log(" => Try modifying width/height using reported value from DDA."); + //width_ = srcDesc.Width; + //height_ = srcDesc.Height; + state_ = MonitorState::TextureSizeInconsistent; + //SendMessageToUnity(Message::TextureSizeChanged); + } + else + { + ComPtr context; + GetDevice()->GetImmediateContext(&context); + context->CopyResource(unityTexture_, texture.Get()); + } + } + + UpdateMetadata(frameInfo); + + if (frameInfo.PointerPosition.Visible) + { + GetMonitorManager()->SetCursorMonitorId(id_); + } + + if (GetMonitorManager()->GetCursorMonitorId() == id_) + { + UpdateCursor(frameInfo); + } + + if (UseGetPixels()) + { + CopyTextureFromGpuToCpu(texture.Get()); + } + + hasBeenUpdated_ = true; +} + +void Monitor::DuplicateAndCopyLoop() +{ + if (!deskDupl_)return; + + while (!m_stopLoop) + { + ComPtr resource; + DXGI_OUTDUPL_FRAME_INFO frameInfo; + auto hr = deskDupl_->AcquireNextFrame(INFINITE, &frameInfo, &resource); + + if (FAILED(hr)) { + switch (hr) + { + case DXGI_ERROR_ACCESS_LOST: + { + // If any monitor setting has changed (e.g. monitor size has changed), + // it is necessary to re-initialize monitors. + //Debug::Log("Monitor::Render() => DXGI_ERROR_ACCESS_LOST."); + state_ = State::AccessLost; + break; + } + case DXGI_ERROR_WAIT_TIMEOUT: + { + // This often occurs when timeout value is small and it is not problem. + // Debug::Log("Monitor::Render() => DXGI_ERROR_WAIT_TIMEOUT."); + break; + } + case DXGI_ERROR_INVALID_CALL: + { + //Debug::Error("Monitor::Render() => DXGI_ERROR_INVALID_CALL."); + break; + } + case E_INVALIDARG: + { + //Debug::Error("Monitor::Render() => E_INVALIDARG."); + break; + } + default: + { + state_ = State::Unknown; + //Debug::Error("Monitor::Render() => Unknown Error."); + break; + } + } + } + else + { + ReleaseGuard guard(deskDupl_); + + ComPtr texture; + if (FAILED(resource.As(&texture))) { + return; + } + + // copy target + auto copyTarget = m_pIsolated->CreateCopyTargetWithSharedFlag(texture); + if (!copyTarget) { + return; + } + + // copy + ComPtr context; + m_pIsolated->GetDevice()->GetImmediateContext(&context); + context->CopyResource(copyTarget.Get(), texture.Get()); + + m_textureQueue->Enqueue(QueueItem(copyTarget, frameInfo)); + } + } +} + +void Monitor::DuplicateAndMapLoop() +{ + // not implemented; +} diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp b/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp index f686d25..df4da29 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/main.cpp @@ -7,6 +7,7 @@ #include "IUnityInterface.h" #include "IUnityGraphics.h" +#include "include/IUnityGraphicsD3D11.h" #include "Common.h" #include "Debug.h" @@ -36,7 +37,25 @@ extern "C" if (g_unity && !g_manager) { Debug::Initialize(); - g_manager = std::make_unique(); + + auto device = g_unity->Get()->GetDevice(); + + Microsoft::WRL::ComPtr dxgiDevice; + if (FAILED(device->QueryInterface(IID_PPV_ARGS(&dxgiDevice)))){ + Debug::Error("fatal"); + return; + } + + Microsoft::WRL::ComPtr dxgiAdapter; + if (FAILED(dxgiDevice->GetAdapter(&dxgiAdapter))) { + Debug::Error("fatal"); + return; + } + + DXGI_ADAPTER_DESC desc; + dxgiAdapter->GetDesc(&desc); + + g_manager = std::make_unique(desc.AdapterLuid); } } @@ -87,7 +106,12 @@ extern "C" if (!g_manager) return; if (auto monitor = g_manager->GetMonitor(id)) { - monitor->Render(g_manager->GetTimeout()); + if (g_manager->UseThread()) { + monitor->CopyTextureFromThread(); + } + else { + monitor->Render(g_manager->GetTimeout()); + } } } @@ -96,10 +120,10 @@ extern "C" return OnRenderEvent; } - UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API Reinitialize() + UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API Reinitialize(int useThread) { if (!g_manager) return; - return g_manager->Reinitialize(); + return g_manager->Reinitialize(useThread); } UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API Update() diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj b/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj index 63a01f2..b39a6c5 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj @@ -90,7 +90,8 @@ true - copy /Y "$(SolutionDir)$(Platform)\$(Configuration)\$(TargetFileName)" "$(SolutionDir)..\..\Assets\$(ProjectName)\Plugins\x86_64" + copy /Y "$(SolutionDir)$(Platform)\$(Configuration)\$(TargetFileName)" "$(SolutionDir)..\..\Assets\$(ProjectName)\Plugins\x86_64" +copy /Y "$(SolutionDir)$(Platform)\$(Configuration)\$(TargetName).pdb" "$(SolutionDir)..\..\Assets\$(ProjectName)\Plugins\x86_64" @@ -144,6 +145,7 @@ + diff --git a/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj.filters b/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj.filters index 48179e2..b584073 100644 --- a/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj.filters +++ b/Plugins/uDesktopDuplication/uDesktopDuplication/uDesktopDuplication.vcxproj.filters @@ -28,5 +28,6 @@ + \ No newline at end of file