@ringozz/godot 4.7.2-641 → 4.7.2-646
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/package.json +5 -4
- package/src/dump-deps.gd +109 -0
- package/src/editor.ts +10 -4
- package/src/export_presets.example.cfg +37 -0
- package/src/load.ts +65 -23
- package/src/pack.ts +87 -0
- package/src/preload.ts +88 -157
- package/src/web-image.ts +47 -29
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ await import('./app.ts'); // may now import @ringozz/godot / react-godot freely
|
|
|
52
52
|
| `@ringozz/godot/ClassName` | Classes, one per module — e.g. `@ringozz/godot/Label`, `@ringozz/godot/Node` |
|
|
53
53
|
| `@ringozz/godot/runtime` | Low-level dispatch: `_C`, `_S`, `_R`, `_G`, `_P`, `_V`, `toValueType`, `getGodot`, `GodotVar` |
|
|
54
54
|
| `@ringozz/godot/boot` | Platform boot: `getNativeModule()`; web `preloadGodot()` (awaited before importing the app; no-op on desktop) |
|
|
55
|
-
| `@ringozz/godot/load` | Async resource loading: `loadResourceAsync`, `loadAsset` |
|
|
55
|
+
| `@ringozz/godot/load` | Async resource loading: `loadResourceAsync`, `loadAsset`, `loadAssetPack` (native `.pck` packs) |
|
|
56
|
+
| `@ringozz/godot/pack` | `buildAssetPack()` + the `godot pack` subcommand: bake a project's `.pck` + loose-texture manifest |
|
|
56
57
|
| `@ringozz/godot/debug` | `initDebug()`, `dumpStr`, `snapshot`, `inspect`, `click`, `find`, `gc` |
|
|
57
58
|
|
|
58
59
|
Class-scoped enums use bare names (import `ProcessMode` from `@ringozz/godot/Node`, not `NodeProcessMode`).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ringozz/godot",
|
|
3
3
|
"author": "Vladimir Davidovich",
|
|
4
|
-
"version": "4.7.2-
|
|
4
|
+
"version": "4.7.2-646",
|
|
5
5
|
"description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"default": "./src/boot.ts"
|
|
27
27
|
},
|
|
28
28
|
"./load": "./src/load.ts",
|
|
29
|
+
"./pack": "./src/pack.ts",
|
|
29
30
|
"./transcoder": {
|
|
30
31
|
"browser": "./src/transcoder.browser.ts",
|
|
31
32
|
"default": "./src/transcoder.ts"
|
|
@@ -65,9 +66,9 @@
|
|
|
65
66
|
"precision": "single"
|
|
66
67
|
},
|
|
67
68
|
"optionalDependencies": {
|
|
68
|
-
"@ringozz/godot-macos-arm64": "^4.7.2-
|
|
69
|
-
"@ringozz/godot-windows-x86_64": "^4.7.2-
|
|
70
|
-
"@ringozz/godot-web-wasm32": "^4.7.2-
|
|
69
|
+
"@ringozz/godot-macos-arm64": "^4.7.2-646",
|
|
70
|
+
"@ringozz/godot-windows-x86_64": "^4.7.2-646",
|
|
71
|
+
"@ringozz/godot-web-wasm32": "^4.7.2-646"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
|
73
74
|
"@types/bun": "*"
|
package/src/dump-deps.gd
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends SceneTree
|
|
3
|
+
## Dumps, for every packed source resource, the set of **loose** (WebP-backed)
|
|
4
|
+
## texture sources that must stay out of the asset pack and be staged/transcoded
|
|
5
|
+
## by the host. The preload plugin (`@ringozz/godot/preload`) reads deps.json so
|
|
6
|
+
## it never scans source text for references.
|
|
7
|
+
##
|
|
8
|
+
## Sources of loose refs (union): the engine's `ResourceLoader.get_dependencies`
|
|
9
|
+
## (for imported sources the real refs live on the imported product, reached via
|
|
10
|
+
## the `.import` `dest_files`), plus a text scan for string-valued paths
|
|
11
|
+
## (`albedo_array = "res://.../*.webparray"`, `load("res://...")`) that no static
|
|
12
|
+
## resource edge exposes. A dep is *loose* when it has an `.import` sidecar but is
|
|
13
|
+
## not a packed resource type — the set is derived, not listed.
|
|
14
|
+
##
|
|
15
|
+
## Driven by `@ringozz/godot/pack`'s `buildAssetPack`, which sets:
|
|
16
|
+
## GODOT_DUMP_OUT – output res:// path (default res://.godot/packs/deps.json)
|
|
17
|
+
## GODOT_DUMP_ROOTS – comma-separated res:// roots to scan (default res://)
|
|
18
|
+
|
|
19
|
+
const PACKED := ["tscn", "tres", "gltf", "obj", "gd", "gdshader", "gdshaderinc", "ttf", "otf", "po"]
|
|
20
|
+
const SCANNED := ["tscn", "tres", "gd", "gdshader", "gdshaderinc", "gltf", "obj"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
func _initialize() -> void:
|
|
24
|
+
var out := OS.get_environment("GODOT_DUMP_OUT")
|
|
25
|
+
if out.is_empty():
|
|
26
|
+
out = "res://.godot/packs/deps.json"
|
|
27
|
+
var roots := OS.get_environment("GODOT_DUMP_ROOTS").split(",", false)
|
|
28
|
+
if roots.is_empty():
|
|
29
|
+
roots = ["res://"]
|
|
30
|
+
var deps := {}
|
|
31
|
+
for r in roots:
|
|
32
|
+
_scan(String(r).rstrip("/") + "/", deps)
|
|
33
|
+
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out.get_base_dir()))
|
|
34
|
+
var f := FileAccess.open(out, FileAccess.WRITE)
|
|
35
|
+
f.store_string(JSON.stringify(deps, " "))
|
|
36
|
+
f.close()
|
|
37
|
+
print("[dump-deps] ", deps.size(), " resources -> ", out)
|
|
38
|
+
quit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func _scan(path: String, out: Dictionary) -> void:
|
|
42
|
+
var d := DirAccess.open(path)
|
|
43
|
+
if d == null:
|
|
44
|
+
return
|
|
45
|
+
for dir in d.get_directories():
|
|
46
|
+
if dir.begins_with(".") or dir == "node_modules":
|
|
47
|
+
continue
|
|
48
|
+
_scan(path.path_join(dir) + "/", out)
|
|
49
|
+
for file in d.get_files():
|
|
50
|
+
var p := path.path_join(file)
|
|
51
|
+
if p.get_extension().to_lower() in PACKED:
|
|
52
|
+
out[p] = _loose(p)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## The loose texture sources reachable from `path` (transitive), sorted.
|
|
56
|
+
func _loose(path: String) -> Array:
|
|
57
|
+
var loose := {}
|
|
58
|
+
_collect(path, loose, {})
|
|
59
|
+
var arr := loose.keys()
|
|
60
|
+
arr.sort()
|
|
61
|
+
return arr
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
func _classify(dep: String, loose: Dictionary, seen: Dictionary) -> void:
|
|
65
|
+
if dep.is_empty():
|
|
66
|
+
return
|
|
67
|
+
if dep.get_extension().to_lower() in PACKED:
|
|
68
|
+
_collect(dep, loose, seen)
|
|
69
|
+
elif FileAccess.file_exists(dep + ".import"):
|
|
70
|
+
loose[dep] = true
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
func _collect(path: String, loose: Dictionary, seen: Dictionary) -> void:
|
|
74
|
+
if seen.has(path):
|
|
75
|
+
return
|
|
76
|
+
seen[path] = true
|
|
77
|
+
# An imported source's own `get_dependencies` is empty (the importer hides the
|
|
78
|
+
# refs); the imported *product* carries them — recurse through the sidecar.
|
|
79
|
+
var imp := path + ".import"
|
|
80
|
+
if FileAccess.file_exists(imp):
|
|
81
|
+
var cfg := ConfigFile.new()
|
|
82
|
+
if cfg.load(imp) == OK:
|
|
83
|
+
for dst in cfg.get_value("deps", "dest_files", []):
|
|
84
|
+
_collect(String(dst), loose, seen)
|
|
85
|
+
for dep in ResourceLoader.get_dependencies(path):
|
|
86
|
+
_classify(_res_path(String(dep)), loose, seen)
|
|
87
|
+
# String-valued resource paths are invisible to dependency analysis; scan the
|
|
88
|
+
# text for loose refs the engine cannot report.
|
|
89
|
+
if path.get_extension().to_lower() in SCANNED:
|
|
90
|
+
for p in _text_refs(path):
|
|
91
|
+
_classify(p, loose, seen)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## `res://` path from a dependency entry (`uid://…::::res://…` or plain).
|
|
95
|
+
func _res_path(dep: String) -> String:
|
|
96
|
+
var i := dep.find("res://")
|
|
97
|
+
if i < 0:
|
|
98
|
+
return ""
|
|
99
|
+
return dep.substr(i)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Literal `res://` paths appearing in a text source.
|
|
103
|
+
func _text_refs(path: String) -> Array:
|
|
104
|
+
var re := RegEx.new()
|
|
105
|
+
re.compile('res://[^"\']+')
|
|
106
|
+
var refs := []
|
|
107
|
+
for m in re.search_all(FileAccess.get_file_as_string(path)):
|
|
108
|
+
refs.append(m.get_string())
|
|
109
|
+
return refs
|
package/src/editor.ts
CHANGED
|
@@ -94,7 +94,7 @@ async function provision(): Promise<void> {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function resolveEditor(): Promise<string> {
|
|
97
|
+
export async function resolveEditor(): Promise<string> {
|
|
98
98
|
if (process.env.GODOT_PATH) return process.env.GODOT_PATH;
|
|
99
99
|
if (!process.env.GODOT_NODE_WRAPPER) {
|
|
100
100
|
const system = systemGodot();
|
|
@@ -105,7 +105,13 @@ async function resolveEditor(): Promise<string> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (import.meta.main) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const command = process.argv[2];
|
|
109
|
+
if (command === 'pack') {
|
|
110
|
+
const { buildAssetPack, parsePackArgs } = await import('./pack.ts');
|
|
111
|
+
await buildAssetPack(parsePackArgs(process.argv.slice(3)));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
const exe = await resolveEditor();
|
|
115
|
+
const result = spawnSync(exe, process.argv.slice(2), { stdio: 'inherit', env: { ...process.env, GODOT_NODE_WRAPPER: '1' } });
|
|
116
|
+
process.exit(result.status ?? 1);
|
|
111
117
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
; Example Godot export preset for `@ringozz/godot/pack`'s `buildAssetPack()`.
|
|
2
|
+
; Copy to the project root as `export_presets.cfg` (the project owns it) and add
|
|
3
|
+
; your own non-asset trees to `exclude_filter`.
|
|
4
|
+
;
|
|
5
|
+
; The texture sources (`*.webp`/`*.png`/… below) MUST stay excluded: the wasm
|
|
6
|
+
; engine has no WebP decoder, so those products are staged/transcoded by the host
|
|
7
|
+
; (see `dump-deps.gd`), not read from the pack. `export_filter="all_resources"`
|
|
8
|
+
; ignores `include_filter` — scope with `exclude_filter` only.
|
|
9
|
+
|
|
10
|
+
[preset.0]
|
|
11
|
+
|
|
12
|
+
name="Pack"
|
|
13
|
+
platform="Web"
|
|
14
|
+
runnable=true
|
|
15
|
+
dedicated_server=false
|
|
16
|
+
custom_features=""
|
|
17
|
+
export_filter="all_resources"
|
|
18
|
+
include_filter=""
|
|
19
|
+
exclude_filter="node_modules/*,.git/*,.godot/packs/*,*.webp,*.png,*.svg,*.jpg,*.jpeg,*.hdr,*.exr,*.webparray"
|
|
20
|
+
export_path=""
|
|
21
|
+
script_export_mode=2
|
|
22
|
+
|
|
23
|
+
[preset.0.options]
|
|
24
|
+
|
|
25
|
+
custom_template/debug=""
|
|
26
|
+
custom_template/release=""
|
|
27
|
+
variant/extensions_support=false
|
|
28
|
+
variant/thread_support=false
|
|
29
|
+
vram_texture_compression/for_desktop=false
|
|
30
|
+
vram_texture_compression/for_mobile=false
|
|
31
|
+
html/export_icon=true
|
|
32
|
+
html/custom_html_shell=""
|
|
33
|
+
html/head_include=""
|
|
34
|
+
html/canvas_resize_policy=2
|
|
35
|
+
html/focus_canvas_on_start=true
|
|
36
|
+
html/experimental_virtual_keyboard=false
|
|
37
|
+
progressive_web_app/enabled=false
|
package/src/load.ts
CHANGED
|
@@ -148,34 +148,19 @@ async function materialize([resPath, entry]: [string, AssetFile]): Promise<void>
|
|
|
148
148
|
|
|
149
149
|
const nextTick = () => new Promise(requestAnimationFrame);
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
* An asset module as generated by `@ringozz/godot/preload`: `default` is the
|
|
153
|
-
* load promise, `materialize` resolves once its bundled files (plus its deps'
|
|
154
|
-
* files) are staged on the engine's filesystem.
|
|
155
|
-
*/
|
|
156
|
-
interface AssetModule {
|
|
157
|
-
default: Promise<unknown>;
|
|
158
|
-
materialize: Promise<unknown>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
151
|
/**
|
|
162
152
|
* Stages the asset's bundled files before loading: `.import` sidecars as text
|
|
163
|
-
* and imported products (`.
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* supplies referenced asset modules whose file-staging is awaited (so Godot can
|
|
167
|
-
* resolve them) and whose load failures are logged.
|
|
153
|
+
* and imported products (`.ctex`) as fetched paths. On web the bytes are written
|
|
154
|
+
* straight into Emscripten's MEMFS via Godot's `copyToFS` (JS-heap only, no wasm
|
|
155
|
+
* copy); a no-op on desktop where the files already exist.
|
|
168
156
|
*/
|
|
169
|
-
function materializeFiles(files: AssetFiles
|
|
170
|
-
for (const dep of deps) {
|
|
171
|
-
dep.default.catch((err) => console.error('[godot] dependency load failed:', err));
|
|
172
|
-
}
|
|
157
|
+
function materializeFiles(files: AssetFiles): Promise<unknown> {
|
|
173
158
|
if (stageFile === undefined) {
|
|
174
159
|
// Desktop: the files already exist on disk (res:// = cwd) and uids come
|
|
175
160
|
// from `.godot/uid_cache.bin`, so there is nothing to stage.
|
|
176
161
|
return Promise.resolve();
|
|
177
162
|
}
|
|
178
|
-
return Promise.all(
|
|
163
|
+
return Promise.all(Object.entries(files).map(materialize));
|
|
179
164
|
}
|
|
180
165
|
|
|
181
166
|
/**
|
|
@@ -200,18 +185,22 @@ function cachedResource<C extends ResourceConstructor>(path: string): Promise<In
|
|
|
200
185
|
* where modules evaluate once). Memoization keeps `use()` seeing the **same**
|
|
201
186
|
* fulfilled promise object across re-evaluations — no Suspense fallback flash on
|
|
202
187
|
* hot reload; a rejected load is evicted so the next evaluation retries. Returns
|
|
203
|
-
* the `materialize` promise (
|
|
188
|
+
* the `materialize` promise (bundled files staged) and the load promise.
|
|
204
189
|
*/
|
|
205
190
|
export function loadAsset<C extends ResourceConstructor>(
|
|
206
191
|
path: string,
|
|
207
192
|
cls: C,
|
|
208
193
|
files: AssetFiles,
|
|
209
|
-
deps: AssetModule[] = [],
|
|
210
194
|
data: Record<string, unknown> = {},
|
|
195
|
+
ready?: Promise<unknown>,
|
|
211
196
|
): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
|
|
212
197
|
const cached = cachedResource<C>(path);
|
|
213
198
|
if (cached) cached.then(track);
|
|
214
|
-
|
|
199
|
+
// `ready` mounts the pack(s) holding this asset (see `loadAssetPack`);
|
|
200
|
+
// staging waits for that so the pack's remaps are in place when the resource
|
|
201
|
+
// resolves. A no-op when omitted (desktop / no pack).
|
|
202
|
+
const mounted = ready ?? Promise.resolve();
|
|
203
|
+
const materialize = cached ? Promise.resolve() : mounted.then(() => materializeFiles(files));
|
|
215
204
|
const existing = data[path] as Promise<InstanceType<C>> | undefined;
|
|
216
205
|
const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
|
|
217
206
|
data[path] = load;
|
|
@@ -270,3 +259,56 @@ export async function loadResourceAsync<C extends ResourceConstructor>(
|
|
|
270
259
|
}
|
|
271
260
|
return track(result);
|
|
272
261
|
}
|
|
262
|
+
|
|
263
|
+
// ── Asset packs ──────────────────────────────────────────────────────────────
|
|
264
|
+
// Mounting for Godot `.pck` packs. A pack is fetched, its bytes staged into the
|
|
265
|
+
// engine's filesystem (web: MEMFS via `copyToFS`; desktop: nothing — the working
|
|
266
|
+
// tree is `res://`), then handed to `ProjectSettings.loadResourcePack`, whose
|
|
267
|
+
// remap table resolves a source path (`res://scene.tscn`) to the packed binary.
|
|
268
|
+
// Memoized per URL (shared by every module that references it); a failed mount is
|
|
269
|
+
// evicted so a later call retries. PackedData is add-only — packs are never unloaded.
|
|
270
|
+
|
|
271
|
+
const mounted = new Map<string, Promise<void>>();
|
|
272
|
+
|
|
273
|
+
/** Mounts the `.pck` at `url` (fetch + stage + `loadResourcePack`). */
|
|
274
|
+
export function loadAssetPack(url: string): Promise<void> {
|
|
275
|
+
const existing = mounted.get(url);
|
|
276
|
+
if (existing) {
|
|
277
|
+
return existing;
|
|
278
|
+
}
|
|
279
|
+
const p = mount(url);
|
|
280
|
+
mounted.set(url, p);
|
|
281
|
+
p.catch(() => {
|
|
282
|
+
if (mounted.get(url) === p) {
|
|
283
|
+
mounted.delete(url);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function mount(url: string): Promise<void> {
|
|
290
|
+
if (stageFile === undefined) {
|
|
291
|
+
// Desktop: the app's assets exist on disk. External packs (a path) mount directly.
|
|
292
|
+
const external = /^([A-Za-z]:[\\/]|\/)/.test(url) || url.startsWith('res://');
|
|
293
|
+
if (external && !ProjectSettings.loadResourcePack(url)) {
|
|
294
|
+
throw new Error(`loadAssetPack(${url}): loadResourcePack failed`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const res = await fetch(url);
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
throw new Error(`loadAssetPack(${url}): fetch failed (${res.status})`);
|
|
301
|
+
}
|
|
302
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
303
|
+
const resPath = `res://.packs/${packName(url)}`;
|
|
304
|
+
stageFile(ProjectSettings.globalizePath(resPath), bytes);
|
|
305
|
+
if (!ProjectSettings.loadResourcePack(resPath)) {
|
|
306
|
+
throw new Error(`loadAssetPack(${url}): loadResourcePack failed for ${resPath}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A filesystem-safe pack name derived from the URL (basename, query stripped). */
|
|
311
|
+
function packName(url: string): string {
|
|
312
|
+
const base = url.split('?')[0].split('/').pop() ?? 'pack.pck';
|
|
313
|
+
return base.replace(/[^A-Za-z0-9._-]/g, '_') || 'pack.pck';
|
|
314
|
+
}
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { resolveEditor } from './editor.ts';
|
|
10
|
+
|
|
11
|
+
// Bakes a Godot project's assets for the runtime: imports, dumps the loose
|
|
12
|
+
// (WebP-backed) texture dependency manifest (`dump-deps.gd`, via
|
|
13
|
+
// `ResourceLoader.get_dependencies` + a text scan for string-valued paths — no
|
|
14
|
+
// host-side scanning), and exports a native `.pck` carrying the engine-converted
|
|
15
|
+
// binary resources + remap table. The preload plugin (`preload.ts`) consumes both.
|
|
16
|
+
//
|
|
17
|
+
// The project owns `export_presets.cfg` (a committed, editor-managed file): this
|
|
18
|
+
// runs `--export-pack <preset>` against it. That preset must exclude the texture
|
|
19
|
+
// sources (`*.webp`/`*.png`/…): the wasm build has no WebP decoder, so those
|
|
20
|
+
// products stay loose and are staged/transcoded by the host. See
|
|
21
|
+
// `export_presets.example.cfg` for a canonical preset.
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
export interface BuildAssetPackOptions {
|
|
26
|
+
/** res:// roots whose assets are scanned for loose textures (default `['res://src']`). */
|
|
27
|
+
roots?: string[];
|
|
28
|
+
/** Output `.pck` path, project-relative (default `.godot/packs/app.pck`). */
|
|
29
|
+
pack?: string;
|
|
30
|
+
/** Output `deps.json` path, project-relative (default `.godot/packs/deps.json`). */
|
|
31
|
+
deps?: string;
|
|
32
|
+
/** `export_presets.cfg` preset name to export (default `Pack`). */
|
|
33
|
+
preset?: string;
|
|
34
|
+
/** Editor executable override (default: `resolveEditor()`). */
|
|
35
|
+
godot?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function buildAssetPack(opts: BuildAssetPackOptions = {}): Promise<void> {
|
|
39
|
+
const roots = opts.roots ?? ['res://src'];
|
|
40
|
+
const pack = opts.pack ?? '.godot/packs/app.pck';
|
|
41
|
+
const deps = opts.deps ?? '.godot/packs/deps.json';
|
|
42
|
+
const preset = opts.preset ?? 'Pack';
|
|
43
|
+
const editor = opts.godot ?? (await resolveEditor());
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
|
|
46
|
+
const presets = resolve(cwd, 'export_presets.cfg');
|
|
47
|
+
if (!existsSync(presets) || !new RegExp(`^name=${JSON.stringify(preset)}$`, 'm').test(readFileSync(presets, 'utf8'))) {
|
|
48
|
+
throw new Error(`godot-assets: no "${preset}" preset in export_presets.cfg (see export_presets.example.cfg).`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const run = (args: string[], env?: Record<string, string>) => {
|
|
52
|
+
const r = spawnSync(editor, args, { cwd, stdio: 'inherit', env: { ...process.env, ...env } });
|
|
53
|
+
if ((r.status ?? 1) !== 0) {
|
|
54
|
+
throw new Error(`godot-assets: \`${editor} ${args.join(' ')}\` failed (${r.status})`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
mkdirSync(resolve(cwd, dirname(pack)), { recursive: true });
|
|
59
|
+
mkdirSync(resolve(cwd, dirname(deps)), { recursive: true });
|
|
60
|
+
|
|
61
|
+
run(['--headless', '--path', '.', '--import']);
|
|
62
|
+
|
|
63
|
+
const dump = resolve(cwd, '.godot', 'pack-dump-deps.gd');
|
|
64
|
+
copyFileSync(resolve(HERE, 'dump-deps.gd'), dump);
|
|
65
|
+
run(['--headless', '--path', '.', '--script', 'res://.godot/pack-dump-deps.gd'], {
|
|
66
|
+
GODOT_DUMP_OUT: `res://${deps.replace(/^res:\/\//, '')}`,
|
|
67
|
+
GODOT_DUMP_ROOTS: roots.join(','),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
run(['--headless', '--path', '.', '--export-pack', preset, pack]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Parses `godot pack` CLI args into {@link BuildAssetPackOptions}. */
|
|
74
|
+
export function parsePackArgs(args: string[]): BuildAssetPackOptions {
|
|
75
|
+
const opt = (flag: string): string | undefined => {
|
|
76
|
+
const i = args.indexOf(flag);
|
|
77
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
78
|
+
};
|
|
79
|
+
const list = (flag: string): string[] | undefined => opt(flag)?.split(',').filter(Boolean);
|
|
80
|
+
return {
|
|
81
|
+
roots: list('--roots'),
|
|
82
|
+
pack: opt('--pack'),
|
|
83
|
+
deps: opt('--deps'),
|
|
84
|
+
preset: opt('--preset'),
|
|
85
|
+
godot: opt('--godot'),
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/preload.ts
CHANGED
|
@@ -8,52 +8,52 @@ import { plugin } from 'bun';
|
|
|
8
8
|
import type { BunPlugin } from 'bun';
|
|
9
9
|
|
|
10
10
|
// Registers the godot-assets plugin: imports of Godot source assets become
|
|
11
|
-
// virtual modules (namespace `godot`) that
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
11
|
+
// virtual modules (namespace `godot`) that mount the project's asset pack and
|
|
12
|
+
// load the resource.
|
|
13
|
+
//
|
|
14
|
+
// A packed source asset (`tscn`/`tres`/`gltf`/`gd`/font/…) is loaded from the
|
|
15
|
+
// `.pck` baked by `godot pack`: the module mounts the pack (`loadAssetPack`),
|
|
16
|
+
// whose remap table resolves the source path to the packed binary (`.scn`/`.res`)
|
|
17
|
+
// with refs the engine resolves itself. The
|
|
18
|
+
// plugin never scans source text for references — the only per-file staging is
|
|
19
|
+
// for *loose* (WebP-backed) textures the wasm engine cannot decode, whose list
|
|
20
|
+
// is computed by the engine (`@ringozz/godot/pack`'s `dump-deps.gd` ->
|
|
21
|
+
// `.godot/packs/deps.json`, via `ResourceLoader.get_dependencies`).
|
|
22
|
+
//
|
|
23
|
+
// The load promise persists on `import.meta.hot.data` (carried across HMR
|
|
24
|
+
// re-evaluations) so `use()` never re-suspends; a rejected load is evicted so
|
|
25
|
+
// the next evaluation retries. The default export is the single plugin —
|
|
26
|
+
// registered globally via bunfig preload and loaded by `[serve.static] plugins`.
|
|
26
27
|
|
|
27
28
|
const ROOT = process.cwd();
|
|
28
29
|
const NS = 'godot';
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
const FORMATS: Record<string, { cls?: string;
|
|
32
|
-
gltf: { cls: 'PackedScene'
|
|
33
|
-
tscn: { cls: 'PackedScene'
|
|
34
|
-
obj: { cls: 'PackedScene'
|
|
35
|
-
gd: { cls: 'GDScript'
|
|
36
|
-
gdshader: { cls: 'Shader'
|
|
37
|
-
gdshaderinc: { cls: 'ShaderInclude'
|
|
38
|
-
jpg: { cls: 'CompressedTexture2D' },
|
|
39
|
-
jpeg: { cls: 'CompressedTexture2D' },
|
|
40
|
-
png: { cls: 'CompressedTexture2D' },
|
|
41
|
-
webp: { cls: 'CompressedTexture2D' },
|
|
31
|
+
/** Extension -> class. `loose` = WebP-backed texture excluded from the pack, staged/transcoded per-file. */
|
|
32
|
+
const FORMATS: Record<string, { cls?: string; loose?: boolean }> = {
|
|
33
|
+
gltf: { cls: 'PackedScene' },
|
|
34
|
+
tscn: { cls: 'PackedScene' },
|
|
35
|
+
obj: { cls: 'PackedScene' },
|
|
36
|
+
gd: { cls: 'GDScript' },
|
|
37
|
+
gdshader: { cls: 'Shader' },
|
|
38
|
+
gdshaderinc: { cls: 'ShaderInclude' },
|
|
39
|
+
jpg: { cls: 'CompressedTexture2D', loose: true },
|
|
40
|
+
jpeg: { cls: 'CompressedTexture2D', loose: true },
|
|
41
|
+
png: { cls: 'CompressedTexture2D', loose: true },
|
|
42
|
+
webp: { cls: 'CompressedTexture2D', loose: true },
|
|
42
43
|
// The layer manifest `godot/addons/sourcewebp` imports into a `.ctexarray` (a texture array whose
|
|
43
|
-
// layers are source WebP bytes).
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
hdr: { cls: 'TextureLayered' },
|
|
44
|
+
// layers are source WebP bytes). It has no runtime deps of its own; the host stages its product.
|
|
45
|
+
webparray: { cls: 'CompressedTexture2DArray', loose: true },
|
|
46
|
+
svg: { cls: 'CompressedTexture2D', loose: true },
|
|
47
|
+
exr: { cls: 'TextureLayered', loose: true },
|
|
48
|
+
hdr: { cls: 'TextureLayered', loose: true },
|
|
49
49
|
wav: { cls: 'AudioStreamWAV' },
|
|
50
50
|
ogg: { cls: 'AudioStreamOggVorbis' },
|
|
51
51
|
mp3: { cls: 'AudioStreamMP3' },
|
|
52
|
-
tres: { cls: 'Resource'
|
|
53
|
-
po: { cls: 'Translation'
|
|
52
|
+
tres: { cls: 'Resource' },
|
|
53
|
+
po: { cls: 'Translation' },
|
|
54
54
|
ttf: { cls: 'FontFile' },
|
|
55
55
|
otf: { cls: 'FontFile' },
|
|
56
|
-
mtl: {
|
|
56
|
+
mtl: {},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const ASSET_RE = new RegExp(
|
|
@@ -63,14 +63,23 @@ const ASSET_RE = new RegExp(
|
|
|
63
63
|
const CLASS_BY_EXT: Record<string, string> = Object.fromEntries(
|
|
64
64
|
Object.entries(FORMATS).filter(([, f]) => f.cls).map(([k, f]) => [k, f.cls!]),
|
|
65
65
|
);
|
|
66
|
-
const
|
|
67
|
-
const NATIVE = new Set(Object.entries(FORMATS).filter(([, f]) => f.native).map(([k]) => k));
|
|
66
|
+
const LOOSE = new Set(Object.entries(FORMATS).filter(([, f]) => f.loose).map(([k]) => k));
|
|
68
67
|
|
|
69
68
|
const extOf = (abs: string) => abs.slice(abs.lastIndexOf('.') + 1).toLowerCase();
|
|
70
69
|
const resOf = (abs: string) => 'res://' + abs.slice(ROOT.length + 1).replaceAll('\\', '/');
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const isLoose = (abs: string) => LOOSE.has(extOf(abs));
|
|
71
|
+
|
|
72
|
+
// A project may bake a native `.pck` (`godot --export-pack`, path overridable via
|
|
73
|
+
// `GODOT_PACK`) carrying the engine-converted binary resources + remap table. Its
|
|
74
|
+
// sibling `deps.json` (from `scripts/dump-deps.gd`) maps each packed source to the
|
|
75
|
+
// loose textures it references. Both are optional: without them (desktop, or a
|
|
76
|
+
// project that hasn't baked a pack) modules load source paths directly.
|
|
77
|
+
const PACK_ABS = resolve(ROOT, process.env.GODOT_PACK ?? '.godot/packs/app.pck');
|
|
78
|
+
const HAS_PACK = existsSync(PACK_ABS);
|
|
79
|
+
const DEPS_ABS = resolve(dirname(PACK_ABS), 'deps.json');
|
|
80
|
+
const DEPS: Record<string, string[]> = existsSync(DEPS_ABS)
|
|
81
|
+
? (JSON.parse(readFileSync(DEPS_ABS, 'utf8')) as Record<string, string[]>)
|
|
82
|
+
: {};
|
|
74
83
|
|
|
75
84
|
/** Reads the imported destination (`dest_files`) out of an `.import` sidecar. */
|
|
76
85
|
function parseSidecarDest(text: string): string | null {
|
|
@@ -85,120 +94,52 @@ function parseSidecarDest(text: string): string | null {
|
|
|
85
94
|
return remap ? remap[1] : null;
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// extension (glTF uris, OBJ/MTL textures). The literal dot before the extension
|
|
91
|
-
// avoids matching MIME types inside `data:` URIs. Regexes are built fresh per
|
|
92
|
-
// call: a `/g` instance is stateful, and reusing one module-level regex across
|
|
93
|
-
// many files misbehaves under the dev server's bundler (a stale lastIndex
|
|
94
|
-
// shifted a scan's results). The `//` fragment right after `res:` is a mere
|
|
95
|
-
// path remainder of the `res://` match, not a real relative ref, so it's
|
|
96
|
-
// skipped (otherwise it mis-resolves as a Windows UNC `\\…` path).
|
|
97
|
-
//
|
|
98
|
-
// The extension must TERMINATE a token: `(?![\w.])` rejects matches where the
|
|
99
|
-
// extension is only the head of a longer identifier or a dotted sub-field, e.g.
|
|
100
|
-
// `X.po` in `X.position`, `foo.gd` in `foo.gd.position`, or `scene.tscn2`. A
|
|
101
|
-
// real relative ref (`foo.po`, `x.gd`, `ui/scene.tscn`) always ends its token.
|
|
102
|
-
// `String.raw` keeps the `\w` escapes intact — a plain template literal would
|
|
103
|
-
// drop the backslashes, turning `\w` into `w` (a wrong guard `(?![w.])`).
|
|
104
|
-
const REL_TERM = String.raw`(?![\w.])`;
|
|
105
|
-
const REL_PATTERN = String.raw`([\w./\\-]+\.(?:` + Object.keys(FORMATS).join('|') + `))` + REL_TERM;
|
|
106
|
-
function extractRefs(text: string): string[] {
|
|
107
|
-
const refs: string[] = [];
|
|
108
|
-
for (const m of text.matchAll(/res:\/\/[\w./\\-]+/g)) refs.push(m[0]);
|
|
109
|
-
const rel = new RegExp(REL_PATTERN, 'g');
|
|
110
|
-
for (const m of text.matchAll(rel)) if (!m[1].startsWith('/')) refs.push(m[1]);
|
|
111
|
-
return refs;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Adds a source asset's own files to `files`: imported assets contribute their
|
|
116
|
-
* `.import` sidecar (text) + imported product (file); native text formats
|
|
117
|
-
* (tscn/tres/po/gd) contribute the file itself (text).
|
|
118
|
-
*/
|
|
119
|
-
function addSource(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
|
|
97
|
+
/** Adds a loose texture source to `files`: its `.import` sidecar (text) + product (file). */
|
|
98
|
+
function addLoose(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
|
|
120
99
|
const impAbs = srcAbs + '.import';
|
|
121
|
-
if (existsSync(impAbs)) {
|
|
122
|
-
files.set(resOf(srcAbs) + '.import', 'text');
|
|
123
|
-
const dest = parseSidecarDest(readFileSync(impAbs, 'utf8'));
|
|
124
|
-
if (dest) {
|
|
125
|
-
files.set(dest, 'file');
|
|
126
|
-
}
|
|
127
|
-
} else if (isNativeText(srcAbs)) {
|
|
128
|
-
stageNativeText(srcAbs, files);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Stages a native text source (`res://` path → `text`) so web staging + inlining picks it up. */
|
|
133
|
-
function stageNativeText(srcAbs: string, files: Map<string, 'text' | 'file'>): void {
|
|
134
|
-
files.set(resOf(srcAbs), 'text');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Collects the referenced source assets (deps) of a text source: every referenced
|
|
139
|
-
* source asset is staged (native text → `files`, products → dep modules), and
|
|
140
|
-
* loadable source assets also become dep modules so their own files + transitive
|
|
141
|
-
* deps are handled. Raw text formats without a sidecar (`.mtl`) are recursed into.
|
|
142
|
-
* Buffers (`.bin`) are skipped.
|
|
143
|
-
*/
|
|
144
|
-
function collectDeps(abs: string, visited: Set<string>, deps: string[], files: Map<string, 'text' | 'file'>): void {
|
|
145
|
-
if (visited.has(abs)) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
visited.add(abs);
|
|
149
|
-
if (!isScannableText(abs)) {
|
|
100
|
+
if (!existsSync(impAbs)) {
|
|
150
101
|
return;
|
|
151
102
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (visited.has(refAbs)) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
// Stage native text (e.g. `.gd`, nested `.tscn`) so the engine loads it
|
|
160
|
-
// by `res://` on web (idempotent); loadable assets also become dep
|
|
161
|
-
// modules for their own files + transitive deps.
|
|
162
|
-
if (isSourceAsset(refAbs)) {
|
|
163
|
-
if (isNativeText(refAbs)) {
|
|
164
|
-
visited.add(refAbs);
|
|
165
|
-
stageNativeText(refAbs, files);
|
|
166
|
-
}
|
|
167
|
-
if (CLASS_BY_EXT[extOf(refAbs)]) {
|
|
168
|
-
deps.push(refAbs);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// Any scannable ref (a source's own text, or a raw leaf like `.mtl`) is
|
|
172
|
-
// recursed into so its internal `res://` refs are traced/staged too.
|
|
173
|
-
if (isScannableText(refAbs)) {
|
|
174
|
-
collectDeps(refAbs, visited, deps, files);
|
|
175
|
-
}
|
|
103
|
+
files.set(resOf(srcAbs) + '.import', 'text');
|
|
104
|
+
const dest = parseSidecarDest(readFileSync(impAbs, 'utf8'));
|
|
105
|
+
if (dest) {
|
|
106
|
+
files.set(dest, 'file');
|
|
176
107
|
}
|
|
177
108
|
}
|
|
178
109
|
|
|
179
|
-
function analyze(abs: string):
|
|
110
|
+
function analyze(abs: string): Map<string, 'text' | 'file'> {
|
|
180
111
|
const files = new Map<string, 'text' | 'file'>();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
112
|
+
// A directly imported loose texture stages itself; a packed resource stages the
|
|
113
|
+
// loose textures the engine's dependency dump found for it.
|
|
114
|
+
if (isLoose(abs)) {
|
|
115
|
+
addLoose(abs, files);
|
|
116
|
+
}
|
|
117
|
+
for (const loose of DEPS[resOf(abs)] ?? []) {
|
|
118
|
+
addLoose(resolve(ROOT, loose.slice('res://'.length)), files);
|
|
119
|
+
}
|
|
120
|
+
return files;
|
|
185
121
|
}
|
|
186
122
|
|
|
187
123
|
function ensureModule(abs: string): string {
|
|
188
124
|
const res = resOf(abs);
|
|
189
125
|
const cls = CLASS_BY_EXT[extOf(abs)] ?? 'Resource';
|
|
190
|
-
const
|
|
126
|
+
const files = analyze(abs);
|
|
191
127
|
|
|
192
128
|
const imports = [
|
|
193
|
-
`import { loadAsset } from '@ringozz/godot/load';`,
|
|
129
|
+
`import { loadAsset, loadAssetPack } from '@ringozz/godot/load';`,
|
|
194
130
|
`import { ${cls} } from '@ringozz/godot/${cls}';`,
|
|
195
131
|
];
|
|
132
|
+
let ready = 'undefined';
|
|
133
|
+
if (HAS_PACK) {
|
|
134
|
+
imports.push(`import pack0 from ${JSON.stringify(PACK_ABS)} with { type: 'file' };`);
|
|
135
|
+
ready = 'loadAssetPack(pack0)';
|
|
136
|
+
}
|
|
196
137
|
const map: string[] = [];
|
|
197
138
|
let fi = 0;
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
139
|
+
// Loose texture files: sidecars as inlined string literals (Bun's
|
|
140
|
+
// `with { type: 'text' }` import resolves to a null/default under the dev
|
|
141
|
+
// server's HMR wrapper for a module's own path), products fetched by path so
|
|
142
|
+
// the host can transcode them.
|
|
202
143
|
for (const [resPath, kind] of files) {
|
|
203
144
|
const absP = resolve(ROOT, resPath.slice('res://'.length));
|
|
204
145
|
if (kind === 'text') {
|
|
@@ -209,10 +150,9 @@ function ensureModule(abs: string): string {
|
|
|
209
150
|
imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: 'file' };`);
|
|
210
151
|
map.push(` ${JSON.stringify(resPath)}: { path: ${v} },`);
|
|
211
152
|
}
|
|
212
|
-
deps.forEach((dep, i) => imports.push(`import * as dep${i} from ${JSON.stringify(dep)};`));
|
|
213
153
|
|
|
214
154
|
const filesVar = files.size ? `const files = {\n${map.join('\n')}\n};` : `const files = {};`;
|
|
215
|
-
const loadAsset = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files,
|
|
155
|
+
const loadAsset = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, import.meta.hot.data, ${ready});`;
|
|
216
156
|
return imports.join('\n') + '\n' + filesVar + '\n' + loadAsset + '\n' + `export { materialize, load as default };` + '\n';
|
|
217
157
|
}
|
|
218
158
|
|
|
@@ -220,13 +160,13 @@ const assetPlugin: BunPlugin = {
|
|
|
220
160
|
name: 'godot-assets',
|
|
221
161
|
setup(build) {
|
|
222
162
|
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
223
|
-
// Only JS module imports of Godot source assets become `godot`
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
163
|
+
// Only JS module imports of Godot source assets become `godot` modules.
|
|
164
|
+
// `Bun.build` reports JS imports as `import-statement` (and
|
|
165
|
+
// `dynamic-import`/`require-*`) but HTML asset references (<link>/<img>)
|
|
166
|
+
// and CSS url() refs as `internal`/`url-token` — those fall through so
|
|
167
|
+
// Bun copies/hashes the file normally. The runtime preload (bun run /
|
|
168
|
+
// bun test) reports JS imports with no kind (`undefined`), so treat that
|
|
169
|
+
// as a JS import too.
|
|
230
170
|
if (
|
|
231
171
|
args.kind !== undefined &&
|
|
232
172
|
args.kind !== 'import-statement' &&
|
|
@@ -236,17 +176,8 @@ const assetPlugin: BunPlugin = {
|
|
|
236
176
|
) {
|
|
237
177
|
return;
|
|
238
178
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
: args.importer;
|
|
242
|
-
const base = importerAbs ? dirname(importerAbs) : (args.resolveDir ?? ROOT);
|
|
243
|
-
const abs = resolve(base, args.path);
|
|
244
|
-
// A module's own native-text source (e.g. `.tscn`) self-imports the
|
|
245
|
-
// same path — let it load as a real file.
|
|
246
|
-
if (abs === importerAbs) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
return { path: abs, namespace: NS };
|
|
179
|
+
const base = args.importer ? dirname(args.importer) : (args.resolveDir ?? ROOT);
|
|
180
|
+
return { path: resolve(base, args.path), namespace: NS };
|
|
250
181
|
});
|
|
251
182
|
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
252
183
|
contents: ensureModule(args.path),
|
package/src/web-image.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// a single parse/allocate pair covers everything — a container is just a list of
|
|
13
13
|
// layers.
|
|
14
14
|
//
|
|
15
|
-
// Exposes the container/format helpers, `pickGodotFormats` (
|
|
16
|
-
//
|
|
15
|
+
// Exposes the container/format helpers, `pickGodotFormats` (the driver's
|
|
16
|
+
// compressed-format list) and `decodeCtex` — the default staging path, which
|
|
17
17
|
// decodes the PNG/WebP payloads with the browser and rebuilds a raw RGBA8
|
|
18
18
|
// container so no image codec runs in wasm. The optional compressed-transcode
|
|
19
19
|
// hook lives in load.ts.
|
|
@@ -209,16 +209,50 @@ export function allocateCtex(
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// ---------------------------------------------------------------------------
|
|
212
|
-
// Capability probing.
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// `
|
|
218
|
-
//
|
|
219
|
-
//
|
|
212
|
+
// Capability probing. GLES3 uploads a compressed format only when its config
|
|
213
|
+
// reports it supported, and this build compiles in NO decompressors, so a format
|
|
214
|
+
// the device can't upload breaks the texture rather than degrading. Ask the
|
|
215
|
+
// driver (`COMPRESSED_TEXTURE_FORMATS`) rather than reading extension names:
|
|
216
|
+
// Chrome on Windows lists no ETC2/EAC and rejects those uploads, while
|
|
217
|
+
// `config.cpp` would still route one to it. RGTC arrays are the one place 2D and
|
|
218
|
+
// layered differ — `_texture_set_data` force-decompresses them for
|
|
219
|
+
// `TEXTURE_2D_ARRAY`, gated here on `Image::can_decompress("s3tc")`.
|
|
220
220
|
// ---------------------------------------------------------------------------
|
|
221
221
|
|
|
222
|
+
// `CompressedTexture` GL enums -> Godot `Image::Format`, for the targets the
|
|
223
|
+
// encoder can emit; only the unsigned-normal variants (their siblings share it).
|
|
224
|
+
const GL_TO_GODOT_FORMAT: Record<number, number> = {
|
|
225
|
+
0x83f0: FORMAT_DXT1, // COMPRESSED_RGB_S3TC_DXT1_EXT
|
|
226
|
+
0x8dbb: FORMAT_RGTC_R, // COMPRESSED_RED_RGTC1_EXT
|
|
227
|
+
0x8dbd: FORMAT_RGTC_RG, // COMPRESSED_RED_GREEN_RGTC2_EXT
|
|
228
|
+
0x8e8c: FORMAT_BPTC_RGBA, // COMPRESSED_RGBA_BPTC_UNORM_EXT
|
|
229
|
+
0x9270: FORMAT_ETC2_R11, // COMPRESSED_R11_EAC
|
|
230
|
+
0x9272: FORMAT_ETC2_RG11, // COMPRESSED_RG11_EAC
|
|
231
|
+
0x9274: FORMAT_ETC2_RGB8, // COMPRESSED_RGB8_ETC2
|
|
232
|
+
0x93b0: FORMAT_ASTC_4x4, // COMPRESSED_RGBA_ASTC_4x4_KHR
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** Enabled before reading the list — a browser may only list a format once its extension is enabled. */
|
|
236
|
+
const PROBED_EXTENSIONS = [
|
|
237
|
+
'EXT_texture_compression_bptc',
|
|
238
|
+
'EXT_texture_compression_rgtc',
|
|
239
|
+
'WEBGL_compressed_texture_s3tc',
|
|
240
|
+
'WEBGL_compressed_texture_astc',
|
|
241
|
+
'WEBGL_compressed_texture_etc',
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/** The Godot targets among a driver's `COMPRESSED_TEXTURE_FORMATS` list, in list order and deduplicated (pure). */
|
|
245
|
+
export function godotFormatsFor(glFormats: ArrayLike<number>): number[] {
|
|
246
|
+
const formats = new Set<number>();
|
|
247
|
+
for (let i = 0; i < glFormats.length; i++) {
|
|
248
|
+
const godot = GL_TO_GODOT_FORMAT[glFormats[i]!];
|
|
249
|
+
if (godot !== undefined) {
|
|
250
|
+
formats.add(godot);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return [...formats];
|
|
254
|
+
}
|
|
255
|
+
|
|
222
256
|
/**
|
|
223
257
|
* Compressed `Image::Format` targets the running device can upload. A list
|
|
224
258
|
* because the encoder may also reject a format the browser exposes (e.g. spark
|
|
@@ -248,31 +282,15 @@ export function pickGodotFormats(): number[] {
|
|
|
248
282
|
if (!gl) {
|
|
249
283
|
return [];
|
|
250
284
|
}
|
|
251
|
-
const formats: number[] = [];
|
|
252
285
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
formats.push(FORMAT_BPTC_RGBA);
|
|
256
|
-
}
|
|
257
|
-
if (exts.has('EXT_texture_compression_rgtc')) {
|
|
258
|
-
formats.push(FORMAT_RGTC_RG);
|
|
259
|
-
formats.push(FORMAT_RGTC_R);
|
|
260
|
-
}
|
|
261
|
-
if (exts.has('WEBGL_compressed_texture_astc')) {
|
|
262
|
-
formats.push(FORMAT_ASTC_4x4);
|
|
263
|
-
}
|
|
264
|
-
if (exts.has('WEBGL_compressed_texture_s3tc')) {
|
|
265
|
-
formats.push(FORMAT_DXT1);
|
|
286
|
+
for (const name of PROBED_EXTENSIONS) {
|
|
287
|
+
gl.getExtension(name);
|
|
266
288
|
}
|
|
267
|
-
|
|
268
|
-
// reports it (`etc2_supported = true` for non-GLES-over-GL) — so these
|
|
269
|
-
// are always uploadable, independent of any extension.
|
|
270
|
-
formats.push(FORMAT_ETC2_RGB8, FORMAT_ETC2_RG11, FORMAT_ETC2_R11);
|
|
289
|
+
return godotFormatsFor(gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS) as ArrayLike<number>);
|
|
271
290
|
} finally {
|
|
272
291
|
// Don't retain the probe context for the page's lifetime.
|
|
273
292
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
274
293
|
}
|
|
275
|
-
return formats;
|
|
276
294
|
}
|
|
277
295
|
|
|
278
296
|
/**
|