blob: 4dfaf85e402dfa9fbf8e888c52bcc36f8acdf449 [file] [log] [blame]
/*
* 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();
}
}
}
}