@gjsify/node-gi 0.51.1 → 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.
@@ -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