Compare commits

...

8 Commits

Author SHA1 Message Date
bicarus-dev 1e6b4c16cc asio bit difference message 2026-03-04 03:59:03 -08:00
bicarus-dev d6eb8f7125 fix duplicated text 2026-03-04 02:50:02 -08:00
bicarus dc6850e479 overlay: helper modal dialog for picking option values (#561)
## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Introduce a new modal dialog for picking out some of the options to
improve UX.

Implemented a few for now:

1. UI for picking ASIO drivers from a list (for -iidxasio, -sdvxasio,
-asioconvert, etc)
2. Button to generate card numbers for -card0 and card1
3. Processor Affinity selector (very similar to what's in Windows Task
Manager, list of checkboxes)
4. file selector (file open dialog for various file path overrides like
log file)
5. folder selector (folder override)

I wrote a selector for COM ports as well but how we use the value was
inconsistent (\\\\.\\COMx vs. COMx) and didn't see a huge point in it so
I didn't check it in.

## Testing
2026-03-02 12:08:20 -08:00
bicarus 2d623e179b audio: simplify ASIO backend conversion (#560)
## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Deprecate backend conversion options and replace with one unified option
for enabling WASAPI exclusive conversion to ASIO.

The new option takes in a string (ASIO driver name) and when filled out,
it automatically enables the ASIO backend.

This is to avoid confusion that was caused by the previous option - the
fact that it was split into two (one to enable the conversion, another
to optionally pick which ASIO driver to use) and that user needed to
specify an integer as opposed to the driver name.

## Testing
Tested on IIDX33. Old options should continue to work.
2026-03-01 13:06:18 -08:00
bicarus 884d665c1c sdvx, touch: account for 270 degree monitor rotation in wintouchemu (#559)
## Link to GitHub Issue or related Pull Request, if one exists
#558 

## Description of change
Same as #558 but this PR fixes the non-native touch cases (when
wintouchemu is active).

## Testing
Tested 90 and 270 degree rotation, 1080p and 1440p.
2026-02-27 17:17:00 -08:00
bicarus 2842fe7ec6 Update pull_request_template.md 2026-02-27 16:56:03 -08:00
bicarus 954e6022d9 sdvx, touch: hook Windows touch API when main monitor is rotated 270 degrees (portrait flipped) (#558)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Game expects primary monitor to be in 90 deg rotation, and does the
touch calculation accordingly.

If the user does 270 deg rotation ("portrait, flipped") then the game's
touch calculation is flipped upside down. Detect this and provide the
fixed up values in the touch hook.

## Testing
Tested with SDVX in 90 deg, 270 deg rotation, and 1080p, forced 1440p.
2026-02-27 14:22:56 -08:00
Emma 955c50a9f3 api: fix for setting drs touch state (#557)
## Link to GitHub Issue, if one exists
N/A

## Description of change
Changes the array size used for collecting touch inputs from the API to
be dynamic according to the size of the input data, rather than fixed.
This fixes a crash.

## Testing
An API client was made sending touch states, both valid and invalid
(oversized). The valid states are handled identically in the test menu
while the oversized states no longer crash.
2026-02-26 10:47:08 -08:00
20 changed files with 508 additions and 33 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
>
> Feel free to remove this section after you have read it.
## Link to GitHub Issue, if one exists
## Link to GitHub Issue or related Pull Request, if one exists
#0
## Description of change
+2 -2
View File
@@ -34,7 +34,7 @@ namespace api::modules {
void DRS::touch_set(Request &req, Response &res) {
// get all touch points
games::drs::drs_touch_t touches[16];
auto touches = std::make_unique<games::drs::drs_touch_t[]>(req.params.Size());
size_t i = 0;
for (Value &param : req.params.GetArray()) {
@@ -86,6 +86,6 @@ namespace api::modules {
}
// apply touch points
games::drs::fire_touches(touches, i);
games::drs::fire_touches(touches.get(), i);
}
}
+13
View File
@@ -13,6 +13,15 @@ enum class OptionType {
Hex,
};
enum class OptionPickerType {
None,
AsioDriver,
EACard,
CpuAffinity,
FilePath,
DirectoryPath,
};
struct OptionDefinition {
std::string title;
// unique identifier used for flag matching but also stored in config files
@@ -32,6 +41,10 @@ struct OptionDefinition {
bool sensitive = false;
std::vector<std::pair<std::string, std::string>> elements = {};
bool disabled = false;
OptionPickerType picker = OptionPickerType::None;
// for OptionPickerType::FilePath
std::string file_extension = "";
};
class Option {
+7 -4
View File
@@ -21,8 +21,9 @@
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/libutils.h"
#include "misc/wintouchemu.h"
#include "misc/eamuse.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "bi2x_hook.h"
#include "camera.h"
#include "io.h"
@@ -416,9 +417,11 @@ namespace games::sdvx {
}
if (is_valkyrie_model()) {
// hook touch window
// in windowed mode, game can accept mouse input on the second screen
if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
if (NATIVETOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
// hook touch window
// in windowed mode, game can accept mouse input on the second screen
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
+2 -1
View File
@@ -42,7 +42,8 @@ namespace hooks::audio {
bool USE_DUMMY = false;
WAVEFORMATEXTENSIBLE FORMAT {};
std::optional<Backend> BACKEND = std::nullopt;
size_t ASIO_DRIVER_ID = 0;
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
// private globals
+3 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <optional>
#include <string>
#include <windows.h>
#include <mmreg.h>
@@ -20,7 +21,8 @@ namespace hooks::audio {
extern bool USE_DUMMY;
extern WAVEFORMATEXTENSIBLE FORMAT;
extern std::optional<Backend> BACKEND;
extern size_t ASIO_DRIVER_ID;
extern std::optional<size_t> ASIO_DRIVER_ID;
extern std::string ASIO_DRIVER_NAME;
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
extern bool LOW_LATENCY_SHARED_WASAPI;
@@ -218,13 +218,40 @@ void AsioBackend::set_thread_state(AsioThreadState state) {
bool AsioBackend::load_driver() {
AsioDriverList asio_driver_list;
for (const auto &driver : asio_driver_list.driver_list) {
log_info("audio::asio", "Driver {}", driver.id);
log_info("audio::asio", "... Name : {}", driver.name);
log_info("audio::asio", "... Path : {}", driver.dll_path);
size_t driver_id = 0;
std::string driver_name = "";
if (hooks::audio::ASIO_DRIVER_ID.has_value()) {
driver_id = hooks::audio::ASIO_DRIVER_ID.value();
}
const auto driver_id = hooks::audio::ASIO_DRIVER_ID;
for (const auto &driver : asio_driver_list.driver_list) {
log_info("audio::asio", " Driver ID : {}", driver.id);
log_info("audio::asio", " ... Name : {}", driver.name);
log_info("audio::asio", " ... Path : {}", driver.dll_path);
if (!hooks::audio::ASIO_DRIVER_NAME.empty()) {
// match on driver name
if (driver.name == hooks::audio::ASIO_DRIVER_NAME) {
driver_id = driver.id;
driver_name = driver.name;
}
} else if (driver.id == driver_id) {
// match on driver index (0 for default or user-specified)
driver_name = driver.name;
}
}
if (driver_name.empty()) {
if (!hooks::audio::ASIO_DRIVER_NAME.empty()) {
log_warning(
"audio::asio",
"tried to look for ASIO driver but failed: {}", hooks::audio::ASIO_DRIVER_NAME);
}
log_fatal(
"audio::asio",
"could not find ASIO driver to use for conversion, fix your audio options and try again");
}
log_info("audio::asio", "will attempt to use the following ASIO driver: ID = {}, Name = {}", driver_id, driver_name);
IAsio *driver = nullptr;
auto ret = asio_driver_list.open_driver(driver_id, reinterpret_cast<void **>(&driver));
+9 -1
View File
@@ -1011,18 +1011,26 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) {
hooks::audio::VOLUME_HOOK_ENABLED = false;
}
if (options[launcher::Options::AudioBackend].is_active()) {
auto &name = options[launcher::Options::AudioBackend].value_text();
auto backend = hooks::audio::name_to_backend(name.c_str());
if (!backend.has_value() && !cfg::CONFIGURATOR_STANDALONE) {
log_fatal("launcher", "invalid audio backend: {}", name);
}
hooks::audio::BACKEND = backend;
}
if (options[launcher::Options::AsioDriverId].is_active()) {
hooks::audio::ASIO_DRIVER_ID = options[launcher::Options::AsioDriverId].value_uint32();
}
if (options[launcher::Options::AsioDriverName].is_active()) {
hooks::audio::BACKEND = hooks::audio::name_to_backend("asio");
hooks::audio::ASIO_DRIVER_NAME = options[launcher::Options::AsioDriverName].value_text();
// this new option overrides the old one
hooks::audio::ASIO_DRIVER_ID.reset();
}
if (options[launcher::Options::AudioDummy].value_bool()) {
hooks::audio::USE_DUMMY = true;
}
+44 -3
View File
@@ -71,6 +71,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Text,
.setting_name = "*.dll",
.category = "Path Overrides",
// intentionally not setting a file picker here to discourage people setting this without a good reason
},
{
.title = "Open Configurator",
@@ -121,6 +122,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.setting_name = "E004010000000000",
.category = "Network",
.sensitive = true,
.picker = OptionPickerType::EACard,
},
{
.title = "Player 2 Card",
@@ -130,6 +132,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.setting_name = "E004010000000000",
.category = "Network",
.sensitive = true,
.picker = OptionPickerType::EACard,
},
{
// Player1PinMacro
@@ -618,6 +621,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Text,
.game_name = "Beatmania IIDX",
.category = "Game Options",
.picker = OptionPickerType::AsioDriver,
},
{
.title = "IIDX BIO2 Firmware Update",
@@ -778,6 +782,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Text,
.game_name = "Sound Voltex",
.category = "Game Options (Advanced)",
.picker = OptionPickerType::DirectoryPath,
},
{
.title = "SDVX Printer Output Clear",
@@ -884,11 +889,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.display_name = "sdvxasio",
.aliases= "sdvxasio",
.desc = "ASIO driver name to use, replacing XONAR SOUND CARD(64). "
"String should match registry key under HLKM\\SOFTWARE\\ASIO\\ \n\n"
"String should match registry key under HKLM\\SOFTWARE\\ASIO\\ \n\n"
"SDVX is EXTREMELY picky about ASIO devices it can support!",
.type = OptionType::Text,
.game_name = "Sound Voltex",
.category = "Game Options",
.picker = OptionPickerType::AsioDriver,
},
{
// spice2x_SDVXSubPos
@@ -1284,6 +1290,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to the modules folder.",
.type = OptionType::Text,
.category = "Path Overrides",
// intentionally not setting a folder picker here to discourage people setting this without a good reason
},
{
.title = "Screenshot Folder Override",
@@ -1291,6 +1298,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to the screenshots folder.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::DirectoryPath,
},
{
.title = "Configuration Path Override",
@@ -1310,6 +1318,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"If left empty, %appdata%\\spice2x\\spicetools_screen_resize.json will be used.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "JSON",
},
{
// PatchManagerConfigPath
@@ -1319,6 +1329,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"If left empty, %appdata%\\spice2x\\spicetools_patch_manager.json will be used.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "JSON",
},
{
.title = "Intel SDE",
@@ -1326,6 +1338,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Path to Intel SDE kit path for automatic attaching.",
.type = OptionType::Text,
.category = "Development",
.picker = OptionPickerType::DirectoryPath
},
{
.title = "ea3-config.xml Override",
@@ -1333,6 +1346,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to ea3-config.xml.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "XML",
},
{
.title = "app-config.xml Override",
@@ -1340,6 +1355,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to app-config.xml.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "XML",
},
{
.title = "avs-config.xml Override",
@@ -1347,6 +1364,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to avs-config.xml.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "XML",
},
{
.title = "bootstrap.xml Override",
@@ -1354,6 +1373,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to bootstrap.xml.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "XML",
},
{
.title = "log.txt Override",
@@ -1361,6 +1382,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Sets a custom path to log.txt.",
.type = OptionType::Text,
.category = "Path Overrides",
.picker = OptionPickerType::FilePath,
.file_extension = "TXT",
},
{
.title = "API TCP Port",
@@ -1670,6 +1693,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"Must provide a hexadecimal mask (e.g., 0x1ff00).",
.type = OptionType::Hex,
.category = "Performance",
.picker = OptionPickerType::CpuAffinity,
},
{
// spice2x_ProcessorEfficiencyClass
@@ -1742,11 +1766,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Audio (Hacks)",
},
{
.title = "Spice Audio Hook Backend",
// AudioBackend
.title = "Spice Audio Hook Backend (DEPRECATED - use -asioconvert instead)",
.name = "audiobackend",
.desc = "Selects the audio backend to use when spice audio hook is enabled, overriding exclusive WASAPI. "
"Does nothing for games that do not output to exclusive WASAPI.",
.type = OptionType::Enum,
.hidden = true,
.category = "Audio",
.elements = {
{"asio", "ASIO"},
@@ -1754,12 +1780,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
},
{
.title = "Spice Audio Hook ASIO Driver ID",
// AsioDriverId
.title = "Spice Audio Hook ASIO Driver ID (DEPRECATED - use -asioconvert instead)",
.name = "asiodriverid",
.desc = "Selects the ASIO driver id to use when Spice Audio Backend is set to ASIO.",
.type = OptionType::Integer,
.hidden = true,
.category = "Audio",
},
{
// AsioDriverName
.title = "WASAPI Exclusive to ASIO Conversion",
.name = "asioconvert",
.desc = "Converts WASAPI Exclusive audio output to ASIO. Value here should match registry key under HKLM\\SOFTWARE\\ASIO\\\n\n"
"Use this if the game is configured to use WASAPI Exclusive but you want to use your ASIO driver instead.\n\n"
"This should only be used as last resort if your audio device does not support WASAPI Exclusive.\n\n"
"Does nothing for games that do not output to exclusive WASAPI.",
.type = OptionType::Text,
.category = "Audio",
.picker = OptionPickerType::AsioDriver,
},
{
.title = "WASAPI Dummy Context",
.name = "audiodummy",
@@ -2592,6 +2632,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Text,
.game_name = "LovePlus",
.category = "Game Options (Advanced)",
.picker = OptionPickerType::DirectoryPath,
},
{
.title = "LovePlus Printer Output Clear",
+1
View File
@@ -187,6 +187,7 @@ namespace launcher {
spice2x_DisableVolumeHook,
AudioBackend,
AsioDriverId,
AsioDriverName,
AudioDummy,
DelayBy5Seconds,
spice2x_DelayByNSeconds,
+28 -1
View File
@@ -2,7 +2,10 @@
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "avs/game.h"
#include "wintouchemu.h"
#include "rawinput/touch.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/logging.h"
@@ -63,6 +66,11 @@ namespace nativetouchhook {
point->cyContact = 0;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
}
static BOOL WINAPI GetTouchInputInfoHook(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
@@ -72,9 +80,28 @@ namespace nativetouchhook {
return result;
}
bool flip_values = false;
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
log_debug(
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
rawinput::touch::DISPLAY_ORIENTATION,
rawinput::touch::DISPLAY_SIZE_X,
rawinput::touch::DISPLAY_SIZE_Y);
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_values = true;
}
}
for (size_t i = 0; i < cInputs; i++) {
PTOUCHINPUT point = &pInputs[i];
strip_contact_size(point);
if (avs::game::is_model("LDJ")) {
strip_contact_size(point);
}
if (flip_values) {
flip_touch_points(point);
}
}
return result;
+12
View File
@@ -20,6 +20,7 @@
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
@@ -126,6 +127,11 @@ namespace wintouchemu {
WINDOW_TITLE_END = window_title_end;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
}
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// check if original should be called
@@ -220,6 +226,12 @@ namespace wintouchemu {
touch_input->cxContact = 0;
touch_input->cyContact = 0;
if (avs::game::is_model("KFC") &&
rawinput::touch::DISPLAY_INITIALIZED &&
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_touch_points(touch_input);
}
} else if (USE_MOUSE && !mouse_used) {
// disable further mouse inputs this call
-2
View File
@@ -142,7 +142,6 @@ namespace ImGui {
ImGui::PopStyleVar();
ImGui::PopStyleColor();
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
ImGui::SameLine();
ImGui::HelpTooltip(tooltip.c_str());
}
ImGui::PopID();
@@ -159,7 +158,6 @@ namespace ImGui {
bool clicked = ImGui::Button("\u00D7"); // multiplication sign (×)
ImGui::PopStyleColor(4);
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
ImGui::SameLine();
ImGui::HelpTooltip(tooltip.c_str());
}
ImGui::PopID();
+1 -1
View File
@@ -283,7 +283,7 @@ void ImGui_ImplSpice_NewFrame() {
memset(io.MouseDown, false, sizeof(io.MouseDown));
// early quit if window not in focus
if (!superexit::has_focus()) {
if (!superexit::has_focus() || rawinput::OS_WINDOW_ACTIVE) {
return;
}
+323 -7
View File
@@ -4,13 +4,16 @@
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <commdlg.h>
#include "build/defs.h"
#include "build/resource.h"
#include "cfg/config.h"
#include "cfg/configurator.h"
#include "external/asio/asiolist.h"
#include "external/imgui/imgui_internal.h"
#include "external/imgui/misc/cpp/imgui_stdlib.h"
#include "games/io.h"
#include "avs/core.h"
#include "avs/ea3.h"
@@ -26,6 +29,7 @@
#include "util/fileutils.h"
#include "util/logging.h"
#include "util/resutils.h"
#include "util/scope_guard.h"
#include "util/time.h"
#include "util/utils.h"
@@ -33,6 +37,17 @@
#undef min
#endif
static int CALLBACK BrowseCallbackProc(
HWND hwnd,
UINT uMsg,
LPARAM lParam,
LPARAM lpData) {
if (uMsg == BFFM_INITIALIZED) {
SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, lpData);
}
return 0;
}
namespace overlay::windows {
// same width as dummy marker
@@ -40,6 +55,8 @@ namespace overlay::windows {
const auto PROJECT_URL = "https://spice2x.github.io";
constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f);
std::unique_ptr<AsioDriverList> asio_driver_list;
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
this->title = "Configuration";
@@ -2707,17 +2724,17 @@ namespace overlay::windows {
// open dialog to get path
auto ofn_path = std::make_unique<wchar_t[]>(512);
ofn_path[0] = L'\0';
OPENFILENAMEW ofn {};
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = nullptr;
ofn.lpstrFilter = L"";
ofn.lpstrFile = ofn_path.get();
ofn.nMaxFile = 512;
ofn.Flags = OFN_EXPLORER;
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = L"txt";
ofn.lpstrInitialDir = L".";
// check for success
auto guard = rawinput::set_os_window_focus_guard();
if (GetSaveFileNameW(&ofn)) {
// update card path
@@ -2905,6 +2922,255 @@ namespace overlay::windows {
ImGui::EndDisabled();
}
std::string Config::build_option_value_picker_title(const OptionDefinition& definition) {
// need to make these all unique since they are also used as ID
// if not unique, append ## per ImGui rules to create unique ones
switch (definition.picker) {
case OptionPickerType::AsioDriver:
return "ASIO Driver Picker";
case OptionPickerType::EACard:
return "EA Card Picker";
case OptionPickerType::CpuAffinity:
return "CPU Affinity Picker";
case OptionPickerType::FilePath:
return "File Picker";
case OptionPickerType::DirectoryPath:
return "Folder Picker";
default:
return "Unknown Picker";
};
}
void Config::build_option_value_picker(Option& option) {
auto &definition = option.get_definition();
if (definition.picker == OptionPickerType::AsioDriver) {
if (asio_driver_list == nullptr) {
asio_driver_list = std::make_unique<AsioDriverList>();
}
ImGui::TextUnformatted("If your ASIO driver is not shown here, close this");
ImGui::TextUnformatted("popup and enter the driver name manually.");
ImGui::SameLine();
ImGui::HelpMarker(
"This list is populated by scanning the registry for ASIO drivers.\n\n"
"If your driver is not showing up, it may be because it is not properly registered in the system.\n\n"
"For 64-bit games, check in HKLM\\SOFTWARE\\ASIO\\.\n\n"
"For 32-bit games on 64-bit Windows, check in HKLM\\SOFTWARE\\WOW6432Node\\ASIO\\.\n\n"
"spicecfg runs in 32-bit, so it may not see 64-bit-only drivers.");
ImGui::TextUnformatted("");
if (asio_driver_list->driver_list.empty()) {
ImGui::TextUnformatted("No ASIO drivers found.");
} else {
ImGui::TextUnformatted("Pick from ASIO drivers:");
ImGui::SetNextItemWidth(300.f);
if (ImGui::BeginListBox("##asiodrivers")) {
for (const auto &driver : asio_driver_list->driver_list) {
const bool is_selected = option.value == std::string(driver.name);
if (ImGui::Selectable(fmt::format("[{}] {}", driver.id, driver.name).c_str(), is_selected)) {
option.value = driver.name;
}
}
ImGui::EndListBox();
}
}
} else if (definition.picker == OptionPickerType::EACard) {
ImGui::TextUnformatted("Generate a new card number:");
if (ImGui::Button("Generate")) {
char new_card[17];
generate_ea_card(new_card);
option.value = new_card;
}
} else if (definition.picker == OptionPickerType::CpuAffinity) {
ImGui::TextUnformatted("Requires restart! Showing all procs in Group 0.");
ImGui::TextUnformatted("");
ImGui::TextUnformatted("Pick CPU cores to use:");
const uint64_t cpu_count = GetActiveProcessorCount(0);
uint64_t affinity = 0;
if (!option.value.empty()) {
try {
affinity = std::stoull(option.value, nullptr, 16);
} catch (const std::exception &ex) {
option.value = "";
affinity = 0;
}
}
ImGui::BeginChild(
"##cpuaffinity",
ImVec2(
0,
ImGui::GetFrameHeightWithSpacing() * std::clamp(cpu_count / 4, 2ull, 4ull))
);
bool selection_changed = false;
uint64_t set_bits = 0;
for (uint64_t i = 0; i < cpu_count; i++) {
if (i % 4 != 0) {
ImGui::SameLine();
}
bool selected = (affinity & (1ULL << i)) != 0;
if (ImGui::Checkbox(fmt::format("CPU {}", i).c_str(), &selected)) {
selection_changed = true;
if (selected) {
affinity |= (1ULL << i);
} else {
affinity &= ~(1ULL << i);
}
}
if (selected) {
set_bits++;
}
}
if (selection_changed) {
if (set_bits == 0) {
option.value = "";
} else {
option.value = fmt::format("0x{:X}", affinity);
}
}
ImGui::EndChild();
if (option.value.empty()) {
ImGui::TextUnformatted("Using all CPUs (option default).");
} else {
ImGui::TextUnformatted("Using selected CPUs.");
}
} else if (definition.picker == OptionPickerType::FilePath) {
if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::BeginDisabled();
ImGui::TextUnformatted("File browser only works in spicecfg.");
}
if (ImGui::Button("Select File...")) {
// run in separate thread otherwise we get a crash
if (!file_picker_thread) {
file_picker_done = false;
file_picker_path = "";
file_picker_thread = new std::thread([this, &definition] {
std::wstring extensions;
if (!definition.file_extension.empty()) {
const std::wstring ext = s2ws(definition.file_extension);
// filter to file extension preferred by the option (e.g., DLL)
extensions = ext + L" Files (*." + ext + L")";
extensions.push_back(L'\0');
extensions += L"*." + ext;
extensions.push_back(L'\0');
// also add "All files" filter
extensions += L"All Files (*.*)";
extensions.push_back(L'\0');
extensions += L"*.*";
extensions.push_back(L'\0');
// eol
extensions.push_back(L'\0');
} else {
extensions = L"All Files (*.*)";
extensions.push_back(L'\0');
extensions += L"*.*";
extensions.push_back(L'\0');
extensions.push_back(L'\0');
}
// open dialog to get path
auto ofn_path = std::make_unique<wchar_t[]>(512);
ofn_path[0] = L'\0';
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFilter = extensions.c_str();
ofn.lpstrFile = ofn_path.get();
ofn.nMaxFile = 512;
ofn.nFilterIndex = 1;
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
ofn.lpstrInitialDir = L".";
// check for success
auto guard = rawinput::set_os_window_focus_guard();
if (GetSaveFileNameW(&ofn)) {
file_picker_path = std::filesystem::path(ofn_path.get());
}
// clean up
file_picker_done = true;
});
}
}
if (file_picker_done) {
file_picker_done = false;
file_picker_thread->join();
delete file_picker_thread;
file_picker_thread = nullptr;
if (!file_picker_path.empty()) {
option.value = file_picker_path.string();
}
}
if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::EndDisabled();
}
} else if (definition.picker == OptionPickerType::DirectoryPath) {
if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::BeginDisabled();
ImGui::TextUnformatted("File browser only works in spicecfg.");
}
if (ImGui::Button("Browse...")) {
// run in separate thread otherwise we get a crash
if (!file_picker_thread) {
file_picker_done = false;
file_picker_path = "";
file_picker_thread = new std::thread([this] {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
const auto spice_bin_path =
libutils::module_file_name(nullptr).parent_path().wstring();
// SHBrowseForFolderW sucks, but the alternatives are:
// 1. Use IFileDialog, which requires pulling in more Windows dependencies
// 2. use ImGui::FileBrowser
// both are acceptable but sticking to legacy UI for now for simplicity
BROWSEINFOW info{};
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
info.lpfn = BrowseCallbackProc;
info.lParam = reinterpret_cast<LPARAM>(spice_bin_path.c_str());
auto guard = rawinput::set_os_window_focus_guard();
auto pidl = SHBrowseForFolderW(&info);
if (pidl) {
wchar_t path[MAX_PATH];
std::filesystem::path result;
if (SHGetPathFromIDListW(pidl, path)) {
file_picker_path = path;
}
CoTaskMemFree(pidl);
} else {
file_picker_path = "";
}
// clean up
file_picker_done = true;
CoUninitialize();
});
}
}
if (file_picker_done) {
file_picker_done = false;
file_picker_thread->join();
delete file_picker_thread;
file_picker_thread = nullptr;
if (!file_picker_path.empty()) {
option.value = file_picker_path.string();
}
}
if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::EndDisabled();
}
} else {
ImGui::TextUnformatted("No picker available for this option. How did you get here?");
}
}
void Config::build_options(
std::vector<Option> *options, const std::string &category, const std::string *filter) {
int options_count;
@@ -3183,12 +3449,24 @@ namespace overlay::windows {
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
}
ImGui::PopStyleVar();
const std::string option_popup_id = build_option_value_picker_title(definition);
if (definition.picker != OptionPickerType::None && !option.disabled && !definition.disabled) {
ImGui::SameLine();
if (ImGui::Button("Pick")) {
ImGui::OpenPopup(option_popup_id.c_str());
}
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
ImGui::HelpTooltip(definition.desc.c_str());
}
}
ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing
// clean up disabled item flags
if (option.disabled || definition.disabled) {
ImGui::PopItemFlag();
ImGui::PopStyleVar();
ImGui::PopItemFlag(); // ImGuiItemFlags_Disabled
ImGui::PopStyleVar(); // ImGuiStyleVar_Alpha
}
// disabled help
@@ -3199,6 +3477,44 @@ namespace overlay::windows {
"Run spicecfg.exe to configure the options and then run spice(64).exe directly.");
}
// value picker
if (definition.picker != OptionPickerType::None &&
!option.disabled && !definition.disabled) {
if (ImGui::BeginPopupModal(option_popup_id.c_str(),
nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
// for min width enforcement
ImGui::Dummy(ImVec2(320.f, 0.f));
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", definition.title.c_str());
ImGui::TextUnformatted("");
ImGui::TextUnformatted("Current value:");
ImGui::BeginDisabled();
// keeping it read only; if you want to make this editable, we need to refactor the
// input validation logic above (for int/hex) so people don't end up with invalid
// values that can crash spicecfg
ImGui::InputText("", &option.value, ImGuiInputTextFlags_ReadOnly);
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::ClearButton("Reset to default")) {
option.value = "";
}
ImGui::TextUnformatted("");
build_option_value_picker(option);
ImGui::TextUnformatted("");
if (ImGui::Button("Save & Close")) {
ImGui::CloseCurrentPopup();
this->options_dirty = true;
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
ImGui::EndPopup();
}
}
// next item
ImGui::PopID();
}
+6
View File
@@ -76,6 +76,10 @@ namespace overlay::windows {
std::string search_filter = "";
std::string search_filter_in_lower_case = "";
std::filesystem::path file_picker_path;
std::thread *file_picker_thread = nullptr;
bool file_picker_done = false;
void build_buttons(const std::string &name, std::vector<Button> *buttons, int min = 0, int max = -1);
void build_button(
const std::string &name,
@@ -104,6 +108,8 @@ namespace overlay::windows {
void edit_light_popup(Light &primary_light, Light *light, const int alt_index);
void build_cards();
std::string build_option_value_picker_title(const OptionDefinition& option);
void build_option_value_picker(Option& option);
void build_options(
std::vector<Option> *options, const std::string &category, const std::string *filter=nullptr);
void build_about();
+4
View File
@@ -38,6 +38,10 @@ namespace rawinput {
uint32_t MIDI_NOTE_SUSTAIN = 20;
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
// the price we pay for making spice overlay consume from raw input
// making focus detection a nightmare
bool OS_WINDOW_ACTIVE = false;
}
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
+10
View File
@@ -10,6 +10,7 @@
#include "device.h"
#include "hotplug.h"
#include "util/scope_guard.h"
namespace rawinput {
@@ -31,6 +32,15 @@ namespace rawinput {
extern bool NAIVE_REQUIRE_FOCUS;
extern bool RAWINPUT_REQUIRE_FOCUS;
// while active, prevents overlay from accepting any input
// can be used while OS modal dialog is shown on top of overlay/spicecfg
// always prefer RAII set_os_window_focus_guard instead of the global bool
extern bool OS_WINDOW_ACTIVE;
inline scope_guard set_os_window_focus_guard() {
rawinput::OS_WINDOW_ACTIVE = true;
return scope_guard {[]() { rawinput::OS_WINDOW_ACTIVE = false; }};
}
struct DeviceCallback {
void *data;
std::function<void(void*, Device*)> f;
+4 -4
View File
@@ -26,10 +26,10 @@ namespace rawinput::touch {
bool INVERTED = false;
// state
static bool DISPLAY_INITIALIZED = false;
static DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
static long DISPLAY_SIZE_X = 1920L;
static long DISPLAY_SIZE_Y = 1080L;
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
long DISPLAY_SIZE_X = 1920L;
long DISPLAY_SIZE_Y = 1080L;
bool DISPLAY_INITIALIZED = false;
bool is_touchscreen(Device *device) {
+6
View File
@@ -9,6 +9,12 @@ namespace rawinput::touch {
extern bool DISABLED;
extern bool INVERTED;
// global state
extern DWORD DISPLAY_ORIENTATION;
extern long DISPLAY_SIZE_X;
extern long DISPLAY_SIZE_Y;
extern bool DISPLAY_INITIALIZED;
bool is_touchscreen(Device *device);
void enable(Device *device);
void disable(Device *device);