@ringozz/godot 4.7.2-639 → 4.7.2-641

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-641",
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-641",
69
+ "@ringozz/godot-windows-x86_64": "^4.7.2-641",
70
+ "@ringozz/godot-web-wasm32": "^4.7.2-641"
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
@@ -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);
@@ -192,10 +220,10 @@ export function allocateCtex(
192
220
  // ---------------------------------------------------------------------------
193
221
 
194
222
  /**
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.
223
+ * Compressed `Image::Format` targets the running device can upload. A list
224
+ * because the encoder may also reject a format the browser exposes (e.g. spark
225
+ * skips BC on iOS); the caller intersects it with spark's supported set, and
226
+ * order here is not significant (the encoder has its own preference chain).
199
227
  */
200
228
  export function pickGodotFormats(): number[] {
201
229
  let gl: WebGL2RenderingContext | null = null;
@@ -226,9 +254,20 @@ export function pickGodotFormats(): number[] {
226
254
  if (exts.has('EXT_texture_compression_bptc')) {
227
255
  formats.push(FORMAT_BPTC_RGBA);
228
256
  }
257
+ if (exts.has('EXT_texture_compression_rgtc')) {
258
+ formats.push(FORMAT_RGTC_RG);
259
+ formats.push(FORMAT_RGTC_R);
260
+ }
229
261
  if (exts.has('WEBGL_compressed_texture_astc')) {
230
262
  formats.push(FORMAT_ASTC_4x4);
231
263
  }
264
+ if (exts.has('WEBGL_compressed_texture_s3tc')) {
265
+ formats.push(FORMAT_DXT1);
266
+ }
267
+ // ETC2/EAC is core WebGL2 (ES 3.0), which is how Godot's web config
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);
232
271
  } finally {
233
272
  // Don't retain the probe context for the page's lifetime.
234
273
  gl.getExtension('WEBGL_lose_context')?.loseContext();
@@ -238,10 +277,11 @@ export function pickGodotFormats(): number[] {
238
277
 
239
278
  /**
240
279
  * 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.
280
+ * (staging then keeps the raw RGBA8 decode). `path` is the resource path being
281
+ * staged, so a platform transcoder can pick a block format from the file name.
282
+ * Produced by `transcoder*.ts` and installed by `load.ts`'s staging.
243
283
  */
244
- export type CtexTranscoder = (bytes: Uint8Array) => Promise<Uint8Array | null>;
284
+ export type CtexTranscoder = (bytes: Uint8Array, path: string) => Promise<Uint8Array | null>;
245
285
 
246
286
  /**
247
287
  * Decodes the embedded PNG/WebP payloads of a `.ctex` with the browser's