Files
ReSkate/Extension/UI/Overlay/overlay.cpp
T
2026-10-02 14:40:41 -05:00

434 lines
20 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "overlay_internal.h"
#include "input_capture.h"
#include "cursor.h"
#include "park_previews.h"
#include "chat_emotes.h"
using namespace dingosdk::overlay::detail;
namespace dingosdk::overlay::detail {
State& state() { static State* value = new State; return *value; }
thread_local bool inside_present = false;
bool install(Hook& hook, void* target, void* detour) {
if (hook.target) return hook.target == target;
if (!target) return false;
void* trampoline = nullptr;
const auto created = dingosdk::hook_prepare(target, detour, &trampoline);
if (created != dingosdk::HookOk) {
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics, "Hook creation failed: target=%p status=%ld (%s)", target,
static_cast<long>(created), dingosdk::hook_status_string(created));
return false;
}
hook.target = target;
hook.original = trampoline;
const auto enabled = dingosdk::hook_enable(target);
if (enabled != dingosdk::HookOk) {
const auto failure = dingosdk::hook_last_failure();
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::hooks,
"Hook activation failed: target=%p status=%ld (%s), operation=%s thread=%lu", target,
static_cast<long>(enabled), dingosdk::hook_status_string(enabled), failure.operation, failure.thread_id);
dingosdk::hook_remove(target);
hook = {};
return false;
}
return true;
}
bool is_input(UINT message) {
return (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST)
|| (message >= WM_KEYFIRST && message <= WM_KEYLAST)
|| message == WM_INPUT;
}
bool interactive_visible(const State& s) {
return s.visible.load() || s.console_visible.load() || s.editor_visible.load() || s.chat_visible.load();
}
void restore_input(bool hide_menu) {
auto& s = state();
if (hide_menu) {
s.visible.store(false);
s.console_visible.store(false);
s.chat_visible.store(false);
s.editor_visible.store(false);
s.editor_flight.store(false);
}
// Also release ownership during swapchain recreation and shutdown.
s.input_attached.store(false);
sync_menu_cursor(true);
request_window_cursor_sync(s.window.load());
// Keep the pinned forwarding link until HWND destruction, including while
// detached. The posted release must still reach its owning window thread;
// removing the WndProc here can otherwise strand mouse capture after resize.
}
void request_stop_locked() {
auto& s = state();
if (s.editor_visible.load() && s.callbacks.queue_debug) {
std::array<char, 256> result{};
s.callbacks.queue_debug(s.callbacks.user, {dingosdk::overlay::DebugAction::set_park_editor, false}, result.data(), result.size());
}
s.stop.store(true);
if (s.stop_event) SetEvent(s.stop_event);
restore_input(true);
}
void request_stop() {
auto& s = state();
std::lock_guard lock(s.render_mutex);
request_stop_locked();
}
HRESULT STDMETHODCALLTYPE present(IDXGISwapChain* chain, UINT interval, UINT flags) {
PresentGuard guard;
if (guard.outermost) guarded_render(chain, flags);
const HRESULT result = original<PresentFn>(state().present)(chain, interval, flags);
if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) {
ComPtr<ID3D12Device> device;
chain->GetDevice(IID_PPV_ARGS(&device));
record_device_failure(device.Get(), result, "Present");
request_stop();
}
return result;
}
HRESULT STDMETHODCALLTYPE present1(IDXGISwapChain1* chain, UINT interval, UINT flags,
const DXGI_PRESENT_PARAMETERS* parameters) {
PresentGuard guard;
if (guard.outermost) guarded_render(chain, flags);
const HRESULT result = original<Present1Fn>(state().present1)(chain, interval, flags, parameters);
if (result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET) {
ComPtr<ID3D12Device> device;
chain->GetDevice(IID_PPV_ARGS(&device));
record_device_failure(device.Get(), result, "Present1");
request_stop();
}
return result;
}
bool before_resize(IDXGISwapChain* chain) {
auto& s = state();
if (!s.swapchain) return true;
const auto candidate = object_identity(chain);
if (!candidate || candidate.Get() != s.swapchain_identity) return true;
if (!completed(s.next_fence, 500)) {
// Do not reset an allocator or release a buffer still used by the GPU.
// The caller can retry resizing without changing menu visibility.
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "Resize postponed: overlay GPU work has not completed.");
return false;
}
s.show_on_ready = s.visible.load();
destroy_graphics();
return true;
}
HRESULT STDMETHODCALLTYPE resize(IDXGISwapChain* chain, UINT count, UINT width, UINT height,
DXGI_FORMAT format, UINT flags) {
std::lock_guard lock(state().render_mutex);
if (!before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING;
return original<ResizeFn>(state().resize)(chain, count, width, height, format, flags);
}
HRESULT STDMETHODCALLTYPE resize1(IDXGISwapChain3* chain, UINT count, UINT width, UINT height,
DXGI_FORMAT format, UINT flags, const UINT* masks, IUnknown* const* queues) {
std::lock_guard lock(state().render_mutex);
const auto candidate = object_identity(chain);
if (!candidate || !find_binding(candidate.Get())) {
return original<Resize1Fn>(state().resize1)(
chain, count, width, height, format, flags, masks, queues);
}
const bool selected = candidate.Get() == state().swapchain_identity;
if (selected && !before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING;
// ResizeBuffers1 may change the presentation queue. A heterogeneous queue
// list cannot be rendered with a single queue. BufferCount == 0 retains the
// current count, so its queue array still describes every existing buffer.
ComPtr<ID3D12CommandQueue> replacement_queue;
bool unsupported_queues = false;
UINT queue_count = count;
if (queues && queue_count == 0) {
DXGI_SWAP_CHAIN_DESC description{};
if (FAILED(chain->GetDesc(&description))) unsupported_queues = true;
else queue_count = description.BufferCount;
}
if (queues && !unsupported_queues) {
if (queue_count == 0 || queue_count > DXGI_MAX_SWAP_CHAIN_BUFFERS || !queues[0]
|| FAILED(queues[0]->QueryInterface(IID_PPV_ARGS(&replacement_queue)))
|| replacement_queue->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT) {
unsupported_queues = true;
replacement_queue.Reset();
} else {
const auto first_identity = object_identity(replacement_queue.Get());
if (!first_identity) unsupported_queues = true;
for (UINT i = 1; i < queue_count && !unsupported_queues; ++i) {
ComPtr<ID3D12CommandQueue> entry;
const auto entry_identity = queues[i] ? object_identity(queues[i]) : nullptr;
if (!entry_identity || entry_identity.Get() != first_identity.Get()
|| FAILED(queues[i]->QueryInterface(IID_PPV_ARGS(&entry)))
|| entry->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT)
unsupported_queues = true;
}
if (unsupported_queues) replacement_queue.Reset();
}
}
const auto result = original<Resize1Fn>(state().resize1)(
chain, count, width, height, format, flags, masks, queues);
// DXGI owns the queue transition. Commit the candidate only when the real
// resize accepts it; a failed resize keeps the previous presentation queue.
if (SUCCEEDED(result) && replacement_queue) {
if (selected) state().queue = replacement_queue;
if (auto* binding = find_binding(candidate.Get()))
binding->queue = replacement_queue;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "ResizeBuffers1 accepted presentation queue change: chain=%p queue=%p buffers=%u",
chain, replacement_queue.Get(), queue_count);
} else if (SUCCEEDED(result) && unsupported_queues) {
dingosdk::logging::printf(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "ResizeBuffers1 accepted an unsupported presentation queue list; stopping overlay drawing");
request_stop_locked();
}
return result;
}
HRESULT STDMETHODCALLTYPE create(IDXGIFactory* factory, IUnknown* device,
DXGI_SWAP_CHAIN_DESC* description, IDXGISwapChain** output) {
DXGI_SWAP_CHAIN_DESC windowed{};
if (state().force_windowed && description && !description->Windowed) {
windowed = *description;
windowed.Windowed = TRUE;
description = &windowed;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: changed initial swapchain fullscreen request to windowed");
}
const HRESULT result = original<CreateFn>(state().create)(factory, device, description, output);
if (SUCCEEDED(result) && output && *output && device) {
ComPtr<ID3D12CommandQueue> queue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&queue)))) DingoSDKOverlayBindDx12(*output, queue.Get());
}
return result;
}
HRESULT STDMETHODCALLTYPE create_hwnd(IDXGIFactory2* factory, IUnknown* device, HWND window,
const DXGI_SWAP_CHAIN_DESC1* description, const DXGI_SWAP_CHAIN_FULLSCREEN_DESC* fullscreen,
IDXGIOutput* restrict_output, IDXGISwapChain1** output) {
DXGI_SWAP_CHAIN_FULLSCREEN_DESC windowed{};
if (state().force_windowed && fullscreen && !fullscreen->Windowed) {
windowed = *fullscreen;
windowed.Windowed = TRUE;
fullscreen = &windowed;
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: changed initial HWND swapchain fullscreen request to windowed");
}
const HRESULT result = original<CreateHwndFn>(state().create_hwnd)(factory, device, window,
description, fullscreen, restrict_output, output);
if (SUCCEEDED(result) && output && *output && device) {
ComPtr<ID3D12CommandQueue> queue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&queue)))) DingoSDKOverlayBindDx12(*output, queue.Get());
}
return result;
}
HRESULT STDMETHODCALLTYPE set_fullscreen(IDXGISwapChain* chain, BOOL fullscreen, IDXGIOutput* output) {
if (state().force_windowed && fullscreen) {
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Windowed startup: redirected SetFullscreenState(TRUE) to windowed");
fullscreen = FALSE;
output = nullptr;
}
return original<FullscreenFn>(state().fullscreen)(chain, fullscreen, output);
}
HRESULT WINAPI factory(REFIID iid, void** output) {
const HRESULT result = original<FactoryFn>(state().factory)(iid, output);
if (FAILED(result) || !output || !*output || state().stop.load()) return result;
ComPtr<IDXGIFactory> base;
auto* object = static_cast<IUnknown*>(*output);
if (FAILED(object->QueryInterface(IID_PPV_ARGS(&base)))) return result;
std::lock_guard lock(state().hook_mutex);
auto** table = *reinterpret_cast<void***>(base.Get());
const bool basic = install(state().create, table[10], reinterpret_cast<void*>(create));
ComPtr<IDXGIFactory2> extended;
bool hwnd = true;
if (SUCCEEDED(base.As(&extended))) {
table = *reinterpret_cast<void***>(extended.Get());
hwnd = install(state().create_hwnd, table[15], reinterpret_cast<void*>(create_hwnd));
}
if (!basic || !hwnd) dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "DXGI factory differs from the hooked implementation; unsupported path ignored.");
return result;
}
bool install_factory_hook_locked() {
auto& s = state();
if (s.factory.target) return true;
constexpr const wchar_t* providers[] = {L"sl.interposer.dll", L"dxgi.dll"};
for (const auto* provider : providers) {
const HMODULE module = GetModuleHandleW(provider);
if (!module) continue;
const auto address = reinterpret_cast<void*>(GetProcAddress(module, "CreateDXGIFactory1"));
if (address && install(s.factory, address, reinterpret_cast<void*>(factory))) return true;
}
return false;
}
void CALLBACK dll_notification(ULONG reason, const void*, void*) {
// LDR_DLL_NOTIFICATION_REASON_LOADED is 1. Do only the loader-lock-safe
// event signal here; the worker performs module inspection and hook setup.
if (reason == 1 && state().provider_event) SetEvent(state().provider_event);
}
DWORD WINAPI factory_bootstrap(void*) noexcept {
auto& s = state();
while (!s.stop.load()) {
{
std::lock_guard lock(s.hook_mutex);
if (s.stop.load()) return 0;
if (install_factory_hook_locked()) {
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "DXGI provider hooked; waiting for a DX12 swapchain.");
return 0;
}
}
const HANDLE events[] = {s.stop_event, s.provider_event};
const DWORD count = s.provider_event ? 2 : 1;
if (WaitForMultipleObjects(count, events, FALSE, 25) == WAIT_OBJECT_0) return 0;
}
return 0;
}
bool start_overlay(const dingosdk::overlay::CallbacksV3* callbacks) {
auto& s = state();
std::lock_guard lock(s.hook_mutex);
if (s.started || s.stop.load()) return false;
dingosdk::overlay::initialize_graphics_diagnostics();
s.force_windowed = dingosdk::overlay::graphics_option_enabled(L"RESKATE_FORCE_WINDOWED");
{
wchar_t game[32768]{};
const auto length = GetModuleFileNameW(nullptr, game, 32768);
if (length && length < 32768)
{
dingosdk::overlay::start_park_previews(std::filesystem::path(std::wstring(game, length)).parent_path());
dingosdk::overlay::start_chat_emotes();
}
}
HMODULE pinned = nullptr;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
reinterpret_cast<LPCWSTR>(&start_overlay), &pinned)) return false;
const auto status = dingosdk::hook_initialize();
if (status != dingosdk::HookOk && status != dingosdk::HookAlreadyInitialized) return false;
const auto user32 = GetModuleHandleW(L"user32.dll");
if (!user32 || !install(s.clip_cursor, reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor")),
reinterpret_cast<void*>(clip_cursor)) ||
!install(s.set_cursor_pos, reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")),
reinterpret_cast<void*>(set_cursor_pos)) || !install_input_capture()) return false;
if (!s.stop_event) s.stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!s.stop_event) return false;
if (callbacks) s.callbacks = *callbacks;
s.started = true;
if (install_factory_hook_locked()) {
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "Waiting for the game's DX12 swapchain creation.");
return true;
}
if (!s.provider_event) s.provider_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (s.provider_event && !s.dll_notification_cookie) {
const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
const auto register_notification = ntdll
? reinterpret_cast<RegisterDllNotificationFn>(
GetProcAddress(ntdll, "LdrRegisterDllNotification"))
: nullptr;
if (register_notification)
register_notification(0, dll_notification, nullptr, &s.dll_notification_cookie);
}
const HANDLE thread = CreateThread(nullptr, 0, factory_bootstrap, nullptr, 0, nullptr);
if (!thread) {
s.started = false;
s.callbacks = {};
return false;
}
CloseHandle(thread);
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "DXGI provider not loaded yet; deferred hook bootstrap started.");
return true;
}
}
extern "C" bool DingoSDKOverlayStart(const dingosdk::overlay::Callbacks* callbacks) {
dingosdk::overlay::CallbacksV3 upgraded;
if (callbacks) {
upgraded.user = callbacks->user;
upgraded.read_model = callbacks->read_model;
upgraded.queue_load = callbacks->queue_load;
upgraded.queue_debug = callbacks->queue_debug;
}
return start_overlay(&upgraded);
}
extern "C" bool DingoSDKOverlayStartV2(const dingosdk::overlay::CallbacksV2* callbacks) {
dingosdk::overlay::CallbacksV3 upgraded;
if (callbacks) {
upgraded.user = callbacks->user;
upgraded.read_model = callbacks->read_model;
upgraded.queue_load = callbacks->queue_load;
upgraded.queue_debug = callbacks->queue_debug;
upgraded.queue_offline_feature = callbacks->queue_offline_feature;
}
return start_overlay(&upgraded);
}
extern "C" bool DingoSDKOverlayStartV3(const dingosdk::overlay::CallbacksV3* callbacks) {
return start_overlay(callbacks);
}
extern "C" bool DingoSDKOverlayBindDx12(IDXGISwapChain* chain, ID3D12CommandQueue* queue) {
if (!chain || !queue || queue->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT) return false;
auto& s = state();
std::lock_guard render_lock(s.render_mutex);
std::lock_guard hook_lock(s.hook_mutex);
if (!s.started || s.stop.load()) return false;
DXGI_SWAP_CHAIN_DESC description{};
if (FAILED(chain->GetDesc(&description)) || !description.OutputWindow
|| description.BufferCount < 2 || description.BufferCount > 8) return false;
DWORD process = 0;
GetWindowThreadProcessId(description.OutputWindow, &process);
if (process != GetCurrentProcessId()) return false;
ComPtr<IDXGISwapChain3> extended;
if (FAILED(chain->QueryInterface(IID_PPV_ARGS(&extended)))) return false;
const auto identity = object_identity(chain);
if (!identity) return false;
auto** base = *reinterpret_cast<void***>(chain);
auto** table = *reinterpret_cast<void***>(extended.Get());
// COM method indices are the Windows SDK's IDXGISwapChain[1/3] ABI.
if (!install(s.present, base[8], reinterpret_cast<void*>(present))
|| (s.force_windowed && !install(s.fullscreen, base[10], reinterpret_cast<void*>(set_fullscreen)))
|| !install(s.resize, base[13], reinterpret_cast<void*>(resize))
|| !install(s.present1, table[22], reinterpret_cast<void*>(present1))
|| !install(s.resize1, table[39], reinterpret_cast<void*>(resize1))) {
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "Swapchain hook setup incomplete; candidate ignored.");
return false;
}
if (auto* existing = find_binding(identity.Get())) {
existing->queue = queue;
existing->window = description.OutputWindow;
if (s.swapchain_identity == identity.Get()) s.queue = queue;
return true;
}
if (s.bindings.size() >= maximum_bindings) {
const auto evict = std::find_if(s.bindings.begin(), s.bindings.end(),
[&](const Binding& candidate) { return candidate.identity != s.swapchain_identity; });
if (evict == s.bindings.end()) return false;
s.bindings.erase(evict);
}
s.bindings.push_back({identity.Get(), queue, description.OutputWindow});
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "Registered DX12 swapchain and its creation queue.");
return true;
}
extern "C" void DingoSDKOverlayRequestStop() {
request_stop();
}
bool dingosdk::overlay::keyboard_shortcuts_allowed() noexcept {
auto &s = state();
return s.started.load() && !s.stop.load() && !interactive_visible(s);
}
extern "C" void DingoSDKOverlayGetStatus(dingosdk::overlay::Status* output) {
if (!output) return;
auto& s = state();
std::lock_guard lock(s.render_mutex);
*output = {s.started.load(), s.swapchain != nullptr, s.ready, s.visible.load(),
s.stop.load(), s.failed.load(), s.rendered_frames};
}