mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-09-29 02:08:03 +03:00
sdk: add get_module_info, get_plugin_directory, d3d9 callbacks (#908)
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change To make plugin development easier, add `get_module_info` and `get_plugin_directory` to deal with common tasks for hooking DLLs and reading INI files. Also, add the ability to register for d3d9 present callbacks via `register_d3d9`, which makes it significantly easier for plugins to draw things on screen. ## Testing See sample code.
This commit is contained in:
@@ -719,6 +719,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
reader/crypt.cpp
|
||||
|
||||
# sdk
|
||||
sdk/d3d9.cpp
|
||||
sdk/modules.cpp
|
||||
sdk/sdk.cpp
|
||||
|
||||
# stubs
|
||||
@@ -1047,8 +1049,16 @@ if(NOT MSVC)
|
||||
endif()
|
||||
|
||||
# sdk_sample_v0_cpp.dll (64 bit)
|
||||
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
|
||||
set(SOURCE_FILES
|
||||
sdk/sample/v0/cpp/v0_cpp.cpp
|
||||
sdk/sample/v0/cpp/v0_cpp_imgui.cpp
|
||||
external/imgui/imgui.cpp
|
||||
external/imgui/imgui_draw.cpp
|
||||
external/imgui/imgui_tables.cpp
|
||||
external/imgui/imgui_widgets.cpp
|
||||
external/imgui/backends/imgui_impl_dx9.cpp)
|
||||
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
|
||||
target_link_libraries(spicetools_sdk_sample_v0_cpp_64 PRIVATE imm32)
|
||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "sdk/d3d9.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/flags_helper.h"
|
||||
@@ -1511,6 +1512,8 @@ void graphics_d3d9_on_present(
|
||||
device->EndScene();
|
||||
}
|
||||
|
||||
sdk::d3d9::draw(hFocusWindow, device);
|
||||
|
||||
// after the overlay render so the screenshot includes toasts / menus
|
||||
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "sdk/d3d9.h"
|
||||
#include "util/flags_helper.h"
|
||||
#include "util/utils.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
@@ -136,6 +137,7 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
||||
|
||||
// release owned objects if there are no more references
|
||||
if (local_refs == 0) {
|
||||
sdk::d3d9::destroy(this->pReal);
|
||||
if (this->main_swapchain) {
|
||||
this->main_swapchain->Release();
|
||||
this->main_swapchain = nullptr;
|
||||
@@ -632,6 +634,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||
}
|
||||
}
|
||||
|
||||
sdk::d3d9::invalidate(pReal);
|
||||
|
||||
// reset overlay
|
||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
@@ -643,6 +647,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||
d3d9_readback::discard_snapshot_targets(pReal);
|
||||
|
||||
HRESULT res = pReal->Reset(pPresentationParameters);
|
||||
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
|
||||
|
||||
// recreate overlay
|
||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
||||
@@ -673,7 +678,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
|
||||
gfdm_small_head.compose(this);
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
|
||||
const HRESULT result = pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
|
||||
sdk::d3d9::present_complete(pReal, result);
|
||||
CHECK_RESULT(result);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
||||
@@ -2088,8 +2095,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::PresentEx(
|
||||
|
||||
graphics_d3d9_on_present(hFocusWindow, pReal, this);
|
||||
|
||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
|
||||
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
|
||||
const HRESULT result = static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
|
||||
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
|
||||
sdk::d3d9::present_complete(pReal, result);
|
||||
CHECK_RESULT(result);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGPUThreadPriority(
|
||||
@@ -2336,6 +2345,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
||||
}
|
||||
}
|
||||
|
||||
sdk::d3d9::invalidate(pReal);
|
||||
|
||||
// reset overlay
|
||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
@@ -2350,6 +2361,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
||||
gfdm_parameters.presentation_parameters,
|
||||
gfdm_parameters.fullscreen_display_modes);
|
||||
|
||||
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
|
||||
|
||||
if (is_gfdm_two_head_exclusive()
|
||||
&& SUCCEEDED(res)
|
||||
&& gfdm_parameters.recovery_candidate)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "sdk/d3d9.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "d3d9_device.h"
|
||||
@@ -558,6 +559,7 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||
temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate;
|
||||
temporary_modes[1] = alternate_small;
|
||||
|
||||
sdk::d3d9::invalidate(device);
|
||||
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
|
||||
const bool temporary_settled =
|
||||
temporary_result == D3D_OK
|
||||
@@ -574,6 +576,8 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||
restore_result == D3D_OK
|
||||
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]);
|
||||
|
||||
sdk::d3d9::reset_complete(device, SUCCEEDED(restore_result));
|
||||
|
||||
if (temporary_result != D3D_OK) {
|
||||
return temporary_result;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "sdk/d3d9.h"
|
||||
|
||||
#include "d3d9_backend.h"
|
||||
#include "d3d9_device.h"
|
||||
@@ -140,6 +141,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
||||
result = recovery_failure;
|
||||
}
|
||||
}
|
||||
if (should_run_hooks) {
|
||||
sdk::d3d9::present_complete(pDev->pReal, result);
|
||||
}
|
||||
CHECK_RESULT(result);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
@@ -396,7 +397,12 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
|
||||
// terminate
|
||||
if (uMsg == WM_CLOSE) {
|
||||
log_info("graphics", "detected WM_CLOSE, terminating...");
|
||||
launcher::shutdown(0);
|
||||
static std::once_flag shutdown_requested;
|
||||
std::call_once(shutdown_requested, [] {
|
||||
std::thread([] {
|
||||
launcher::shutdown(0);
|
||||
}).detach();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2908,7 +2908,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
bt5api_dispose();
|
||||
}
|
||||
|
||||
sdk::fini_sdk_modules();
|
||||
sdk::fini_sdk_modules(true);
|
||||
|
||||
// stop raw input
|
||||
hotkeys::disable_raw_input();
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
#include "d3d9.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace sdk::d3d9 {
|
||||
namespace {
|
||||
|
||||
struct Renderer {
|
||||
spice_sdk_d3d9_callback_func *callback;
|
||||
void *userdata;
|
||||
bool ready = false;
|
||||
bool started = false;
|
||||
};
|
||||
|
||||
// keep registration separate from serialized device access and callback execution.
|
||||
std::mutex registry_mutex;
|
||||
std::recursive_mutex execution_mutex;
|
||||
std::condition_variable_any shutdown_condition;
|
||||
std::vector<std::shared_ptr<Renderer>> renderers;
|
||||
|
||||
SPICE_SDK_D3D9_FRAME frame{sizeof(SPICE_SDK_D3D9_FRAME), nullptr, nullptr, 0, 0};
|
||||
bool stopped = false;
|
||||
bool resetting = false;
|
||||
bool device_lost = false;
|
||||
bool reset_in_progress = false;
|
||||
bool shutdown_complete = false;
|
||||
DWORD device_thread = 0;
|
||||
|
||||
// reject reentrant SDK graphics calls made by a renderer callback.
|
||||
thread_local bool dispatching = false;
|
||||
struct DispatchScope {
|
||||
DispatchScope() {
|
||||
dispatching = true;
|
||||
}
|
||||
|
||||
~DispatchScope() {
|
||||
dispatching = false;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Interface>
|
||||
struct ComReference {
|
||||
Interface *value = nullptr;
|
||||
|
||||
~ComReference() {
|
||||
if (value) {
|
||||
value->Release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// restore game state after each renderer, including early exits during setup.
|
||||
struct GraphicsState {
|
||||
IDirect3DDevice9 *device;
|
||||
ComReference<IDirect3DStateBlock9> block;
|
||||
std::array<ComReference<IDirect3DSurface9>, 4> targets;
|
||||
ComReference<IDirect3DSurface9> depth;
|
||||
D3DVIEWPORT9 viewport{};
|
||||
std::array<D3DMATRIX, 3> transforms{};
|
||||
std::array<bool, 3> have_transform{};
|
||||
DWORD target_count = 0;
|
||||
bool captured = false;
|
||||
|
||||
explicit GraphicsState(IDirect3DDevice9 *device) : device(device) {
|
||||
D3DCAPS9 caps{};
|
||||
if (FAILED(device->GetDeviceCaps(&caps)) ||
|
||||
FAILED(device->CreateStateBlock(D3DSBT_ALL, &block.value)) ||
|
||||
FAILED(block.value->Capture()) || FAILED(device->GetViewport(&viewport))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// state blocks do not capture render targets or the depth surface.
|
||||
target_count = std::min<DWORD>(caps.NumSimultaneousRTs, targets.size());
|
||||
for (DWORD index = 0; index < target_count; ++index) {
|
||||
const auto status = device->GetRenderTarget(index, &targets[index].value);
|
||||
if (FAILED(status) && status != D3DERR_NOTFOUND) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto status = device->GetDepthStencilSurface(&depth.value);
|
||||
if (FAILED(status) && status != D3DERR_NOTFOUND) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
|
||||
for (size_t index = 0; index < kinds.size(); ++index) {
|
||||
have_transform[index] = SUCCEEDED(
|
||||
device->GetTransform(kinds[index], &transforms[index]));
|
||||
}
|
||||
captured = true;
|
||||
}
|
||||
|
||||
~GraphicsState() {
|
||||
if (!captured) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unbind auxiliary surfaces before restoring targets of potentially different sizes.
|
||||
device->SetDepthStencilSurface(nullptr);
|
||||
for (DWORD index = 1; index < target_count; ++index) {
|
||||
device->SetRenderTarget(index, nullptr);
|
||||
}
|
||||
device->SetRenderTarget(0, targets[0].value);
|
||||
for (DWORD index = 1; index < target_count; ++index) {
|
||||
device->SetRenderTarget(index, targets[index].value);
|
||||
}
|
||||
device->SetDepthStencilSurface(depth.value);
|
||||
|
||||
block.value->Apply();
|
||||
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
|
||||
for (size_t index = 0; index < kinds.size(); ++index) {
|
||||
if (have_transform[index]) {
|
||||
device->SetTransform(kinds[index], &transforms[index]);
|
||||
}
|
||||
}
|
||||
device->SetViewport(&viewport);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::shared_ptr<Renderer>> snapshot() {
|
||||
// callbacks run without the registry lock, while their entries remain alive.
|
||||
std::lock_guard lock(registry_mutex);
|
||||
return renderers;
|
||||
}
|
||||
|
||||
void destroy_renderers() {
|
||||
for (const auto &renderer : snapshot()) {
|
||||
if (renderer->started) {
|
||||
renderer->callback(SPICE_SDK_D3D9_DESTROY, &frame, renderer->userdata);
|
||||
}
|
||||
renderer->ready = false;
|
||||
renderer->started = false;
|
||||
}
|
||||
|
||||
frame = {sizeof(frame), nullptr, nullptr, 0, 0};
|
||||
resetting = false;
|
||||
device_lost = false;
|
||||
reset_in_progress = false;
|
||||
device_thread = 0;
|
||||
}
|
||||
|
||||
void finish_shutdown() {
|
||||
if (shutdown_complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
destroy_renderers();
|
||||
{
|
||||
std::lock_guard lock(registry_mutex);
|
||||
renderers.clear();
|
||||
}
|
||||
|
||||
shutdown_complete = true;
|
||||
shutdown_condition.notify_all();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
|
||||
spice_sdk_d3d9_callback_func *callback, void *userdata) {
|
||||
if (dispatching) {
|
||||
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||
}
|
||||
if (!callback) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||
}
|
||||
|
||||
// only registered SDK modules may supply callbacks.
|
||||
HMODULE owner = nullptr;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(callback), &owner) ||
|
||||
std::none_of(modules.begin(), modules.end(), [&](const SdkModule &module) {
|
||||
return module.module == owner;
|
||||
})) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||
}
|
||||
|
||||
std::lock_guard lock(registry_mutex);
|
||||
if (stopped) {
|
||||
return SPICE_SDK_STATUS_TOO_LATE;
|
||||
}
|
||||
if (std::any_of(renderers.begin(), renderers.end(), [&](const auto &renderer) {
|
||||
return renderer->callback == callback && renderer->userdata == userdata;
|
||||
})) {
|
||||
return SPICE_SDK_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
// keep callback code loaded for the lifetime of the process.
|
||||
try {
|
||||
auto renderer = std::make_shared<Renderer>(Renderer{callback, userdata});
|
||||
if (!GetModuleHandleExW(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
||||
reinterpret_cast<LPCWSTR>(callback), &owner)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
renderers.push_back(std::move(renderer));
|
||||
} catch (...) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
return SPICE_SDK_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
void draw(HWND window, IDirect3DDevice9 *device) {
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(execution_mutex);
|
||||
DispatchScope scope;
|
||||
|
||||
if (frame.device && frame.device != device) {
|
||||
return;
|
||||
}
|
||||
device_thread = GetCurrentThreadId();
|
||||
if (stopped) {
|
||||
if (!reset_in_progress) {
|
||||
finish_shutdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// latch the first presented device even before any renderers register.
|
||||
frame.device = device;
|
||||
frame.window = window;
|
||||
|
||||
const auto current = snapshot();
|
||||
if (current.empty() || resetting || device_lost) {
|
||||
return;
|
||||
}
|
||||
|
||||
ComReference<IDirect3DSurface9> backbuffer;
|
||||
D3DSURFACE_DESC description{};
|
||||
if (FAILED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer.value)) ||
|
||||
FAILED(backbuffer.value->GetDesc(&description))) {
|
||||
return;
|
||||
}
|
||||
frame = {sizeof(frame), device, window, description.Width, description.Height};
|
||||
|
||||
for (const auto &renderer : current) {
|
||||
// isolate each renderer and give it the full backbuffer in its own scene.
|
||||
GraphicsState state(device);
|
||||
if (!state.captured) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (FAILED(device->SetDepthStencilSurface(nullptr))) {
|
||||
continue;
|
||||
}
|
||||
bool target_ready = true;
|
||||
for (DWORD index = 1; index < state.target_count; ++index) {
|
||||
if (FAILED(device->SetRenderTarget(index, nullptr))) {
|
||||
target_ready = false;
|
||||
}
|
||||
}
|
||||
|
||||
const D3DVIEWPORT9 viewport{0, 0, frame.width, frame.height, 0.0f, 1.0f};
|
||||
if (!target_ready || FAILED(device->SetRenderTarget(0, backbuffer.value)) ||
|
||||
FAILED(device->SetViewport(&viewport)) || FAILED(device->BeginScene())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// dispatch READY to also recreate resources released before a successful reset.
|
||||
if (!renderer->ready) {
|
||||
renderer->started = true;
|
||||
renderer->callback(SPICE_SDK_D3D9_READY, &frame, renderer->userdata);
|
||||
renderer->ready = true;
|
||||
}
|
||||
|
||||
renderer->callback(SPICE_SDK_D3D9_DRAW, &frame, renderer->userdata);
|
||||
device->EndScene();
|
||||
}
|
||||
}
|
||||
|
||||
void present_complete(IDirect3DDevice9 *device, HRESULT result) {
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(execution_mutex);
|
||||
DispatchScope scope;
|
||||
|
||||
if (frame.device == device) {
|
||||
device_thread = GetCurrentThreadId();
|
||||
if (stopped && !reset_in_progress) {
|
||||
finish_shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// a busy nonblocking present does not change the known device-loss state.
|
||||
if (SUCCEEDED(result)) {
|
||||
device_lost = false;
|
||||
} else if (result != D3DERR_WASSTILLDRAWING) {
|
||||
device_lost = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void invalidate(IDirect3DDevice9 *device) {
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(execution_mutex);
|
||||
DispatchScope scope;
|
||||
|
||||
if (frame.device != device) {
|
||||
return;
|
||||
}
|
||||
|
||||
device_thread = GetCurrentThreadId();
|
||||
if (stopped) {
|
||||
finish_shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// default-pool resources must be released before the native reset starts.
|
||||
reset_in_progress = true;
|
||||
resetting = true;
|
||||
for (const auto &renderer : snapshot()) {
|
||||
if (renderer->ready) {
|
||||
renderer->callback(SPICE_SDK_D3D9_INVALIDATE, &frame, renderer->userdata);
|
||||
}
|
||||
renderer->ready = false;
|
||||
}
|
||||
}
|
||||
|
||||
void reset_complete(IDirect3DDevice9 *device, bool success) {
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(execution_mutex);
|
||||
DispatchScope scope;
|
||||
|
||||
if (frame.device == device) {
|
||||
device_thread = GetCurrentThreadId();
|
||||
reset_in_progress = false;
|
||||
resetting = !success;
|
||||
device_lost = !success;
|
||||
|
||||
if (stopped) {
|
||||
finish_shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void destroy(IDirect3DDevice9 *device) {
|
||||
if (dispatching) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(execution_mutex);
|
||||
DispatchScope scope;
|
||||
|
||||
if (frame.device == device) {
|
||||
if (stopped) {
|
||||
finish_shutdown();
|
||||
} else {
|
||||
destroy_renderers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool shutdown(bool graphics_stopped) {
|
||||
std::unique_lock lock(execution_mutex);
|
||||
if (shutdown_complete) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// close registration before deciding where device resources can be released.
|
||||
{
|
||||
std::lock_guard registry_lock(registry_mutex);
|
||||
stopped = true;
|
||||
}
|
||||
|
||||
const auto current = snapshot();
|
||||
const bool resources_started = std::any_of(
|
||||
current.begin(), current.end(), [](const auto &renderer) {
|
||||
return renderer->started;
|
||||
});
|
||||
|
||||
if (!resources_started || (!reset_in_progress &&
|
||||
(graphics_stopped || device_thread == GetCurrentThreadId()))) {
|
||||
DispatchScope scope;
|
||||
finish_shutdown();
|
||||
return true;
|
||||
}
|
||||
|
||||
// a graphics boundary must drain callbacks when another thread requests shutdown.
|
||||
return shutdown_condition.wait_for(lock, std::chrono::milliseconds(100), [] {
|
||||
return shutdown_complete;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <d3d9.h>
|
||||
#include "modules.h"
|
||||
|
||||
namespace sdk::d3d9 {
|
||||
|
||||
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
|
||||
spice_sdk_d3d9_callback_func *callback, void *userdata);
|
||||
void draw(HWND window, IDirect3DDevice9 *device);
|
||||
void present_complete(IDirect3DDevice9 *device, HRESULT result);
|
||||
void invalidate(IDirect3DDevice9 *device);
|
||||
void reset_complete(IDirect3DDevice9 *device, bool success);
|
||||
void destroy(IDirect3DDevice9 *device);
|
||||
bool shutdown(bool graphics_stopped);
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
|
||||
@@ -247,6 +248,95 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)(
|
||||
uint8_t amount
|
||||
);
|
||||
|
||||
typedef struct SPICE_SDK_MODULE_INFO {
|
||||
uint32_t size; // initialize to sizeof(SPICE_SDK_MODULE_INFO)
|
||||
uintptr_t base; // loaded address, not the preferred PE image base
|
||||
uint32_t image_size;
|
||||
uint32_t timestamp;
|
||||
uint32_t entry_point; // RVA, not an absolute address
|
||||
uint16_t machine; // PE IMAGE_FILE_MACHINE_* value
|
||||
} SPICE_SDK_MODULE_INFO;
|
||||
|
||||
// get_module_info (v0.4 and up)
|
||||
// gets PE info about an already loaded module
|
||||
//
|
||||
// does not load or permanently pin the module; the caller must keep it loaded
|
||||
// while using the returned base address
|
||||
// returns GENERIC_ERROR if not found or unreadable, NOT_SUPPORTED for non-PE32/PE32+
|
||||
//
|
||||
// module_name: null-terminated UTF-16 module name or full path
|
||||
// info: receives module info; initialize size to sizeof(SPICE_SDK_MODULE_INFO)
|
||||
|
||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_module_info_func)(
|
||||
const wchar_t *module_name,
|
||||
SPICE_SDK_MODULE_INFO *info
|
||||
);
|
||||
|
||||
// get_plugin_directory (v0.4 and up)
|
||||
// gets the directory containing a registered SDK DLL
|
||||
//
|
||||
// available during entry-point initialization and until destroy callbacks finish
|
||||
// returns INVALID_ARGUMENT_1 for addresses outside registered SDK DLLs
|
||||
//
|
||||
// plugin_address: address of a function or static object in the plugin DLL
|
||||
// used only to identify the DLL; the pointed-to object is not read
|
||||
// address of any global variable in your DLL will work
|
||||
// buffer: caller-owned UTF-16 output, or NULL to query the required size
|
||||
// size: input capacity and output required wchar_t count, including the terminator
|
||||
// NULL buffer or insufficient capacity returns TOO_SMALL and sets the
|
||||
// required size without partial output
|
||||
|
||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_plugin_directory_func)(
|
||||
const void *plugin_address,
|
||||
wchar_t *buffer,
|
||||
uint32_t *size
|
||||
);
|
||||
|
||||
typedef enum SPICE_SDK_D3D9_EVENT {
|
||||
SPICE_SDK_D3D9_READY = 0,
|
||||
SPICE_SDK_D3D9_DRAW = 1,
|
||||
SPICE_SDK_D3D9_INVALIDATE = 2,
|
||||
SPICE_SDK_D3D9_DESTROY = 3,
|
||||
} SPICE_SDK_D3D9_EVENT;
|
||||
|
||||
typedef struct SPICE_SDK_D3D9_FRAME {
|
||||
uint32_t size;
|
||||
void *device; // borrowed IDirect3DDevice9*, not Spice's wrapper
|
||||
void *window; // HWND
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} SPICE_SDK_D3D9_FRAME;
|
||||
|
||||
typedef void (__cdecl spice_sdk_d3d9_callback_func)(
|
||||
SPICE_SDK_D3D9_EVENT event,
|
||||
const SPICE_SDK_D3D9_FRAME *frame,
|
||||
void *userdata
|
||||
);
|
||||
|
||||
// register_d3d9 (v0.4 and up)
|
||||
// registers a process-lifetime renderer for the primary D3D9 presentation target
|
||||
//
|
||||
// READY precedes drawing on a usable device and follows each successful reset
|
||||
// DRAW runs on top of all Spice overlays, even when the overlay is closed; the host
|
||||
// binds the backbuffer, brackets the scene, and restores graphics state
|
||||
// INVALIDATE precedes reset; release default-pool resources, even if reset fails
|
||||
// DESTROY releases all device references before device destruction or SDK shutdown
|
||||
// callbacks are serialized; DRAW/READY run on the presentation thread; teardown
|
||||
// runs at a graphics-thread boundary, or after the game stops rendering
|
||||
// do not throw, block, reset/present the
|
||||
// device, or shut down Spice from a callback; frame is valid only during the call
|
||||
// no callbacks run after the plugin's SDK destroy callback begins
|
||||
// registration from inside a render callback is not supported
|
||||
// input capture and non-D3D9 backends are not provided
|
||||
//
|
||||
// callback: function in a registered plugin DLL; the DLL is retained until exit
|
||||
// userdata: opaque plugin state passed unchanged to each callback
|
||||
|
||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_register_d3d9_func)(
|
||||
spice_sdk_d3d9_callback_func *callback,
|
||||
void *userdata
|
||||
);
|
||||
|
||||
typedef struct SPICE_SDK_V0 {
|
||||
uint32_t size;
|
||||
|
||||
@@ -274,6 +364,11 @@ typedef struct SPICE_SDK_V0 {
|
||||
|
||||
spice_sdk_insert_coin_func *insert_coin;
|
||||
|
||||
spice_sdk_get_module_info_func *get_module_info;
|
||||
spice_sdk_get_plugin_directory_func *get_plugin_directory;
|
||||
|
||||
spice_sdk_register_d3d9_func *register_d3d9;
|
||||
|
||||
} SPICE_SDK_V0;
|
||||
|
||||
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#include "modules.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
|
||||
namespace sdk::modules {
|
||||
|
||||
namespace {
|
||||
|
||||
// RAII wrapper for a module handle
|
||||
struct ModuleReference {
|
||||
HMODULE handle = nullptr;
|
||||
|
||||
ModuleReference() = default;
|
||||
ModuleReference(const ModuleReference &) = delete;
|
||||
ModuleReference &operator=(const ModuleReference &) = delete;
|
||||
~ModuleReference() {
|
||||
if (handle) {
|
||||
FreeLibrary(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Value>
|
||||
bool read_module_value(uintptr_t base, size_t offset, Value &value) {
|
||||
if (offset > std::numeric_limits<uintptr_t>::max() - base ||
|
||||
sizeof(Value) > std::numeric_limits<uintptr_t>::max() - (base + offset)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T copied = 0;
|
||||
return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void *>(base + offset),
|
||||
&value, sizeof(value), &copied) && copied == sizeof(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
|
||||
if (!module_name || !module_name[0]) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||
}
|
||||
if (!info) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
||||
}
|
||||
if (info->size < sizeof(SPICE_SDK_MODULE_INFO)) {
|
||||
return SPICE_SDK_STATUS_TOO_SMALL;
|
||||
}
|
||||
|
||||
// Keep the module loaded while inspecting its headers.
|
||||
ModuleReference module;
|
||||
if (!GetModuleHandleExW(0, module_name, &module.handle)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
// Locate the PE header through the DOS header.
|
||||
const auto base = reinterpret_cast<uintptr_t>(module.handle);
|
||||
IMAGE_DOS_HEADER dos{};
|
||||
if (!read_module_value(base, 0, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
|
||||
dos.e_lfanew < static_cast<LONG>(sizeof(dos))) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
const auto nt_offset = static_cast<size_t>(dos.e_lfanew);
|
||||
DWORD signature{};
|
||||
IMAGE_FILE_HEADER header{};
|
||||
if (!read_module_value(base, nt_offset, signature) || signature != IMAGE_NT_SIGNATURE ||
|
||||
!read_module_value(base, nt_offset + sizeof(signature), header)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
// The optional-header magic selects the PE32 or PE32+ layout.
|
||||
const auto optional_offset = nt_offset + sizeof(signature) + sizeof(header);
|
||||
WORD magic{};
|
||||
if (header.SizeOfOptionalHeader < sizeof(magic) || !read_module_value(base, optional_offset, magic)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
SPICE_SDK_MODULE_INFO result{};
|
||||
result.size = info->size;
|
||||
result.base = base;
|
||||
result.timestamp = header.TimeDateStamp;
|
||||
result.machine = header.Machine;
|
||||
|
||||
if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
|
||||
IMAGE_OPTIONAL_HEADER64 optional{};
|
||||
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
result.image_size = optional.SizeOfImage;
|
||||
result.entry_point = optional.AddressOfEntryPoint;
|
||||
} else if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
|
||||
IMAGE_OPTIONAL_HEADER32 optional{};
|
||||
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
result.image_size = optional.SizeOfImage;
|
||||
result.entry_point = optional.AddressOfEntryPoint;
|
||||
} else {
|
||||
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Reject headers or entry points outside the image.
|
||||
if (optional_offset > result.image_size || header.SizeOfOptionalHeader > result.image_size - optional_offset ||
|
||||
result.entry_point >= result.image_size) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
// Leave the caller's output untouched on failure.
|
||||
*info = result;
|
||||
return SPICE_SDK_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> ®istered_modules,
|
||||
const void *plugin_address, wchar_t *buffer, uint32_t *size) {
|
||||
if (!plugin_address) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||
}
|
||||
if (!size) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_3;
|
||||
}
|
||||
|
||||
// Resolve the address and require a registered plugin.
|
||||
ModuleReference module;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
||||
reinterpret_cast<LPCWSTR>(plugin_address), &module.handle) ||
|
||||
std::none_of(registered_modules.begin(), registered_modules.end(), [&](const SdkModule &entry) {
|
||||
return entry.module == module.handle;
|
||||
})) {
|
||||
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||
}
|
||||
|
||||
try {
|
||||
// Reject truncated paths before extracting the directory.
|
||||
std::array<wchar_t, 32768> filename{};
|
||||
const auto length = GetModuleFileNameW(module.handle, filename.data(), static_cast<DWORD>(filename.size()));
|
||||
if (!length || length >= filename.size()) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
|
||||
const auto path = std::filesystem::path(filename.data()).parent_path();
|
||||
|
||||
// Report the full UTF-16 capacity, including the terminator.
|
||||
const auto &text = path.native();
|
||||
const auto required = static_cast<uint32_t>(text.size() + 1);
|
||||
const auto capacity = *size;
|
||||
*size = required;
|
||||
if (!buffer || capacity < required) {
|
||||
return SPICE_SDK_STATUS_TOO_SMALL;
|
||||
}
|
||||
|
||||
// Copy only when the entire path fits.
|
||||
std::copy_n(text.c_str(), required, buffer);
|
||||
return SPICE_SDK_STATUS_SUCCESS;
|
||||
} catch (const std::exception &) {
|
||||
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
#include "sdk/include/spicesdk.h"
|
||||
|
||||
namespace sdk {
|
||||
|
||||
struct SdkModule {
|
||||
std::string dll;
|
||||
HINSTANCE module;
|
||||
};
|
||||
|
||||
namespace modules {
|
||||
|
||||
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info);
|
||||
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> ®istered_modules,
|
||||
const void *plugin_address, wchar_t *buffer, uint32_t *size);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
#include <format>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <windows.h>
|
||||
|
||||
#include "v0_cpp_imgui.h"
|
||||
#include "sdk/include/spicesdk.h"
|
||||
#include "sdk/include/spicesdk_io.h"
|
||||
|
||||
@@ -17,6 +21,24 @@ static spice_sdk_destroy_callback_func destroy_callback;
|
||||
static std::jthread worker_thread;
|
||||
static void worker_thread_main(std::stop_token stop_token);
|
||||
|
||||
static std::optional<std::string> utf16_to_utf8(const wchar_t *text) {
|
||||
const auto bytes = WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (bytes == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string result(bytes, '\0');
|
||||
if (WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1,
|
||||
result.data(), bytes, nullptr, nullptr) != bytes) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
// main entry point into the DLL
|
||||
// spice executable will call into this shortly after the game is partially
|
||||
// initialized (but not quite fully booted just yet)
|
||||
@@ -42,6 +64,37 @@ spice_sdk_entry_point(
|
||||
|
||||
LOG_INFO("plugin loaded");
|
||||
|
||||
if (spice.get_plugin_directory) {
|
||||
std::array<wchar_t, 32768> directory{};
|
||||
uint32_t size = static_cast<uint32_t>(directory.size());
|
||||
if (spice.get_plugin_directory(&spice, directory.data(), &size) ==
|
||||
SPICE_SDK_STATUS_SUCCESS) {
|
||||
if (const auto path = utf16_to_utf8(directory.data())) {
|
||||
LOG_INFO(std::format("plugin directory: {}", *path).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spice.get_module_info) {
|
||||
SPICE_SDK_MODULE_INFO info{};
|
||||
info.size = sizeof(info);
|
||||
if (spice.get_module_info(L"kernel32.dll", &info) == SPICE_SDK_STATUS_SUCCESS) {
|
||||
LOG_INFO(std::format(
|
||||
"kernel32 PE: {:x}_{:x}, base={:x}, size={:x}",
|
||||
info.timestamp, info.entry_point, info.base, info.image_size).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (spice.register_d3d9) {
|
||||
status = sample_imgui::initialize(spice);
|
||||
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||
LOG_INFO(std::format(
|
||||
"D3D9 registration failed: {}", static_cast<int>(status)).c_str());
|
||||
} else {
|
||||
LOG_INFO("D3D9 renderer registered; Ctrl+Enter toggles the sample window");
|
||||
}
|
||||
}
|
||||
|
||||
// spin up a worker thread
|
||||
worker_thread = std::jthread(worker_thread_main);
|
||||
return 1;
|
||||
@@ -77,10 +130,18 @@ static ArrowButton arrow_buttons[] = {
|
||||
// worker thread for I/O
|
||||
static void worker_thread_main(std::stop_token stop_token) {
|
||||
bool coin_previous_state[10] = {};
|
||||
bool window_toggle_previous_state = false;
|
||||
while (!stop_token.stop_requested()) {
|
||||
|
||||
// insert coin
|
||||
const bool control_pressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
const bool window_toggle_pressed = control_pressed &&
|
||||
((GetAsyncKeyState(VK_RETURN) & 0x8000) != 0);
|
||||
if (window_toggle_pressed && !window_toggle_previous_state) {
|
||||
sample_imgui::toggle();
|
||||
}
|
||||
window_toggle_previous_state = window_toggle_pressed;
|
||||
|
||||
// insert coin
|
||||
for (uint8_t amount = 0; amount < 10; amount++) {
|
||||
const bool coin_pressed = control_pressed &&
|
||||
((GetAsyncKeyState('0' + amount) & 0x8000) != 0);
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "v0_cpp_imgui.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <windows.h>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "backends/imgui_impl_dx9.h"
|
||||
|
||||
namespace sample_imgui {
|
||||
namespace {
|
||||
|
||||
struct SampleRenderer {
|
||||
ImGuiContext *context = nullptr;
|
||||
bool backend_initialized = false;
|
||||
bool was_visible = false;
|
||||
bool dummy_enabled = true;
|
||||
float dummy_value = 0.5f;
|
||||
int32_t dummy_mode = 0;
|
||||
uint32_t click_count = 0;
|
||||
std::chrono::steady_clock::time_point previous_frame;
|
||||
};
|
||||
|
||||
SampleRenderer renderer;
|
||||
std::atomic_bool window_visible = false;
|
||||
|
||||
void update_mouse(const SPICE_SDK_D3D9_FRAME &frame) {
|
||||
auto &io = ImGui::GetIO();
|
||||
const auto window = static_cast<HWND>(frame.window);
|
||||
const bool focused = window &&
|
||||
GetAncestor(GetForegroundWindow(), GA_ROOT) == GetAncestor(window, GA_ROOT);
|
||||
io.AddFocusEvent(focused);
|
||||
|
||||
POINT position{};
|
||||
RECT client{};
|
||||
|
||||
// can window receive mouse input?
|
||||
const bool mouse_available =
|
||||
focused &&
|
||||
GetCursorPos(&position) &&
|
||||
ScreenToClient(window, &position) &&
|
||||
GetClientRect(window, &client) &&
|
||||
client.right > 0 &&
|
||||
client.bottom > 0;
|
||||
|
||||
// add mouse position
|
||||
if (mouse_available) {
|
||||
io.AddMousePosEvent(
|
||||
static_cast<float>(position.x) * frame.width / client.right,
|
||||
static_cast<float>(position.y) * frame.height / client.bottom);
|
||||
} else {
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
|
||||
// add click to imgui
|
||||
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
|
||||
io.AddMouseButtonEvent(0, mouse_available &&
|
||||
((GetAsyncKeyState(swapped ? VK_RBUTTON : VK_LBUTTON) & 0x8000) != 0));
|
||||
io.AddMouseButtonEvent(1, mouse_available &&
|
||||
((GetAsyncKeyState(swapped ? VK_LBUTTON : VK_RBUTTON) & 0x8000) != 0));
|
||||
}
|
||||
|
||||
void draw_window(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
|
||||
auto &io = ImGui::GetIO();
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
|
||||
ImGuiCond_Once, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowBgAlpha(0.8f);
|
||||
|
||||
if (ImGui::Begin("SDK sample", nullptr,
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse)) {
|
||||
|
||||
ImGui::Text("%u x %u", frame.width, frame.height);
|
||||
ImGui::Text("%.1f FPS", io.Framerate);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Checkbox("Enable dummy widgets", &state.dummy_enabled);
|
||||
|
||||
ImGui::BeginDisabled(!state.dummy_enabled);
|
||||
{
|
||||
ImGui::SliderFloat("Value", &state.dummy_value, 0.0f, 1.0f, "%.2f",
|
||||
ImGuiSliderFlags_NoInput);
|
||||
ImGui::Combo("Mode", &state.dummy_mode, "Off\0Low\0High\0");
|
||||
if (ImGui::Button("Increment")) {
|
||||
++state.click_count;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset")) {
|
||||
state.click_count = 0;
|
||||
state.dummy_value = 0.5f;
|
||||
state.dummy_mode = 0;
|
||||
}
|
||||
ImGui::Text("Count: %u", state.click_count);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void draw_frame(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
|
||||
if (!state.backend_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float elapsed = std::chrono::duration<float>(now - state.previous_frame).count();
|
||||
state.previous_frame = now;
|
||||
const bool visible = window_visible.load(std::memory_order_relaxed);
|
||||
if (!visible && !state.was_visible) {
|
||||
return;
|
||||
}
|
||||
state.was_visible = visible;
|
||||
|
||||
// update imgui state
|
||||
auto &io = ImGui::GetIO();
|
||||
io.DisplaySize = ImVec2(
|
||||
static_cast<float>(frame.width), static_cast<float>(frame.height));
|
||||
io.DeltaTime = elapsed > 0.0f ? elapsed : 1.0f / 60.0f;
|
||||
|
||||
// update mouse i/o
|
||||
if (visible) {
|
||||
update_mouse(frame);
|
||||
} else {
|
||||
io.ClearEventsQueue();
|
||||
io.ClearInputMouse();
|
||||
io.AddFocusEvent(false);
|
||||
}
|
||||
|
||||
ImGui_ImplDX9_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
if (!visible) {
|
||||
ImGui::SetWindowFocus(nullptr);
|
||||
ImGui::EndFrame();
|
||||
return;
|
||||
}
|
||||
|
||||
draw_window(state, frame);
|
||||
ImGui::Render();
|
||||
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
|
||||
}
|
||||
|
||||
void __cdecl render_callback(
|
||||
SPICE_SDK_D3D9_EVENT event,
|
||||
const SPICE_SDK_D3D9_FRAME *frame,
|
||||
void *userdata) {
|
||||
|
||||
auto &state = *static_cast<SampleRenderer *>(userdata);
|
||||
auto *previous_context = ImGui::GetCurrentContext();
|
||||
|
||||
// init imgui context
|
||||
if (event == SPICE_SDK_D3D9_READY && !state.context) {
|
||||
ImGui::SetAllocatorFunctions(
|
||||
[](size_t size, void *) -> void * { return std::malloc(size); },
|
||||
[](void *memory, void *) { std::free(memory); });
|
||||
IMGUI_CHECKVERSION();
|
||||
state.context = ImGui::CreateContext();
|
||||
ImGui::SetCurrentContext(state.context);
|
||||
auto &io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr;
|
||||
io.LogFilename = nullptr;
|
||||
io.MouseDrawCursor = true;
|
||||
ImGui::StyleColorsDark();
|
||||
state.backend_initialized = ImGui_ImplDX9_Init(
|
||||
static_cast<IDirect3DDevice9 *>(frame->device));
|
||||
}
|
||||
|
||||
if (state.context) {
|
||||
ImGui::SetCurrentContext(state.context);
|
||||
switch (event) {
|
||||
case SPICE_SDK_D3D9_READY:
|
||||
state.previous_frame = std::chrono::steady_clock::now();
|
||||
if (state.backend_initialized) {
|
||||
ImGui_ImplDX9_CreateDeviceObjects();
|
||||
}
|
||||
break;
|
||||
case SPICE_SDK_D3D9_DRAW:
|
||||
draw_frame(state, *frame);
|
||||
break;
|
||||
case SPICE_SDK_D3D9_INVALIDATE:
|
||||
if (state.backend_initialized) {
|
||||
ImGui_ImplDX9_InvalidateDeviceObjects();
|
||||
}
|
||||
break;
|
||||
case SPICE_SDK_D3D9_DESTROY:
|
||||
if (state.backend_initialized) {
|
||||
ImGui_ImplDX9_Shutdown();
|
||||
}
|
||||
ImGui::DestroyContext(state.context);
|
||||
state.context = nullptr;
|
||||
state.backend_initialized = false;
|
||||
state.was_visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(previous_context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice) {
|
||||
if (!spice.register_d3d9) {
|
||||
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||
}
|
||||
return spice.register_d3d9(render_callback, &renderer);
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
const bool visible = !window_visible.load(std::memory_order_relaxed);
|
||||
window_visible.store(visible, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "sdk/include/spicesdk.h"
|
||||
|
||||
namespace sample_imgui {
|
||||
|
||||
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice);
|
||||
void toggle();
|
||||
|
||||
}
|
||||
+58
-7
@@ -1,8 +1,11 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <algorithm>
|
||||
|
||||
#include "sdk.h"
|
||||
#include "modules.h"
|
||||
#include "d3d9.h"
|
||||
#include "avs/game.h"
|
||||
#include "games/io.h"
|
||||
#include "launcher/launcher.h"
|
||||
@@ -32,11 +35,9 @@ static spice_sdk_insert_card_func sdk_insert_card;
|
||||
static spice_sdk_set_keypad_func sdk_set_keypad;
|
||||
static spice_sdk_add_toast_func sdk_add_toast;
|
||||
static spice_sdk_insert_coin_func sdk_insert_coin;
|
||||
|
||||
struct SdkModule {
|
||||
std::string dll;
|
||||
HINSTANCE module;
|
||||
};
|
||||
static spice_sdk_get_module_info_func sdk_get_module_info;
|
||||
static spice_sdk_get_plugin_directory_func sdk_get_plugin_directory;
|
||||
static spice_sdk_register_d3d9_func sdk_register_d3d9;
|
||||
|
||||
// DLLs
|
||||
static int sdk_modules_count = 0;
|
||||
@@ -46,6 +47,7 @@ static std::shared_mutex sdk_global_mutex;
|
||||
// internal
|
||||
static bool sdk_initialized = false;
|
||||
static bool sdk_shutting_down = false;
|
||||
static bool sdk_finalizing = false;
|
||||
static std::vector<Button> *buttons;
|
||||
static std::vector<Analog> *analogs;
|
||||
static std::vector<Light> *lights;
|
||||
@@ -91,14 +93,22 @@ void init_sdk_modules() {
|
||||
}
|
||||
}
|
||||
|
||||
void fini_sdk_modules() {
|
||||
void fini_sdk_modules(bool graphics_stopped) {
|
||||
// prevent multiple calls and further calls into sdk_init
|
||||
{
|
||||
std::unique_lock lock(sdk_global_mutex);
|
||||
if (!sdk_initialized) {
|
||||
if (!sdk_initialized || sdk_finalizing) {
|
||||
return;
|
||||
}
|
||||
sdk_shutting_down = true;
|
||||
sdk_finalizing = true;
|
||||
}
|
||||
|
||||
if (!d3d9::shutdown(graphics_stopped)) {
|
||||
std::unique_lock lock(sdk_global_mutex);
|
||||
sdk_finalizing = false;
|
||||
log_warning("sdk", "deferring plugin teardown: waiting for D3D9 rendering to stop");
|
||||
return;
|
||||
}
|
||||
|
||||
// call into destroy callback of each DLL
|
||||
@@ -183,6 +193,17 @@ sdk_init(
|
||||
}
|
||||
// end of 0.3
|
||||
|
||||
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_module_info)) {
|
||||
v0->get_module_info = sdk_get_module_info;
|
||||
}
|
||||
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_plugin_directory)) {
|
||||
v0->get_plugin_directory = sdk_get_plugin_directory;
|
||||
}
|
||||
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, register_d3d9)) {
|
||||
v0->register_d3d9 = sdk_register_d3d9;
|
||||
}
|
||||
// end of 0.4
|
||||
|
||||
// any newer minor iterations will need to check the size
|
||||
|
||||
{
|
||||
@@ -195,6 +216,36 @@ sdk_init(
|
||||
return SPICE_SDK_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE
|
||||
__cdecl
|
||||
sdk_get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
|
||||
std::shared_lock lock(sdk_global_mutex);
|
||||
if (!sdk_initialized) {
|
||||
return SPICE_SDK_STATUS_TOO_LATE;
|
||||
}
|
||||
return modules::get_module_info(module_name, info);
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE
|
||||
__cdecl
|
||||
sdk_get_plugin_directory(const void *plugin_address, wchar_t *buffer, uint32_t *size) {
|
||||
std::shared_lock lock(sdk_global_mutex);
|
||||
if (!sdk_initialized) {
|
||||
return SPICE_SDK_STATUS_TOO_LATE;
|
||||
}
|
||||
return modules::get_plugin_directory(sdk_modules_list, plugin_address, buffer, size);
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE
|
||||
__cdecl
|
||||
sdk_register_d3d9(spice_sdk_d3d9_callback_func *callback, void *userdata) {
|
||||
std::shared_lock lock(sdk_global_mutex);
|
||||
if (!sdk_initialized || sdk_shutting_down) {
|
||||
return SPICE_SDK_STATUS_TOO_LATE;
|
||||
}
|
||||
return d3d9::register_d3d9(sdk_modules_list, callback, userdata);
|
||||
}
|
||||
|
||||
SPICE_SDK_STATUS_CODE
|
||||
__cdecl
|
||||
sdk_log(
|
||||
|
||||
@@ -7,6 +7,6 @@ namespace sdk {
|
||||
|
||||
void register_sdk_hooks(std::string dll, HINSTANCE module);
|
||||
void init_sdk_modules();
|
||||
void fini_sdk_modules();
|
||||
void fini_sdk_modules(bool graphics_stopped = false);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user