@ringozz/godot 4.7.2-639 → 4.7.2-643

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 CHANGED
@@ -77,7 +77,7 @@ const b = basis.fromAxisAngle([0, 1, 0], 1.57);
77
77
 
78
78
  ### Async resource loading
79
79
 
80
- The engine loads pre-baked native resources (`.scn`, `.ctex`, …) through `ResourceLoader`. On desktop these are real files (`res://` = cwd); on web there is no filesystem, so the asset's bundled files — `.import` sidecars as text and imported products (`.scn`/`.ctex`) as emitted asset paths — are staged into Emscripten's in-memory MEMFS (transient, no IndexedDB, no persistent copy) by the generated asset modules, handing the fetched bytes straight to MEMFS via Godot's own `copyToFS` (`FS.createDataFile` with `canOwn` — zero copy, JS-heap only). On web, compressed-texture products are decoded first: for `.ctex` (and the layered `.ctexarray` / `.ccube` / `.ccubearray`) the embedded PNG/WebP blobs go through the browser's `createImageBitmap` and the container is rebuilt as raw RGBA (`DATA_FORMAT_IMAGE`) — per layer for arrays — so no image codec runs in wasm. A built-in **transcoder** instead rebuilds the container as a device-native block-compressed texture (BC7/ASTC, via `@ludicon/spark.js`), encoding every layer of an array/cubemap in parallel; its chunk is fetched along with the rest of the app (so it isn't on the critical path to the first texture), the WebGPU device is warmed as soon as it lands, and any decode/encode failure falls back to the raw RGBA path. `.ctex3d` has its own layout and is left to the engine. The source file itself is never fetched.
80
+ The engine loads pre-baked native resources (`.scn`, `.ctex`, …) through `ResourceLoader`. On desktop these are real files (`res://` = cwd); on web there is no filesystem, so the asset's bundled files — `.import` sidecars as text and imported products (`.scn`/`.ctex`) as emitted asset paths — are staged into Emscripten's in-memory MEMFS (transient, no IndexedDB, no persistent copy) by the generated asset modules, handing the fetched bytes straight to MEMFS via Godot's own `copyToFS` (`FS.createDataFile` with `canOwn` — zero copy, JS-heap only). On web, compressed-texture products are decoded first: for `.ctex` (and the layered `.ctexarray` / `.ccube` / `.ccubearray`) the embedded PNG/WebP blobs go through the browser's `createImageBitmap` and the container is rebuilt as raw RGBA (`DATA_FORMAT_IMAGE`) — per layer for arrays — so no image codec runs in wasm. A built-in **transcoder** instead rebuilds the container as a device-native block-compressed texture via `@ludicon/spark.js`, choosing the format from the source file's name — normal maps → BC5/EAC-RG, alpha albedo and HDR/sky → BC7/ASTC, terrain weight planes → BC4/EAC-R, everything else (colour/ORM) → BC1/ETC2 first, each with a BC7/ASTC and finally a raw-RGBA fallback — and encoding every layer of an array/cubemap in parallel; its chunk is fetched along with the rest of the app (so it isn't on the critical path to the first texture), the WebGPU device is warmed as soon as it lands, and any decode/encode failure falls back to the raw RGBA path. `.ctex3d` has its own layout and is left to the engine. The source file itself is never fetched.
81
81
 
82
82
  ```ts
83
83
  import { loadResourceAsync } from '@ringozz/godot/load';
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-639",
4
+ "version": "4.7.2-643",
5
5
  "description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -65,9 +65,9 @@
65
65
  "precision": "single"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@ringozz/godot-macos-arm64": "^4.7.2-639",
69
- "@ringozz/godot-windows-x86_64": "^4.7.2-639",
70
- "@ringozz/godot-web-wasm32": "^4.7.2-639"
68
+ "@ringozz/godot-macos-arm64": "^4.7.2-643",
69
+ "@ringozz/godot-windows-x86_64": "^4.7.2-643",
70
+ "@ringozz/godot-web-wasm32": "^4.7.2-643"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@types/bun": "*"
package/src/load.ts CHANGED
@@ -15,14 +15,15 @@ import { decodeCtex } from './web-image.ts';
15
15
  /**
16
16
  * Produces the bytes staged for a `.ctex`: the platform transcoder (resolved
17
17
  * once by `@ringozz/godot/transcoder`) when it yields a result, otherwise the raw
18
- * RGBA8 browser decode. Never throws — a failed transcode degrades to the
19
- * uncompressed path.
18
+ * RGBA8 browser decode. `path` (the resource path being staged) is forwarded so
19
+ * the transcoder can pick a block format from the file name. Never throws — a
20
+ * failed transcode degrades to the uncompressed path.
20
21
  */
21
- async function transcodeCtex(bytes: Uint8Array): Promise<Uint8Array> {
22
+ async function transcodeCtex(bytes: Uint8Array, path: string): Promise<Uint8Array> {
22
23
  const encode = await transcoder;
23
24
  if (encode) {
24
25
  try {
25
- const out = await encode(bytes);
26
+ const out = await encode(bytes, path);
26
27
  if (out) {
27
28
  return out;
28
29
  }
@@ -137,7 +138,7 @@ async function materialize([resPath, entry]: [string, AssetFile]): Promise<void>
137
138
  // transcoder (e.g. spark.js) may instead rebuild the container as a
138
139
  // device-native block-compressed texture by the built-in transcoder —
139
140
  // except for `EXACT_TEXTURE_RE`'s routes, which must stay raw (see there).
140
- bytes = EXACT_TEXTURE_RE.test(resPath) ? await decodeCtex(bytes) : await transcodeCtex(bytes);
141
+ bytes = EXACT_TEXTURE_RE.test(resPath) ? await decodeCtex(bytes) : await transcodeCtex(bytes, resPath);
141
142
  }
142
143
  } else {
143
144
  return;
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` (probing the WebGL2
16
- // extensions GLES3 checks) and `decodeCtex` — the default staging path, which
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.
@@ -26,9 +26,16 @@ const SUB_HEADER_SIZE = 16;
26
26
  const DATA_FORMAT_PNG = 1;
27
27
  const DATA_FORMAT_WEBP = 2;
28
28
 
29
- // `Image::Format` targets we emit (`core/io/image.h`). Only BC7 and ASTC: both
30
- // are 4x4/16-byte formats, which is why the size math needs no format table.
31
- export const FORMAT_BPTC_RGBA = 22;
29
+ // `Image::Format` targets we emit (`core/io/image.h`). Every compressed target
30
+ // is a 4x4 block format; they differ only in bytes per block, so the size math
31
+ // is a small table rather than a per-format switch.
32
+ export const FORMAT_DXT1 = 17; // BC1
33
+ export const FORMAT_RGTC_R = 20; // BC4
34
+ export const FORMAT_RGTC_RG = 21; // BC5
35
+ export const FORMAT_BPTC_RGBA = 22; // BC7
36
+ export const FORMAT_ETC2_R11 = 26; // EAC R
37
+ export const FORMAT_ETC2_RG11 = 28; // EAC RG
38
+ export const FORMAT_ETC2_RGB8 = 30;
32
39
  export const FORMAT_ASTC_4x4 = 35;
33
40
 
34
41
  // Image-body data starts here: outer header + sub-header.
@@ -37,9 +44,30 @@ export const BLOB_TABLE_OFFSET = OUTER_HEADER_SIZE + SUB_HEADER_SIZE;
37
44
  const DATA_FORMAT_IMAGE = 0;
38
45
  const FORMAT_RGBA8 = 5;
39
46
  const RGBA8_PIXEL_SIZE = 4;
47
+
40
48
  const FORMAT_BIT_STREAM = 1 << 22;
41
49
  const MAGIC_LAYERED = 0x4c; // 'GSTL'; 'GST2' (0x32) is the single-image layout
42
50
 
51
+ // Bytes per 4x4 block for each compressed target; RGBA8 is 4 bytes/pixel.
52
+ const BLOCK_BYTES: Record<number, number> = {
53
+ [FORMAT_DXT1]: 8,
54
+ [FORMAT_ETC2_RGB8]: 8,
55
+ [FORMAT_ETC2_R11]: 8,
56
+ [FORMAT_RGTC_R]: 8,
57
+ [FORMAT_RGTC_RG]: 16,
58
+ [FORMAT_ETC2_RG11]: 16,
59
+ [FORMAT_BPTC_RGBA]: 16,
60
+ [FORMAT_ASTC_4x4]: 16,
61
+ };
62
+
63
+ /**
64
+ * Bytes per 4x4 block of a compressed `Image::Format` target (RGBA8 reports 4,
65
+ * its bytes per pixel). Unknown formats fall back to a 16-byte block.
66
+ */
67
+ export function blockBytes(format: number): number {
68
+ return format === FORMAT_RGBA8 ? RGBA8_PIXEL_SIZE : (BLOCK_BYTES[format] ?? 16);
69
+ }
70
+
43
71
  export interface CtexLayer {
44
72
  /** `DATA_FORMAT_PNG` (1) / `DATA_FORMAT_WEBP` (2). */
45
73
  dataFormat: number;
@@ -132,12 +160,12 @@ export function mipChainLength(width: number, height: number): number {
132
160
  }
133
161
 
134
162
  // Bytes of one `DATA_FORMAT_IMAGE` image body: sub-header + tightly packed
135
- // levels. RGBA8 is 4 bytes/pixel; BC7/ASTC are 16-byte 4x4 blocks whose
136
- // dimensions round up, matching `Image::_get_dst_image_size`.
163
+ // levels. RGBA8 is 4 bytes/pixel; compressed targets are 4x4 blocks (8 or 16
164
+ // bytes) whose dimensions round up, matching `Image::_get_dst_image_size`.
137
165
  function bodySize(width: number, height: number, mipmaps: number, format: number): number {
138
166
  const rgba8 = format === FORMAT_RGBA8;
139
167
  const block = rgba8 ? 1 : 4;
140
- const bytesPerBlock = rgba8 ? RGBA8_PIXEL_SIZE : 16;
168
+ const bytesPerBlock = blockBytes(format);
141
169
  let dataSize = 0;
142
170
  for (let level = 0; level <= mipmaps; level++) {
143
171
  const [w, h] = mipLevelDimensions(width, height, level);
@@ -181,21 +209,55 @@ export function allocateCtex(
181
209
  }
182
210
 
183
211
  // ---------------------------------------------------------------------------
184
- // Capability probing. Godot's Compatibility (GLES3) renderer is what the web
185
- // wasm build uses, and it only uploads a compressed format natively when the
186
- // matching GL extension is present; this build compiles in NO decompressors, so
187
- // a format the device can't upload breaks the texture rather than degrading.
188
- // It selects formats for layered/3D textures exactly as for 2D — the
189
- // `astc_layered_supported`/`astc_3d_supported` flags it computes are only used
190
- // to derive `astc_supported`/`astc_hdr_supported` — so this probe covers texture
191
- // arrays and cubemaps too.
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")`.
192
220
  // ---------------------------------------------------------------------------
193
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
+
194
256
  /**
195
- * Compressed `Image::Format` targets for an RGBA texture, best first, filtered
196
- * to what the running device can actually upload. A list because the encoder may
197
- * also reject a format the browser exposes (e.g. spark skips BC on iOS); the
198
- * caller walks it until one is encodable.
257
+ * Compressed `Image::Format` targets the running device can upload. A list
258
+ * because the encoder may also reject a format the browser exposes (e.g. spark
259
+ * skips BC on iOS); the caller intersects it with spark's supported set, and
260
+ * order here is not significant (the encoder has its own preference chain).
199
261
  */
200
262
  export function pickGodotFormats(): number[] {
201
263
  let gl: WebGL2RenderingContext | null = null;
@@ -220,28 +282,24 @@ export function pickGodotFormats(): number[] {
220
282
  if (!gl) {
221
283
  return [];
222
284
  }
223
- const formats: number[] = [];
224
285
  try {
225
- const exts = new Set(gl.getSupportedExtensions() ?? []);
226
- if (exts.has('EXT_texture_compression_bptc')) {
227
- formats.push(FORMAT_BPTC_RGBA);
228
- }
229
- if (exts.has('WEBGL_compressed_texture_astc')) {
230
- formats.push(FORMAT_ASTC_4x4);
286
+ for (const name of PROBED_EXTENSIONS) {
287
+ gl.getExtension(name);
231
288
  }
289
+ return godotFormatsFor(gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS) as ArrayLike<number>);
232
290
  } finally {
233
291
  // Don't retain the probe context for the page's lifetime.
234
292
  gl.getExtension('WEBGL_lose_context')?.loseContext();
235
293
  }
236
- return formats;
237
294
  }
238
295
 
239
296
  /**
240
297
  * Transforms fetched `.ctex` bytes into replacement bytes, or `null` to decline
241
- * (staging then keeps the raw RGBA8 decode). Produced by `transcoder*.ts` and
242
- * installed by `load.ts`'s staging.
298
+ * (staging then keeps the raw RGBA8 decode). `path` is the resource path being
299
+ * staged, so a platform transcoder can pick a block format from the file name.
300
+ * Produced by `transcoder*.ts` and installed by `load.ts`'s staging.
243
301
  */
244
- export type CtexTranscoder = (bytes: Uint8Array) => Promise<Uint8Array | null>;
302
+ export type CtexTranscoder = (bytes: Uint8Array, path: string) => Promise<Uint8Array | null>;
245
303
 
246
304
  /**
247
305
  * Decodes the embedded PNG/WebP payloads of a `.ctex` with the browser's