diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index e5dddc6..cb30b7a 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -343,6 +343,7 @@ set(SOURCE_FILES ${SOURCE_FILES} api/modules/control.cpp api/modules/touch.cpp api/modules/iidx.cpp + api/modules/sdvx.cpp api/serial.cpp api/modules/drs.cpp api/modules/lcd.cpp @@ -551,6 +552,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/graphics/nvapi_hook.cpp hooks/graphics/nvenc_hook.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp + hooks/graphics/backends/d3d9/d3d9_screenshot.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp hooks/graphics/backends/d3d9/d3d9_gfdm.cpp hooks/graphics/backends/d3d9/d3d9_live2d.cpp diff --git a/src/spice2x/README.md b/src/spice2x/README.md index 63bfbe6..e6eae30 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly. - `Side Panel Right Inner` - `Side Panel Right` +#### SDVX +- tapeled_get(name: str, ...) + - returns a list containing a dict of the current tape LED states. The dict keys are: + - `Title` + - `Upper Left Speaker` + - `Upper Right Speaker` + - `Left Wing` + - `Right Wing` + - `Control Panel` + - `Lower Left Speaker` + - `Lower Right Speaker` + - `Woofer` + - `V Unit` + #### LCD - info() - returns information about the serial LCD controller some games use diff --git a/src/spice2x/api/client.h b/src/spice2x/api/client.h new file mode 100644 index 0000000..c73fc6d --- /dev/null +++ b/src/spice2x/api/client.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace api { + + extern std::atomic_uint32_t CLIENT_COUNT; + + inline bool has_clients() { + return CLIENT_COUNT.load(std::memory_order_relaxed) > 0; + } +} diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 19713fa..1fd41d5 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -5,6 +5,7 @@ #include +#include "client.h" #include "cfg/configurator.h" #include "external/rapidjson/document.h" #include "util/crypt.h" @@ -28,6 +29,7 @@ #include "modules/lcd.h" #include "modules/lights.h" #include "modules/memory.h" +#include "modules/sdvx.h" #include "modules/touch.h" #include "modules/resize.h" #include "request.h" @@ -36,6 +38,8 @@ using namespace rapidjson; using namespace api; +std::atomic_uint32_t api::CLIENT_COUNT = 0; + Controller::Controller(unsigned short port, std::string password, bool pretty) : port(port), password(std::move(password)), pretty(pretty) { @@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) { state->modules.push_back(new modules::LCD()); state->modules.push_back(new modules::Lights()); state->modules.push_back(new modules::Memory()); + state->modules.push_back(new modules::SDVX()); state->modules.push_back(new modules::Touch()); state->modules.push_back(new modules::Resize()); + + CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed); } void Controller::free_state(api::ClientState *state) { @@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) { // free cipher delete state->cipher; + + CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed); } void Controller::free_socket() { diff --git a/src/spice2x/api/modules/iidx.cpp b/src/spice2x/api/modules/iidx.cpp index b65423f..cb47df7 100644 --- a/src/spice2x/api/modules/iidx.cpp +++ b/src/spice2x/api/modules/iidx.cpp @@ -106,14 +106,16 @@ namespace api::modules { void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) { // Create an array for the light state Value light_state(kArrayType); - light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator()); + light_state.Reserve( + static_cast(mapping.data.size() * 3), + res.doc()->GetAllocator()); for (const auto [r, g, b] : mapping.data) { light_state.PushBack(r, res.doc()->GetAllocator()); light_state.PushBack(g, res.doc()->GetAllocator()); light_state.PushBack(b, res.doc()->GetAllocator()); } - // Can't use StringRef here, turns some strings partially into null bytes for some reason + // can't use StringRef here, turns some strings partially into null bytes for some reason Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator()); response_object.AddMember(light_name, light_state, res.doc()->GetAllocator()); } diff --git a/src/spice2x/api/modules/sdvx.cpp b/src/spice2x/api/modules/sdvx.cpp new file mode 100644 index 0000000..b4c059c --- /dev/null +++ b/src/spice2x/api/modules/sdvx.cpp @@ -0,0 +1,67 @@ +#include "sdvx.h" + +#include + +using namespace std::placeholders; +using namespace rapidjson; + +namespace api::modules { + + SDVX::SDVX() : Module("sdvx") { + functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2); + + for (auto &light : games::sdvx::TAPELED_MAPPING) { + lights_by_names.emplace(light.lightName, light); + } + } + + /** + * tapeled_get() + * tapeled_get(name: str, ...) + */ + void SDVX::tapeled_get(Request &req, Response &res) { + Value response_object(kObjectType); + + // all tape leds + if (req.params.Size() == 0) { + // iterate through each device and dump its lights data into the response + for (const auto &mapping : games::sdvx::TAPELED_MAPPING) { + copy_tapeled_data(res, response_object, mapping); + } + } else { + // specified light names + for (Value ¶m : req.params.GetArray()) { + // check params + if (!param.IsString()) { + error_type(res, "name", "string"); + return; + } + + const auto name = param.GetString(); + if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) { + copy_tapeled_data(res, response_object, it->second.get()); + } + } + } + + res.add_data(response_object); + } + + void SDVX::copy_tapeled_data(Response &res, Value &response_object, + const tapeledutils::tape_led &mapping) + { + Value light_state(kArrayType); + light_state.Reserve( + static_cast(mapping.data.size() * 3), + res.doc()->GetAllocator()); + for (const auto [r, g, b] : mapping.data) { + light_state.PushBack(r, res.doc()->GetAllocator()); + light_state.PushBack(g, res.doc()->GetAllocator()); + light_state.PushBack(b, res.doc()->GetAllocator()); + } + + // can't use StringRef here, turns some strings partially into null bytes for some reason + Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator()); + response_object.AddMember(light_name, light_state, res.doc()->GetAllocator()); + } +} diff --git a/src/spice2x/api/modules/sdvx.h b/src/spice2x/api/modules/sdvx.h new file mode 100644 index 0000000..d9519d0 --- /dev/null +++ b/src/spice2x/api/modules/sdvx.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "api/module.h" +#include "api/request.h" +#include "external/robin_hood.h" +#include "games/sdvx/sdvx.h" + +namespace api::modules { + + class SDVX : public Module { + public: + SDVX(); + + private: + robin_hood::unordered_map> lights_by_names; + + void tapeled_get(Request &req, Response &res); + void copy_tapeled_data(Response &res, rapidjson::Value &response_object, + const tapeledutils::tape_led &mapping); + }; +} diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index 49be819..3986651 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -55,9 +55,12 @@ namespace api::modules { native_canvas_w = 0; native_canvas_h = 0; if (is_sdvx) { - // exceed gear subscreen, portrait after the rotation applied in apply_touch_errata - native_canvas_w = 1080; - native_canvas_h = 1920; + // windowed and landscape API coordinates already match the primary screen orientation; + // fullscreen portrait coordinates are rotated by apply_touch_errata + const bool landscape_coordinates = + GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP; + native_canvas_w = landscape_coordinates ? 1920 : 1080; + native_canvas_h = landscape_coordinates ? 1080 : 1920; } else if (avs::game::is_model("LDJ")) { // TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata native_canvas_w = is_tdj_fhd ? 1920 : 1280; @@ -218,8 +221,8 @@ namespace api::modules { // the target of the touch events so just assume it's the sub screen x = x_raw * 1920 / 1280; y = y_raw * 1080 / 720; - } else if (is_sdvx) { - // for exceed gear, they are both 1080p screens, but need to apply transformation + } else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) { + // rotate API coordinates into SDVX's portrait touch space x = 1080 - y_raw; y = x_raw; } diff --git a/src/spice2x/api/resources/python/spiceapi/__init__.py b/src/spice2x/api/resources/python/spiceapi/__init__.py index b7fe465..f0da213 100644 --- a/src/spice2x/api/resources/python/spiceapi/__init__.py +++ b/src/spice2x/api/resources/python/spiceapi/__init__.py @@ -7,6 +7,7 @@ from .coin import * from .control import * from .exceptions import * from .iidx import * +from .sdvx import * from .info import * from .keypads import * from .lights import * diff --git a/src/spice2x/api/resources/python/spiceapi/sdvx.py b/src/spice2x/api/resources/python/spiceapi/sdvx.py new file mode 100644 index 0000000..57ebdab --- /dev/null +++ b/src/spice2x/api/resources/python/spiceapi/sdvx.py @@ -0,0 +1,12 @@ +from .connection import Connection +from .request import Request + + +def sdvx_tapeled_get(con: Connection, *light_names): + req = Request("sdvx", "tapeled_get") + + for light_name in light_names: + req.add_param(light_name) + + res = con.request(req) + return res.get_data() diff --git a/src/spice2x/games/iidx/bi2x_hook.cpp b/src/spice2x/games/iidx/bi2x_hook.cpp index 4c9dc81..8e04a27 100644 --- a/src/spice2x/games/iidx/bi2x_hook.cpp +++ b/src/spice2x/games/iidx/bi2x_hook.cpp @@ -3,6 +3,7 @@ #if SPICE64 #include +#include "api/client.h" #include "util/detour.h" #include "util/logging.h" #include "util/utils.h" @@ -443,10 +444,12 @@ namespace games::iidx { GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); - for (unsigned int i = 0; i < data_size; ++i) { - map.data[i].r = data[i * 3]; - map.data[i].g = data[i * 3 + 1]; - map.data[i].b = data[i * 3 + 2]; + if (api::has_clients()) { + for (size_t i = 0; i < data_size; ++i) { + map.data[i].r = data[i * 3]; + map.data[i].g = data[i * 3 + 1]; + map.data[i].b = data[i * 3 + 2]; + } } } diff --git a/src/spice2x/games/sdvx/bi2x_hook.cpp b/src/spice2x/games/sdvx/bi2x_hook.cpp index 09cb08f..dc53f81 100644 --- a/src/spice2x/games/sdvx/bi2x_hook.cpp +++ b/src/spice2x/games/sdvx/bi2x_hook.cpp @@ -3,6 +3,7 @@ #if SPICE64 #include +#include "api/client.h" #include "util/detour.h" #include "util/logging.h" #include "util/utils.h" @@ -349,43 +350,29 @@ namespace games::sdvx { * 9 - v unit - 258 bytes - 86 colors * * data is stored in RGB order, 3 bytes per color - * - * TODO: expose this data via API */ - // data mapping - static struct TapeLedMapping { - size_t data_size; - int index_r, index_g, index_b; - - TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b) - : data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {} - - } mapping[] = { - { 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B }, - { 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B }, - { 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B }, - { 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B }, - { 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B }, - { 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B }, - { 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B }, - { 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B }, - { 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B }, - { 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B }, - }; - // check index bounds - if (tapeledutils::is_enabled() && index < std::size(mapping)) { - auto &map = mapping[index]; + if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) { + auto &map = TAPELED_MAPPING[index]; + const auto data_size = map.data.size(); // pick a color to use - const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size); + const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size); // program the lights into API auto &lights = get_lights(); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); + + if (api::has_clients()) { + for (size_t i = 0; i < data_size; ++i) { + map.data[i].r = data[i * 3]; + map.data[i].g = data[i * 3 + 1]; + map.data[i].b = data[i * 3 + 2]; + } + } } if (This != custom_node) { diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index 3a6a81c..729b7fb 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -61,6 +61,19 @@ namespace games::sdvx { static HKEY real_asio_reg_handle = nullptr; static HKEY real_asio_device_reg_handle = nullptr; + tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = { + { 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" }, + { 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" }, + { 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" }, + { 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" }, + { 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" }, + { 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" }, + { 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" }, + { 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" }, + { 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" }, + { 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" }, + }; + static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { if (lpSubKey != nullptr && phkResult != nullptr && diff --git a/src/spice2x/games/sdvx/sdvx.h b/src/spice2x/games/sdvx/sdvx.h index bc868fe..6e54054 100644 --- a/src/spice2x/games/sdvx/sdvx.h +++ b/src/spice2x/games/sdvx/sdvx.h @@ -6,6 +6,7 @@ #include "avs/game.h" #include "games/game.h" +#include "util/tapeled.h" namespace games::sdvx { @@ -25,6 +26,9 @@ namespace games::sdvx { // states extern bool SHOW_VM_MONITOR_WARNING; + constexpr int SDVX_TAPELED_TOTAL = 10; + extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL]; + static inline bool is_valkyrie_model() { return ( avs::game::is_model("KFC") && diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 94a98c1..17755d1 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -5,12 +5,8 @@ #include #include #include -#include #include -#ifdef __GNUC__ -#include -#endif #include "avs/game.h" #include "cfg/screen_resize.h" @@ -19,28 +15,23 @@ #include "games/popn/popn.h" #include "games/sdvx/sdvx.h" #include "games/mfc/mfc.h" -#include "games/io.h" #include "hooks/graphics/graphics.h" #include "launcher/launcher.h" #include "launcher/options.h" #include "launcher/signal.h" #include "launcher/shutdown.h" -#include "misc/clipboard.h" -#include "misc/eamuse.h" #include "misc/wintouchemu.h" #include "overlay/overlay.h" -#include "overlay/notifications.h" #include "util/detour.h" #include "util/deferlog.h" -#include "util/fileutils.h" #include "util/flags_helper.h" #include "util/libutils.h" #include "util/logging.h" #include "util/utils.h" #include "util/memutils.h" -#include "util/threadpool.h" #include "d3d9_device.h" +#include "d3d9_screenshot.h" #ifdef min #undef min @@ -64,19 +55,6 @@ return __ret; \ } while (0) -#ifdef __GNUC__ -typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t; -#else -#define D3DXIFF_PNG ((DWORD) 3) - -typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)( - LPCSTR pDestFile, - DWORD DestFormat, - LPDIRECT3DSURFACE9 pSrcSurface, - CONST PALETTEENTRY *pSrcPalette, - CONST RECT *pSrcRect); -#endif - /* * 9 on 12 */ @@ -100,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)( static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr; static char D3D9_DIRECT3D_CREATE9_CONTENTS[16]; -static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false; - // settings std::optional D3D9_ADAPTER = std::nullopt; DWORD D3D9_BEHAVIOR_DISABLE = 0; @@ -1499,178 +1475,6 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) { } } -static void save_capture( - int screen, - D3DFORMAT format, - UINT width, - UINT height, - IDirect3DSurface9 *surface) { - HRESULT hr; - - // lock surface to be able to access the data - D3DLOCKED_RECT finished_copy {}; - hr = surface->LockRect(&finished_copy, nullptr, 0); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); - graphics_capture_skip(screen); - return; - } - - // copy pixel data - size_t pitch = finished_copy.Pitch; - auto data = reinterpret_cast(finished_copy.pBits); - auto pixels = new uint8_t[width * height * 3]; - for (size_t row = 0; row < height; row++) { - size_t offset_pixels = 0; - size_t offset_row = row * width * 3; - switch (format) { - case D3DFMT_R8G8B8: { - for (size_t offset = 0; offset < pitch; offset += 3) { - auto cell = data + row * pitch + offset; - auto pixel = &pixels[offset_row + offset_pixels]; - pixel[0] = cell[0]; - pixel[1] = cell[1]; - pixel[2] = cell[2]; - offset_pixels += 3; - } - break; - } - case D3DFMT_X8R8G8B8: - case D3DFMT_A8R8G8B8: { - for (size_t offset = 0; offset < pitch; offset += 4) { - auto cell = data + row * pitch + offset; - auto pixel = &pixels[offset_row + offset_pixels]; - pixel[0] = cell[2]; - pixel[1] = cell[1]; - pixel[2] = cell[0]; - offset_pixels += 3; - } - break; - } - case D3DFMT_X8B8G8R8: - case D3DFMT_A8B8G8R8: { - for (size_t offset = 0; offset < pitch; offset += 4) { - auto cell = data + row * pitch + offset; - auto pixel = &pixels[offset_row + offset_pixels]; - pixel[0] = cell[0]; - pixel[1] = cell[1]; - pixel[2] = cell[2]; - offset_pixels += 3; - } - break; - } - default: { - for (size_t offset = 0; offset < width; offset++) { - auto pixel = &pixels[offset_row + offset_pixels]; - pixel[0] = 0; - pixel[1] = 0; - pixel[2] = 0; - offset_pixels += 3; - } - } - } - } - - // unlock surface - hr = surface->UnlockRect(); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); - graphics_capture_skip(screen); - return; - } - - // enqueue - graphics_capture_enqueue(screen, pixels, width, height); -} - -static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) { - HRESULT hr; - - D3DLOCKED_RECT finished_copy {}; - hr = surface->LockRect(&finished_copy, nullptr, 0); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; - } - - // set alpha channel to 255 - { - auto pitch = finished_copy.Pitch; - auto data = reinterpret_cast(finished_copy.pBits); - - for (size_t i = 0; i < height; i++) { - for (int j = 3; j < pitch; j += 4) { - data[i * pitch + j] = 255; - } - } - } - - hr = surface->UnlockRect(); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; - } - - // lazy load function - static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr; - if (D3DXSaveSurfaceToFileA_ptr == nullptr) { - D3DXSaveSurfaceToFileA_ptr = libutils::try_proc("D3DXSaveSurfaceToFileA"); - - // check if function was not found, likely because d3dx9 is not loaded - if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) { - ATTEMPTED_D3DX9_LOAD_LIBRARY = true; - - for (size_t i = 43; i >= 24; i--) { - auto lib_name = fmt::format("d3dx9_{}.dll", i); - auto d3dx9 = libutils::try_library(lib_name); - - // Check if library was not found - if (d3dx9 == nullptr) { - continue; - } - - D3DXSaveSurfaceToFileA_ptr = libutils::try_proc( - d3dx9, "D3DXSaveSurfaceToFileA"); - - // Check if function was not found - if (D3DXSaveSurfaceToFileA_ptr == nullptr) { - FreeLibrary(d3dx9); - d3dx9 = nullptr; - - continue; - } - - log_info("graphics::d3d9", "found surface save function in '{}'", lib_name); - break; - } - } - } - - if (D3DXSaveSurfaceToFileA_ptr != nullptr) { - - // save to file - log_info("graphics::d3d9", "saving screenshot to {}", file_path); - auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr); - - if (FAILED(hr)) { - log_warning("graphics::d3d9", "Failed to save screenshot"); - overlay::notifications::add( - overlay::notifications::Severity::Error, - "Screenshot failed to save"); - return; - } - - // save to clipboard - clipboard::copy_image(file_path); - - overlay::notifications::add( - overlay::notifications::Severity::Success, - fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); - } else { - log_warning("graphics::d3d9", "Direct3D save helper function not available"); - } -} - void graphics_d3d9_on_present( HWND hFocusWindow, IDirect3DDevice9 *device, @@ -1718,135 +1522,8 @@ void graphics_d3d9_on_present( wintouchemu::update(); } - // check screenshot key - static bool trigger_last = false; - auto buttons = games::get_buttons_overlay(eamuse_get_game()); - if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) && - GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot))) - { - if (!trigger_last) { - graphics_screenshot_trigger(); - } - trigger_last = true; - } else { - trigger_last = false; - } - - // process pending screenshot - bool screenshot = false; - bool capture = false; - int capture_screen = 0; - if ((screenshot = graphics_screenshot_consume()) - || ((capture = graphics_capture_consume(&capture_screen)))) { - HRESULT hr = S_OK; - - // TODO: verify capture_screen is a valid swapchain - - // get back buffer - IDirect3DSurface9 *buffer = nullptr; - if (SUB_SWAP_CHAIN != nullptr && capture_screen & 1) { - hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } else { - hr = device->GetBackBuffer(capture_screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } - if (FAILED(hr) || buffer == nullptr) { - log_warning("graphics::d3d9", - "failed to get back buffer, hr={}", - FMT_HRESULT(hr)); - if (capture) { - graphics_capture_skip(capture_screen); - } - return; - } - - D3DSURFACE_DESC desc {}; - hr = buffer->GetDesc(&desc); - if (FAILED(hr)) { - log_warning("graphics::d3d9", - "failed to acquire back buffer descriptor, hr={}", - FMT_HRESULT(hr)); - buffer->Release(); - if (capture) { - graphics_capture_skip(capture_screen); - } - return; - } - - // TODO: cache render targets - IDirect3DSurface9 *temp_surface = nullptr; - hr = device->CreateRenderTarget( - desc.Width, desc.Height, desc.Format, desc.MultiSampleType, - desc.MultiSampleQuality, TRUE, &temp_surface, nullptr); - if (FAILED(hr) || temp_surface == nullptr) { - log_warning("graphics::d3d9", - "failed to acquire temporary surface, hr={}", - FMT_HRESULT(hr)); - buffer->Release(); - if (capture) { - graphics_capture_skip(capture_screen); - } - return; - } - - hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE); - if (FAILED(hr)) { - log_warning("graphics::d3d9", - "failed to copy back buffer contents, hr={}", - FMT_HRESULT(hr)); - temp_surface->Release(); - buffer->Release(); - if (capture) { - graphics_capture_skip(capture_screen); - } - return; - } - - // release original back buffer reference - buffer->Release(); - - // function for storing the surface - auto surface_process = [=]() { - - // capture - if (capture) { - save_capture(capture_screen, desc.Format, desc.Width, desc.Height, temp_surface); - } - - // screenshot - if (screenshot) { - - // check where we can save it - auto file_path = graphics_screenshot_genpath(); - if (!file_path.empty()) { - - // write to file - save_screenshot(file_path, desc.Height, temp_surface); - } - } - - // release surface - temp_surface->Release(); - }; - - // list of games that crash when running the screenshot processor on another thread - static const robin_hood::unordered_set THREAD_BAN { - "JMA", -#ifndef SPICE64 - "KFC", -#endif - "KMA", - "KLP", - "LMA", - }; - - // run the save operation on another thread for supported games - if (THREAD_BAN.contains(avs::game::MODEL)) { - surface_process(); - } else { - static auto pool = ThreadPool(2); - pool.add(surface_process); - } - } + graphics_d3d9_poll_screenshot_hotkey(); + graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN); } void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp new file mode 100644 index 0000000..4b53643 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -0,0 +1,409 @@ +#include "d3d9_screenshot.h" + +#include +#include +#include +#include +#include + +#include + +#ifdef __GNUC__ +#include +#endif + +#include "avs/game.h" +#include "games/io.h" +#include "hooks/graphics/graphics.h" +#include "launcher/launcher.h" +#include "misc/clipboard.h" +#include "misc/eamuse.h" +#include "overlay/notifications.h" +#include "overlay/overlay.h" +#include "util/fileutils.h" +#include "util/libutils.h" +#include "util/logging.h" +#include "util/threadpool.h" + +#ifdef __GNUC__ +typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t; +#else +#define D3DXIFF_PNG ((DWORD) 3) + +typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)( + LPCSTR pDestFile, + DWORD DestFormat, + LPDIRECT3DSURFACE9 pSrcSurface, + CONST PALETTEENTRY *pSrcPalette, + CONST RECT *pSrcRect); +#endif + +static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false; + +namespace { + +enum class ImageRequestKind { + Screenshot, + Capture, +}; + +struct ImageRequest { + ImageRequestKind kind; + int screen; +}; + +struct SurfaceReleaser { + void operator()(IDirect3DSurface9 *surface) const { + surface->Release(); + } +}; + +using SurfacePtr = std::unique_ptr; + +struct BackbufferCopy { + D3DSURFACE_DESC desc {}; + SurfacePtr surface; +}; + +} // namespace + +static void save_capture( + int screen, + D3DFORMAT format, + UINT width, + UINT height, + IDirect3DSurface9 *surface) { + HRESULT hr; + + // lock surface to be able to access the data + D3DLOCKED_RECT finished_copy {}; + hr = surface->LockRect(&finished_copy, nullptr, 0); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); + graphics_capture_skip(screen); + return; + } + + // normalize supported D3D formats to packed RGB for API capture + size_t pitch = finished_copy.Pitch; + auto data = reinterpret_cast(finished_copy.pBits); + auto pixels = std::unique_ptr(new uint8_t[width * height * 3]); + for (size_t row = 0; row < height; row++) { + size_t offset_row = row * width * 3; + switch (format) { + case D3DFMT_R8G8B8: { + for (size_t column = 0; column < width; column++) { + auto cell = data + row * pitch + column * 3; + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = cell[0]; + pixel[1] = cell[1]; + pixel[2] = cell[2]; + } + break; + } + case D3DFMT_X8R8G8B8: + case D3DFMT_A8R8G8B8: { + for (size_t column = 0; column < width; column++) { + auto cell = data + row * pitch + column * 4; + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = cell[2]; + pixel[1] = cell[1]; + pixel[2] = cell[0]; + } + break; + } + case D3DFMT_X8B8G8R8: + case D3DFMT_A8B8G8R8: { + for (size_t column = 0; column < width; column++) { + auto cell = data + row * pitch + column * 4; + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = cell[0]; + pixel[1] = cell[1]; + pixel[2] = cell[2]; + } + break; + } + default: { + for (size_t column = 0; column < width; column++) { + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = 0; + pixel[1] = 0; + pixel[2] = 0; + } + } + } + } + + // unlock surface + hr = surface->UnlockRect(); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); + graphics_capture_skip(screen); + return; + } + + // enqueue + graphics_capture_enqueue(screen, pixels.release(), width, height); +} + +static void save_screenshot( + const std::string &file_path, + D3DFORMAT format, + UINT width, + UINT height, + IDirect3DSurface9 *surface) { + // 32-bit XRGB and ARGB surfaces use byte 3 as alpha; force opaque PNG output + if (format == D3DFMT_X8R8G8B8 || format == D3DFMT_A8R8G8B8 || + format == D3DFMT_X8B8G8R8 || format == D3DFMT_A8B8G8R8) { + + D3DLOCKED_RECT finished_copy {}; + HRESULT hr = surface->LockRect(&finished_copy, nullptr, 0); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); + return; + } + + const size_t pitch = finished_copy.Pitch; + auto data = reinterpret_cast(finished_copy.pBits); + for (size_t row = 0; row < height; row++) { + for (size_t column = 0; column < width; column++) { + data[row * pitch + column * 4 + 3] = 255; + } + } + + hr = surface->UnlockRect(); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); + return; + } + } + + // lazy load function + static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr; + if (D3DXSaveSurfaceToFileA_ptr == nullptr) { + D3DXSaveSurfaceToFileA_ptr = libutils::try_proc("D3DXSaveSurfaceToFileA"); + + // check if function was not found, likely because d3dx9 is not loaded + if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) { + ATTEMPTED_D3DX9_LOAD_LIBRARY = true; + + // prefer the newest installed helper while supporting older D3DX9 runtimes + for (size_t i = 43; i >= 24; i--) { + auto lib_name = fmt::format("d3dx9_{}.dll", i); + auto d3dx9 = libutils::try_library(lib_name); + + // Check if library was not found + if (d3dx9 == nullptr) { + continue; + } + + D3DXSaveSurfaceToFileA_ptr = libutils::try_proc( + d3dx9, "D3DXSaveSurfaceToFileA"); + + // Check if function was not found + if (D3DXSaveSurfaceToFileA_ptr == nullptr) { + FreeLibrary(d3dx9); + d3dx9 = nullptr; + + continue; + } + + log_info("graphics::d3d9", "found surface save function in '{}'", lib_name); + break; + } + } + } + + if (D3DXSaveSurfaceToFileA_ptr != nullptr) { + + // save to file + log_info("graphics::d3d9", "saving screenshot to {}", file_path); + const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr( + file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr); + + if (FAILED(save_result)) { + log_warning("graphics::d3d9", "Failed to save screenshot"); + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to save"); + return; + } + + // save to clipboard + clipboard::copy_image(file_path); + + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); + } else { + log_warning("graphics::d3d9", "Direct3D save helper function not available"); + } +} + +void graphics_d3d9_poll_screenshot_hotkey() { + static bool trigger_last = false; + auto buttons = games::get_buttons_overlay(eamuse_get_game()); + if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) && + GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot))) + { + if (!trigger_last) { + graphics_screenshot_trigger(); + } + trigger_last = true; + } else { + trigger_last = false; + } +} + +static std::optional acquire_backbuffer_copy( + IDirect3DDevice9 *device, IDirect3DSwapChain9 *sub_swap_chain, int screen) { + + HRESULT hr = S_OK; + + // TODO: verify screen is a valid swapchain + + IDirect3DSurface9 *buffer = nullptr; + if (sub_swap_chain != nullptr && screen & 1) { + hr = sub_swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); + } else { + hr = device->GetBackBuffer(screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer); + } + if (FAILED(hr) || buffer == nullptr) { + log_warning("graphics::d3d9", + "failed to get back buffer, hr={}", + FMT_HRESULT(hr)); + return std::nullopt; + } + + D3DSURFACE_DESC desc {}; + hr = buffer->GetDesc(&desc); + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to acquire back buffer descriptor, hr={}", + FMT_HRESULT(hr)); + buffer->Release(); + return std::nullopt; + } + + // TODO: cache render targets + IDirect3DSurface9 *temp_surface = nullptr; + hr = device->CreateRenderTarget( + desc.Width, desc.Height, desc.Format, desc.MultiSampleType, + desc.MultiSampleQuality, TRUE, &temp_surface, nullptr); + if (FAILED(hr) || temp_surface == nullptr) { + log_warning("graphics::d3d9", + "failed to acquire temporary surface, hr={}", + FMT_HRESULT(hr)); + buffer->Release(); + return std::nullopt; + } + + hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE); + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to copy back buffer contents, hr={}", + FMT_HRESULT(hr)); + temp_surface->Release(); + buffer->Release(); + return std::nullopt; + } + + // release original back buffer reference + buffer->Release(); + + return BackbufferCopy { + .desc = desc, + .surface = SurfacePtr(temp_surface), + }; +} + +static void dispatch_surface_save( + const ImageRequest &request, + BackbufferCopy copy) { + auto surface_process = [request, copy = std::move(copy)]() { + switch (request.kind) { + case ImageRequestKind::Capture: + save_capture( + request.screen, + copy.desc.Format, + copy.desc.Width, + copy.desc.Height, + copy.surface.get()); + break; + + case ImageRequestKind::Screenshot: { + auto file_path = graphics_screenshot_genpath(); + if (!file_path.empty()) { + save_screenshot( + file_path, + copy.desc.Format, + copy.desc.Width, + copy.desc.Height, + copy.surface.get()); + } + break; + } + } + }; + + // list of games that crash when running the screenshot processor on another thread + static const robin_hood::unordered_set THREAD_BAN { + "JMA", +#ifndef SPICE64 + // KFC only crashes under threaded processing in 32-bit builds + "KFC", +#endif + "KMA", + "KLP", + "LMA", + }; + + // run the save operation on another thread for supported games + if (THREAD_BAN.contains(avs::game::MODEL)) { + surface_process(); + } else { + static auto pool = ThreadPool(2); + pool.add(std::move(surface_process)); + } +} + +static std::optional consume_image_request() { + if (graphics_screenshot_consume()) { + return ImageRequest { + .kind = ImageRequestKind::Screenshot, + .screen = 0, + }; + } + + int capture_screen = 0; + if (graphics_capture_consume(&capture_screen)) { + return ImageRequest { + .kind = ImageRequestKind::Capture, + .screen = capture_screen, + }; + } + + return std::nullopt; +} + +void graphics_d3d9_process_screenshot_and_capture( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *sub_swap_chain) { + const auto request = consume_image_request(); + if (!request.has_value()) { + return; + } + + auto copy = acquire_backbuffer_copy( + device, + sub_swap_chain, + request->screen); + if (!copy.has_value()) { + if (request->kind == ImageRequestKind::Capture) { + graphics_capture_skip(request->screen); + } + return; + } + + dispatch_surface_save(*request, std::move(*copy)); +} diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h new file mode 100644 index 0000000..efa9096 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +void graphics_d3d9_poll_screenshot_hotkey(); + +void graphics_d3d9_process_screenshot_and_capture( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *sub_swap_chain); diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp index 267ca1d..3bfc128 100644 --- a/src/spice2x/touch/native/transform.cpp +++ b/src/spice2x/touch/native/transform.cpp @@ -1,5 +1,6 @@ #include "transform.h" +#include "avs/game.h" #include "hooks/graphics/graphics.h" #include "overlay/overlay.h" #include "settings.h" @@ -82,6 +83,25 @@ namespace nativetouch::transform { return overlay::OVERLAY->transform_touch_point(&position->x, &position->y); } + // the digitizer is mapped to the zero-based primary display, while SDVX + // still expects portrait coordinates when its image is rendered in landscape: + // (x, y) -> (width * (1 - y / height), height * x / width). + static bool transform_sdvx_landscape_touch_position(POINT *position) { + const auto landscape_width = static_cast(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? + GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT); + const auto landscape_height = static_cast(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? + GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH); + if (landscape_width <= 0 || landscape_height <= 0) { + return false; + } + + const auto input_x = position->x; + position->x = landscape_width - + MulDiv(position->y, landscape_width, landscape_height); + position->y = MulDiv(input_x, landscape_height, landscape_width); + return true; + } + // convert physical screen coordinates to game touch coordinates for a known target bool screen_to_game(HWND window, POINT *position) { if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) { @@ -127,6 +147,12 @@ namespace nativetouch::transform { return screen_to_game(window, position); } + // exception: sdvx windowed subscreen does not use the subscreen overlay transform + if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) { + POINT client_position = *position; + return screen_to_game_client(window, &client_position); + } + // if this game has a subscreen overlay that can transform touch input // but the window is hidden or not under the cursor, reject mouse-as-touch // (e.g., iidx/sdvx are rejected here, but nostalgia is allowed) @@ -144,6 +170,13 @@ namespace nativetouch::transform { const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW); const auto active_overlay = has_active_overlay_transform(); + // special case for SDVX landscape mode + if (!dedicated_subscreen && !active_overlay && + GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) { + return transform_sdvx_landscape_touch_position(position) ? + Result::Transformed : Result::Rejected; + } + // no dedicated subscreen or active overlay mapping; pass the point through unchanged if (!dedicated_subscreen && !active_overlay) { return Result::Unchanged;