From d0ae3f7d245a146b4ae1e75be6e0a383d6e4bdfc Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 31 May 2015 16:21:47 -0500 Subject: [PATCH] Break out the disassembler code from the WXWidgets UI. This cleans up some of the code between core and UI for disassembling and dumping code blocks. Should help the QT UI in bringing up its debug UI since it won't have to deal with this garbage now. --- Source/Core/Core/PowerPC/JitInterface.cpp | 107 +++++++--- Source/Core/Core/PowerPC/JitInterface.h | 3 + Source/Core/Core/PowerPC/Profiler.h | 13 +- Source/Core/DolphinWX/Debugger/JitWindow.cpp | 193 +------------------ Source/Core/DolphinWX/Debugger/JitWindow.h | 22 +-- Source/Core/UICommon/CMakeLists.txt | 3 +- Source/Core/UICommon/Disassembler.cpp | 176 +++++++++++++++++ Source/Core/UICommon/Disassembler.h | 17 ++ Source/Core/UICommon/UICommon.vcxproj | 4 +- 9 files changed, 300 insertions(+), 238 deletions(-) create mode 100644 Source/Core/UICommon/Disassembler.cpp create mode 100644 Source/Core/UICommon/Disassembler.h diff --git a/Source/Core/Core/PowerPC/JitInterface.cpp b/Source/Core/Core/PowerPC/JitInterface.cpp index e5dab22d75..f76011ec8a 100644 --- a/Source/Core/Core/PowerPC/JitInterface.cpp +++ b/Source/Core/Core/PowerPC/JitInterface.cpp @@ -116,20 +116,44 @@ namespace JitInterface void WriteProfileResults(const std::string& filename) { + ProfileStats prof_stats; + GetProfileResults(&prof_stats); + + File::IOFile f(filename, "w"); + if (!f) + { + PanicAlert("Failed to open %s", filename.c_str()); + return; + } + fprintf(f.GetHandle(), "origAddr\tblkName\tcost\ttimeCost\tpercent\ttimePercent\tOvAllinBlkTime(ms)\tblkCodeSize\n"); + for (auto& stat : prof_stats.block_stats) + { + std::string name = g_symbolDB.GetDescription(stat.addr); + double percent = 100.0 * (double)stat.cost / (double)prof_stats.cost_sum; + double timePercent = 100.0 * (double)stat.tick_counter / (double)prof_stats.timecost_sum; + fprintf(f.GetHandle(), "%08x\t%s\t%" PRIu64 "\t%" PRIu64 "\t%.2f\t%.2f\t%.2f\t%i\n", + stat.addr, name.c_str(), stat.cost, + stat.tick_counter, percent, timePercent, + (double)stat.tick_counter*1000.0/(double)prof_stats.countsPerSec, stat.block_size); + } + } + + void GetProfileResults(ProfileStats* prof_stats) + { + prof_stats->cost_sum = 0; + prof_stats->timecost_sum = 0; + prof_stats->block_stats.clear(); + prof_stats->block_stats.reserve(jit->GetBlockCache()->GetNumBlocks()); + // Can't really do this with no jit core available if (!jit) return; - PowerPC::CPUState old_state = PowerPC::GetState(); - if (old_state == PowerPC::CPUState::CPU_RUNNING) - PowerPC::Pause(); + Core::EState old_state = Core::GetState(); + if (old_state == Core::CORE_RUN) + Core::SetState(Core::CORE_PAUSE); - std::vector stats; - stats.reserve(jit->GetBlockCache()->GetNumBlocks()); - u64 cost_sum = 0; - u64 timecost_sum = 0; - u64 countsPerSec; - QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec); + QueryPerformanceFrequency((LARGE_INTEGER*)&prof_stats->countsPerSec); for (int i = 0; i < jit->GetBlockCache()->GetNumBlocks(); i++) { const JitBlock *block = jit->GetBlockCache()->GetBlock(i); @@ -138,37 +162,60 @@ namespace JitInterface u64 timecost = block->ticCounter; // Todo: tweak. if (block->runCount >= 1) - stats.emplace_back(i, cost); - cost_sum += cost; - timecost_sum += timecost; + prof_stats->block_stats.emplace_back(i, block->originalAddress, + cost, timecost, block->codeSize); + prof_stats->cost_sum += cost; + prof_stats->timecost_sum += timecost; } - sort(stats.begin(), stats.end()); - File::IOFile f(filename, "w"); - if (!f) + sort(prof_stats->block_stats.begin(), prof_stats->block_stats.end()); + if (old_state == Core::CORE_RUN) + Core::SetState(Core::CORE_RUN); + } + + int GetHostCode(u32* address, const u8** code, u32* code_size) + { + if (!jit) { - PanicAlert("Failed to open %s", filename.c_str()); - return; + *code_size = 0; + return 1; } - fprintf(f.GetHandle(), "origAddr\tblkName\tcost\ttimeCost\tpercent\ttimePercent\tOvAllinBlkTime(ms)\tblkCodeSize\n"); - for (auto& stat : stats) + + int block_num = jit->GetBlockCache()->GetBlockNumberFromStartAddress(*address); + if (block_num < 0) { - const JitBlock *block = jit->GetBlockCache()->GetBlock(stat.blockNum); - if (block) + for (int i = 0; i < 500; i++) { - std::string name = g_symbolDB.GetDescription(block->originalAddress); - double percent = 100.0 * (double)stat.cost / (double)cost_sum; - double timePercent = 100.0 * (double)block->ticCounter / (double)timecost_sum; - fprintf(f.GetHandle(), "%08x\t%s\t%" PRIu64 "\t%" PRIu64 "\t%.2f\t%.2f\t%.2f\t%i\n", - block->originalAddress, name.c_str(), stat.cost, - block->ticCounter, percent, timePercent, - (double)block->ticCounter*1000.0/(double)countsPerSec, block->codeSize); + block_num = jit->GetBlockCache()->GetBlockNumberFromStartAddress(*address - 4 * i); + if (block_num >= 0) + break; + } + + if (block_num >= 0) + { + JitBlock* block = jit->GetBlockCache()->GetBlock(block_num); + if (!(block->originalAddress <= *address && + block->originalSize + block->originalAddress >= *address)) + block_num = -1; + } + + // Do not merge this "if" with the above - block_num changes inside it. + if (block_num < 0) + { + *code_size = 0; + return 2; } } - if (old_state == PowerPC::CPUState::CPU_RUNNING) - PowerPC::Start(); + JitBlock* block = jit->GetBlockCache()->GetBlock(block_num); + + *code = (const u8*)jit->GetBlockCache()->GetCompiledCodeFromBlock(block_num); + + *code_size = block->codeSize; + *address = block->originalAddress; + return 0; } + bool HandleFault(uintptr_t access_address, SContext* ctx) { return jit->HandleFault(access_address, ctx); diff --git a/Source/Core/Core/PowerPC/JitInterface.h b/Source/Core/Core/PowerPC/JitInterface.h index 08208f2280..bc5601a769 100644 --- a/Source/Core/Core/PowerPC/JitInterface.h +++ b/Source/Core/Core/PowerPC/JitInterface.h @@ -8,6 +8,7 @@ #include "Common/ChunkFile.h" #include "Core/MachineContext.h" #include "Core/PowerPC/CPUCoreBase.h" +#include "Core/PowerPC/Profiler.h" namespace JitInterface { @@ -25,6 +26,8 @@ namespace JitInterface // Debugging void WriteProfileResults(const std::string& filename); + void GetProfileResults(ProfileStats* prof_stats); + int GetHostCode(u32* address, const u8** code, u32* code_size); // Memory Utilities bool HandleFault(uintptr_t access_address, SContext* ctx); diff --git a/Source/Core/Core/PowerPC/Profiler.h b/Source/Core/Core/PowerPC/Profiler.h index 3df533de29..e3785847fe 100644 --- a/Source/Core/Core/PowerPC/Profiler.h +++ b/Source/Core/Core/PowerPC/Profiler.h @@ -44,13 +44,24 @@ struct BlockStat { - BlockStat(int bn, u64 c) : blockNum(bn), cost(c) {} + BlockStat(int bn, u32 _addr, u64 c, u64 ticks, u32 size) : + blockNum(bn), addr(_addr), cost(c), tick_counter(ticks), block_size(size) {} int blockNum; + u32 addr; u64 cost; + u64 tick_counter; + u32 block_size; bool operator <(const BlockStat &other) const { return cost > other.cost; } }; +struct ProfileStats +{ + std::vector block_stats; + u64 cost_sum; + u64 timecost_sum; + u64 countsPerSec; +}; namespace Profiler { diff --git a/Source/Core/DolphinWX/Debugger/JitWindow.cpp b/Source/Core/DolphinWX/Debugger/JitWindow.cpp index 1e78d76eca..bf13cd7f08 100644 --- a/Source/Core/DolphinWX/Debugger/JitWindow.cpp +++ b/Source/Core/DolphinWX/Debugger/JitWindow.cpp @@ -7,14 +7,6 @@ #include // Bochs #include -#if defined(HAS_LLVM) -// PowerPC.h defines PC. -// This conflicts with a function that has an argument named PC -#undef PC -#include -#include -#endif - #include #include #include @@ -23,177 +15,12 @@ #include "Common/CommonTypes.h" #include "Common/GekkoDisassembler.h" -#include "Common/StringUtil.h" #include "Core/PowerPC/Gekko.h" #include "Core/PowerPC/PPCAnalyst.h" -#include "Core/PowerPC/JitCommon/JitBase.h" -#include "Core/PowerPC/JitCommon/JitCache.h" #include "DolphinWX/Globals.h" #include "DolphinWX/WxUtils.h" #include "DolphinWX/Debugger/JitWindow.h" - -#if defined(HAS_LLVM) -// This class declaration should be in the header -// Due to the conflict with the PC define and the function with PC as an argument -// it has to be in this file instead. -// Once that conflict is resolved this can be moved to the header -class HostDisassemblerLLVM : public HostDisassembler -{ -public: - HostDisassemblerLLVM(const std::string host_disasm, int inst_size = -1, const std::string cpu = ""); - ~HostDisassemblerLLVM() - { - if (m_can_disasm) - LLVMDisasmDispose(m_llvm_context); - } - -private: - bool m_can_disasm; - LLVMDisasmContextRef m_llvm_context; - int m_instruction_size; - - std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) override; -}; - -HostDisassemblerLLVM::HostDisassemblerLLVM(const std::string host_disasm, int inst_size, const std::string cpu) - : m_can_disasm(false), m_instruction_size(inst_size) -{ - LLVMInitializeAllTargetInfos(); - LLVMInitializeAllTargetMCs(); - LLVMInitializeAllDisassemblers(); - - m_llvm_context = LLVMCreateDisasmCPU(host_disasm.c_str(), cpu.c_str(), nullptr, 0, 0, nullptr); - - // Couldn't create llvm context - if (!m_llvm_context) - return; - - LLVMSetDisasmOptions(m_llvm_context, - LLVMDisassembler_Option_AsmPrinterVariant | - LLVMDisassembler_Option_PrintLatency); - - m_can_disasm = true; -} - -std::string HostDisassemblerLLVM::DisassembleHostBlock(const u8* code_start, const u32 code_size, u32 *host_instructions_count) -{ - if (!m_can_disasm) - return "(No LLVM context)"; - - u8* disasmPtr = (u8*)code_start; - const u8 *end = code_start + code_size; - - std::ostringstream x86_disasm; - while ((u8*)disasmPtr < end) - { - char inst_disasm[256]; - size_t inst_size = LLVMDisasmInstruction(m_llvm_context, disasmPtr, (u64)(end - disasmPtr), (u64)disasmPtr, inst_disasm, 256); - if (!inst_size) - { - x86_disasm << "Invalid inst:"; - - if (m_instruction_size != -1) - { - // If we are on an architecture that has a fixed instruction size - // We can continue onward past this bad instruction. - std::string inst_str = ""; - for (int i = 0; i < m_instruction_size; ++i) - inst_str += StringFromFormat("%02x", disasmPtr[i]); - - x86_disasm << inst_str << std::endl; - disasmPtr += m_instruction_size; - } - else - { - // We can't continue if we are on an architecture that has flexible instruction sizes - // Dump the rest of the block instead - std::string code_block = ""; - for (int i = 0; (disasmPtr + i) < end; ++i) - code_block += StringFromFormat("%02x", disasmPtr[i]); - - x86_disasm << code_block << std::endl; - break; - } - } - else - { - x86_disasm << inst_disasm << std::endl; - disasmPtr += inst_size; - } - - (*host_instructions_count)++; - } - - return x86_disasm.str(); -} -#endif - -std::string HostDisassembler::DisassembleBlock(u32* address, u32* host_instructions_count, u32* code_size) -{ - if (!jit) - { - *host_instructions_count = 0; - *code_size = 0; - return "(No JIT active)"; - } - - int block_num = jit->GetBlockCache()->GetBlockNumberFromStartAddress(*address); - if (block_num < 0) - { - for (int i = 0; i < 500; i++) - { - block_num = jit->GetBlockCache()->GetBlockNumberFromStartAddress(*address - 4 * i); - if (block_num >= 0) - break; - } - - if (block_num >= 0) - { - JitBlock* block = jit->GetBlockCache()->GetBlock(block_num); - if (!(block->originalAddress <= *address && - block->originalSize + block->originalAddress >= *address)) - block_num = -1; - } - - // Do not merge this "if" with the above - block_num changes inside it. - if (block_num < 0) - { - host_instructions_count = 0; - code_size = 0; - return "(No translation)"; - } - } - - JitBlock* block = jit->GetBlockCache()->GetBlock(block_num); - - const u8* code = (const u8*)jit->GetBlockCache()->GetCompiledCodeFromBlock(block_num); - - *code_size = block->codeSize; - *address = block->originalAddress; - return DisassembleHostBlock(code, block->codeSize, host_instructions_count); -} - -HostDisassemblerX86::HostDisassemblerX86() -{ - m_disasm.set_syntax_intel(); -} - -std::string HostDisassemblerX86::DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) -{ - u64 disasmPtr = (u64)code_start; - const u8* end = code_start + code_size; - - std::ostringstream x86_disasm; - while ((u8*)disasmPtr < end) - { - char inst_disasm[256]; - disasmPtr += m_disasm.disasm64(disasmPtr, disasmPtr, (u8*)disasmPtr, inst_disasm); - x86_disasm << inst_disasm << std::endl; - (*host_instructions_count)++; - } - - return x86_disasm.str(); -} +#include "UICommon/Disassembler.h" CJitWindow::CJitWindow(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name) @@ -219,16 +46,14 @@ CJitWindow::CJitWindow(wxWindow* parent, wxWindowID id, const wxPoint& pos, sizerSplit->Fit(this); sizerBig->Fit(this); -#if defined(_M_X86) && defined(HAS_LLVM) - m_disassembler.reset(new HostDisassemblerLLVM("x86_64-none-unknown")); -#elif defined(_M_X86) - m_disassembler.reset(new HostDisassemblerX86()); -#elif defined(_M_ARM_64) && defined(HAS_LLVM) - m_disassembler.reset(new HostDisassemblerLLVM("aarch64-none-unknown", 4, "cortex-a57")); -#elif defined(_M_ARM_32) && defined(HAS_LLVM) - m_disassembler.reset(new HostDisassemblerLLVM("armv7-none-unknown", 4, "cortex-a15")); +#if defined(_M_X86) + m_disassembler.reset(GetNewDisassembler("x86")); +#elif defined(_M_ARM_64) + m_disassembler.reset(GetNewDisassembler("aarch64")); +#elif defined(_M_ARM_32) + m_disassembler.reset(GetNewDisassembler("armv7")); #else - m_disassembler.reset(new HostDisassembler()); + m_disassembler.reset(GetNewDisassembler("UNK")); #endif } @@ -250,7 +75,7 @@ void CJitWindow::Compare(u32 em_address) u32 host_instructions_count = 0; u32 host_code_size = 0; std::string host_instructions_disasm; - host_instructions_disasm = m_disassembler->DisassembleBlock(&em_address, &host_instructions_count, &host_code_size); + host_instructions_disasm = DisassembleBlock(m_disassembler.get(), &em_address, &host_instructions_count, &host_code_size); x86_box->SetValue(host_instructions_disasm); diff --git a/Source/Core/DolphinWX/Debugger/JitWindow.h b/Source/Core/DolphinWX/Debugger/JitWindow.h index f9daeed550..e49bbe68af 100644 --- a/Source/Core/DolphinWX/Debugger/JitWindow.h +++ b/Source/Core/DolphinWX/Debugger/JitWindow.h @@ -4,7 +4,6 @@ #pragma once -#include // Bochs #include #include @@ -12,6 +11,7 @@ #include #include "Common/CommonTypes.h" +#include "UICommon/Disassembler.h" class wxButton; class wxListBox; @@ -26,26 +26,6 @@ public: void Update() override; }; -class HostDisassembler -{ -public: - virtual ~HostDisassembler() {} - std::string DisassembleBlock(u32* address, u32* host_instructions_count, u32* code_size); - -private: - virtual std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) { return "(No disassembler)"; } -}; - -class HostDisassemblerX86 : public HostDisassembler -{ -public: - HostDisassemblerX86(); - -private: - disassembler m_disasm; - - std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) override; -}; class CJitWindow : public wxPanel { diff --git a/Source/Core/UICommon/CMakeLists.txt b/Source/Core/UICommon/CMakeLists.txt index 59e9443da7..65630b6b8e 100644 --- a/Source/Core/UICommon/CMakeLists.txt +++ b/Source/Core/UICommon/CMakeLists.txt @@ -1,4 +1,5 @@ -set(SRCS UICommon.cpp) +set(SRCS Disassembler.cpp + UICommon.cpp) set(LIBS common) diff --git a/Source/Core/UICommon/Disassembler.cpp b/Source/Core/UICommon/Disassembler.cpp new file mode 100644 index 0000000000..795ff57307 --- /dev/null +++ b/Source/Core/UICommon/Disassembler.cpp @@ -0,0 +1,176 @@ +#include // Bochs + +#if defined(HAS_LLVM) +// PowerPC.h defines PC. +// This conflicts with a function that has an argument named PC +#undef PC +#include +#include +#endif + +#include "Common/StringUtil.h" + +#include "Core/PowerPC/JitInterface.h" +#include "Core/PowerPC/JitCommon/JitBase.h" +#include "Core/PowerPC/JitCommon/JitCache.h" + +#include "UICommon/Disassembler.h" + +class HostDisassemblerX86 : public HostDisassembler +{ +public: + HostDisassemblerX86(); + +private: + disassembler m_disasm; + + std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) override; +}; + +#if defined(HAS_LLVM) +class HostDisassemblerLLVM : public HostDisassembler +{ +public: + HostDisassemblerLLVM(const std::string host_disasm, int inst_size = -1, const std::string cpu = ""); + ~HostDisassemblerLLVM() + { + if (m_can_disasm) + LLVMDisasmDispose(m_llvm_context); + } + +private: + bool m_can_disasm; + LLVMDisasmContextRef m_llvm_context; + int m_instruction_size; + + std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) override; +}; + +HostDisassemblerLLVM::HostDisassemblerLLVM(const std::string host_disasm, int inst_size, const std::string cpu) + : m_can_disasm(false), m_instruction_size(inst_size) +{ + LLVMInitializeAllTargetInfos(); + LLVMInitializeAllTargetMCs(); + LLVMInitializeAllDisassemblers(); + + m_llvm_context = LLVMCreateDisasmCPU(host_disasm.c_str(), cpu.c_str(), nullptr, 0, 0, nullptr); + + // Couldn't create llvm context + if (!m_llvm_context) + return; + + LLVMSetDisasmOptions(m_llvm_context, + LLVMDisassembler_Option_AsmPrinterVariant | + LLVMDisassembler_Option_PrintLatency); + + m_can_disasm = true; +} + +std::string HostDisassemblerLLVM::DisassembleHostBlock(const u8* code_start, const u32 code_size, u32 *host_instructions_count) +{ + if (!m_can_disasm) + return "(No LLVM context)"; + + u8* disasmPtr = (u8*)code_start; + const u8 *end = code_start + code_size; + + std::ostringstream x86_disasm; + while ((u8*)disasmPtr < end) + { + char inst_disasm[256]; + size_t inst_size = LLVMDisasmInstruction(m_llvm_context, disasmPtr, (u64)(end - disasmPtr), (u64)disasmPtr, inst_disasm, 256); + if (!inst_size) + { + x86_disasm << "Invalid inst:"; + + if (m_instruction_size != -1) + { + // If we are on an architecture that has a fixed instruction size + // We can continue onward past this bad instruction. + std::string inst_str = ""; + for (int i = 0; i < m_instruction_size; ++i) + inst_str += StringFromFormat("%02x", disasmPtr[i]); + + x86_disasm << inst_str << std::endl; + disasmPtr += m_instruction_size; + } + else + { + // We can't continue if we are on an architecture that has flexible instruction sizes + // Dump the rest of the block instead + std::string code_block = ""; + for (int i = 0; (disasmPtr + i) < end; ++i) + code_block += StringFromFormat("%02x", disasmPtr[i]); + + x86_disasm << code_block << std::endl; + break; + } + } + else + { + x86_disasm << inst_disasm << std::endl; + disasmPtr += inst_size; + } + + (*host_instructions_count)++; + } + + return x86_disasm.str(); +} +#endif + +HostDisassemblerX86::HostDisassemblerX86() +{ + m_disasm.set_syntax_intel(); +} + +std::string HostDisassemblerX86::DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) +{ + u64 disasmPtr = (u64)code_start; + const u8* end = code_start + code_size; + + std::ostringstream x86_disasm; + while ((u8*)disasmPtr < end) + { + char inst_disasm[256]; + disasmPtr += m_disasm.disasm64(disasmPtr, disasmPtr, (u8*)disasmPtr, inst_disasm); + x86_disasm << inst_disasm << std::endl; + (*host_instructions_count)++; + } + + return x86_disasm.str(); +} + +HostDisassembler* GetNewDisassembler(const std::string& arch) +{ +#if defined(HAS_LLVM) + if (arch == "x86") + return new HostDisassemblerLLVM("x86_64-none-unknown"); + else if (arch == "aarch64") + return new HostDisassemblerLLVM("aarch64-none-unknown", 4, "cortex-a57"); + else if (arch == "armv7") + return new HostDisassemblerLLVM("armv7-none-unknown", 4, "cortex-a15"); +#elif defined(_M_X86) + if (arch == "x86") + new HostDisassemblerX86(); +#endif + return new HostDisassembler(); +} + +std::string DisassembleBlock(HostDisassembler* disasm, u32* address, u32* host_instructions_count, u32* code_size) +{ + const u8* code; + int res = JitInterface::GetHostCode(address, &code, code_size); + + if (res == 1) + { + *host_instructions_count = 0; + return "(No JIT active)"; + } + else if (res == 2) + { + host_instructions_count = 0; + return "(No translation)"; + } + return disasm->DisassembleHostBlock(code, *code_size, host_instructions_count); +} diff --git a/Source/Core/UICommon/Disassembler.h b/Source/Core/UICommon/Disassembler.h new file mode 100644 index 0000000000..af3cf6f388 --- /dev/null +++ b/Source/Core/UICommon/Disassembler.h @@ -0,0 +1,17 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include "Common/CommonTypes.h" + +class HostDisassembler +{ +public: + virtual ~HostDisassembler() {} + virtual std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, u32* host_instructions_count) { return "(No disassembler)"; } +}; + +HostDisassembler* GetNewDisassembler(const std::string& arch); +std::string DisassembleBlock(HostDisassembler* disasm, u32* address, u32* host_instructions_count, u32* code_size); diff --git a/Source/Core/UICommon/UICommon.vcxproj b/Source/Core/UICommon/UICommon.vcxproj index ca9a9e4dbc..cd542b24c1 100644 --- a/Source/Core/UICommon/UICommon.vcxproj +++ b/Source/Core/UICommon/UICommon.vcxproj @@ -44,11 +44,13 @@ + + - \ No newline at end of file +