#include #include "graphics.h" #include #include #include #include #include "avs/game.h" #include "cfg/icon.h" #include "cfg/screen_resize.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "launcher/shutdown.h" #include "overlay/overlay.h" #include "touch/touch.h" #include "util/detour.h" #include "util/logging.h" #include "util/fileutils.h" #include "util/utils.h" #include "util/time.h" struct CaptureData { std::shared_ptr data; unsigned short width, height; uint64_t timestamp; }; // icon static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); // state static WNDPROC WNDPROC_ORIG = nullptr; static std::vector WNDPROC_CUSTOM {}; static bool GRAPHICS_SCREENSHOT_TRIGGER = false; static std::set GRAPHICS_SCREENS { 0 }; static std::mutex GRAPHICS_SCREENS_M {}; static std::vector GRAPHICS_CAPTURE_SCREENS; static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4; static std::mutex GRAPHICS_CAPTURE_SCREENS_M {}; static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {}; // flag settings bool GRAPHICS_CAPTURE_CURSOR = false; bool GRAPHICS_LOG_HRESULT = false; bool GRAPHICS_SDVX_FORCE_720 = false; bool GRAPHICS_SHOW_CURSOR = false; bool GRAPHICS_WINDOWED = false; bool GRAPHICS_ADJUST_ORIENTATION = false; std::vector GRAPHICS_WINDOWS; UINT GRAPHICS_FORCE_REFRESH = 0; bool GRAPHICS_FORCE_SINGLE_ADAPTER = false; bool GRAPHICS_9_ON_12 = false; // settings std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146"; std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots"; static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr; static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr; static decltype(ClipCursor) *ClipCursor_orig = nullptr; static decltype(CreateWindowExA) *CreateWindowExA_orig = nullptr; static decltype(CreateWindowExW) *CreateWindowExW_orig = nullptr; static decltype(EnableWindow) *EnableWindow_orig = nullptr; static decltype(EnumDisplayDevicesA) *EnumDisplayDevicesA_orig = nullptr; static decltype(MoveWindow) *MoveWindow_orig = nullptr; static decltype(PeekMessageA) *PeekMessageA_orig = nullptr; static decltype(RegisterClassA) *RegisterClassA_orig = nullptr; static decltype(RegisterClassExA) *RegisterClassExA_orig = nullptr; static decltype(RegisterClassW) *RegisterClassW_orig = nullptr; static decltype(RegisterClassExW) *RegisterClassExW_orig = nullptr; static decltype(ShowCursor) *ShowCursor_orig = nullptr; static decltype(SetCursor) *SetCursor_orig = nullptr; static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr; static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr; static decltype(SetWindowPos) *SetWindowPos_orig = nullptr; static void reset_window_hook(HWND hWnd) { overlay::destroy(hWnd); if (WNDPROC_ORIG) { SetWindowLongPtrA(hWnd, GWLP_WNDPROC, (LONG_PTR) WNDPROC_ORIG); WNDPROC_ORIG = nullptr; } } // window procedure static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { // terminate if (uMsg == WM_CLOSE) { log_info("graphics", "detected WM_CLOSE, terminating..."); launcher::shutdown(0); return false; } // overlay specific if (overlay::OVERLAY) { switch (uMsg) { case WM_CHAR: { // input characters if overlay is active if (overlay::OVERLAY->has_focus()) { overlay::OVERLAY->input_char((unsigned int) wParam); return true; } break; } case WM_DESTROY: { auto wndproc = WNDPROC_ORIG; reset_window_hook(hWnd); return CallWindowProcA(wndproc, hWnd, uMsg, wParam, lParam); } case WM_SETCURSOR: { // set cursor back to the overlay one if (LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) { return true; } break; } default: break; } } // call custom procedures for (WNDPROC wndProc : WNDPROC_CUSTOM) { wndProc(hWnd, uMsg, wParam, lParam); } // capture mouse if (GRAPHICS_CAPTURE_CURSOR) { switch (uMsg) { case WM_SETFOCUS: { // capture mouse RECT WINDOW_RECT; GetWindowRect(hWnd, &WINDOW_RECT); ClipCursor(&WINDOW_RECT); return true; } case WM_KILLFOCUS: { // free mouse ClipCursor(0); return true; } default: break; } } // drop keydown messages switch (uMsg) { case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: return true; default: break; } // update the size of an emulated touch window is the window changes size or moves if (SPICETOUCH_TOUCH_HWND) { switch (uMsg) { case WM_MOVE: case WM_SIZE: { // get window rect RECT windowRect {}; GetWindowRect(hWnd, &windowRect); // adjust to client area POINT p {}; ClientToScreen(hWnd, &p); windowRect.top = p.y; int new_x = windowRect.left; int new_y = windowRect.top; int new_width = windowRect.right - new_x; int new_height = windowRect.bottom - new_y; log_misc("graphics", "detected window change ({}x{} @ {}, {} -> {}x{} @ {}, {}), updating touch window to match", SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y, new_width, new_height, new_x, new_y); SPICETOUCH_TOUCH_X = new_x; SPICETOUCH_TOUCH_Y = new_y; SPICETOUCH_TOUCH_WIDTH = new_width; SPICETOUCH_TOUCH_HEIGHT = new_height; SetWindowPos(SPICETOUCH_TOUCH_HWND, HWND_TOP, new_x, new_y, new_width, new_height, SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE); } default: break; } } // call default return CallWindowProcA(WNDPROC_ORIG, hWnd, uMsg, wParam, lParam); } static LONG WINAPI ChangeDisplaySettingsA_hook(DEVMODEA *lpDevMode, DWORD dwflags) { log_misc("graphics", "ChangeDisplaySettingsA hook hit"); // ignore display settings changes when running windowed if (GRAPHICS_WINDOWED) { return DISP_CHANGE_SUCCESSFUL; } // call original return ChangeDisplaySettingsA_orig(lpDevMode, dwflags); } static LONG WINAPI ChangeDisplaySettingsExA_hook(LPCSTR lpszDeviceName, DEVMODEA *lpDevMode, HWND hwnd, DWORD dwflags, LPVOID lParam) { log_misc("graphics", "ChangeDisplaySettingsExA hook hit"); // ignore display settings changes when running windowed if (GRAPHICS_WINDOWED) { return DISP_CHANGE_SUCCESSFUL; } // call original return ChangeDisplaySettingsExA_orig(lpszDeviceName, lpDevMode, hwnd, dwflags, lParam); } static BOOL WINAPI ClipCursor_hook(const RECT *lpRect) { log_misc("graphics", "ClipCursor hook hit"); // ignore cursor confine when having no explicit cursor confine if (!GRAPHICS_CAPTURE_CURSOR) { return TRUE; } // call original return ClipCursor_orig(lpRect); } static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam) { log_misc("graphics", "CreateWindowExA hook hit (0x{:08x}, {}, {}, 0x{:08x}, {}, {}, {}, {}, {}, {}, {}, {})", dwExStyle, fmt::ptr(lpClassName), lpWindowName != nullptr ? lpWindowName : "(null)", dwStyle, x, y, nWidth, nHeight, fmt::ptr(hWndParent), fmt::ptr(hMenu), fmt::ptr(hInstance), fmt::ptr(lpParam)); // set display orientation if (GRAPHICS_ADJUST_ORIENTATION) { if (nHeight > nWidth) { DEVMODE mode {}; // get display settings if (EnumDisplaySettings(0, ENUM_CURRENT_SETTINGS, &mode)) { log_misc("graphics", "rotating display"); mode.dmPelsWidth = nWidth; mode.dmPelsHeight = nHeight; mode.dmDisplayOrientation = DMDO_90; auto disp_res = ChangeDisplaySettings(&mode, CDS_FULLSCREEN); if (disp_res != DISP_CHANGE_SUCCESSFUL) { log_warning("graphics", "failed to change display settings: {}", disp_res); } } else { log_warning("graphics", "failed to get display settings"); } } } // call original HWND result = CreateWindowExA_orig(dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); GRAPHICS_WINDOWS.push_back(result); return result; } static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam) { log_misc("graphics", "CreateWindowExW hook hit ({:x}, {}, {}, {:x}, {}, {}, {}, {}, {}, {}, {}, {})", dwExStyle, fmt::ptr(lpClassName), lpWindowName != nullptr ? ws2s(lpWindowName) : "(null)", dwStyle, x, y, nWidth, nHeight, fmt::ptr(hWndParent), fmt::ptr(hMenu), fmt::ptr(hInstance), fmt::ptr(lpParam)); // DDR specific stuff if (avs::game::is_model("MDX")) { // set window name if (!lpWindowName) { lpWindowName = L"Dance Dance Revolution"; } // windowed mode adjustments if (GRAPHICS_WINDOWED) { // change window style dwExStyle = 0; dwStyle |= WS_OVERLAPPEDWINDOW; // adjust window size to include window decoration RECT rect {}; SetRect(&rect, 0, 0, 1280, 720); AdjustWindowRect(&rect, dwStyle, (hMenu != nullptr)); nWidth = rect.right - rect.left; nHeight = rect.bottom - rect.top; } } // DanEvo specific stuff if (avs::game::is_model("KDM")) { // set window name if (!lpWindowName) { lpWindowName = L"Dance Evolution"; } } // call original HWND result = CreateWindowExW_orig( dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); GRAPHICS_WINDOWS.push_back(result); return result; } static BOOL WINAPI EnableWindow_hook(HWND hWnd, BOOL bEnable) { return TRUE; } static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum, PDISPLAY_DEVICE lpDisplayDevice, DWORD dwFlags) { // call original BOOL value = EnumDisplayDevicesA_orig(lpDevice, iDevNum, lpDisplayDevice, dwFlags); // device ID override if (value) { memcpy(&lpDisplayDevice->DeviceID, GRAPHICS_DEVICEID.c_str(), GRAPHICS_DEVICEID.size() + 1); } // return original result return value; } static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) { log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})", fmt::ptr(hWnd), X, Y, nWidth, nHeight, bRepaint); // sound voltex windowed mode adjustments if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) { RECT rect {}; DWORD dwStyle; dwStyle = GetWindowLongA(hWnd, GWL_STYLE); // luckily, SDVX does not draw a menu. So we can leave the last // argument to `AdjustWindowRect` as `0`. SetRect(&rect, 0, 0, 720, 1280); AdjustWindowRect(&rect, dwStyle, 0); nWidth = rect.right - rect.left; nHeight = rect.bottom - rect.top; } // call original return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint); } static BOOL WINAPI PeekMessageA_hook( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) { // SDVX polls for messages too slowly if (avs::game::is_model("KFC")) { // process remaining messages BOOL ret; while ((ret = PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, PM_REMOVE)) != 0) { if (ret == -1) { return ret; } else { TranslateMessage(lpMsg); DispatchMessageA(lpMsg); } } // return no message return FALSE; } return PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg); } static int WINAPI ShowCursor_hook(BOOL bShow) { // prevent game from hiding cursor when option is enabled if (GRAPHICS_SHOW_CURSOR && !bShow) { return 1; } // call original return ShowCursor_orig(bShow); } static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) { if (GRAPHICS_SHOW_CURSOR && hCursor == NULL) { return GetCursor(); } return SetCursor_orig(hCursor); } static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) { // DDR window style fix if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { dwNewLong |= WS_OVERLAPPEDWINDOW; } // call original return SetWindowLongA_orig(hWnd, nIndex, dwNewLong); } static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { // DDR overlapped window fix if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { dwNewLong |= WS_OVERLAPPEDWINDOW; } // call original return SetWindowLongW_orig(hWnd, nIndex, dwNewLong); } static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags) { // windowed mode adjustments if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) { return TRUE; } // call original return SetWindowPos_orig(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags); } static ATOM WINAPI RegisterClassA_hook(const WNDCLASSA *lpWndClass) { // check for null if (!lpWndClass) { return RegisterClassA_orig(lpWndClass); } // copy struct and use own icon WNDCLASSA wnd = *lpWndClass; wnd.hIcon = WINDOW_ICON; // call original return RegisterClassA_orig(&wnd); } static ATOM WINAPI RegisterClassExA_hook(const WNDCLASSEXA *Arg1) { // check for null if (!Arg1) { return RegisterClassExA_orig(Arg1); } // copy struct and use own icon WNDCLASSEXA wnd = *Arg1; wnd.hIcon = WINDOW_ICON; wnd.hIconSm = WINDOW_ICON; // call original return RegisterClassExA_orig(&wnd); } static ATOM WINAPI RegisterClassW_hook(const WNDCLASSW *lpWndClass) { // check for null if (!lpWndClass) { return RegisterClassW_orig(lpWndClass); } // copy struct and use own icon WNDCLASSW wnd = *lpWndClass; wnd.hIcon = WINDOW_ICON; // call original return RegisterClassW_orig(&wnd); } static ATOM WINAPI RegisterClassExW_hook(const WNDCLASSEXW *Arg1) { // check for null if (!Arg1) { return RegisterClassExW_orig(Arg1); } // copy struct and use own icon WNDCLASSEXW wnd = *Arg1; wnd.hIcon = WINDOW_ICON; wnd.hIconSm = WINDOW_ICON; // call original return RegisterClassExW_orig(&wnd); } static HHOOK WINAPI SetWindowsHookExA_hook(int, HOOKPROC, HINSTANCE, DWORD) { log_misc("graphics", "SetWindowsHookExA hook hit"); // we don't do hooks return nullptr; } static BOOL WINAPI SetCursorPos_hook(int, int) { // prevent games from messing with the cursor position themselves return TRUE; } static int WINAPI MessageBoxA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { auto text = lpText != nullptr ? lpText : "(null)"; auto title = lpCaption != nullptr ? lpCaption : "(null)"; log_info("graphics", "MessageBoxA: {} - {}", title, text); return IDOK; } static int WINAPI MessageBoxExA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType, WORD wLanguageId) { auto text = lpText != nullptr ? lpText : "(null)"; auto title = lpCaption != nullptr ? lpCaption : "(null)"; log_info("graphics", "MessageBoxExA: {} - {}", title, text); return IDOK; } static int WINAPI MessageBoxW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) { auto text = lpText != nullptr ? lpText : L"(null)"; auto title = lpCaption != nullptr ? lpCaption : L"(null)"; log_info("graphics", "MessageBoxW: {} - {}", ws2s(title), ws2s(text)); return IDOK; } static int WINAPI MessageBoxExW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId) { auto text = lpText != nullptr ? lpText : L"(null)"; auto title = lpCaption != nullptr ? lpCaption : L"(null)"; log_info("graphics", "MessageBoxExW: {} - {}", ws2s(title), ws2s(text)); return IDOK; } void graphics_init() { log_info("graphics", "initializing"); // init screen resize log_info("ScreenResize", "initializing"); if(cfg::SCREENRESIZE == nullptr){ cfg::SCREENRESIZE = std::make_unique(); } // init backends graphics_d3d9_init(); // general hooks ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook); ChangeDisplaySettingsExA_orig = detour::iat_try("ChangeDisplaySettingsExA", ChangeDisplaySettingsExA_hook); ClipCursor_orig = detour::iat_try("ClipCursor", ClipCursor_hook); CreateWindowExA_orig = detour::iat_try("CreateWindowExA", CreateWindowExA_hook); CreateWindowExW_orig = detour::iat_try("CreateWindowExW", CreateWindowExW_hook); EnableWindow_orig = detour::iat_try("EnableWindow", EnableWindow_hook); EnumDisplayDevicesA_orig = detour::iat_try("EnumDisplayDevicesA", EnumDisplayDevicesA_hook); MoveWindow_orig = detour::iat_try("MoveWindow", MoveWindow_hook); PeekMessageA_orig = detour::iat_try("PeekMessageA", PeekMessageA_hook); RegisterClassA_orig = detour::iat_try("RegisterClassA", RegisterClassA_hook); RegisterClassExA_orig = detour::iat_try("RegisterClassExA", RegisterClassExA_hook); RegisterClassW_orig = detour::iat_try("RegisterClassW", RegisterClassW_hook); RegisterClassExW_orig = detour::iat_try("RegisterClassExW", RegisterClassExW_hook); ShowCursor_orig = detour::iat_try("ShowCursor", ShowCursor_hook); SetCursor_orig = detour::iat_try("SetCursor", SetCursor_hook); SetWindowLongA_orig = detour::iat_try("SetWindowLongA", SetWindowLongA_hook); SetWindowLongW_orig = detour::iat_try("SetWindowLongW", SetWindowLongW_hook); SetWindowPos_orig = detour::iat_try("SetWindowPos", SetWindowPos_hook); detour::iat_try("MessageBoxA", MessageBoxA_hook); detour::iat_try("MessageBoxExA", MessageBoxExA_hook); detour::iat_try("MessageBoxW", MessageBoxW_hook); detour::iat_try("MessageBoxExW", MessageBoxExW_hook); detour::iat_try("SetWindowsHookExA", SetWindowsHookExA_hook); detour::iat_try("SetCursorPos", SetCursorPos_hook); } void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters) { // update window size for a few games // TODO: make this work on everything if (pPresentationParameters != nullptr && GRAPHICS_WINDOWED && (avs::game::is_model({ "K39", "L39", "M39", "JMP", "LDJ" }))) { // check dimensions auto new_width = pPresentationParameters->BackBufferWidth; auto new_height = pPresentationParameters->BackBufferHeight; if (new_width != 0 && new_height != 0) { RECT rect {}; GetWindowRect(hWnd, &rect); auto width = rect.right - rect.left; auto height = rect.bottom - rect.top; log_info("graphics", "resized window: {}x{} -> {}x{}", width, height, new_width, new_height); DWORD dwStyle = GetWindowLongA(hWnd, GWL_STYLE); DWORD dwExStyle = GetWindowLongA(hWnd, GWL_EXSTYLE); HMENU menu = GetMenu(hWnd); SetRect(&rect, 0, 0, new_width, new_height); AdjustWindowRectEx(&rect, dwStyle, (menu != nullptr), dwExStyle); // make sure the window does not go off the screen if (rect.top < 0) { rect.bottom += -rect.top; rect.top = 0; } width = rect.right - rect.left; height = rect.bottom - rect.top; SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, width, height, 0); } } // show cursor if (GRAPHICS_SHOW_CURSOR) { ShowCursor(TRUE); } // capture mouse if (GRAPHICS_CAPTURE_CURSOR) { RECT rect {}; GetWindowRect(hWnd, &rect); ClipCursor(&rect); } // hook window procedure if (WNDPROC_ORIG == nullptr) { WNDPROC_ORIG = reinterpret_cast(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast(WindowProc)); } } void graphics_add_wnd_proc(WNDPROC wnd_proc) { WNDPROC_CUSTOM.push_back(wnd_proc); } void graphics_remove_wnd_proc(WNDPROC wndProc) { for (size_t x = 0; x < WNDPROC_CUSTOM.size(); x++) { if (WNDPROC_CUSTOM[x] == wndProc) { WNDPROC_CUSTOM.erase(WNDPROC_CUSTOM.begin() + x); } } } void graphics_screens_register(int screen) { std::lock_guard lock(GRAPHICS_SCREENS_M); GRAPHICS_SCREENS.insert(screen); } void graphics_screens_unregister(int screen) { std::lock_guard lock(GRAPHICS_SCREENS_M); GRAPHICS_SCREENS.erase(screen); } void graphics_screens_get(std::vector &screens) { std::lock_guard lock(GRAPHICS_SCREENS_M); screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end()); } void graphics_screenshot_trigger() { GRAPHICS_SCREENSHOT_TRIGGER = true; } bool graphics_screenshot_consume() { auto flag = GRAPHICS_SCREENSHOT_TRIGGER; GRAPHICS_SCREENSHOT_TRIGGER = false; return flag; } void graphics_capture_trigger(int screen) { std::lock_guard lock(GRAPHICS_CAPTURE_SCREENS_M); GRAPHICS_CAPTURE_SCREENS.push_back(screen); } bool graphics_capture_consume(int *screen) { auto flag = !GRAPHICS_CAPTURE_SCREENS.empty(); if (flag) { std::lock_guard lock(GRAPHICS_CAPTURE_SCREENS_M); *screen = GRAPHICS_CAPTURE_SCREENS.back(); GRAPHICS_CAPTURE_SCREENS.pop_back(); } return flag; } void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height) { GRAPHICS_CAPTURE_BUFFER_M[screen].lock(); auto &capture = GRAPHICS_CAPTURE_BUFFER[screen]; capture.data.reset(data); capture.width = width; capture.height = height; capture.timestamp = get_performance_milliseconds(); GRAPHICS_CAPTURE_BUFFER_M[screen].unlock(); GRAPHICS_CAPTURE_CV[screen].notify_one(); } void graphics_capture_skip(int screen) { GRAPHICS_CAPTURE_CV[screen].notify_one(); } bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp, int *width, int *height) { // wait for capture event std::unique_lock lock(GRAPHICS_CAPTURE_BUFFER_M[screen]); GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] { return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr; }); auto &capture = GRAPHICS_CAPTURE_BUFFER[screen]; auto capture_data = capture.data; auto capture_width = capture.width; auto capture_height = capture.height; auto capture_timestamp = capture.timestamp; lock.unlock(); // validate data if (!capture_data || !capture_width || !capture_height) { return false; } // divide image size if (divide > 1) { // get new resolution (round up) int width_new = (capture_width + divide - 1) / divide; int height_new = (capture_height + divide - 1) / divide; // allocate new data auto data_old = capture_data.get(); auto data_new = new uint8_t[width_new * height_new * 3]; // copy pixel data int data_y = 0; for (int y = 0; y < capture_height; y += divide) { int data_y_offset_old = y * capture_width; int data_y_offset = data_y * width_new; int data_x = 0; for (int x = 0; x < capture_width; x += divide) { auto pixel_new = &data_new[(data_x + data_y_offset) * 3]; auto pixel_old = &data_old[(data_y_offset_old + x) * 3]; memcpy(pixel_new, pixel_old, 3); data_x++; } data_y++; } // update capture data capture_data.reset(data_new); capture_width = width_new; capture_height = height_new; } // compress auto success = TooJpeg::writeJpeg( receiver, capture_data.get(), capture_width, capture_height, rgb, quality, downsample); // status if (timestamp) { *timestamp = capture_timestamp; } if (width) { *width = capture_width; } if (height) { *height = capture_height; } // clean up return success; } std::string graphics_screenshot_genpath() { // verify dir path if (GRAPHICS_SCREENSHOT_DIR.empty()) { return ""; } else { auto last_char = GRAPHICS_SCREENSHOT_DIR.back(); if (last_char == '\\' || last_char == '/') { GRAPHICS_SCREENSHOT_DIR.pop_back(); } } // ensure the output directory exists if (!fileutils::dir_exists(GRAPHICS_SCREENSHOT_DIR)) { if (!fileutils::dir_create_recursive(GRAPHICS_SCREENSHOT_DIR)) { log_warning("graphics", "could not create screenshot dir: {}", GRAPHICS_SCREENSHOT_DIR); return ""; } } // generate date prefix auto t_now = std::time(nullptr); auto tm_now = *std::gmtime(&t_now); auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d")); // find next filename size_t id = 0; while (true) { auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id); if (!fileutils::file_exists(filepath)) { return filepath; } id++; } }