| /* |
| * Copyright © 2019 Intel Corporation |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a |
| * copy of this software and associated documentation files (the "Software"), |
| * to deal in the Software without restriction, including without limitation |
| * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| * and/or sell copies of the Software, and to permit persons to whom the |
| * Software is furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice (including the next |
| * paragraph) shall be included in all copies or substantial portions of the |
| * Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| * IN THE SOFTWARE. |
| */ |
| |
| #include "glmemshadow.hpp" |
| |
| #include <unordered_map> |
| #include <algorithm> |
| |
| #include <assert.h> |
| |
| #ifdef _WIN32 |
| |
| #include <windows.h> |
| |
| #else |
| |
| #include <unistd.h> |
| #include <signal.h> |
| #include <sys/mman.h> |
| |
| #endif |
| |
| #include "gltrace.hpp" |
| #include "os_thread.hpp" |
| #include "os.hpp" |
| |
| static bool sInitialized = false; |
| |
| static std::unordered_map<size_t, GLMemoryShadow*> sPages; |
| static size_t sPageSize; |
| |
| static os::mutex mutex; |
| |
| enum class MemProtection { |
| #ifdef _WIN32 |
| NO_ACCESS = PAGE_NOACCESS, |
| READ_ONLY = PAGE_READONLY, |
| READ_WRITE = PAGE_READWRITE, |
| #else |
| NO_ACCESS = PROT_NONE, |
| READ_ONLY = PROT_READ, |
| READ_WRITE = PROT_READ | PROT_WRITE, |
| #endif |
| }; |
| |
| size_t getSystemPageSize() { |
| #ifdef _WIN32 |
| SYSTEM_INFO info; |
| GetSystemInfo(&info); |
| return info.dwPageSize; |
| #else |
| return sysconf(_SC_PAGESIZE); |
| #endif |
| } |
| |
| void memProtect(void *addr, size_t size, MemProtection protection) { |
| #ifdef _WIN32 |
| DWORD flOldProtect; |
| BOOL bRet = VirtualProtect(addr, size, static_cast<DWORD>(protection), &flOldProtect); |
| if (!bRet) { |
| DWORD dwLastError = GetLastError(); |
| os::log("apitrace: error: VirtualProtect failed with error 0x%lx\n", dwLastError); |
| os::abort(); |
| } |
| #else |
| const int err = mprotect(addr, size, static_cast<int>(protection)); |
| if (err) { |
| const char *errorStr = strerror(err); |
| os::log("apitrace: error: mprotect failed with error \"%s\"\n", errorStr); |
| os::abort(); |
| } |
| #endif |
| } |
| |
| template<typename T, typename U> |
| auto divRoundUp(T a, U b) -> decltype(a / b) { |
| return (a + b - 1) / b; |
| } |
| |
| #ifdef _WIN32 |
| static LONG CALLBACK |
| VectoredHandler(PEXCEPTION_POINTERS pExceptionInfo) |
| { |
| PEXCEPTION_RECORD pExceptionRecord = pExceptionInfo->ExceptionRecord; |
| DWORD ExceptionCode = pExceptionRecord->ExceptionCode; |
| |
| if (ExceptionCode == EXCEPTION_ACCESS_VIOLATION && |
| pExceptionRecord->NumberParameters >= 2 && |
| pExceptionRecord->ExceptionInformation[0] == 1) { // writing |
| |
| const uintptr_t addr = static_cast<uintptr_t>(pExceptionRecord->ExceptionInformation[1]); |
| const size_t page = addr / sPageSize; |
| |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| const auto it = sPages.find(page); |
| if (it != sPages.end()) { |
| GLMemoryShadow *shadow = it->second; |
| shadow->onAddressWrite(addr, page); |
| return EXCEPTION_CONTINUE_EXECUTION; |
| } else { |
| os::log("apitrace: error: %s: access violation at non-tracked page\n", __FUNCTION__); |
| os::abort(); |
| } |
| } |
| |
| return EXCEPTION_CONTINUE_SEARCH; |
| } |
| |
| #else |
| |
| void PageGuardExceptionHandler(int sig, siginfo_t *si, void *unused) { |
| if (sig == SIGSEGV && si->si_code == SEGV_ACCERR) { |
| const uintptr_t addr = reinterpret_cast<uintptr_t>(si->si_addr); |
| const size_t page = addr / sPageSize; |
| |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| const auto it = sPages.find(page); |
| if (it != sPages.end()) { |
| GLMemoryShadow *shadow = it->second; |
| shadow->onAddressWrite(addr, page); |
| } else { |
| os::log("apitrace: error: %s: access violation at non-tracked page\n", __FUNCTION__); |
| os::abort(); |
| } |
| } |
| } |
| #endif |
| |
| void initializeGlobals() |
| { |
| sPageSize = getSystemPageSize(); |
| |
| #ifdef _WIN32 |
| if (AddVectoredExceptionHandler(1, VectoredHandler) == NULL) { |
| os::log("apitrace: error: %s: add vectored exception handler failed\n", __FUNCTION__); |
| } |
| #else |
| struct sigaction sa, oldSa; |
| sa.sa_flags = SA_SIGINFO; |
| sigemptyset(&sa.sa_mask); |
| sa.sa_sigaction = PageGuardExceptionHandler; |
| if (sigaction(SIGSEGV, &sa, &oldSa) == -1) { |
| os::log("apitrace: error: %s: set page guard exception handler failed\n", __FUNCTION__); |
| } |
| #endif |
| } |
| |
| GLMemoryShadow::~GLMemoryShadow() |
| { |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| const size_t startPage = reinterpret_cast<uintptr_t>(shadowMemory) / sPageSize; |
| for (size_t i = 0; i < nPages; i++) { |
| sPages.erase(startPage + i); |
| } |
| |
| #ifdef _WIN32 |
| VirtualFree(shadowMemory, nPages * sPageSize, MEM_RELEASE); |
| #else |
| munmap(shadowMemory, nPages * sPageSize); |
| #endif |
| } |
| |
| bool GLMemoryShadow::init(const void *data, size_t size) |
| { |
| if (!sInitialized) { |
| initializeGlobals(); |
| sInitialized = true; |
| } |
| |
| nPages = divRoundUp(size, sPageSize); |
| const size_t adjustedSize = nPages * sPageSize; |
| |
| #ifdef _WIN32 |
| shadowMemory = reinterpret_cast<uint8_t*>(VirtualAlloc(nullptr, adjustedSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); |
| #else |
| shadowMemory = reinterpret_cast<uint8_t*>(mmap(nullptr, adjustedSize, PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); |
| #endif |
| |
| if (!shadowMemory) { |
| os::log("apitrace: error: %s: Failed to allocate shadow memory!\n", __FUNCTION__); |
| return false; |
| } |
| |
| if (data != nullptr) { |
| memcpy(shadowMemory, data, size); |
| } |
| |
| memProtect(shadowMemory, adjustedSize, MemProtection::NO_ACCESS); |
| |
| { |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| const size_t startPage = reinterpret_cast<uintptr_t>(shadowMemory) / sPageSize; |
| for (size_t i = 0; i < nPages; i++) { |
| sPages.emplace(startPage + i, this); |
| } |
| } |
| |
| dirtyPages.resize(divRoundUp(nPages, 32)); |
| |
| return true; |
| } |
| |
| void *GLMemoryShadow::map(gltrace::Context *_ctx, void *_glMemory, GLbitfield _flags, size_t start, size_t size) |
| { |
| sharedRes = _ctx->sharedRes; |
| glMemory = reinterpret_cast<uint8_t*>(_glMemory); |
| flags = _flags; |
| mappedStart = start; |
| mappedSize = size; |
| |
| mappedStartPage = start / sPageSize; |
| mappedEndPage = divRoundUp(start + size, sPageSize); |
| |
| uint8_t *protectStart = shadowMemory + mappedStartPage * sPageSize; |
| const size_t protectSize = (mappedEndPage - mappedStartPage) * sPageSize; |
| |
| // The buffer may have been updated before the mapping. |
| // TODO: handle write only buffers |
| if (flags & GL_MAP_READ_BIT) { |
| memProtect(protectStart, protectSize, MemProtection::READ_WRITE); |
| memcpy(shadowMemory + start, glMemory, size); |
| } |
| |
| memProtect(protectStart, protectSize, MemProtection::READ_ONLY); |
| |
| return shadowMemory + start; |
| } |
| |
| void GLMemoryShadow::unmap(Callback callback) |
| { |
| if (isDirty) { |
| os::unique_lock<os::mutex> lock(mutex); |
| commitWrites(callback); |
| } |
| |
| { |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| shared_context_res_ptr_t res = sharedRes.lock(); |
| if (res) { |
| auto it = std::find(res->dirtyShadows.begin(), res->dirtyShadows.end(), this); |
| if (it != res->dirtyShadows.end()) { |
| res->dirtyShadows.erase(it); |
| } |
| } else { |
| os::log("apitrace: error: %s: context(s) are destroyed!\n", __FUNCTION__); |
| } |
| } |
| |
| memProtect(shadowMemory, nPages * sPageSize, MemProtection::NO_ACCESS); |
| |
| sharedRes.reset(); |
| glMemory = nullptr; |
| flags = 0; |
| mappedStart = 0; |
| mappedSize = 0; |
| pagesToDirtyOnConsecutiveWrites = 1; |
| } |
| |
| void GLMemoryShadow::onAddressWrite(uintptr_t addr, size_t page) |
| { |
| const size_t relativePage = (addr - reinterpret_cast<uintptr_t>(shadowMemory)) / sPageSize; |
| if (isPageDirty(relativePage)) { |
| // It is possible if writing to the same buffer from two threads |
| return; |
| } |
| |
| if ((relativePage == lastDirtiedRelativePage + 1) && isPageDirty(relativePage - 1)) { |
| /* Ensure that we would have log(n) page exceptions if traced application writes |
| * to n consecutive pages. |
| */ |
| pagesToDirtyOnConsecutiveWrites *= 2; |
| } else { |
| pagesToDirtyOnConsecutiveWrites = 1; |
| } |
| |
| const size_t endPageToDirty = std::min(relativePage + pagesToDirtyOnConsecutiveWrites, nPages); |
| for (size_t pageToDirty = relativePage; pageToDirty < endPageToDirty; pageToDirty++) { |
| setPageDirty(pageToDirty); |
| } |
| |
| lastDirtiedRelativePage = endPageToDirty - 1; |
| |
| memProtect(reinterpret_cast<void*>(page * sPageSize), |
| (endPageToDirty - relativePage) * sPageSize, MemProtection::READ_WRITE); |
| } |
| |
| GLbitfield GLMemoryShadow::getMapFlags() const |
| { |
| return flags; |
| } |
| |
| void GLMemoryShadow::setPageDirty(size_t relativePage) |
| { |
| assert(relativePage < nPages); |
| dirtyPages[relativePage / 32] |= 1U << (relativePage % 32); |
| |
| if (!isDirty) { |
| shared_context_res_ptr_t res = sharedRes.lock(); |
| if (res) { |
| res->dirtyShadows.push_back(this); |
| isDirty = true; |
| } else { |
| os::log("apitrace: error: %s: context(s) are destroyed!\n", __FUNCTION__); |
| } |
| } |
| } |
| |
| bool GLMemoryShadow::isPageDirty(size_t relativePage) |
| { |
| assert(relativePage < nPages); |
| return dirtyPages[relativePage / 32] & (1U << (relativePage % 32)); |
| } |
| |
| void GLMemoryShadow::commitWrites(Callback callback) |
| { |
| assert(isDirty); |
| |
| uint8_t *shadowSlice = shadowMemory + mappedStartPage * sPageSize; |
| const size_t glStartOffset = mappedStart % sPageSize; |
| |
| /* Other thread may write to the buffers at this very moment |
| * so we need to protect pages before we read from them. |
| * The other thread will have to wait until we commit all writes we want. |
| */ |
| for (size_t i = mappedStartPage; i < mappedEndPage; i++) { |
| if (isPageDirty(i)) { |
| memProtect(shadowMemory + i * sPageSize, sPageSize, MemProtection::READ_ONLY); |
| } |
| } |
| |
| for (size_t i = mappedStartPage; i < mappedEndPage; i++) { |
| if (isPageDirty(i)) { |
| // We coalesce consecutive writes into one |
| size_t firstDirty = i; |
| while (++i < mappedEndPage && isPageDirty(i)) { } |
| |
| const size_t pages = i - firstDirty; |
| if (firstDirty != mappedStartPage) { |
| const size_t shadowOffset = (firstDirty - mappedStartPage) * sPageSize; |
| const size_t glOffset = shadowOffset - glStartOffset; |
| const size_t size = std::min(glStartOffset + mappedSize - shadowOffset, sPageSize * pages); |
| |
| memcpy(glMemory + glOffset, shadowSlice + shadowOffset, size); |
| callback(shadowSlice + shadowOffset, size); |
| } else { |
| const size_t size = std::min(sPageSize * pages - glStartOffset, mappedSize); |
| |
| memcpy(glMemory, shadowSlice + glStartOffset, size); |
| callback(shadowSlice + glStartOffset, size); |
| } |
| } |
| } |
| |
| std::fill(dirtyPages.begin(), dirtyPages.end(), 0); |
| isDirty = false; |
| pagesToDirtyOnConsecutiveWrites = 1; |
| lastDirtiedRelativePage = UINT32_MAX - 1; |
| } |
| |
| void GLMemoryShadow::updateForReads() |
| { |
| uint8_t *protectStart = shadowMemory + mappedStartPage * sPageSize; |
| const size_t protectSize = (mappedEndPage - mappedStartPage) * sPageSize; |
| |
| memProtect(protectStart, protectSize, MemProtection::READ_WRITE); |
| |
| memcpy(shadowMemory + mappedStart, glMemory, mappedSize); |
| |
| memProtect(protectStart, protectSize, MemProtection::READ_ONLY); |
| } |
| |
| void GLMemoryShadow::commitAllWrites(gltrace::Context *_ctx, Callback callback) |
| { |
| if (!_ctx->sharedRes->dirtyShadows.empty()) { |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| for (GLMemoryShadow *memoryShadow : _ctx->sharedRes->dirtyShadows) { |
| memoryShadow->commitWrites(callback); |
| } |
| |
| _ctx->sharedRes->dirtyShadows.clear(); |
| } |
| } |
| |
| void GLMemoryShadow::syncAllForReads(gltrace::Context *_ctx) |
| { |
| if (!_ctx->sharedRes->bufferToShadowMemory.empty()) { |
| os::unique_lock<os::mutex> lock(mutex); |
| |
| for (auto& it : _ctx->sharedRes->bufferToShadowMemory) { |
| GLMemoryShadow* memoryShadow = it.second.get(); |
| if (memoryShadow->getMapFlags() & GL_MAP_READ_BIT) { |
| memoryShadow->updateForReads(); |
| } |
| } |
| } |
| } |