mirror of
https://github.com/nefarius/ViGEmBus.git
synced 2025-08-10 00:52:17 +00:00
Merge pull request #7 from mika-n/master
Removed NotificationPool and boost references. Created a new notification thread handler logic
This commit is contained in:
19
.gitignore
vendored
19
.gitignore
vendored
@@ -48,3 +48,22 @@
|
||||
/app/Tester/Debug
|
||||
/Debug
|
||||
/app/Tester/x64/Debug
|
||||
/app/Tester/x64/Release/Tester.tlog
|
||||
/app/Tester/x64/Release/Tester.log
|
||||
/app/Tester/x64/Release/Tester.obj
|
||||
/app/Tester/x64/Release/vc141.pdb
|
||||
/app/Tester/x64/Release/vcpkg.applocal.log
|
||||
/app/Tester/Release/Tester.tlog/CL.command.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/CL.read.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/CL.write.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/link.command.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/link.read.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/link.write.1.tlog
|
||||
/app/Tester/Release/Tester.tlog/Tester.lastbuildstate
|
||||
/app/Tester/Release/Tester.tlog/Tester.write.1u.tlog
|
||||
/app/Tester/Release/Tester.Build.CppClean.log
|
||||
/app/Tester/Release/Tester.log
|
||||
/app/Tester/Release/Tester.obj
|
||||
/app/Tester/Release/vc141.pdb
|
||||
/app/Tester/Release/vcpkg.applocal.log
|
||||
/app/Tester/x64/Release/Tester.Build.CppClean.log
|
||||
|
||||
@@ -51,7 +51,7 @@ typedef enum _VIGEM_TARGET_STATE
|
||||
VIGEM_TARGET_DISCONNECTED
|
||||
} VIGEM_TARGET_STATE, *PVIGEM_TARGET_STATE;
|
||||
|
||||
class NotificationRequestPool;
|
||||
|
||||
|
||||
//
|
||||
// Represents a virtual gamepad object.
|
||||
@@ -66,6 +66,7 @@ typedef struct _VIGEM_TARGET_T
|
||||
VIGEM_TARGET_TYPE Type;
|
||||
FARPROC Notification;
|
||||
|
||||
std::unique_ptr<NotificationRequestPool> NotificationPool;
|
||||
|
||||
bool closingNotificationThreads;
|
||||
HANDLE cancelNotificationThreadEvent;
|
||||
std::unique_ptr<std::vector<std::thread>> notificationThreadList;
|
||||
} VIGEM_TARGET;
|
||||
|
||||
@@ -44,9 +44,6 @@ SOFTWARE.
|
||||
|
||||
#include "Internal.h"
|
||||
|
||||
#include "NotificationRequestPool.h"
|
||||
|
||||
|
||||
typedef BOOL(WINAPI *MINIDUMPWRITEDUMP)(
|
||||
HANDLE hProcess,
|
||||
DWORD dwPid,
|
||||
@@ -60,6 +57,74 @@ typedef BOOL(WINAPI *MINIDUMPWRITEDUMP)(
|
||||
LONG WINAPI vigem_internal_exception_handler(struct _EXCEPTION_POINTERS* apExceptionInfo);
|
||||
|
||||
|
||||
//
|
||||
// DeviceIOControl request notification handler classes for X360 and DS4 controller types.
|
||||
// vigem_target_XXX_register_notification functions use x360 or DS4 derived class instances in a notification thread handlers.
|
||||
//
|
||||
class NotificationRequestPayload
|
||||
{
|
||||
public:
|
||||
LPVOID lpPayloadBuffer;
|
||||
DWORD payloadBufferSize;
|
||||
DWORD ioControlCode;
|
||||
|
||||
public:
|
||||
NotificationRequestPayload(DWORD _bufferSize, DWORD _ioControlCode)
|
||||
{
|
||||
lpPayloadBuffer = malloc(_bufferSize);
|
||||
payloadBufferSize = _bufferSize;
|
||||
ioControlCode = _ioControlCode;
|
||||
}
|
||||
|
||||
virtual ~NotificationRequestPayload()
|
||||
{
|
||||
free(lpPayloadBuffer);
|
||||
}
|
||||
|
||||
virtual void ProcessNotificationRequest(PVIGEM_CLIENT client, PVIGEM_TARGET target) = 0;
|
||||
};
|
||||
|
||||
class NotificationRequestPayloadX360 : public NotificationRequestPayload
|
||||
{
|
||||
public:
|
||||
NotificationRequestPayloadX360(ULONG _serialNo) : NotificationRequestPayload(sizeof(XUSB_REQUEST_NOTIFICATION), IOCTL_XUSB_REQUEST_NOTIFICATION)
|
||||
{
|
||||
// Let base class to allocate required buffer size, but initialize it here with a correct type of initialization function
|
||||
XUSB_REQUEST_NOTIFICATION_INIT((PXUSB_REQUEST_NOTIFICATION)lpPayloadBuffer, _serialNo);
|
||||
}
|
||||
|
||||
void ProcessNotificationRequest(PVIGEM_CLIENT client, PVIGEM_TARGET target) override
|
||||
{
|
||||
if(target->Notification != nullptr)
|
||||
PFN_VIGEM_X360_NOTIFICATION(target->Notification)(client, target,
|
||||
((PXUSB_REQUEST_NOTIFICATION)lpPayloadBuffer)->LargeMotor,
|
||||
((PXUSB_REQUEST_NOTIFICATION)lpPayloadBuffer)->SmallMotor,
|
||||
((PXUSB_REQUEST_NOTIFICATION)lpPayloadBuffer)->LedNumber
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
class NotificationRequestPayloadDS4 : public NotificationRequestPayload
|
||||
{
|
||||
public:
|
||||
NotificationRequestPayloadDS4(ULONG _serialNo) : NotificationRequestPayload(sizeof(DS4_REQUEST_NOTIFICATION), IOCTL_DS4_REQUEST_NOTIFICATION)
|
||||
{
|
||||
// Let base class to allocate required buffer size, but initialize it here with a correct type of initialization function
|
||||
DS4_REQUEST_NOTIFICATION_INIT((PDS4_REQUEST_NOTIFICATION)lpPayloadBuffer, _serialNo);
|
||||
}
|
||||
|
||||
void ProcessNotificationRequest(PVIGEM_CLIENT client, PVIGEM_TARGET target) override
|
||||
{
|
||||
if (target->Notification != nullptr)
|
||||
PFN_VIGEM_DS4_NOTIFICATION(target->Notification)(client, target,
|
||||
((PDS4_REQUEST_NOTIFICATION)lpPayloadBuffer)->Report.LargeMotor,
|
||||
((PDS4_REQUEST_NOTIFICATION)lpPayloadBuffer)->Report.SmallMotor,
|
||||
((PDS4_REQUEST_NOTIFICATION)lpPayloadBuffer)->Report.LightbarColor
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Initializes a virtual gamepad object.
|
||||
//
|
||||
@@ -77,7 +142,7 @@ PVIGEM_TARGET FORCEINLINE VIGEM_TARGET_ALLOC_INIT(
|
||||
target->Size = sizeof(VIGEM_TARGET);
|
||||
target->State = VIGEM_TARGET_INITIALIZED;
|
||||
target->Type = Type;
|
||||
|
||||
target->notificationThreadList = nullptr;
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -288,8 +353,8 @@ PVIGEM_TARGET vigem_target_ds4_alloc(void)
|
||||
|
||||
void vigem_target_free(PVIGEM_TARGET target)
|
||||
{
|
||||
if (target)
|
||||
free(target);
|
||||
if (target)
|
||||
free(target);
|
||||
}
|
||||
|
||||
VIGEM_ERROR vigem_target_add(PVIGEM_CLIENT vigem, PVIGEM_TARGET target)
|
||||
@@ -463,6 +528,109 @@ VIGEM_ERROR vigem_target_remove(PVIGEM_CLIENT vigem, PVIGEM_TARGET target)
|
||||
return VIGEM_ERROR_REMOVAL_FAILED;
|
||||
}
|
||||
|
||||
// Num of items in Notification DeviceIOControl queue (at any time there should be at least one extra call waiting for the new events or there is danger that notification events are lost).
|
||||
// The size of this queue is based on "scientific" experimentals and estimations (few games seem to sometimes flood the FFB driver interface).
|
||||
#define NOTIFICATION_OVERLAPPED_QUEUE_SIZE 6
|
||||
|
||||
void vigem_notification_thread_worker(
|
||||
PVIGEM_CLIENT client,
|
||||
PVIGEM_TARGET target,
|
||||
std::unique_ptr <std::vector<std::unique_ptr<NotificationRequestPayload>>> pNotificationRequestPayload
|
||||
)
|
||||
{
|
||||
int idx;
|
||||
DWORD error;
|
||||
|
||||
HANDLE waitObjects[2];
|
||||
|
||||
BOOL devIoResult[NOTIFICATION_OVERLAPPED_QUEUE_SIZE];
|
||||
DWORD lastIoError[NOTIFICATION_OVERLAPPED_QUEUE_SIZE];
|
||||
DWORD transferred[NOTIFICATION_OVERLAPPED_QUEUE_SIZE];
|
||||
OVERLAPPED lOverlapped[NOTIFICATION_OVERLAPPED_QUEUE_SIZE];
|
||||
|
||||
int currentOverlappedIdx;
|
||||
int futureOverlappedIdx;
|
||||
|
||||
memset(lOverlapped, 0, sizeof(lOverlapped));
|
||||
for(idx = 0; idx < NOTIFICATION_OVERLAPPED_QUEUE_SIZE; idx++)
|
||||
lOverlapped[idx].hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
||||
|
||||
waitObjects[0] = target->cancelNotificationThreadEvent;
|
||||
|
||||
currentOverlappedIdx = 0;
|
||||
futureOverlappedIdx = NOTIFICATION_OVERLAPPED_QUEUE_SIZE - 1;
|
||||
|
||||
// Send out DeviceIOControl calls to wait for incoming feedback notifications. Use N pending requests to make sure that events are not lost even when application would flood FFB events.
|
||||
// The order of DeviceIoControl calls and GetOverlappedResult requests is important to ensure that feedback callback function is called in correct order (ie. FIFO buffer with FFB events).
|
||||
// Note! This loop doesn't call DevIo for the last lOverlapped item on purpose. The DO while loop does it as a first step (futureOverlappedIdx=NOTIFICATION_OVERLAPPED_QUEUE_SIZE-1 in the first loop round).
|
||||
for (idx = 0; idx < NOTIFICATION_OVERLAPPED_QUEUE_SIZE-1; idx++)
|
||||
{
|
||||
devIoResult[idx] = DeviceIoControl(client->hBusDevice,
|
||||
(*pNotificationRequestPayload)[idx]->ioControlCode,
|
||||
(*pNotificationRequestPayload)[idx]->lpPayloadBuffer,
|
||||
(*pNotificationRequestPayload)[idx]->payloadBufferSize,
|
||||
(*pNotificationRequestPayload)[idx]->lpPayloadBuffer,
|
||||
(*pNotificationRequestPayload)[idx]->payloadBufferSize,
|
||||
&transferred[idx],
|
||||
&lOverlapped[idx]);
|
||||
|
||||
lastIoError[idx] = GetLastError();
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Before reading data from "current overlapped request" then send a new DeviceIoControl request to wait for upcoming new FFB events (ring buffer of overlapped objects).
|
||||
devIoResult[futureOverlappedIdx] = DeviceIoControl(client->hBusDevice,
|
||||
(*pNotificationRequestPayload)[futureOverlappedIdx]->ioControlCode,
|
||||
(*pNotificationRequestPayload)[futureOverlappedIdx]->lpPayloadBuffer,
|
||||
(*pNotificationRequestPayload)[futureOverlappedIdx]->payloadBufferSize,
|
||||
(*pNotificationRequestPayload)[futureOverlappedIdx]->lpPayloadBuffer,
|
||||
(*pNotificationRequestPayload)[futureOverlappedIdx]->payloadBufferSize,
|
||||
&transferred[futureOverlappedIdx],
|
||||
&lOverlapped[futureOverlappedIdx]);
|
||||
|
||||
lastIoError[futureOverlappedIdx] = GetLastError();
|
||||
|
||||
// currentOverlappedIdx is an index to the "oldest" DeviceIOControl call, so it will receive the next FFB event in a FFB sequence (in case there are multiple FFB events coming in)
|
||||
if (!devIoResult[currentOverlappedIdx])
|
||||
{
|
||||
if (lastIoError[currentOverlappedIdx] == ERROR_IO_PENDING)
|
||||
{
|
||||
// DeviceIoControl is not yet completed to return all data. Wait for overlapped completion and thread cancellation events
|
||||
waitObjects[1] = lOverlapped[currentOverlappedIdx].hEvent;
|
||||
error = WaitForMultipleObjects(2, waitObjects, FALSE, INFINITE);
|
||||
if (error != (WAIT_OBJECT_0 + 1))
|
||||
break; // Cancel event signaled or error while waiting for events (maybe handles were closed?). Quit this thread worker
|
||||
|
||||
// At this point overlapped event was signaled by a device driver and data is ready and waiting for in a buffer. The next GetOverlappedResult call should return immediately.
|
||||
}
|
||||
else
|
||||
// Hmm... DeviceIoControl failed and is not just in async pending state. Quit the notification thread because the virtual controller may be in unknown state or device handles were closed
|
||||
break;
|
||||
}
|
||||
|
||||
if (GetOverlappedResult(client->hBusDevice, &lOverlapped[currentOverlappedIdx], &transferred[currentOverlappedIdx], TRUE) != 0)
|
||||
(*pNotificationRequestPayload)[currentOverlappedIdx]->ProcessNotificationRequest(client, target);
|
||||
|
||||
if (currentOverlappedIdx >= NOTIFICATION_OVERLAPPED_QUEUE_SIZE-1)
|
||||
currentOverlappedIdx = 0;
|
||||
else
|
||||
currentOverlappedIdx++;
|
||||
|
||||
if (futureOverlappedIdx >= NOTIFICATION_OVERLAPPED_QUEUE_SIZE-1)
|
||||
futureOverlappedIdx = 0;
|
||||
else
|
||||
futureOverlappedIdx++;
|
||||
} while (target->closingNotificationThreads != TRUE && target->Notification != nullptr);
|
||||
|
||||
for (idx = 0; idx < NOTIFICATION_OVERLAPPED_QUEUE_SIZE; idx++)
|
||||
if(lOverlapped[idx].hEvent)
|
||||
CloseHandle(lOverlapped[idx].hEvent);
|
||||
|
||||
// Caller created the unique_ptr object, but this thread worker function should delete the object because it is no longer needed (thread specific object)
|
||||
pNotificationRequestPayload.reset();
|
||||
}
|
||||
|
||||
VIGEM_ERROR vigem_target_x360_register_notification(
|
||||
PVIGEM_CLIENT vigem,
|
||||
PVIGEM_TARGET target,
|
||||
@@ -486,10 +654,25 @@ VIGEM_ERROR vigem_target_x360_register_notification(
|
||||
|
||||
target->Notification = reinterpret_cast<FARPROC>(notification);
|
||||
|
||||
target->NotificationPool = std::make_unique<NotificationRequestPool>(
|
||||
vigem,
|
||||
target
|
||||
);
|
||||
if (target->cancelNotificationThreadEvent == 0)
|
||||
target->cancelNotificationThreadEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||
else
|
||||
ResetEvent(target->cancelNotificationThreadEvent);
|
||||
|
||||
if(target->notificationThreadList == nullptr)
|
||||
target->notificationThreadList = std::make_unique<std::vector<std::thread>>();
|
||||
|
||||
target->closingNotificationThreads = FALSE;
|
||||
|
||||
std::unique_ptr<std::vector<std::unique_ptr<NotificationRequestPayload>>> payloadVector = std::make_unique<std::vector<std::unique_ptr<NotificationRequestPayload>>>();
|
||||
payloadVector->reserve(NOTIFICATION_OVERLAPPED_QUEUE_SIZE);
|
||||
for (int idx = 0; idx < NOTIFICATION_OVERLAPPED_QUEUE_SIZE; idx++)
|
||||
payloadVector->push_back(std::make_unique<NotificationRequestPayloadX360>(target->SerialNo));
|
||||
|
||||
// Nowadays there is only one background thread listening for incoming FFB events, but there used to be more. This code still uses notificationThreadList vector even
|
||||
// when there is only one item in the vector. If it is someday find out that this logic needs more background threads then it is easy to do because the thread vector is already in place.
|
||||
//for (int i = 0; i < 1; i++)
|
||||
target->notificationThreadList->emplace_back(std::thread(&vigem_notification_thread_worker, vigem, target, std::move(payloadVector)));
|
||||
|
||||
return VIGEM_ERROR_NONE;
|
||||
}
|
||||
@@ -517,75 +700,54 @@ VIGEM_ERROR vigem_target_ds4_register_notification(
|
||||
|
||||
target->Notification = reinterpret_cast<FARPROC>(notification);
|
||||
|
||||
std::vector<std::thread> threadList;
|
||||
if (target->cancelNotificationThreadEvent == 0)
|
||||
target->cancelNotificationThreadEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||
else
|
||||
ResetEvent(target->cancelNotificationThreadEvent);
|
||||
|
||||
for (int i = 0; i < 20 /* TODO: legacy, remove */; i++)
|
||||
{
|
||||
threadList.emplace_back(std::thread([](
|
||||
PVIGEM_TARGET _Target,
|
||||
PVIGEM_CLIENT _Client)
|
||||
{
|
||||
DWORD error = ERROR_SUCCESS;
|
||||
DWORD transferred = 0;
|
||||
OVERLAPPED lOverlapped = { 0 };
|
||||
lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
||||
if (target->notificationThreadList == nullptr)
|
||||
target->notificationThreadList = std::make_unique<std::vector<std::thread>>();
|
||||
|
||||
DS4_REQUEST_NOTIFICATION notify;
|
||||
DS4_REQUEST_NOTIFICATION_INIT(¬ify, _Target->SerialNo);
|
||||
target->closingNotificationThreads = FALSE;
|
||||
|
||||
do
|
||||
{
|
||||
DeviceIoControl(_Client->hBusDevice,
|
||||
IOCTL_DS4_REQUEST_NOTIFICATION,
|
||||
¬ify,
|
||||
notify.Size,
|
||||
¬ify,
|
||||
notify.Size,
|
||||
&transferred,
|
||||
&lOverlapped);
|
||||
std::unique_ptr<std::vector<std::unique_ptr<NotificationRequestPayload>>> payloadVector = std::make_unique<std::vector<std::unique_ptr<NotificationRequestPayload>>>();
|
||||
payloadVector->reserve(NOTIFICATION_OVERLAPPED_QUEUE_SIZE);
|
||||
for (int idx = 0; idx < NOTIFICATION_OVERLAPPED_QUEUE_SIZE; idx++)
|
||||
payloadVector->push_back(std::make_unique<NotificationRequestPayloadDS4>(target->SerialNo));
|
||||
|
||||
if (GetOverlappedResult(_Client->hBusDevice, &lOverlapped, &transferred, TRUE) != 0)
|
||||
{
|
||||
if (_Target->Notification == NULL)
|
||||
{
|
||||
if (lOverlapped.hEvent)
|
||||
CloseHandle(lOverlapped.hEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
PFN_VIGEM_DS4_NOTIFICATION(_Target->Notification)(
|
||||
_Client,
|
||||
_Target,
|
||||
notify.Report.LargeMotor,
|
||||
notify.Report.SmallMotor,
|
||||
notify.Report.LightbarColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
error = GetLastError();
|
||||
}
|
||||
} while (error != ERROR_OPERATION_ABORTED && error != ERROR_ACCESS_DENIED);
|
||||
|
||||
if (lOverlapped.hEvent)
|
||||
CloseHandle(lOverlapped.hEvent);
|
||||
|
||||
}, target, vigem));
|
||||
}
|
||||
|
||||
std::for_each(threadList.begin(), threadList.end(), std::mem_fn(&std::thread::detach));
|
||||
//for (int i = 0; i < 1; i++)
|
||||
target->notificationThreadList->emplace_back(std::thread(&vigem_notification_thread_worker, vigem, target, std::move(payloadVector)));
|
||||
|
||||
return VIGEM_ERROR_NONE;
|
||||
}
|
||||
|
||||
void vigem_target_x360_unregister_notification(PVIGEM_TARGET target)
|
||||
{
|
||||
target->NotificationPool.reset();
|
||||
target->Notification = nullptr;
|
||||
target->closingNotificationThreads = TRUE;
|
||||
|
||||
if (target->cancelNotificationThreadEvent != 0)
|
||||
SetEvent(target->cancelNotificationThreadEvent);
|
||||
|
||||
if (target->notificationThreadList != nullptr)
|
||||
{
|
||||
// Wait for completion of all notification threads before cleaning up target object and Notification function pointer (a thread may be in the middle of handling a notification request, so close it cleanly)
|
||||
std::for_each(target->notificationThreadList->begin(), target->notificationThreadList->end(), std::mem_fn(&std::thread::join));
|
||||
target->notificationThreadList.reset();
|
||||
target->notificationThreadList = nullptr;
|
||||
}
|
||||
|
||||
if (target->cancelNotificationThreadEvent != 0)
|
||||
{
|
||||
CloseHandle(target->cancelNotificationThreadEvent);
|
||||
target->cancelNotificationThreadEvent = 0;
|
||||
}
|
||||
|
||||
target->Notification = nullptr;
|
||||
}
|
||||
|
||||
void vigem_target_ds4_unregister_notification(PVIGEM_TARGET target)
|
||||
{
|
||||
target->Notification = NULL;
|
||||
vigem_target_x360_unregister_notification(target); // The same x360_unregister handler works for DS4_unregister also
|
||||
}
|
||||
|
||||
void vigem_target_set_vid(PVIGEM_TARGET target, USHORT vid)
|
||||
|
||||
@@ -306,13 +306,9 @@
|
||||
<ClInclude Include="$(SolutionDir)\include\ViGEm\Util.h" />
|
||||
<ClInclude Include="$(SolutionDir)\include\ViGEm\km\BusShared.h" />
|
||||
<ClInclude Include="Internal.h" />
|
||||
<ClInclude Include="NotificationRequestPool.h" />
|
||||
<ClInclude Include="XusbNotificationRequest.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="NotificationRequestPool.cpp" />
|
||||
<ClCompile Include="ViGEmClient.cpp" />
|
||||
<ClCompile Include="XusbNotificationRequest.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -33,12 +33,6 @@
|
||||
<ClInclude Include="$(SolutionDir)\include\ViGEm\km\BusShared.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XusbNotificationRequest.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="NotificationRequestPool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Internal.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -47,11 +41,5 @@
|
||||
<ClCompile Include="ViGEmClient.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XusbNotificationRequest.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="NotificationRequestPool.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user