@ringozz/godot 4.7.1-11 → 4.7.1-13
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 +1 -1
- package/package.json +2 -2
- package/src/index.ts +15 -3
- package/src/load.ts +23 -2
- package/src/preload.ts +16 -0
- package/src/runtime.ts +3 -0
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ const tex = await loadResourceAsync('res://textures/icon.jpg', Texture2D); //
|
|
|
74
74
|
- `loadResourceAsync(path, cls, files?)` — generic over the Godot class: `cls.name` is the `ResourceLoader` type hint, and the return type is `InstanceType<typeof cls>`. Loads in the background (`loadThreadedRequest` + status polling). Godot default arguments apply: the napi dispatch truncates trailing `undefined` args, so `loadThreadedRequest` uses its default `CACHE_MODE_REUSE` and the resource stays cached in the engine (scene re-references don't re-read the file). On web it only works for paths already staged into MEMFS (generated asset modules handle this). When the asset's `files` map is passed, the staged product files (`.ctex`/`.scn`/native sources) are deleted from MEMFS after the resource loads — only the `.import` sidecars are kept, since they route imported source paths to their products. The decoded texture bytes don't linger in the JS heap.
|
|
75
75
|
- `loadAsset(path, cls, files, deps, data)` — the generated asset modules' entry point (also usable directly): returns `{ materialize, load }`. It checks the engine's `ResourceCache` first (when the asset is already loaded, `load` resolves to the cached instance and staging is skipped), stages the bundled files via Godot's `copyToFS` (zero-copy handover of the fetched bytes; a no-op on desktop where the files already exist), loads via `loadResourceAsync`, and memoizes `load` on `data` (pass `import.meta.hot.data`) so HMR re-evaluations reuse the same fulfilled promise.
|
|
76
76
|
|
|
77
|
-
Asset imports are resolved by `@ringozz/godot/preload` (a Bun plugin, registered via `bunfig.toml` preload for `bun run`/`bun test` and via `[serve.static] plugins` for `Bun.serve`'s fullstack HTML routes; the module's default export is the single plugin object) into a module per asset:
|
|
77
|
+
Asset imports are resolved by `@ringozz/godot/preload` (a Bun plugin, registered via `bunfig.toml` preload for `bun run`/`bun test` and via `[serve.static] plugins` for `Bun.serve`'s fullstack HTML routes; the module's default export is the single plugin object) into a module per asset. Only **JS imports** are virtualized — HTML asset references (`<link rel="icon">`, `<img src>`) and CSS `url()` refs fall through to Bun's normal (content-hashed) asset handling, so favicons and other web assets co-exist with Godot imports:
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
import foo from './models/foo.gltf';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ringozz/godot",
|
|
3
3
|
"author": "Vladimir Davidovich",
|
|
4
|
-
"version": "4.7.1-
|
|
4
|
+
"version": "4.7.1-13",
|
|
5
5
|
"description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"godot"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"optionalDependencies": {
|
|
39
39
|
"@ringozz/godot-macos-arm64": "^4.7.1-3",
|
|
40
40
|
"@ringozz/godot-windows-x86_64": "^4.7.1-3",
|
|
41
|
-
"@ringozz/godot-web-wasm32": "^4.7.1-
|
|
41
|
+
"@ringozz/godot-web-wasm32": "^4.7.1-12"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@types/bun": "*"
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { GodotInstance } from '../gen/classes/GodotInstance.ts';
|
|
|
10
10
|
import { SceneTree } from '../gen/classes/SceneTree.ts';
|
|
11
11
|
import { Window } from '../gen/classes/Window.ts';
|
|
12
12
|
import { gc } from './debug.ts';
|
|
13
|
-
import { cancelAnimationFrame, getGodot, requestAnimationFrame } from './runtime.ts';
|
|
13
|
+
import { cancelAnimationFrame, cleanupHooks, getGodot, requestAnimationFrame } from './runtime.ts';
|
|
14
14
|
|
|
15
15
|
// ---- make sure these classes are not tree-shaked ----
|
|
16
16
|
void ValueTypes;
|
|
@@ -34,7 +34,19 @@ export async function runGodot(signal?: AbortSignal, unmount?: () => PromiseLike
|
|
|
34
34
|
await new Promise(requestAnimationFrame);
|
|
35
35
|
|
|
36
36
|
await unmount?.();
|
|
37
|
-
|
|
37
|
+
while (cleanupHooks.size) {
|
|
38
|
+
const hooks = Array.from(cleanupHooks); cleanupHooks.clear();
|
|
39
|
+
await Promise.all(hooks.map((hook) => hook()));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Let pending Napi wrapper finalizers run before tearing down the engine:
|
|
43
|
+
// Bun.gc(true) marks unreachable wrappers but their ~GodotVar/~Variant
|
|
44
|
+
// (which unrefs RefCounted objects) only runs on later macrotasks — and one
|
|
45
|
+
// wrapper's finalizer can free others. Freeing the engine first would report
|
|
46
|
+
// every still-referenced RefCounted as leaked, so drain a few gc+tick rounds.
|
|
47
|
+
for (let i = 0; i < 4; i++) {
|
|
48
|
+
console.log(gc());
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
50
|
+
}
|
|
38
51
|
return godot.free();
|
|
39
52
|
}
|
|
40
|
-
|
package/src/load.ts
CHANGED
|
@@ -8,9 +8,25 @@ import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
|
|
|
8
8
|
import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
|
|
9
9
|
import { ResourceUID } from '../gen/classes/ResourceUID.ts';
|
|
10
10
|
import type { Resource } from '../gen/classes/Resource.ts';
|
|
11
|
-
import { stageFile } from './runtime.ts';
|
|
11
|
+
import { cleanupHooks, stageFile } from './runtime.ts';
|
|
12
12
|
import { decodeCtex } from './web-image.ts';
|
|
13
13
|
|
|
14
|
+
// Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
|
|
15
|
+
// holding a WeakRef: module-scope asset imports (the preload modules' memoized
|
|
16
|
+
// load promises) keep their wrappers reachable, so GC alone can't collect them
|
|
17
|
+
// before the engine stops. WeakRef keeps the entries from pinning resources
|
|
18
|
+
// during the app's lifetime.
|
|
19
|
+
function track<T extends Resource>(res: T): T {
|
|
20
|
+
const ref = new WeakRef<Resource>(res);
|
|
21
|
+
cleanupHooks.add(() => {
|
|
22
|
+
const r = ref.deref();
|
|
23
|
+
if (r) {
|
|
24
|
+
try { r.free(); } catch {}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* A Godot `Resource` subclass constructor: its `.name` is the registered class
|
|
16
32
|
* name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
|
|
@@ -153,6 +169,7 @@ export function loadAsset<C extends ResourceConstructor>(
|
|
|
153
169
|
data: Record<string, unknown> = {},
|
|
154
170
|
): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
|
|
155
171
|
const cached = cachedResource<C>(path);
|
|
172
|
+
if (cached) cached.then(track);
|
|
156
173
|
const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
|
|
157
174
|
const existing = data[path] as Promise<InstanceType<C>> | undefined;
|
|
158
175
|
const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
|
|
@@ -189,6 +206,10 @@ export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
|
189
206
|
break;
|
|
190
207
|
}
|
|
191
208
|
if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
|
|
209
|
+
// Collect the engine's LoadToken so a failed load doesn't leave it
|
|
210
|
+
// registered (a bare `RefCounted` leaked at exit). Safe no-op when
|
|
211
|
+
// no token exists.
|
|
212
|
+
ResourceLoader.loadThreadedGet(path);
|
|
192
213
|
throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
|
|
193
214
|
}
|
|
194
215
|
await nextTick();
|
|
@@ -206,5 +227,5 @@ export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
|
206
227
|
}
|
|
207
228
|
}
|
|
208
229
|
}
|
|
209
|
-
return result;
|
|
230
|
+
return track(result);
|
|
210
231
|
}
|
package/src/preload.ts
CHANGED
|
@@ -175,6 +175,22 @@ const assetPlugin: BunPlugin = {
|
|
|
175
175
|
name: 'godot-assets',
|
|
176
176
|
setup(build) {
|
|
177
177
|
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
178
|
+
// Only JS module imports of Godot source assets become `godot`
|
|
179
|
+
// modules. `Bun.build` reports JS imports as `import-statement`
|
|
180
|
+
// (and `dynamic-import`/`require-*`) but HTML asset references
|
|
181
|
+
// (<link>/<img>) and CSS url() refs as `internal`/`url-token` —
|
|
182
|
+
// those fall through so Bun copies/hashes the file normally. The
|
|
183
|
+
// runtime preload (bun run / bun test) reports JS imports with no
|
|
184
|
+
// kind (`undefined`), so treat that as a JS import too.
|
|
185
|
+
if (
|
|
186
|
+
args.kind !== undefined &&
|
|
187
|
+
args.kind !== 'import-statement' &&
|
|
188
|
+
args.kind !== 'dynamic-import' &&
|
|
189
|
+
args.kind !== 'require-call' &&
|
|
190
|
+
args.kind !== 'require-resolve'
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
178
194
|
const importerAbs = args.importer?.startsWith(`${NS}:`)
|
|
179
195
|
? args.importer.slice(NS.length + 1)
|
|
180
196
|
: args.importer;
|
package/src/runtime.ts
CHANGED
|
@@ -79,3 +79,6 @@ export function _P(signal: any, onfulfilled?: any, onrejected?: any) {
|
|
|
79
79
|
if (res !== 0) reject(new Error('Failed to connect signal: error code ' + res));
|
|
80
80
|
}).then(onfulfilled, onrejected);
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/** Cleanup callbacks run once by `runGodot` just before the engine is freed; may be async. */
|
|
84
|
+
export const cleanupHooks: Set<() => void | Promise<void>> = new Set();
|