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:
bicarus
2026-09-11 02:49:07 -07:00
committed by GitHub
parent 603fbfc69d
commit 4c77a3f148
17 changed files with 1095 additions and 15 deletions
+11 -1
View File
@@ -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 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+403
View File
@@ -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;
});
}
}
+17
View File
@@ -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);
}
+95
View File
@@ -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)(
+161
View File
@@ -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> &registered_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;
}
}
}
+24
View File
@@ -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> &registered_modules,
const void *plugin_address, wchar_t *buffer, uint32_t *size);
}
}
+62 -1
View File
@@ -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
View File
@@ -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(
+1 -1
View File
@@ -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);
}