1
0
mirror of https://github.com/RPCS3/rpcs3.git synced 2024-11-22 02:32:36 +01:00

semaphore_t, RSX fixes

1) GS_LOCK_WAIT_FLUSH semaphore eliminated
2) GS_LOCK_WAIT_FLIP semaphore left unused
3) cellRescSetWaitFlip/cellGcmSetWaitFlip purged: they don't wait for
flip, it's a nonsense, they only generate some RSX command
4) Semaphores rewritten
This commit is contained in:
Nekotekina 2015-07-27 04:27:33 +03:00
parent 71a378a3fb
commit 8e1991c1e1
13 changed files with 213 additions and 257 deletions

View File

@ -1,85 +0,0 @@
#include "stdafx.h"
#include "Utilities/SSemaphore.h"
#include "Emu/System.h"
void SSemaphore::wait()
{
u32 order;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_count && m_out_order == m_in_order)
{
m_count--;
return;
}
order = m_in_order++;
}
std::unique_lock<std::mutex> cv_lock(m_cv_mutex);
while (true)
{
CHECK_EMU_STATUS;
m_cond.wait_for(cv_lock, std::chrono::milliseconds(1));
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_count)
{
if (m_out_order == order)
{
m_count--;
m_out_order++;
return;
}
else
{
m_cond.notify_one();
}
}
}
}
}
bool SSemaphore::try_wait()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_count && m_in_order == m_out_order)
{
m_count--;
return true;
}
else
{
return false;
}
}
void SSemaphore::post()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_count < m_max)
{
m_count++;
}
else
{
return;
}
m_cond.notify_one();
}
bool SSemaphore::post_and_wait()
{
// TODO: merge these functions? Probably has a race condition.
if (try_wait()) return false;
post();
wait();
return true;
}

View File

@ -1,41 +0,0 @@
#pragma once
class SSemaphore
{
const u32 m_max;
u32 m_count;
u32 m_in_order;
u32 m_out_order;
std::mutex m_cv_mutex;
std::mutex m_mutex;
std::condition_variable m_cond;
public:
SSemaphore(u32 value, u32 max = 1)
: m_max(max > 0 ? max : 0xffffffff)
, m_count(value > m_max ? m_max : value)
, m_in_order(0)
, m_out_order(0)
{
}
SSemaphore()
: m_max(0xffffffff)
, m_count(0)
, m_in_order(0)
, m_out_order(0)
{
}
~SSemaphore()
{
}
void wait();
bool try_wait();
void post();
bool post_and_wait();
};

120
Utilities/Semaphore.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "stdafx.h"
#include "Utilities/Semaphore.h"
bool semaphore_t::try_wait()
{
// check m_value without interlocked op
if (m_var.load().value == 0)
{
return false;
}
// try to decrement m_value atomically
const auto old = m_var.atomic_op([](sync_var_t& var)
{
if (var.value)
{
var.value--;
}
});
// recheck atomic result
if (old.value == 0)
{
return false;
}
return true;
}
bool semaphore_t::try_post()
{
// check m_value without interlocked op
if (m_var.load().value >= max_value)
{
return false;
}
// try to increment m_value atomically
const auto old = m_var.atomic_op([&](sync_var_t& var)
{
if (var.value < max_value)
{
var.value++;
}
});
// recheck atomic result
if (old.value >= max_value)
{
return false;
}
if (old.waiters)
{
// notify waiting thread
std::lock_guard<std::mutex> lock(m_mutex);
m_cv.notify_one();
}
return true;
}
void semaphore_t::wait()
{
if (m_var.atomic_op([](sync_var_t& var) -> bool
{
if (var.value)
{
var.value--;
return true;
}
else
{
//var.waiters++;
return false;
}
}))
{
return;
}
std::unique_lock<std::mutex> lock(m_mutex);
m_var.atomic_op([](sync_var_t& var)
{
var.waiters++;
});
while (!m_var.atomic_op([](sync_var_t& var) -> bool
{
if (var.value)
{
var.value--;
var.waiters--;
return true;
}
else
{
return false;
}
}))
{
m_cv.wait(lock);
}
}
bool semaphore_t::post_and_wait()
{
// TODO: merge these functions? Probably has a race condition.
if (try_wait()) return false;
try_post();
wait();
return true;
}

37
Utilities/Semaphore.h Normal file
View File

@ -0,0 +1,37 @@
#pragma once
class semaphore_t
{
// semaphore mutex
std::mutex m_mutex;
// semaphore condition variable
std::condition_variable m_cv;
struct sync_var_t
{
u32 value; // current semaphore value
u32 waiters; // current amount of waiters
};
// current semaphore value
atomic_t<sync_var_t> m_var;
public:
// max semaphore value
const u32 max_value;
semaphore_t(u32 max_value = 1, u32 value = 0)
: m_var({ value, 0 })
, max_value(max_value)
{
}
bool try_wait();
bool try_post();
void wait();
bool post_and_wait();
};

View File

@ -224,9 +224,9 @@ typedef s32(CellGcmContextCallback)(vm::ptr<CellGcmContextData>, u32);
struct CellGcmContextData
{
be_t<u32> begin;
be_t<u32> end;
be_t<u32> current;
vm::bptr<u32> begin;
vm::bptr<u32> end;
vm::bptr<u32> current;
vm::bptr<CellGcmContextCallback> callback;
};

View File

@ -12,7 +12,6 @@ GSLock::GSLock(GSRender& renderer, GSLockType type)
switch (m_type)
{
case GS_LOCK_NOT_WAIT: m_renderer.m_cs_main.lock(); break;
case GS_LOCK_WAIT_FLUSH: m_renderer.m_sem_flush.wait(); break;
case GS_LOCK_WAIT_FLIP: m_renderer.m_sem_flip.wait(); break;
}
}
@ -22,8 +21,7 @@ GSLock::~GSLock()
switch (m_type)
{
case GS_LOCK_NOT_WAIT: m_renderer.m_cs_main.unlock(); break;
case GS_LOCK_WAIT_FLUSH: m_renderer.m_sem_flush.post(); break;
case GS_LOCK_WAIT_FLIP: m_renderer.m_sem_flip.post(); break;
case GS_LOCK_WAIT_FLIP: m_renderer.m_sem_flip.try_post(); break;
}
}

View File

@ -17,7 +17,6 @@ struct GSRender : public RSXThread
enum GSLockType
{
GS_LOCK_NOT_WAIT,
GS_LOCK_WAIT_FLUSH,
GS_LOCK_WAIT_FLIP,
};

View File

@ -258,6 +258,8 @@ void RSXThread::DoCmd(const u32 fcmd, const u32 cmd, const u32 args_addr, const
});
}
m_sem_flip.post_and_wait();
auto sync = [&]()
{
double limit;
@ -2499,14 +2501,6 @@ void RSXThread::Task()
if (put == get || !Emu.IsRunning())
{
if (put == get)
{
if (m_flip_status == 0)
m_sem_flip.post_and_wait();
m_sem_flush.post_and_wait();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // hack
continue;
}

View File

@ -5,7 +5,7 @@
#include "RSXFragmentProgram.h"
#include <stack>
#include "Utilities/SSemaphore.h"
#include "Utilities/Semaphore.h"
#include "Utilities/Thread.h"
#include "Utilities/Timer.h"
@ -155,8 +155,7 @@ public:
public:
std::mutex m_cs_main;
SSemaphore m_sem_flush;
SSemaphore m_sem_flip;
semaphore_t m_sem_flip;
u64 m_last_flip_time;
vm::ptr<void(u32)> m_flip_handler;
vm::ptr<void(u32)> m_user_handler;

View File

@ -320,9 +320,9 @@ void _cellGcmFunc15(vm::ptr<CellGcmContextData> context)
u32 g_defaultCommandBufferBegin, g_defaultCommandBufferFragmentCount;
// Called by cellGcmInit
s32 _cellGcmInitBody(vm::ptr<CellGcmContextData> context, u32 cmdSize, u32 ioSize, u32 ioAddress)
s32 _cellGcmInitBody(vm::pptr<CellGcmContextData> context, u32 cmdSize, u32 ioSize, u32 ioAddress)
{
cellGcmSys.Warning("_cellGcmInitBody(context=*0x%x, cmdSize=0x%x, ioSize=0x%x, ioAddress=0x%x)", context, cmdSize, ioSize, ioAddress);
cellGcmSys.Warning("_cellGcmInitBody(context=**0x%x, cmdSize=0x%x, ioSize=0x%x, ioAddress=0x%x)", context, cmdSize, ioSize, ioAddress);
if(!local_size && !local_addr)
{
@ -365,8 +365,8 @@ s32 _cellGcmInitBody(vm::ptr<CellGcmContextData> context, u32 cmdSize, u32 ioSiz
g_defaultCommandBufferBegin = ioAddress;
g_defaultCommandBufferFragmentCount = cmdSize / (32 * 1024);
current_context.begin = g_defaultCommandBufferBegin + 4096; // 4 kb reserved at the beginning
current_context.end = g_defaultCommandBufferBegin + 32 * 1024 - 4; // 4b at the end for jump
current_context.begin.set(g_defaultCommandBufferBegin + 4096); // 4 kb reserved at the beginning
current_context.end.set(g_defaultCommandBufferBegin + 32 * 1024 - 4); // 4b at the end for jump
current_context.current = current_context.begin;
current_context.callback.set(Emu.GetRSXCallback() - 4);
@ -376,7 +376,7 @@ s32 _cellGcmInitBody(vm::ptr<CellGcmContextData> context, u32 cmdSize, u32 ioSiz
gcm_info.label_addr = vm::alloc(0x1000, vm::main); // ???
vm::get_ref<CellGcmContextData>(gcm_info.context_addr) = current_context;
vm::write32(context.addr(), gcm_info.context_addr);
context->set(gcm_info.context_addr);
auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
ctrl.put.store(0);
@ -480,59 +480,46 @@ void cellGcmSetFlipStatus()
Emu.GetGSManager().GetRender().m_flip_status = 0;
}
s32 cellGcmSetPrepareFlip(PPUThread& CPU, vm::ptr<CellGcmContextData> ctxt, u32 id)
s32 cellGcmSetPrepareFlip(PPUThread& ppu, vm::ptr<CellGcmContextData> ctxt, u32 id)
{
cellGcmSys.Log("cellGcmSetPrepareFlip(ctx=0x%x, id=0x%x)", ctxt.addr(), id);
cellGcmSys.Log("cellGcmSetPrepareFlip(ctx=*0x%x, id=0x%x)", ctxt, id);
if(id > 7)
if (id > 7)
{
cellGcmSys.Error("cellGcmSetPrepareFlip : CELL_GCM_ERROR_FAILURE");
return CELL_GCM_ERROR_FAILURE;
}
GSLockCurrent gslock(GS_LOCK_WAIT_FLUSH);
u32 current = ctxt->current;
if (current + 8 == ctxt->begin)
if (ctxt->current + 2 >= ctxt->end)
{
cellGcmSys.Error("cellGcmSetPrepareFlip : queue is full");
return CELL_GCM_ERROR_FAILURE;
}
if (current + 8 >= ctxt->end)
{
cellGcmSys.Error("Bad flip!");
if (s32 res = ctxt->callback(CPU, ctxt, 8 /* ??? */))
if (s32 res = ctxt->callback(ppu, ctxt, 8 /* ??? */))
{
cellGcmSys.Error("cellGcmSetPrepareFlip : callback failed (0x%08x)", res);
return res;
}
}
current = ctxt->current;
vm::write32(current, 0x3fead | (1 << 18));
vm::write32(current + 4, id);
ctxt->current += 8;
*ctxt->current++ = 0x3fead | (1 << 18);
*ctxt->current++ = id;
if(ctxt.addr() == gcm_info.context_addr)
if (ctxt.addr() == gcm_info.context_addr)
{
auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
ctrl.put.atomic_op([](be_t<u32>& value)
{
value += 8;
});
vm::get_ref<CellGcmControl>(gcm_info.control_addr).put += 8;
}
return id;
}
s32 cellGcmSetFlip(PPUThread& CPU, vm::ptr<CellGcmContextData> ctxt, u32 id)
s32 cellGcmSetFlip(PPUThread& ppu, vm::ptr<CellGcmContextData> ctxt, u32 id)
{
cellGcmSys.Log("cellGcmSetFlip(ctx=0x%x, id=0x%x)", ctxt.addr(), id);
cellGcmSys.Log("cellGcmSetFlip(ctxt=*0x%x, id=0x%x)", ctxt, id);
s32 res = cellGcmSetPrepareFlip(CPU, ctxt, id);
return res < 0 ? CELL_GCM_ERROR_FAILURE : CELL_OK;
if (s32 res = cellGcmSetPrepareFlip(ppu, ctxt, id))
{
if (res < 0) return CELL_GCM_ERROR_FAILURE;
}
return CELL_OK;
}
s32 cellGcmSetSecondVFrequency(u32 freq)
@ -610,9 +597,10 @@ void cellGcmSetVBlankHandler(vm::ptr<void(u32)> handler)
s32 cellGcmSetWaitFlip(vm::ptr<CellGcmContextData> ctxt)
{
cellGcmSys.Log("cellGcmSetWaitFlip(ctx=*0x%x)", ctxt);
cellGcmSys.Warning("cellGcmSetWaitFlip(ctx=*0x%x)", ctxt);
// TODO: emit RSX command for "wait flip" operation
GSLockCurrent lock(GS_LOCK_WAIT_FLIP);
return CELL_OK;
}
@ -1101,18 +1089,18 @@ void cellGcmSetDefaultCommandBuffer()
// Other
//------------------------------------------------------------------------
s32 _cellGcmSetFlipCommand(PPUThread& CPU, vm::ptr<CellGcmContextData> ctx, u32 id)
s32 _cellGcmSetFlipCommand(PPUThread& ppu, vm::ptr<CellGcmContextData> ctx, u32 id)
{
cellGcmSys.Log("cellGcmSetFlipCommand(ctx=*0x%x, id=0x%x)", ctx, id);
return cellGcmSetPrepareFlip(CPU, ctx, id);
return cellGcmSetPrepareFlip(ppu, ctx, id);
}
s32 _cellGcmSetFlipCommandWithWaitLabel(PPUThread& CPU, vm::ptr<CellGcmContextData> ctx, u32 id, u32 label_index, u32 label_value)
s32 _cellGcmSetFlipCommandWithWaitLabel(PPUThread& ppu, vm::ptr<CellGcmContextData> ctx, u32 id, u32 label_index, u32 label_value)
{
cellGcmSys.Log("cellGcmSetFlipCommandWithWaitLabel(ctx=*0x%x, id=0x%x, label_index=0x%x, label_value=0x%x)", ctx, id, label_index, label_value);
s32 res = cellGcmSetPrepareFlip(CPU, ctx, id);
s32 res = cellGcmSetPrepareFlip(ppu, ctx, id);
vm::write32(gcm_info.label_addr + 0x10 * label_index, label_value);
return res < 0 ? CELL_GCM_ERROR_FAILURE : CELL_OK;
}
@ -1200,9 +1188,7 @@ static bool isInCommandBufferExcept(u32 getPos, u32 bufferBegin, u32 bufferEnd)
return true;
}
// TODO: This function was originally located in lv2/SC_GCM and appears in RPCS3 as a lv2 syscall with id 1023,
// which according to lv2 dumps isn't the case. So, is this a proper place for this function?
// TODO: Avoid using syscall 1023 for calling this function
s32 cellGcmCallback(vm::ptr<CellGcmContextData> context, u32 count)
{
cellGcmSys.Log("cellGcmCallback(context=*0x%x, count=0x%x)", context, count);
@ -1210,16 +1196,16 @@ s32 cellGcmCallback(vm::ptr<CellGcmContextData> context, u32 count)
auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
const std::chrono::time_point<std::chrono::system_clock> enterWait = std::chrono::system_clock::now();
// Flush command buffer (ie allow RSX to read up to context->current)
ctrl.put.exchange(getOffsetFromAddress(context->current));
ctrl.put.exchange(getOffsetFromAddress(context->current.addr()));
std::pair<u32, u32> newCommandBuffer = getNextCommandBufferBeginEnd(context->current);
std::pair<u32, u32> newCommandBuffer = getNextCommandBufferBeginEnd(context->current.addr());
u32 offset = getOffsetFromAddress(newCommandBuffer.first);
// Write jump instruction
vm::write32(context->current, CELL_GCM_METHOD_FLAG_JUMP | offset);
*context->current = CELL_GCM_METHOD_FLAG_JUMP | offset;
// Update current command buffer
context->begin = newCommandBuffer.first;
context->current = newCommandBuffer.first;
context->end = newCommandBuffer.second;
context->begin.set(newCommandBuffer.first);
context->current.set(newCommandBuffer.first);
context->end.set(newCommandBuffer.second);
// Wait for rsx to "release" the new command buffer
while (!Emu.IsStopped())
@ -1235,55 +1221,6 @@ s32 cellGcmCallback(vm::ptr<CellGcmContextData> context, u32 count)
}
return CELL_OK;
//if (0)
//{
// auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
// be_t<u32> res = context->current - context->begin - ctrl.put.load();
// if (res != 0)
// {
// GSLockCurrent gslock(GS_LOCK_WAIT_FLUSH);
// }
// memmove(vm::get_ptr<void>(context->begin), vm::get_ptr<void>(context->current - res), res);
// context->current = context->begin + res;
// ctrl.put.store(res);
// ctrl.get.store(0);
// return CELL_OK;
//}
//auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
// preparations for changing the place (for optimized FIFO mode)
//auto cmd = vm::ptr<u32>::make(context->current);
//cmd[0] = 0x41D6C;
//cmd[1] = 0x20;
//cmd[2] = 0x41D74;
//cmd[3] = 0; // some incrementing by module value
//context->current += 0x10;
//if (0)
//{
// const u32 address = context->begin;
// const u32 upper = offsetTable.ioAddress[address >> 20]; // 12 bits
// assert(upper != 0xFFFF);
// const u32 offset = (upper << 20) | (address & 0xFFFFF);
// vm::write32(context->current, CELL_GCM_METHOD_FLAG_JUMP | offset); // set JUMP cmd
// auto& ctrl = vm::get_ref<CellGcmControl>(gcm_info.control_addr);
// ctrl.put.exchange(offset);
//}
//else
//{
// vm::write32(context->current, CELL_GCM_METHOD_FLAG_JUMP | CELL_GCM_METHOD_FLAG_NON_INCREMENT | (0));
//}
//context->current = context->begin; // rewind to the beginning
// TODO: something is missing
return CELL_OK;
}
//----------------------------------------------------------------------------

View File

@ -17,7 +17,7 @@ extern void cellGcmSetFlipHandler(vm::ptr<void(u32)> handler);
extern void cellGcmSetVBlankHandler(vm::ptr<void(u32)> handler);
extern s32 cellGcmAddressToOffset(u32 address, vm::ptr<u32> offset);
extern s32 cellGcmSetDisplayBuffer(u32 id, u32 offset, u32 pitch, u32 width, u32 height);
extern s32 cellGcmSetPrepareFlip(PPUThread& CPU, vm::ptr<CellGcmContextData> ctx, u32 id);
extern s32 cellGcmSetPrepareFlip(PPUThread& ppu, vm::ptr<CellGcmContextData> ctx, u32 id);
extern s32 cellGcmSetSecondVFrequency(u32 freq);
extern u32 cellGcmGetLabelAddress(u8 index);
extern u32 cellGcmGetTiledPitchSize(u32 size);
@ -467,7 +467,6 @@ void InitMembers()
void SetupRsxRenderingStates(vm::ptr<CellGcmContextData>& cntxt)
{
//TODO: use cntxt
GSLockCurrent lock(GS_LOCK_WAIT_FLUSH);
GSRender& r = Emu.GetGSManager().GetRender();
r.m_set_color_mask = true; r.m_color_mask_a = r.m_color_mask_r = r.m_color_mask_g = r.m_color_mask_b = true;
r.m_set_depth_mask = true; r.m_depth_mask = 0;
@ -514,7 +513,6 @@ void SetupRsxRenderingStates(vm::ptr<CellGcmContextData>& cntxt)
void SetupVertexArrays(vm::ptr<CellGcmContextData>& cntxt)
{
GSLockCurrent lock(GS_LOCK_WAIT_FLUSH);
GSRender& r = Emu.GetGSManager().GetRender();
//TODO
@ -538,7 +536,6 @@ void SetupSurfaces(vm::ptr<CellGcmContextData>& cntxt)
dstOffset1 = s_rescInternalInstance->m_dstOffsets[s_rescInternalInstance->m_bufIdPalMidNow];
}
GSLockCurrent lock(GS_LOCK_WAIT_FLUSH);
GSRender& r = Emu.GetGSManager().GetRender();
r.m_surface_type = CELL_GCM_SURFACE_PITCH;
@ -1023,8 +1020,9 @@ s32 cellRescSetConvertAndFlip(PPUThread& CPU, vm::ptr<CellGcmContextData> cntxt,
s32 cellRescSetWaitFlip()
{
cellResc.Log("cellRescSetWaitFlip()");
GSLockCurrent lock(GS_LOCK_WAIT_FLIP);
cellResc.Warning("cellRescSetWaitFlip()");
// TODO: emit RSX command for "wait flip" operation
return CELL_OK;
}

View File

@ -30,7 +30,7 @@
<ClCompile Include="..\Utilities\rPlatform.cpp" />
<ClCompile Include="..\Utilities\rTime.cpp" />
<ClCompile Include="..\Utilities\rXml.cpp" />
<ClCompile Include="..\Utilities\SSemaphore.cpp" />
<ClCompile Include="..\Utilities\Semaphore.cpp" />
<ClCompile Include="..\Utilities\StrFmt.cpp" />
<ClCompile Include="..\Utilities\Thread.cpp" />
<ClCompile Include="Emu\Cell\PPUInterpreter.cpp" />
@ -318,9 +318,9 @@
<ClInclude Include="..\Utilities\rPlatform.h" />
<ClInclude Include="..\Utilities\rTime.h" />
<ClInclude Include="..\Utilities\rXml.h" />
<ClInclude Include="..\Utilities\Semaphore.h" />
<ClInclude Include="..\Utilities\simpleini\ConvertUTF.h" />
<ClInclude Include="..\Utilities\simpleini\SimpleIni.h" />
<ClInclude Include="..\Utilities\SSemaphore.h" />
<ClInclude Include="..\Utilities\StrFmt.h" />
<ClInclude Include="..\Utilities\Thread.h" />
<ClInclude Include="..\Utilities\Timer.h" />

View File

@ -476,9 +476,6 @@
<ClCompile Include="Emu\SysCalls\Modules\sys_http.cpp">
<Filter>Emu\SysCalls\currently_unused</Filter>
</ClCompile>
<ClCompile Include="..\Utilities\SSemaphore.cpp">
<Filter>Utilities</Filter>
</ClCompile>
<ClCompile Include="Emu\SysCalls\lv2\sys_process.cpp">
<Filter>Emu\SysCalls\lv2</Filter>
</ClCompile>
@ -884,6 +881,9 @@
<ClCompile Include="Emu\SysCalls\Modules\sceNpUtil.cpp">
<Filter>Emu\SysCalls\Modules</Filter>
</ClCompile>
<ClCompile Include="..\Utilities\Semaphore.cpp">
<Filter>Utilities</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Crypto\aes.h">
@ -1267,9 +1267,6 @@
<ClInclude Include="Emu\Event.h">
<Filter>Emu\SysCalls</Filter>
</ClInclude>
<ClInclude Include="..\Utilities\SSemaphore.h">
<Filter>Utilities</Filter>
</ClInclude>
<ClInclude Include="Emu\SysCalls\lv2\sys_process.h">
<Filter>Emu\SysCalls\lv2</Filter>
</ClInclude>
@ -1738,5 +1735,8 @@
<ClInclude Include="Emu\SysCalls\Modules\sceNpUtil.h">
<Filter>Emu\SysCalls\Modules</Filter>
</ClInclude>
<ClInclude Include="..\Utilities\Semaphore.h">
<Filter>Utilities</Filter>
</ClInclude>
</ItemGroup>
</Project>