diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..70290cf --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,657 @@ +#include +#include +#include + +#include +#include + +#include "TSFuncs.hpp" + +// Identifies the graphics adapter and only shrinks the shadow map when that +// adapter cannot back the size the quality setting asked for. Shader source +// is left alone: patching it in memory disabled the NVIDIA shader compile. + +static char gDescription[160] = "unknown"; +static const char *gVendor = "unknown"; +static const char *gDescriptionPtr = gDescription; +static int gDedicatedMB = 0; +static int gShadowCap = 8192; +static bool gHaveProfile = false; +static bool gScriptsReady = false; +static bool gPublished = false; +static bool gLogged = false; +static int *gShadowSize = 0; +static int *gShadowCount = 0; +static int gUserSize = 0; +static int gUserCount = 0; +static int gAppliedSize = -1; +static int gAppliedCount = -1; +static int gLimitSize = 0; +static int gLimitCount = 0; +static int gFrameRate = 0; +static int gInRegen = 0; +static bool gSwapHooked = false; +static bool gScaleActive = false; +static bool gWasPaused = false; + +static const double kFpsLow = 50.0; +static const double kFpsHigh = 70.0; +static const double kFpsUpMargin = 15.0; +static const double kLowHold = 1.0; +static const double kHighHold = 8.0; +static const double kCooldown = 2.5; +static const double kGainSettle = 2.5; +static const double kStepUpWatch = 1.0; +static const double kStepUpBan = 45.0; + +// The FPS gained by lowering shadows is not spare headroom. Raising the map +// again would give that cost back and the counter would jump. +static double gClock = 0.0; +static double gFpsBeforeStep = 0.0; +static double gStepGain = 0.0; +static double gGainReadyAt = 0.0; +static double gStepUpBanUntil = 0.0; +static double gStepUpWatchAt = 0.0; +static bool gHaveGain = false; +static bool gWatchingStepUp = false; + +typedef BOOL (WINAPI *SwapBuffersFn)(HDC); +static SwapBuffersFn wglSwapBuffersOriginal = 0; + +static const unsigned kVendorNvidia = 0x10DE; +static const unsigned kVendorAmd = 0x1002; +static const unsigned kVendorIntel = 0x8086; + +static int powerOfTwoFloor(int value) +{ + int pot = 256; + while (pot <= 32768 / 2 && pot * 2 <= value) + pot *= 2; + return pot; +} + +// regenerateShadowMapFBOs allocates one GL_TEXTURE_2D_ARRAY of +// GL_DEPTH_COMPONENT16: width * height * cascades * 2 bytes. 16384x4 is +// exactly 2 GiB. This 32-bit process was given that size and the driver +// returned out of memory, so the cap stays one byte under 2 GiB. 8192x4 +// is 512 MB. Also stay under a quarter of dedicated memory and inside +// GL_MAX_TEXTURE_SIZE. +static int maxShadowSize(int dedicatedMB, int layers, int glMax) +{ + int glCap = powerOfTwoFloor(glMax); + if (glCap < 1024) + glCap = 8192; + if (layers < 1) + layers = 4; + + long long budget = (1ll << 31) - 1; + if (dedicatedMB > 0) { + const long long quarter = (long long)dedicatedMB * 1024ll * 1024ll / 4ll; + if (quarter < budget) + budget = quarter; + } + + int size = 1024; + int best = 1024; + while (size <= glCap && size <= 32768) { + const long long bytes = (long long)size * (long long)size * (long long)layers * 2ll; + if (bytes > budget) + break; + best = size; + if (size > 32768 / 2) + break; + size *= 2; + } + return best; +} + +static bool textHas(const char *text, const char *needle) +{ + return text != 0 && needle != 0 && std::strstr(text, needle) != 0; +} + +static void applyProfile(unsigned vendorId, const char *description, int dedicatedMB) +{ + const char *vendor = "unknown"; + int cap = 8192; + + if (dedicatedMB > 0) + gDedicatedMB = dedicatedMB; + if (description != 0 && description[0] != '\0' && std::strcmp(description, "unknown") != 0) + std::snprintf(gDescription, sizeof gDescription, "%s", description); + + if (vendorId == kVendorNvidia || textHas(description, "NVIDIA")) { + vendor = "NVIDIA"; + cap = 8192; + } + else if (vendorId == kVendorAmd || textHas(description, "AMD") || textHas(description, "Radeon") || textHas(description, "ATI")) { + vendor = "AMD"; + cap = 4096; + } + else if (vendorId == kVendorIntel || textHas(description, "Intel")) { + vendor = "Intel"; + cap = gDedicatedMB >= 4096 ? 4096 : 1024; + if (gDedicatedMB >= 1536 && gDedicatedMB < 4096) + cap = 2048; + } + + if (gDedicatedMB > 0 && gDedicatedMB < 512) + cap = 1024; + else if (gDedicatedMB > 0 && gDedicatedMB < 1536 && cap > 2048) + cap = 2048; + + gVendor = vendor; + gShadowCap = cap; + gHaveProfile = vendorId != 0 || textHas(gDescription, "NVIDIA") || textHas(gDescription, "AMD") || textHas(gDescription, "Radeon") || textHas(gDescription, "Intel"); +} + +static void detectAdapter() +{ + IDXGIFactory *factory = 0; + if (FAILED(CreateDXGIFactory(IID_IDXGIFactory, (void **)&factory)) || factory == 0) + return; + + IDXGIAdapter *best = 0; + DXGI_ADAPTER_DESC bestDesc; + std::memset(&bestDesc, 0, sizeof bestDesc); + SIZE_T bestMemory = 0; + + for (UINT index = 0; index < 8; ++index) { + IDXGIAdapter *adapter = 0; + if (FAILED(factory->EnumAdapters(index, &adapter)) || adapter == 0) + break; + + DXGI_ADAPTER_DESC desc; + std::memset(&desc, 0, sizeof desc); + if (SUCCEEDED(adapter->GetDesc(&desc)) && desc.DedicatedVideoMemory >= bestMemory) { + if (best != 0) + best->Release(); + best = adapter; + bestDesc = desc; + bestMemory = desc.DedicatedVideoMemory; + } + else + adapter->Release(); + } + + if (best != 0) { + char name[128]; + WideCharToMultiByte(CP_UTF8, 0, bestDesc.Description, -1, name, sizeof name, 0, 0); + int dedicatedMB = (int)(bestDesc.DedicatedVideoMemory / (1024u * 1024u)); + applyProfile(bestDesc.VendorId, name, dedicatedMB); + best->Release(); + } + + factory->Release(); +} + +static void refineFromGl() +{ + if (!gHaveProfile) + detectAdapter(); + if (wglGetCurrentContext() == 0) + return; + + const char *vendor = (const char *)glGetString(GL_VENDOR); + const char *renderer = (const char *)glGetString(GL_RENDERER); + if (vendor != 0 || renderer != 0) { + const char *who = renderer != 0 ? renderer : vendor; + unsigned id = 0; + if (textHas(vendor, "NVIDIA") || textHas(renderer, "NVIDIA")) + id = kVendorNvidia; + else if (textHas(vendor, "AMD") || textHas(vendor, "ATI") || textHas(renderer, "Radeon")) + id = kVendorAmd; + else if (textHas(vendor, "Intel") || textHas(renderer, "Intel")) + id = kVendorIntel; + if (id != 0) + applyProfile(id, who, gDedicatedMB); + } + + for (int i = 0; i < 8; ++i) { + if (glGetError() == GL_NO_ERROR) + break; + } + + GLint maxTex = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTex); + + GLint dedicatedKB = 0; + glGetIntegerv(0x9047, &dedicatedKB); + if (glGetError() != GL_NO_ERROR) + dedicatedKB = 0; + if (dedicatedKB > 0) { + int nvMB = dedicatedKB / 1024; + if (nvMB > gDedicatedMB) + gDedicatedMB = nvMB; + } + + int layers = gUserCount > 0 ? gUserCount : 4; + int cap = maxShadowSize(gDedicatedMB, layers, (int)maxTex); + if (cap >= 1024) + gShadowCap = cap; +} + +static void publishVars() +{ + if (gPublished || !gScriptsReady) + return; + + tsf_AddVar("$ShaderFit::Vendor", &gVendor); + tsf_AddVar("$ShaderFit::Renderer", &gDescriptionPtr); + tsf_AddVar("$ShaderFit::DedicatedMB", &gDedicatedMB); + tsf_AddVar("$ShaderFit::ShadowCap", &gShadowCap); + tsf_AddVar("$ShaderFit::FrameRate", &gFrameRate); + gPublished = true; +} + +static bool varIsOff(const char *value) +{ + return value != 0 && (value[0] == '0' || std::strcmp(value, "false") == 0); +} + +// Off and Minimum both leave the shadow map at size 0. Minimum also keeps the +// shader name "minimum" while Enabled stays on. +static bool shaderModeUsesShadows() +{ + if (gScriptsReady) { + if (varIsOff(tsf_GetVar("$Shader::Enabled"))) + return false; + const char *name = tsf_GetVar("$Shader::ShaderName"); + if (name != 0 && std::strcmp(name, "minimum") == 0) + return false; + } + if (gShadowSize == 0 || gShadowCount == 0) + return false; + return *gShadowSize > 0 && *gShadowCount > 0; +} + +static void resetStepMemory() +{ + gFpsBeforeStep = 0.0; + gStepGain = 0.0; + gGainReadyAt = 0.0; + gStepUpBanUntil = 0.0; + gStepUpWatchAt = 0.0; + gHaveGain = false; + gWatchingStepUp = false; +} + +static void pauseShadowScaling() +{ + gLimitSize = 0; + gLimitCount = 0; + gScaleActive = false; + resetStepMemory(); + BlPrintf("%s: shadow scaling paused", PROJECT_NAME); +} + +static void rememberUserChoice() +{ + if (gShadowSize == 0 || gShadowCount == 0) + return; + + int size = *gShadowSize; + int count = *gShadowCount; + if (size == gAppliedSize && count == gAppliedCount) + return; + gUserSize = size; + gUserCount = count; + gLimitSize = 0; + gLimitCount = 0; + if (size <= 0) { + gLimitSize = 0; + gLimitCount = 0; + gAppliedSize = size; + gAppliedCount = count; + } +} + +static void applyShadowLevel() +{ + refineFromGl(); + publishVars(); + if (!gLogged) { + BlPrintf("%s: %s, %s, %d MB, shadow cap %d", + PROJECT_NAME, gVendor, gDescription, gDedicatedMB, gShadowCap); + gLogged = true; + } + if (gShadowSize == 0 || gShadowCount == 0) + return; + + rememberUserChoice(); + if (gUserSize <= 0) + return; + + int wantSize = gUserSize; + int wantCount = gUserCount > 0 ? gUserCount : *gShadowCount; + if (gLimitSize > 0) + wantSize = gLimitSize; + if (gLimitCount > 0) + wantCount = gLimitCount; + if (wantSize > gShadowCap && wantSize > gUserSize) + wantSize = gShadowCap; + if (wantCount < 1) + wantCount = 1; + + if (*gShadowSize != wantSize) + *gShadowSize = wantSize; + if (*gShadowCount != wantCount) + *gShadowCount = wantCount; + gAppliedSize = *gShadowSize; + gAppliedCount = *gShadowCount; +} + +static bool stepShadowsDown() +{ + int size = gLimitSize > 0 ? gLimitSize : gUserSize; + int count = gLimitCount > 0 ? gLimitCount : gUserCount; + if (size > 1024) { + int next = size / 2; + if (next < 1024) + next = 1024; + gLimitSize = next; + gLimitCount = count; + return true; + } + if (count > 1) { + gLimitSize = size; + gLimitCount = count - 1; + return true; + } + return false; +} + +static bool stepShadowsUp() +{ + int size = gLimitSize > 0 ? gLimitSize : gUserSize; + int count = gLimitCount > 0 ? gLimitCount : gUserCount; + if (count < gUserCount) + count += 1; + else if (size < gShadowCap) { + int next = size * 2; + if (next > gShadowCap) + next = gShadowCap; + if (next <= size) + return false; + size = next; + } + else + return false; + + if (size == gUserSize && count == gUserCount) { + gLimitSize = 0; + gLimitCount = 0; + } + else { + gLimitSize = size; + gLimitCount = count; + } + return true; +} + +static void rebuildShadows(); +static void regenerateFitted(); + +static void sampleFrameRate() +{ + static LARGE_INTEGER freq; + static LARGE_INTEGER last; + static bool haveLast = false; + static double avg = 60.0; + static double warm = 0.0; + static double lowHold = 0.0; + static double highHold = 0.0; + static double cooldown = 0.0; + + if (!shaderModeUsesShadows()) { + if (gScaleActive) { + pauseShadowScaling(); + gWasPaused = true; + } + haveLast = false; + warm = 0.0; + lowHold = 0.0; + highHold = 0.0; + cooldown = 0.0; + avg = 60.0; + return; + } + if (!gScaleActive) { + gScaleActive = true; + haveLast = false; + warm = 0.0; + lowHold = 0.0; + highHold = 0.0; + cooldown = 0.0; + avg = 60.0; + if (gWasPaused) { + gWasPaused = false; + BlPrintf("%s: shadow scaling resumed", PROJECT_NAME); + } + } + + if (freq.QuadPart == 0) + QueryPerformanceFrequency(&freq); + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + if (!haveLast) { + last = now; + haveLast = true; + return; + } + + double dt = (double)(now.QuadPart - last.QuadPart) / (double)freq.QuadPart; + last = now; + if (dt <= 0.0 || dt > 0.25) + return; + + warm += dt; + gClock += dt; + if (cooldown > 0.0) + cooldown -= dt; + avg += ((1.0 / dt) - avg) * 0.1; + gFrameRate = (int)(avg + 0.5); + if (gGainReadyAt > 0.0 && gClock >= gGainReadyAt) { + gStepGain = avg - gFpsBeforeStep; + if (gStepGain < 0.0) + gStepGain = 0.0; + gHaveGain = true; + gGainReadyAt = 0.0; + } + if (warm < 1.0 || cooldown > 0.0 || gUserSize <= 0 || gInRegen != 0) + return; + + if (gWatchingStepUp && gClock >= gStepUpWatchAt) { + if (avg < kFpsLow) { + gWatchingStepUp = false; + gStepUpBanUntil = gClock + kStepUpBan; + gStepGain = gFpsBeforeStep - avg; + if (gStepGain < kFpsUpMargin) + gStepGain = kFpsUpMargin; + gHaveGain = true; + gGainReadyAt = 0.0; + if (stepShadowsDown()) { + BlPrintf("%s: keeping this shadow level, raising it fell to %d fps", PROJECT_NAME, gFrameRate); + lowHold = 0.0; + highHold = 0.0; + cooldown = kCooldown; + rebuildShadows(); + } + return; + } + if (gClock >= gStepUpWatchAt + 3.0) { + gWatchingStepUp = false; + gHaveGain = true; + gStepGain = 0.0; + } + } + + if (avg < kFpsLow) { + lowHold += dt; + highHold = 0.0; + } + else if (avg > kFpsHigh) { + highHold += dt; + lowHold = 0.0; + } + else { + lowHold = 0.0; + highHold = 0.0; + } + + bool changed = false; + bool steppedDown = false; + if (lowHold >= kLowHold) { + changed = stepShadowsDown(); + steppedDown = changed; + } + else if (highHold >= kHighHold && gGainReadyAt == 0.0 && gClock >= gStepUpBanUntil + && (!gHaveGain || (avg - gStepGain) >= (kFpsLow + kFpsUpMargin))) { + changed = stepShadowsUp(); + } + if (!changed) + return; + + if (steppedDown) { + gFpsBeforeStep = avg; + gHaveGain = false; + gGainReadyAt = gClock + kGainSettle; + gWatchingStepUp = false; + } + else { + gFpsBeforeStep = avg; + gWatchingStepUp = true; + gStepUpWatchAt = gClock + kStepUpWatch; + gHaveGain = false; + gGainReadyAt = 0.0; + } + + lowHold = 0.0; + highHold = 0.0; + cooldown = kCooldown; + rebuildShadows(); +} + +static BOOL WINAPI wglSwapBuffersHook(HDC dc) +{ + BOOL ok = wglSwapBuffersOriginal(dc); + sampleFrameRate(); + return ok; +} + +static void installSwapHook() +{ + if (gSwapHooked) + return; + + HMODULE gl = GetModuleHandleA("opengl32.dll"); + void *target = gl == 0 ? 0 : (void *)GetProcAddress(gl, "wglSwapBuffers"); + if (target == 0) + return; + if (MH_CreateHook(target, (void *)wglSwapBuffersHook, (void **)&wglSwapBuffersOriginal) != MH_OK) + return; + if (MH_EnableHook(target) != MH_OK) + return; + gSwapHooked = true; +} + +BlFunctionDef(void, , regenerateShadowMapFBOs); +BlFunctionHookDef(regenerateShadowMapFBOs); + +static unsigned char *shaderEnabledFlag() +{ + unsigned char *base = reinterpret_cast(GetModuleHandle(NULL)); + return base + (0x7C2142 - 0x400000); +} + +static void regenerateFitted() +{ + unsigned char *enabled = shaderEnabledFlag(); + unsigned char wasEnabled = *enabled; + int tried = gShadowSize != 0 ? *gShadowSize : 0; + regenerateShadowMapFBOsOriginal(); + if (wasEnabled != 0 && *enabled == 0 && tried > 8192 && gShadowSize != 0) { + BlPrintf("%s: %d did not fit, keeping 8192", PROJECT_NAME, tried); + gShadowCap = 8192; + gLimitSize = 8192; + *gShadowSize = 8192; + gAppliedSize = 8192; + *enabled = 1; + regenerateShadowMapFBOsOriginal(); + } +} + +void regenerateShadowMapFBOsHook() +{ + if (gInRegen != 0) { + regenerateShadowMapFBOsOriginal(); + return; + } + gInRegen += 1; + installSwapHook(); + applyShadowLevel(); + regenerateFitted(); + gInRegen -= 1; +} + +static void rebuildShadows() +{ + if (regenerateShadowMapFBOsOriginal == 0 || gInRegen != 0) + return; + gInRegen += 1; + applyShadowLevel(); + int size = gShadowSize != 0 ? *gShadowSize : 0; + int count = gShadowCount != 0 ? *gShadowCount : 0; + BlPrintf("%s: %d fps, shadows %dx%d", PROJECT_NAME, gFrameRate, size, count); + regenerateFitted(); + gInRegen -= 1; +} + +bool init() +{ + if (!tsh_InitInternal()) + return true; + + gScriptsReady = tsf_InitInternal(); + + ADDR shadowFn = tsh_ScanFunctionHex((char *)"83 EC 08 56 8B 35 ? ? ? ? FF 15"); + if (shadowFn != 0) { + gShadowSize = *(int **)(shadowFn + 6); + gShadowCount = gShadowSize - 1; + regenerateShadowMapFBOs = (tsh_regenerateShadowMapFBOsFnT)shadowFn; + BlCreateHook(regenerateShadowMapFBOs); + BlTestEnableHook(regenerateShadowMapFBOs); + } + + BlPrintf("%s (v%s): loaded, waiting for the graphics device", PROJECT_NAME, PROJECT_VERSION); + return true; +} + +bool deinit() +{ + if (regenerateShadowMapFBOs != 0) + MH_DisableHook((void *)regenerateShadowMapFBOs); + if (gSwapHooked) { + void *target = (void *)GetProcAddress(GetModuleHandleA("opengl32.dll"), "wglSwapBuffers"); + if (target != 0) + MH_DisableHook(target); + } + return true; +} + +bool __stdcall DllMain(HINSTANCE hinstance, unsigned int reason, void *reserved) +{ + (void)hinstance; + (void)reserved; + switch (reason) { + case DLL_PROCESS_ATTACH: + return init(); + case DLL_PROCESS_DETACH: + return deinit(); + default: + return true; + } +} + +extern "C" void __declspec(dllexport) __cdecl PROJECT_EXPORT() {}