mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-05 01:38:12 -07:00
Dance around early support (#897)
## Link to GitHub Issue or related Pull Request, if one exists N/A ## Description of change This PR add initial supports for running Dance aRound in window mode, with keyboard, cardio supports. ## Testing - Tested game play under Windows 10 using Kinect V1 with my visionpose compatible layer (https://github.com/Avimitin/dance-around-anygear) https://github.com/user-attachments/assets/4b821dcf-751d-4953-b626-4a9491569183 - Tested full game play, user profile saving with Asphyxia Assisted by AI. --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -26,6 +26,7 @@ namespace d3d11_hooks {
|
||||
// ignore_hwnd.
|
||||
void note_main_hwnd(HWND hwnd);
|
||||
HWND main_hwnd();
|
||||
bool is_main_game_swapchain(IDXGISwapChain *swapchain);
|
||||
void ignore_hwnd(HWND hwnd);
|
||||
|
||||
// capture backbuffer to PNG if a screenshot was requested.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <dxgi1_2.h>
|
||||
|
||||
#include "d3d11_internal.h"
|
||||
#include "d3d11_swapchain_udn.h"
|
||||
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||
@@ -115,34 +116,6 @@ bool looks_like_game_window(HWND hwnd) {
|
||||
&& client.bottom > client.top;
|
||||
}
|
||||
|
||||
// only the main game window; ignore sub-screens / IME helpers.
|
||||
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HWND main = d3d11_hooks::main_hwnd();
|
||||
if (!main) {
|
||||
// no creation hook recorded a window, so fall back to the presenting one;
|
||||
// the choice is permanent, so require a plausible game window
|
||||
if (!looks_like_game_window(desc.OutputWindow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"graphics::d3d11",
|
||||
"try to notemain hwnd from swapchain present: 0x{:x}",
|
||||
(uintptr_t)desc.OutputWindow);
|
||||
|
||||
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
||||
|
||||
// it may have been ignored, or another thread may have won the slot
|
||||
main = d3d11_hooks::main_hwnd();
|
||||
}
|
||||
return desc.OutputWindow == main;
|
||||
}
|
||||
|
||||
// checks are ordered cheapest first, since this runs on every present
|
||||
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
if (!swapchain) {
|
||||
@@ -160,7 +133,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
}
|
||||
|
||||
// ignore sub windows
|
||||
if (!is_main_game_swapchain(swapchain)) {
|
||||
if (!d3d11_hooks::is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,7 +171,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
void pump_frame(IDXGISwapChain *swapchain) {
|
||||
const bool has_overlay =
|
||||
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
||||
if (!has_overlay && !d3d11_hooks::is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,8 +213,10 @@ HRESULT STDMETHODCALLTYPE Present_hook(
|
||||
{
|
||||
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||
d3d11_hooks::udn::schedule_window_attach(swapchain);
|
||||
try_create_overlay(swapchain);
|
||||
pump_frame(swapchain);
|
||||
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
|
||||
}
|
||||
return Present_orig(swapchain, SyncInterval, Flags);
|
||||
}
|
||||
@@ -251,8 +226,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
|
||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||
{
|
||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||
d3d11_hooks::udn::schedule_window_attach(swapchain);
|
||||
try_create_overlay(swapchain);
|
||||
pump_frame(swapchain);
|
||||
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
|
||||
}
|
||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||
}
|
||||
@@ -287,6 +264,33 @@ namespace {
|
||||
std::atomic<HWND> g_ignored_hwnd { nullptr };
|
||||
}
|
||||
|
||||
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HWND main = d3d11_hooks::main_hwnd();
|
||||
if (!main) {
|
||||
// no creation hook recorded a window, so fall back to the presenting one;
|
||||
// the choice is permanent, so require a plausible game window
|
||||
if (!looks_like_game_window(desc.OutputWindow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"graphics::d3d11",
|
||||
"try to notemain hwnd from swapchain present: 0x{:x}",
|
||||
(uintptr_t)desc.OutputWindow);
|
||||
|
||||
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
||||
|
||||
// it may have been ignored, or another thread may have won the slot
|
||||
main = d3d11_hooks::main_hwnd();
|
||||
}
|
||||
return desc.OutputWindow == main;
|
||||
}
|
||||
|
||||
void note_main_hwnd(HWND hwnd) {
|
||||
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
||||
return;
|
||||
@@ -326,6 +330,8 @@ void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
||||
}
|
||||
}
|
||||
|
||||
udn::install_swapchain_hooks(swapchain);
|
||||
|
||||
if (!g_swapchain1_hooked) {
|
||||
IDXGISwapChain1 *sc1 = nullptr;
|
||||
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// DANCE aROUND window attachment, frame pacing and display-mode overrides.
|
||||
#include "d3d11_backend.h"
|
||||
|
||||
#ifdef SPICE_D3D11
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d11.h>
|
||||
#include <dxgi.h>
|
||||
|
||||
#include "d3d11_internal.h"
|
||||
#include "d3d11_swapchain_udn.h"
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
namespace d3d11_hooks::udn {
|
||||
namespace {
|
||||
|
||||
using SetFullscreenState_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||
IDXGISwapChain *, BOOL, IDXGIOutput *);
|
||||
using ResizeTarget_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||
IDXGISwapChain *, const DXGI_MODE_DESC *);
|
||||
|
||||
SetFullscreenState_t SetFullscreenState_orig = nullptr;
|
||||
ResizeTarget_t ResizeTarget_orig = nullptr;
|
||||
|
||||
bool g_set_fullscreen_hooked = false;
|
||||
bool g_resize_target_hooked = false;
|
||||
// Present and Present1 share one waitable timer and frame deadline.
|
||||
std::mutex g_forced_present_pacing_mutex;
|
||||
std::chrono::steady_clock::time_point g_next_forced_present {};
|
||||
|
||||
// Unity creates and owns its top-level window on a different thread from the
|
||||
// D3D11 Present thread. Subclassing or resizing that HWND from Present can
|
||||
// synchronously wait for the owner thread and deadlock Unity. A one-time
|
||||
// message-queue hook performs graphics_hook_window on the HWND's owner thread.
|
||||
std::mutex g_window_attach_mutex;
|
||||
HHOOK g_window_attach_hook = nullptr;
|
||||
HWND g_window_attach_target = nullptr;
|
||||
bool g_window_attach_complete = false;
|
||||
|
||||
UINT window_attach_message() {
|
||||
static const UINT message = RegisterWindowMessageW(
|
||||
L"SpiceTools.D3D11.AttachGameWindow");
|
||||
return message;
|
||||
}
|
||||
|
||||
void pace_forced_present(UINT frame_rate) {
|
||||
static timeutils::PreciseSleepTimer timer;
|
||||
const auto period = std::chrono::nanoseconds(
|
||||
1'000'000'000ULL / static_cast<uint64_t>(frame_rate));
|
||||
std::lock_guard<std::mutex> lock(g_forced_present_pacing_mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (g_next_forced_present.time_since_epoch().count() == 0 ||
|
||||
now > g_next_forced_present + period) {
|
||||
g_next_forced_present = now + period;
|
||||
return;
|
||||
}
|
||||
if (now < g_next_forced_present) {
|
||||
timer.sleep(g_next_forced_present - now);
|
||||
}
|
||||
g_next_forced_present += period;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK window_attach_getmessage(
|
||||
int code, WPARAM w_param, LPARAM l_param) {
|
||||
HWND target = nullptr;
|
||||
HHOOK hook = nullptr;
|
||||
if (code >= 0 && w_param == PM_REMOVE) {
|
||||
const auto message = reinterpret_cast<const MSG *>(l_param);
|
||||
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
|
||||
if (!g_window_attach_complete && message->hwnd == g_window_attach_target &&
|
||||
message->message == window_attach_message()) {
|
||||
target = g_window_attach_target;
|
||||
hook = g_window_attach_hook;
|
||||
g_window_attach_hook = nullptr;
|
||||
g_window_attach_complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
const LRESULT result = CallNextHookEx(nullptr, code, w_param, l_param);
|
||||
if (target) {
|
||||
UnhookWindowsHookEx(hook);
|
||||
graphics_hook_window(target, nullptr);
|
||||
log_info(
|
||||
"graphics::d3d11",
|
||||
"attached Spice window hooks on owner thread for hwnd=0x{:x}",
|
||||
(uintptr_t) target);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE SetFullscreenState_hook(
|
||||
IDXGISwapChain *swapchain, BOOL Fullscreen, IDXGIOutput *Target) {
|
||||
if (Fullscreen && GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
|
||||
is_main_game_swapchain(swapchain)) {
|
||||
log_info(
|
||||
"graphics::d3d11",
|
||||
"blocked UDN request to enter DXGI exclusive fullscreen");
|
||||
return SetFullscreenState_orig(swapchain, FALSE, nullptr);
|
||||
}
|
||||
return SetFullscreenState_orig(swapchain, Fullscreen, Target);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ResizeTarget_hook(
|
||||
IDXGISwapChain *swapchain, const DXGI_MODE_DESC *NewTargetParameters) {
|
||||
if (GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
|
||||
is_main_game_swapchain(swapchain)) {
|
||||
if (NewTargetParameters) {
|
||||
log_info(
|
||||
"graphics::d3d11",
|
||||
"blocked UDN output-mode resize to {}x{}",
|
||||
NewTargetParameters->Width,
|
||||
NewTargetParameters->Height);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
return ResizeTarget_orig(swapchain, NewTargetParameters);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void schedule_window_attach(IDXGISwapChain *swapchain) {
|
||||
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
|
||||
!is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||
return;
|
||||
}
|
||||
const HWND hwnd = desc.OutputWindow;
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
|
||||
if (g_window_attach_complete || g_window_attach_hook != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (GRAPHICS_HOOKED_WINDOW.has_value()) {
|
||||
g_window_attach_complete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD process_id = 0;
|
||||
const DWORD thread_id = GetWindowThreadProcessId(hwnd, &process_id);
|
||||
if (!thread_id || process_id != GetCurrentProcessId()) {
|
||||
log_warning(
|
||||
"graphics::d3d11",
|
||||
"could not identify owner thread for hwnd=0x{:x}",
|
||||
(uintptr_t) hwnd);
|
||||
return;
|
||||
}
|
||||
|
||||
const HHOOK hook = SetWindowsHookExW(
|
||||
WH_GETMESSAGE, window_attach_getmessage, nullptr, thread_id);
|
||||
if (!hook) {
|
||||
log_warning(
|
||||
"graphics::d3d11",
|
||||
"could not schedule owner-thread window attach for hwnd=0x{:x}: {}",
|
||||
(uintptr_t) hwnd,
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
g_window_attach_target = hwnd;
|
||||
g_window_attach_hook = hook;
|
||||
if (!PostMessageW(hwnd, window_attach_message(), 0, 0)) {
|
||||
const DWORD error = GetLastError();
|
||||
g_window_attach_hook = nullptr;
|
||||
g_window_attach_target = nullptr;
|
||||
UnhookWindowsHookEx(hook);
|
||||
log_warning(
|
||||
"graphics::d3d11",
|
||||
"could not post owner-thread window attach for hwnd=0x{:x}: {}",
|
||||
(uintptr_t) hwnd,
|
||||
error);
|
||||
return;
|
||||
}
|
||||
|
||||
log_info(
|
||||
"graphics::d3d11",
|
||||
"scheduled Spice window attach on owner thread {} for hwnd=0x{:x}",
|
||||
thread_id,
|
||||
(uintptr_t) hwnd);
|
||||
}
|
||||
|
||||
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval) {
|
||||
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
|
||||
!is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
// Interval two gives 60 FPS on the cabinet's 120 Hz display, but only
|
||||
// 37.5 FPS on a 75 Hz desktop. Pace windowed mode with an explicit clock.
|
||||
// Fullscreen keeps the game's own presentation interval and flags.
|
||||
pace_forced_present(60);
|
||||
sync_interval = 0;
|
||||
}
|
||||
|
||||
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
||||
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN")) {
|
||||
return;
|
||||
}
|
||||
// The shared installer holds its mutex while calling this function.
|
||||
if (!g_set_fullscreen_hooked) {
|
||||
g_set_fullscreen_hooked = hook_vtbl(
|
||||
swapchain, 10, (void *) SetFullscreenState_hook,
|
||||
(void **) &SetFullscreenState_orig,
|
||||
"IDXGISwapChain::SetFullscreenState");
|
||||
}
|
||||
if (!g_resize_target_hooked) {
|
||||
g_resize_target_hooked = hook_vtbl(
|
||||
swapchain, 14, (void *) ResizeTarget_hook,
|
||||
(void **) &ResizeTarget_orig, "IDXGISwapChain::ResizeTarget");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace d3d11_hooks::udn
|
||||
|
||||
#endif // SPICE_D3D11
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef SPICE_D3D11
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
struct IDXGISwapChain;
|
||||
|
||||
namespace d3d11_hooks::udn {
|
||||
|
||||
void schedule_window_attach(IDXGISwapChain *swapchain);
|
||||
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval);
|
||||
void install_swapchain_hooks(IDXGISwapChain *swapchain);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user