Skip to content

Commit 112a8a2

Browse files
committed
Synchronize vJoy startup behavior across code and docs
1 parent a77ec0f commit 112a8a2

7 files changed

Lines changed: 69 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,7 @@ Windows uses [vJoy](https://github.com/BrunnerInnovation/vJoy) for virtual joyst
10131013
- `C:\Program Files\vJoy\bin\vJoyInterface.dll`
10141014
- `C:\Program Files (x86)\vJoy\bin\vJoyInterface.dll`
10151015
- System PATH
1016+
- After first successful load, OmniPanel-go keeps the DLL loaded for the process lifetime to avoid repeated vJoy reinitialization issues on some systems.
10161017
4. **Build with CGO (if building from source):**
10171018
Install MinGW-w64 or MSYS2 GCC. Use the maintained build script for reliable builds:
10181019
```bash

docs/tutorials/08-virtual-input.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ vJoy is a signed kernel driver that creates up to 16 virtual joystick devices. E
354354

355355
#### CGO Integration and DLL Loading
356356

357-
The Windows joystick implementation uses CGO to call `vJoyInterface.dll`. To improve reliability across different Windows installations and build environments, the code uses a **multi-path DLL loading strategy**:
357+
The Windows joystick implementation uses CGO to call `vJoyInterface.dll`. To improve reliability across different Windows installations and build environments, the code uses a **multi-path DLL loading strategy** and **caches the loaded DLL handle for the process lifetime**:
358358

359359
```go
360360
//go:build windows && cgo
@@ -369,7 +369,11 @@ package devices
369369
// 2. Standard vJoy x64 install paths
370370
// 3. Standard vJoy bin paths
371371
// Returns the loaded module handle, or NULL if all paths fail.
372+
// The handle is cached to avoid repeated load/unload reinitialization.
372373
HMODULE load_vJoyInterface(void) {
374+
static HMODULE cached = NULL;
375+
if (cached) return cached;
376+
373377
HMODULE h = NULL;
374378
const char* paths[] = {
375379
"vJoyInterface.dll", // Current dir / PATH
@@ -381,7 +385,10 @@ HMODULE load_vJoyInterface(void) {
381385
};
382386
for (int i = 0; paths[i] != NULL; i++) {
383387
h = LoadLibraryA(paths[i]);
384-
if (h) return h;
388+
if (h) {
389+
cached = h;
390+
return cached;
391+
}
385392
}
386393
return NULL;
387394
}
@@ -391,9 +398,8 @@ int wrap_vJoyEnabled(void) {
391398
HMODULE h = load_vJoyInterface();
392399
if (!h) return 0;
393400
vJoyEnabled_t fn = (vJoyEnabled_t)GetProcAddress(h, "vJoyEnabled");
394-
if (!fn) { FreeLibrary(h); return 0; }
401+
if (!fn) return 0;
395402
BOOL result = fn();
396-
FreeLibrary(h);
397403
return result ? 1 : 0;
398404
}
399405
*/
@@ -403,12 +409,21 @@ import "C"
403409
> **Why wrapper functions?**
404410
> Go CGO can't directly call `__cdecl` functions from a dynamically loaded DLL. The C wrapper functions use `LoadLibraryA`/`GetProcAddress` to dynamically load the DLL at runtime, then call the vJoy functions. This avoids requiring the DLL at link time.
405411
412+
> **Concept (Go/C interop): process-lifetime resource caching**
413+
> In `internal/devices/windows.go`, `load_vJoyInterface` stores the first successful `HMODULE` in a static variable and reuses it. This avoids repeated DLL initialization/cleanup cycles, which can trigger startup dialog errors in some `vJoyInterface.dll` builds.
414+
406415
> **Why multi-path loading?**
407416
> vJoy installs to different paths depending on Windows version, user permissions, and upgrade history. By checking multiple paths in order (PATH first for flexibility, then standard install directories), the code gracefully handles various configurations without requiring users to manually add vJoy to PATH or move DLLs around.
408417
409418
> **Key Pattern: Graceful fallback chains**
410419
> The path array is a fallback chain — try the easiest option first, then progressively try more specific locations. This pattern appears throughout systems engineering: DNS resolution, environment variable lookup, and configuration file discovery all use similar strategies to maximize compatibility.
411420
421+
> **Concept (JavaScript): stable transport contract**
422+
> In `static/client/client.js` (`initJoystick`), the browser only sends normalized `simulate-joystick` messages (`0-255` axis range). It does not know about DLL paths or OS APIs. This separation keeps frontend behavior stable while backend internals evolve.
423+
424+
> **Key Pattern (JavaScript): protocol-first design**
425+
> Keep wire messages small and consistent (`type` + `data` payload), and isolate platform-specific details in backend adapters. The same UI code works for Linux `uinput` and Windows vJoy because both consume the same protocol.
426+
412427
#### Axis Mapping
413428

414429
OmniPanel-go's 8 axes (0-255 range) are mapped to vJoy HID usage codes:

docs/tutorials/19-windows-build-and-ci.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,18 @@ This ensures the built binary has all required runtime dependencies. At runtime,
5757
2. Standard vJoy installation directories
5858
3. System PATH
5959

60-
This two-pronged approach — build-time bundling + runtime multi-path search — maximizes compatibility across different Windows configurations.
60+
After the first successful load, the DLL handle is cached and reused for the rest of the process. This avoids repeated load/unload cycles that can produce vJoy startup dialog errors on some systems.
61+
62+
This two-pronged approach — build-time bundling + runtime multi-path search with process-lifetime caching — maximizes compatibility across different Windows configurations.
63+
64+
> **Concept (Go): cache once, reuse many times**
65+
> `internal/devices/windows.go` keeps the vJoy module handle after the first successful `LoadLibraryA`. Reusing one handle is safer than repeatedly loading and unloading a DLL that does global initialization.
66+
67+
> **Concept (JavaScript): no frontend coupling to runtime DLL state**
68+
> The panel client (`static/client/client.js`) continues sending the same joystick WebSocket payloads regardless of how Windows loads vJoy. This keeps client behavior predictable for users.
69+
70+
> **Key Pattern (Go + JavaScript): transport and adapter split**
71+
> The frontend speaks a stable protocol (`simulate-joystick` messages), while the backend adapter handles platform-specific DLL and driver details. This split reduces regressions when platform internals change.
6172
6273
## CI vs Local Script
6374

docs/user-manual/de/12-virtual-joysticks.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Nach der Installation von vJoy findet OmniPanel-go automatisch die `vJoyInterfac
7777

7878
Wenn du OmniPanel-go von der Quelle mit dem Script `scripts/build-with-vosk.ps1` gebaut hast, wird die DLL automatisch neben deiner ausführbaren Datei kopiert. Andernfalls findet OmniPanel-go die DLL beim ersten Start im vJoy-Installationsverzeichnis.
7979

80+
OmniPanel-go lädt diese DLL einmal und behält sie, solange die App läuft. Dadurch wird eine wiederholte vJoy-Neuinitialisierung beim Start vermieden und die Stabilität auf manchen Windows-Systemen verbessert.
81+
8082
**Hinweis:** vJoy wird nur für virtuelle Joysticks benötigt. Virtuelle Maus- und Tastatureingaben nutzen die eingebaute `SendInput`-Funktion von Windows und brauchen keine zusätzlichen Treiber.
8183

8284
### macOS
@@ -258,6 +260,8 @@ In den Steuerelement-Einstellungen deines Spiels:
258260
- Starte OmniPanel-go nach der Installation von vJoy neu
259261
- Prüfe, dass die DLL nicht von Antivirus-Software unter Quarantäne gestellt wurde
260262

263+
Wenn beim Start Popups von `vJoyInterface DLL` erscheinen (zum Beispiel `RegisterClassEx failed` oder `Creation of dummy window failed`), beende OmniPanel-go vollständig und starte es erneut, nachdem du die vJoy-Installation geprüft hast. Aktuelle OmniPanel-go-Versionen laden die DLL pro Lauf nur einmal, um dieses Problem zu reduzieren.
264+
261265
**Linux:**
262266
- Prüfe, ob du Berechtigung für den Zugriff auf `/dev/uinput` hast
263267
- Versuche, OmniPanel-go mit `sudo` zu starten

docs/user-manual/en/12-virtual-joysticks.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ After installing vJoy, OmniPanel-go will automatically find the `vJoyInterface.d
7777

7878
If you built OmniPanel-go from source using the `scripts/build-with-vosk.ps1` script, the DLL will be automatically copied next to your executable. Otherwise, OmniPanel-go will find it in the vJoy installation directory the first time it runs.
7979

80+
OmniPanel-go loads this DLL once and keeps it loaded while the app is running. This avoids repeated vJoy re-initialization on startup and improves stability on some Windows systems.
81+
8082
**Note:** vJoy is only needed for virtual joysticks. Virtual mouse and keyboard input use Windows' built-in `SendInput` function and don't require any extra drivers.
8183

8284
### macOS
@@ -258,6 +260,8 @@ In your game's control settings:
258260
- Restart OmniPanel-go after installing vJoy
259261
- Check that the DLL is not quarantined by antivirus software
260262

263+
If you see startup popups from `vJoyInterface DLL` (for example `RegisterClassEx failed` or `Creation of dummy window failed`), fully close OmniPanel-go and start it again after verifying your vJoy install. Current versions of OmniPanel-go keep the DLL loaded once per run to reduce this issue.
264+
261265
**Linux:**
262266
- Check that you have permission to access `/dev/uinput`
263267
- Try running OmniPanel-go with `sudo`

internal/devices/windows.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ package devices
2222
// CGO wrapper functions for vJoyInterface.dll calls
2323
// These are needed because Go CGO can't directly call __cdecl DLL functions
2424
// Attempts to load vJoyInterface.dll from multiple paths: current directory, vJoy install dir, and PATH.
25+
// The loaded module is cached for process lifetime because repeated load/unload cycles
26+
// can trigger initialization issues inside some vJoyInterface builds.
2527
2628
HMODULE load_vJoyInterface(void) {
29+
static HMODULE cached = NULL;
2730
HMODULE h = NULL;
28-
// Try: current directory, then vJoy standard install paths
2931
const char* paths[] = {
3032
"vJoyInterface.dll", // Current dir / PATH
3133
"C:\\Program Files\\vJoy\\x64\\vJoyInterface.dll",
@@ -34,9 +36,18 @@ HMODULE load_vJoyInterface(void) {
3436
"C:\\Program Files (x86)\\vJoy\\bin\\vJoyInterface.dll",
3537
NULL
3638
};
39+
40+
if (cached) {
41+
return cached;
42+
}
43+
44+
// Try: current directory, then vJoy standard install paths
3745
for (int i = 0; paths[i] != NULL; i++) {
3846
h = LoadLibraryA(paths[i]);
39-
if (h) return h;
47+
if (h) {
48+
cached = h;
49+
return cached;
50+
}
4051
}
4152
return NULL;
4253
}
@@ -46,9 +57,8 @@ int wrap_vJoyEnabled(void) {
4657
HMODULE h = load_vJoyInterface();
4758
if (!h) return 0;
4859
vJoyEnabled_t fn = (vJoyEnabled_t)GetProcAddress(h, "vJoyEnabled");
49-
if (!fn) { FreeLibrary(h); return 0; }
60+
if (!fn) { return 0; }
5061
BOOL result = fn();
51-
FreeLibrary(h);
5262
return result ? 1 : 0;
5363
}
5464
@@ -57,9 +67,8 @@ int wrap_AcquireVJD(unsigned int rID) {
5767
HMODULE h = load_vJoyInterface();
5868
if (!h) return 0;
5969
AcquireVJD_t fn = (AcquireVJD_t)GetProcAddress(h, "AcquireVJD");
60-
if (!fn) { FreeLibrary(h); return 0; }
70+
if (!fn) { return 0; }
6171
BOOL result = fn(rID);
62-
FreeLibrary(h);
6372
return result ? 1 : 0;
6473
}
6574
@@ -68,19 +77,17 @@ void wrap_RelinquishVJD(unsigned int rID) {
6877
HMODULE h = load_vJoyInterface();
6978
if (!h) return;
7079
RelinquishVJD_t fn = (RelinquishVJD_t)GetProcAddress(h, "RelinquishVJD");
71-
if (!fn) { FreeLibrary(h); return; }
80+
if (!fn) { return; }
7281
fn(rID);
73-
FreeLibrary(h);
7482
}
7583
7684
int wrap_SetAxis(long Value, unsigned int rID, unsigned int Axis) {
7785
typedef BOOL (__cdecl *SetAxis_t)(long, unsigned int, unsigned int);
7886
HMODULE h = load_vJoyInterface();
7987
if (!h) return 0;
8088
SetAxis_t fn = (SetAxis_t)GetProcAddress(h, "SetAxis");
81-
if (!fn) { FreeLibrary(h); return 0; }
89+
if (!fn) { return 0; }
8290
BOOL result = fn(Value, rID, Axis);
83-
FreeLibrary(h);
8491
return result ? 1 : 0;
8592
}
8693
@@ -89,9 +96,8 @@ int wrap_SetBtn(int Value, unsigned int rID, unsigned char nBtn) {
8996
HMODULE h = load_vJoyInterface();
9097
if (!h) return 0;
9198
SetBtn_t fn = (SetBtn_t)GetProcAddress(h, "SetBtn");
92-
if (!fn) { FreeLibrary(h); return 0; }
99+
if (!fn) { return 0; }
93100
BOOL result = fn(Value, rID, nBtn);
94-
FreeLibrary(h);
95101
return result ? 1 : 0;
96102
}
97103
*/
@@ -110,8 +116,10 @@ type windowsJoystick struct {
110116
}
111117

112118
// initVJoy checks if the vJoy driver is installed and available.
113-
// It calls wrap_vJoyEnabled via CGO, which attempts to load vJoyInterface.dll
114-
// from multiple paths (current directory, vJoy install directories, and PATH).
119+
// It calls wrap_vJoyEnabled via CGO, which loads vJoyInterface.dll from a
120+
// fallback path list and keeps the module loaded for the process lifetime.
121+
// Keeping the module loaded avoids repeated DLL reinitialization side effects
122+
// seen on some Windows setups.
115123
// Returns true if vJoy is ready to use, false otherwise.
116124
// This initialization runs once on package load via sync.Once.
117125
func initVJoy() bool {

static/client/client.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,13 @@ function initButtonLayout(blockWrapper, btn) {
568568
}
569569
}
570570

571+
/**
572+
* initJoystick wires pointer events to joystick transport messages.
573+
*
574+
* The client always sends normalized `simulate-joystick` payloads (0-255 axis
575+
* values). Backend runtime details (for example how Windows vJoy DLL loading is
576+
* handled) stay server-side, so the frontend protocol remains stable.
577+
*/
571578
function initJoystick(blockWrapper) {
572579
const hitbox = blockWrapper.querySelector('.joy-hitbox');
573580
const base = blockWrapper.querySelector('.joy-base');

0 commit comments

Comments
 (0)