@gjsify/node-gi 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/binding.gyp +1 -0
- package/gi.js +34 -1
- package/gtk-runtime.d.ts +31 -0
- package/gtk-runtime.js +159 -6
- package/index.d.ts +14 -0
- package/index.js +12 -0
- package/package.json +13 -13
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/calls.cc +10 -34
- package/src/class.cc +14 -37
- package/src/common.h +38 -0
- package/src/loop.cc +28 -4
- package/src/marshal.cc +371 -9
- package/src/opengl-win32.cc +234 -0
- package/src/toggle.cc +1 -1
- package/system-gi.js +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// win32 only: which OpenGL implementation this process will get, and the one lever that
|
|
3
|
+
// changes it (#1097).
|
|
4
|
+
//
|
|
5
|
+
// gtk-4-1.dll imports OPENGL32.dll statically and libepoxy loads it with a bare
|
|
6
|
+
// `LoadLibraryA("OPENGL32")`. Windows answers both from the modules ALREADY LOADED first,
|
|
7
|
+
// then the application directory (node.exe's), then System32 — never from PATH, the only
|
|
8
|
+
// search the bundle loader controls. So a bundled GL implementation (Mesa's opengl32.dll)
|
|
9
|
+
// is reachable only by loading it by absolute path BEFORE gtk-4-1.dll loads; after that
|
|
10
|
+
// every bare-name request resolves to it. Measured on the win11-gjsify VM: the same DLL on
|
|
11
|
+
// PATH or preloaded from JS through `process.dlopen` (which unloads a DLL that fails to
|
|
12
|
+
// self-register) left GDK with "No GL implementation is available"; beside node.exe it gave
|
|
13
|
+
// GL 4.6.
|
|
14
|
+
//
|
|
15
|
+
// Preloading replaces the host's OpenGL for the whole process, so the loader asks first
|
|
16
|
+
// whether the host HAS one. Windows' own opengl32 finds a hardware ICD in two places — the
|
|
17
|
+
// display driver's user-mode OpenGL ICD (WDDM, what every vendor driver installs) and the
|
|
18
|
+
// legacy `OpenGLDrivers` registry key (what mesa-dist-win's system-wide deploy writes) —
|
|
19
|
+
// and this reads both WITHOUT loading opengl32, which would pin the host's copy for good.
|
|
20
|
+
// With neither, the host has only the GDI generic OpenGL 1.1, which GTK 4 rejects.
|
|
21
|
+
|
|
22
|
+
#include "common.h"
|
|
23
|
+
|
|
24
|
+
#ifdef _WIN32
|
|
25
|
+
#include <windows.h>
|
|
26
|
+
|
|
27
|
+
#include <memory>
|
|
28
|
+
#include <string>
|
|
29
|
+
|
|
30
|
+
// Windows 8+ (and 7 with KB2533623); older SDK headers guard them behind _WIN32_WINNT.
|
|
31
|
+
#ifndef LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
|
|
32
|
+
#define LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR 0x00000100
|
|
33
|
+
#endif
|
|
34
|
+
#ifndef LOAD_LIBRARY_SEARCH_SYSTEM32
|
|
35
|
+
#define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800
|
|
36
|
+
#endif
|
|
37
|
+
|
|
38
|
+
namespace {
|
|
39
|
+
|
|
40
|
+
// The D3DKMT thunks gdi32 exports, declared by hand: <d3dkmthk.h> needs the WDK's
|
|
41
|
+
// NTSTATUS plumbing, and resolving them at run time keeps a missing thunk a "no answer"
|
|
42
|
+
// rather than a load failure. Layouts are d3dkmthk.h's (Windows 8+ for EnumAdapters2).
|
|
43
|
+
using KmtHandle = UINT;
|
|
44
|
+
struct KmtAdapterInfo {
|
|
45
|
+
KmtHandle hAdapter;
|
|
46
|
+
LUID AdapterLuid;
|
|
47
|
+
ULONG NumOfSources;
|
|
48
|
+
BOOL bPrecisePresentRegionsPreferred;
|
|
49
|
+
};
|
|
50
|
+
struct KmtEnumAdapters2 {
|
|
51
|
+
ULONG NumAdapters;
|
|
52
|
+
KmtAdapterInfo* pAdapters;
|
|
53
|
+
};
|
|
54
|
+
struct KmtQueryAdapterInfo {
|
|
55
|
+
KmtHandle hAdapter;
|
|
56
|
+
UINT Type;
|
|
57
|
+
void* pPrivateDriverData;
|
|
58
|
+
UINT PrivateDriverDataSize;
|
|
59
|
+
};
|
|
60
|
+
struct KmtOpenGLInfo {
|
|
61
|
+
WCHAR UmdOpenGlIcdFileName[MAX_PATH];
|
|
62
|
+
ULONG Version;
|
|
63
|
+
ULONG Flags;
|
|
64
|
+
};
|
|
65
|
+
struct KmtCloseAdapter {
|
|
66
|
+
KmtHandle hAdapter;
|
|
67
|
+
};
|
|
68
|
+
constexpr UINT kKmtQaiUmOpenGLInfo = 2; // KMTQAITYPE_UMOPENGLINFO
|
|
69
|
+
|
|
70
|
+
using EnumAdapters2Fn = LONG(APIENTRY*)(KmtEnumAdapters2*);
|
|
71
|
+
using QueryAdapterInfoFn = LONG(APIENTRY*)(const KmtQueryAdapterInfo*);
|
|
72
|
+
using CloseAdapterFn = LONG(APIENTRY*)(const KmtCloseAdapter*);
|
|
73
|
+
|
|
74
|
+
std::string Utf8(const wchar_t* w) {
|
|
75
|
+
if (w == nullptr || *w == L'\0') return {};
|
|
76
|
+
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, nullptr, 0, nullptr, nullptr);
|
|
77
|
+
if (n <= 1) return {};
|
|
78
|
+
std::string out(static_cast<size_t>(n - 1), '\0');
|
|
79
|
+
WideCharToMultiByte(CP_UTF8, 0, w, -1, out.data(), n, nullptr, nullptr);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::wstring Wide(const std::string& s) {
|
|
84
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
|
|
85
|
+
if (n <= 1) return {};
|
|
86
|
+
std::wstring out(static_cast<size_t>(n - 1), L'\0');
|
|
87
|
+
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, out.data(), n);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The user-mode OpenGL ICD of the first adapter that names one, or "".
|
|
92
|
+
std::string WddmOpenGLIcd() {
|
|
93
|
+
HMODULE gdi = GetModuleHandleW(L"gdi32.dll");
|
|
94
|
+
if (gdi == nullptr) gdi = LoadLibraryW(L"gdi32.dll");
|
|
95
|
+
if (gdi == nullptr) return {};
|
|
96
|
+
auto enumAdapters = reinterpret_cast<EnumAdapters2Fn>(GetProcAddress(gdi, "D3DKMTEnumAdapters2"));
|
|
97
|
+
auto queryInfo = reinterpret_cast<QueryAdapterInfoFn>(GetProcAddress(gdi, "D3DKMTQueryAdapterInfo"));
|
|
98
|
+
auto closeAdapter = reinterpret_cast<CloseAdapterFn>(GetProcAddress(gdi, "D3DKMTCloseAdapter"));
|
|
99
|
+
if (enumAdapters == nullptr || queryInfo == nullptr || closeAdapter == nullptr) return {};
|
|
100
|
+
|
|
101
|
+
KmtEnumAdapters2 e{};
|
|
102
|
+
if (enumAdapters(&e) != 0 || e.NumAdapters == 0) return {};
|
|
103
|
+
std::unique_ptr<KmtAdapterInfo[]> adapters(new KmtAdapterInfo[e.NumAdapters]());
|
|
104
|
+
e.pAdapters = adapters.get();
|
|
105
|
+
if (enumAdapters(&e) != 0) return {};
|
|
106
|
+
|
|
107
|
+
std::string icd;
|
|
108
|
+
for (ULONG i = 0; i < e.NumAdapters; ++i) {
|
|
109
|
+
KmtOpenGLInfo gl{};
|
|
110
|
+
KmtQueryAdapterInfo q{adapters[i].hAdapter, kKmtQaiUmOpenGLInfo, &gl, sizeof(gl)};
|
|
111
|
+
// A non-zero NTSTATUS is how the Basic Render Driver and the QXL/Hyper-V adapters say
|
|
112
|
+
// they carry no OpenGL ICD, so it is an answer here, not an error.
|
|
113
|
+
if (icd.empty() && queryInfo(&q) == 0) icd = Utf8(gl.UmdOpenGlIcdFileName);
|
|
114
|
+
KmtCloseAdapter c{adapters[i].hAdapter};
|
|
115
|
+
closeAdapter(&c);
|
|
116
|
+
}
|
|
117
|
+
return icd;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The first ICD registered under the legacy key (a subkey or a value), or "".
|
|
121
|
+
std::string RegistryOpenGLIcd() {
|
|
122
|
+
HKEY key = nullptr;
|
|
123
|
+
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\OpenGLDrivers", 0,
|
|
124
|
+
KEY_READ | KEY_WOW64_64KEY, &key) != ERROR_SUCCESS)
|
|
125
|
+
return {};
|
|
126
|
+
wchar_t name[256];
|
|
127
|
+
DWORD len = 256;
|
|
128
|
+
std::string icd;
|
|
129
|
+
if (RegEnumKeyExW(key, 0, name, &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
|
|
130
|
+
icd = Utf8(name);
|
|
131
|
+
} else {
|
|
132
|
+
len = 256;
|
|
133
|
+
if (RegEnumValueW(key, 0, name, &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) icd = Utf8(name);
|
|
134
|
+
}
|
|
135
|
+
RegCloseKey(key);
|
|
136
|
+
return icd;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
std::string LoadedOpenGL32Path() {
|
|
140
|
+
HMODULE mod = GetModuleHandleW(L"opengl32.dll");
|
|
141
|
+
if (mod == nullptr) return {};
|
|
142
|
+
wchar_t path[MAX_PATH * 2];
|
|
143
|
+
DWORD n = GetModuleFileNameW(mod, path, MAX_PATH * 2);
|
|
144
|
+
if (n == 0) return {};
|
|
145
|
+
return Utf8(path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
std::string LastErrorText(DWORD code) {
|
|
149
|
+
wchar_t* buf = nullptr;
|
|
150
|
+
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
151
|
+
nullptr, code, 0, reinterpret_cast<LPWSTR>(&buf), 0, nullptr);
|
|
152
|
+
std::string text = Utf8(buf);
|
|
153
|
+
if (buf != nullptr) LocalFree(buf);
|
|
154
|
+
while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ')) text.pop_back();
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
} // namespace
|
|
159
|
+
|
|
160
|
+
namespace nodegi {
|
|
161
|
+
|
|
162
|
+
// probeHostOpenGL() → { wddmIcd, registryIcd, loadedFrom } — strings, "" for none.
|
|
163
|
+
Napi::Value ProbeHostOpenGL(const Napi::CallbackInfo& info) {
|
|
164
|
+
Napi::Env env = info.Env();
|
|
165
|
+
Napi::Object out = Napi::Object::New(env);
|
|
166
|
+
out.Set("wddmIcd", WddmOpenGLIcd());
|
|
167
|
+
out.Set("registryIcd", RegistryOpenGLIcd());
|
|
168
|
+
out.Set("loadedFrom", LoadedOpenGL32Path());
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// preloadOpenGL(absPath) → the path the loaded opengl32 module reports. Its own imports
|
|
173
|
+
// (Mesa's libgallium_wgl.dll, then system DLLs) resolve from ITS directory and System32 only:
|
|
174
|
+
// LOAD_WITH_ALTERED_SEARCH_PATH would fall through to the working directory and PATH when
|
|
175
|
+
// libgallium_wgl.dll is missing, so a planted copy there would be loaded instead. The
|
|
176
|
+
// DLL_LOAD_DIR flag also refuses a relative path. The module is never freed: gtk and epoxy
|
|
177
|
+
// hold function pointers into it for the process's life.
|
|
178
|
+
Napi::Value PreloadOpenGL(const Napi::CallbackInfo& info) {
|
|
179
|
+
Napi::Env env = info.Env();
|
|
180
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
181
|
+
Napi::TypeError::New(env, "preloadOpenGL(path): path must be a string").ThrowAsJavaScriptException();
|
|
182
|
+
return env.Undefined();
|
|
183
|
+
}
|
|
184
|
+
std::string path = info[0].As<Napi::String>().Utf8Value();
|
|
185
|
+
HMODULE mod = LoadLibraryExW(Wide(path).c_str(), nullptr,
|
|
186
|
+
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);
|
|
187
|
+
if (mod == nullptr) {
|
|
188
|
+
DWORD code = GetLastError();
|
|
189
|
+
Napi::Error::New(env, "preloadOpenGL: LoadLibraryExW(" + path + ") failed: " + LastErrorText(code) + " (" +
|
|
190
|
+
std::to_string(code) + ")")
|
|
191
|
+
.ThrowAsJavaScriptException();
|
|
192
|
+
return env.Undefined();
|
|
193
|
+
}
|
|
194
|
+
return Napi::String::New(env, LoadedOpenGL32Path());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// currentGLStrings() → { vendor, renderer, version } of the context current on this
|
|
198
|
+
// thread, or null. glGetString is an opengl32 export, so this asks whichever
|
|
199
|
+
// implementation GDK actually got — the discriminating half of the proof.
|
|
200
|
+
Napi::Value CurrentGLStrings(const Napi::CallbackInfo& info) {
|
|
201
|
+
Napi::Env env = info.Env();
|
|
202
|
+
HMODULE mod = GetModuleHandleW(L"opengl32.dll");
|
|
203
|
+
if (mod == nullptr) return env.Null();
|
|
204
|
+
using GetStringFn = const unsigned char*(APIENTRY*)(unsigned int);
|
|
205
|
+
auto getString = reinterpret_cast<GetStringFn>(GetProcAddress(mod, "glGetString"));
|
|
206
|
+
if (getString == nullptr) return env.Null();
|
|
207
|
+
constexpr unsigned int kVendor = 0x1F00, kRenderer = 0x1F01, kVersion = 0x1F02;
|
|
208
|
+
const unsigned char* renderer = getString(kRenderer);
|
|
209
|
+
if (renderer == nullptr) return env.Null(); // no current context
|
|
210
|
+
auto str = [](const unsigned char* s) { return std::string(s != nullptr ? reinterpret_cast<const char*>(s) : ""); };
|
|
211
|
+
Napi::Object out = Napi::Object::New(env);
|
|
212
|
+
out.Set("vendor", str(getString(kVendor)));
|
|
213
|
+
out.Set("renderer", str(renderer));
|
|
214
|
+
out.Set("version", str(getString(kVersion)));
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
void InitOpenGLWin32(Napi::Env env, Napi::Object exports) {
|
|
219
|
+
exports.Set("probeHostOpenGL", Napi::Function::New(env, ProbeHostOpenGL));
|
|
220
|
+
exports.Set("preloadOpenGL", Napi::Function::New(env, PreloadOpenGL));
|
|
221
|
+
exports.Set("currentGLStrings", Napi::Function::New(env, CurrentGLStrings));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
} // namespace nodegi
|
|
225
|
+
|
|
226
|
+
#else
|
|
227
|
+
|
|
228
|
+
namespace nodegi {
|
|
229
|
+
// Off win32 the host's GL is the system's own business (Mesa/vendor on Linux, CGL on
|
|
230
|
+
// darwin) and there is nothing to preload, so the surface is simply absent.
|
|
231
|
+
void InitOpenGLWin32(Napi::Env, Napi::Object) {}
|
|
232
|
+
} // namespace nodegi
|
|
233
|
+
|
|
234
|
+
#endif
|
package/src/toggle.cc
CHANGED
|
@@ -528,7 +528,7 @@ static void NodeGiToggleNotify(gpointer /*data*/, GObject* obj, gboolean is_last
|
|
|
528
528
|
// goes weak, and the GObject is immortal. The old code leaked the same toggle ref
|
|
529
529
|
// and took a use-after-free with it. Pinned by gc-identity "run_dispose costs
|
|
530
530
|
// wrapper identity"; the tombstone design that would retire this, and the
|
|
531
|
-
// ref_count discriminator rejected for it, are in status/open-todos.md.
|
|
531
|
+
// ref_count discriminator rejected for it, are in status/open-todos/README.md.
|
|
532
532
|
static void OnGObjectFinalized(gpointer data, GObject* where_the_object_was) {
|
|
533
533
|
NodeGiInstance* inst = static_cast<NodeGiInstance*>(data);
|
|
534
534
|
bool enqueued = false;
|
package/system-gi.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// imports THIS file by relative path and asserts identical arrays for a table of
|
|
57
57
|
// injected inputs. It pins a THIRD copy the same way, `@gjsify/utils/core`'s
|
|
58
58
|
// `system-gi-dirs.ts`. Change one, change all three; the lift that deletes two of
|
|
59
|
-
// them is tracked in `status/open-todos.md`.
|
|
59
|
+
// them is tracked in `status/open-todos/README.md`.
|
|
60
60
|
import { statSync } from 'node:fs';
|
|
61
61
|
import { createRequire } from 'node:module';
|
|
62
62
|
// POSIX path semantics, not the HOST's. This function answers a question about
|