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:
Jiongjia Lu
2026-09-13 00:51:23 +08:00
committed by GitHub
parent 4042b77065
commit 2b6e76730b
24 changed files with 1567 additions and 31 deletions
@@ -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