@ringozz/godot-web-wasm32 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 +4 -0
- package/package.json +1 -1
- package/src/spark.ts +124 -30
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Godot Engine compiled to **WebAssembly** (`wasm32`, no threads), plus the web ad
|
|
|
4
4
|
|
|
5
5
|
On web, `@ringozz/godot` dispatches here and this package initializes the engine with Emscripten + emnapi, binding it to a `<canvas>`.
|
|
6
6
|
|
|
7
|
+
## Web texture transcoder
|
|
8
|
+
|
|
9
|
+
`@ringozz/godot-web-wasm32/spark` exports `createSparkTranscoder(helpers)`, the hook `@ringozz/godot`'s web `.ctex` staging installs. It rebuilds an imported texture's embedded PNG/WebP payloads as a device-native block-compressed container with `@ludicon/spark.js` (WebGPU), choosing the format from the staged resource path: normal maps → BC5/EAC-RG, alpha albedo and HDR/sky → BC7/ASTC, terrain weight planes → BC4/EAC-R, colour and ORM → BC1/ETC2 first (each chain falls back to BC7/ASTC, then to the raw RGBA8 decode). Every layer of an array/cubemap is encoded in parallel. The helpers are injected structurally so this package never imports `@ringozz/godot`; `@ludicon/spark.js`'s codec shaders carry a non-commercial EULA (the JS is MIT), so every install pulls it — see `@ringozz/godot`'s README for the licensing note.
|
|
10
|
+
|
|
7
11
|
## Symbolizing wasm stack traces
|
|
8
12
|
|
|
9
13
|
The package ships a symbol map next to the wasm: `gen/godot.web.template_release.wasm32.nothreads.js.symbols`
|
package/package.json
CHANGED
package/src/spark.ts
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
// codec shaders carry a non-commercial EULA (the JS is MIT), so it is part of
|
|
18
18
|
// every install — see the licensing note in `@ringozz/godot`'s README.
|
|
19
19
|
//
|
|
20
|
+
// The block format is chosen from the staged resource path (see
|
|
21
|
+
// {@link pickSparkFormat}); a `.ctex` product keeps the source asset's name
|
|
22
|
+
// (`Rock05_Normal.png.webp-<hash>.ctex`), which is what the rules key on.
|
|
23
|
+
//
|
|
20
24
|
// WebGPU, not spark's WebGL2 (SparkGL) path: WebGL2 has no
|
|
21
25
|
// `getCompressedTexImage`, so sparkGL never exposes the encoded bytes (it reads
|
|
22
26
|
// its block buffer back with `readPixels` internally). Only the WebGPU encoder's
|
|
@@ -59,25 +63,34 @@ export interface CtexHelpers {
|
|
|
59
63
|
pickGodotFormats(): number[];
|
|
60
64
|
mipChainLength(width: number, height: number): number;
|
|
61
65
|
mipLevelDimensions(width: number, height: number, level: number): [number, number];
|
|
66
|
+
/** Bytes per 4x4 block of a Godot `Image::Format` (see `web-image.ts`). */
|
|
67
|
+
blockBytes(format: number): number;
|
|
62
68
|
BLOB_TABLE_OFFSET: number;
|
|
69
|
+
FORMAT_DXT1: number;
|
|
70
|
+
FORMAT_RGTC_R: number;
|
|
71
|
+
FORMAT_RGTC_RG: number;
|
|
63
72
|
FORMAT_BPTC_RGBA: number;
|
|
73
|
+
FORMAT_ETC2_R11: number;
|
|
74
|
+
FORMAT_ETC2_RG11: number;
|
|
75
|
+
FORMAT_ETC2_RGB8: number;
|
|
64
76
|
FORMAT_ASTC_4x4: number;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The transcoder callback shape installed by `@ringozz/godot`'s `.ctex` staging.
|
|
81
|
+
* `path` is the resource path being staged, used to pick the block format.
|
|
82
|
+
*/
|
|
83
|
+
export type SparkTranscoder = (bytes: Uint8Array, path: string) => Promise<Uint8Array | null>;
|
|
69
84
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// `pickGodotFormats`) and encodable by spark.
|
|
85
|
+
// One encodable block format: the spark name, the Godot `Image::Format` it maps
|
|
86
|
+
// to, and the exact WebGPU format string spark needs to allocate the output
|
|
87
|
+
// texture (validated when `outputMipLevel` is given).
|
|
74
88
|
interface SparkTarget {
|
|
89
|
+
name: string;
|
|
75
90
|
godot: number;
|
|
76
|
-
spark: string;
|
|
77
91
|
webgpu: GPUTextureFormat;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
const BLOCK_BYTES = 16; // BC7 / ASTC 4x4
|
|
81
94
|
// WebGPU usage flags (TS 7's lib.dom declares the `GPU*` types but not the value
|
|
82
95
|
// namespaces, so spell out the fixed ABI values).
|
|
83
96
|
const BUFFER_COPY_DST = 0x0008;
|
|
@@ -90,13 +103,28 @@ const MAP_MODE_READ = 0x0001;
|
|
|
90
103
|
interface SparkContext {
|
|
91
104
|
spark: Spark;
|
|
92
105
|
device: GPUDevice;
|
|
93
|
-
|
|
106
|
+
/** Device ∩ spark-supported targets, in no particular order. */
|
|
107
|
+
targets: SparkTarget[];
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
// Lazily created once per page; `null` means WebGPU
|
|
97
|
-
//
|
|
110
|
+
// Lazily created once per page; `null` means WebGPU is unavailable, so every
|
|
111
|
+
// transcode declines and the raw RGBA8 decode is used.
|
|
98
112
|
let context: Promise<SparkContext | null> | null = null;
|
|
99
113
|
|
|
114
|
+
// The format table, built against the Godot constants core passes in.
|
|
115
|
+
function targetTable(ctex: CtexHelpers): SparkTarget[] {
|
|
116
|
+
return [
|
|
117
|
+
{ name: 'bc1-rgb', godot: ctex.FORMAT_DXT1, webgpu: 'bc1-rgba-unorm' },
|
|
118
|
+
{ name: 'bc4-r', godot: ctex.FORMAT_RGTC_R, webgpu: 'bc4-r-unorm' },
|
|
119
|
+
{ name: 'bc5-rg', godot: ctex.FORMAT_RGTC_RG, webgpu: 'bc5-rg-unorm' },
|
|
120
|
+
{ name: 'bc7-rgba', godot: ctex.FORMAT_BPTC_RGBA, webgpu: 'bc7-rgba-unorm' },
|
|
121
|
+
{ name: 'eac-r', godot: ctex.FORMAT_ETC2_R11, webgpu: 'eac-r11unorm' },
|
|
122
|
+
{ name: 'eac-rg', godot: ctex.FORMAT_ETC2_RG11, webgpu: 'eac-rg11unorm' },
|
|
123
|
+
{ name: 'etc2-rgb', godot: ctex.FORMAT_ETC2_RGB8, webgpu: 'etc2-rgb8unorm' },
|
|
124
|
+
{ name: 'astc-4x4-rgba', godot: ctex.FORMAT_ASTC_4x4, webgpu: 'astc-4x4-unorm' },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
function getContext(ctex: CtexHelpers): Promise<SparkContext | null> {
|
|
101
129
|
context ??= (async () => {
|
|
102
130
|
const gpu = typeof navigator !== 'undefined' ? navigator.gpu : undefined;
|
|
@@ -111,12 +139,8 @@ function getContext(ctex: CtexHelpers): Promise<SparkContext | null> {
|
|
|
111
139
|
const device = await adapter.requestDevice({ requiredFeatures: Spark.getRequiredFeatures(adapter) });
|
|
112
140
|
const spark = await Spark.create(device);
|
|
113
141
|
const caps = new Set(ctex.pickGodotFormats());
|
|
114
|
-
const targets
|
|
115
|
-
|
|
116
|
-
{ godot: ctex.FORMAT_ASTC_4x4, spark: 'astc-rgba', webgpu: 'astc-4x4-unorm' },
|
|
117
|
-
];
|
|
118
|
-
const target = targets.find((t) => caps.has(t.godot) && spark.isFormatSupported(t.spark));
|
|
119
|
-
return target ? { spark, device, target } : null;
|
|
142
|
+
const targets = targetTable(ctex).filter((t) => caps.has(t.godot) && spark.isFormatSupported(t.name));
|
|
143
|
+
return { spark, device, targets };
|
|
120
144
|
})().catch((err) => {
|
|
121
145
|
console.warn('[godot] spark.js unavailable; web textures stay raw RGBA8:', err);
|
|
122
146
|
return null;
|
|
@@ -124,6 +148,48 @@ function getContext(ctex: CtexHelpers): Promise<SparkContext | null> {
|
|
|
124
148
|
return context;
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// File-name -> format rules, mirroring the three.js viewer's policy (Wartales):
|
|
153
|
+
// normals are 2-channel, alpha albedo and the HDR/sky magnitude stay RGBA,
|
|
154
|
+
// terrain weights are 1-channel, and colour + ORM use the smallest 8 B/block
|
|
155
|
+
// format.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
// `(?<![a-z0-9])`/`(?![a-z0-9])` bound a role token to a name separator without
|
|
159
|
+
// matching it inside a word: `tile_normal`/`normal.webparray` match, `abnormal`
|
|
160
|
+
// and `normalize` do not.
|
|
161
|
+
const RGBA = ['bc7-rgba', 'astc-4x4-rgba'];
|
|
162
|
+
const COLOR = ['bc1-rgb', 'etc2-rgb', ...RGBA];
|
|
163
|
+
|
|
164
|
+
// Ordered, first match wins. The normal/weight chains keep an RGBA last resort
|
|
165
|
+
// so a role whose single/dual-channel format the device lacks still compresses
|
|
166
|
+
// (Godot rebuilds a normal's z; a weight shader reads `.r`).
|
|
167
|
+
const RULES: { re: RegExp; chain: string[] }[] = [
|
|
168
|
+
{ re: /albedoalpha/i, chain: RGBA }, // cutout albedo, whose alpha is real
|
|
169
|
+
{ re: /(?<![a-z0-9])(hdr|skybox|sky|panorama)(?![a-z0-9])/i, chain: RGBA }, // magnitude in alpha
|
|
170
|
+
{ re: /(?<![a-z0-9])(normal|nrm|normalmap)s?(?![a-z0-9])/i, chain: ['bc5-rg', 'eac-rg', ...RGBA] },
|
|
171
|
+
{ re: /(?<![a-z0-9])(splat|weight)s?(?![a-z0-9])/i, chain: ['bc4-r', 'eac-r', ...RGBA] },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The block-format preference chain for a staged resource path, most preferred
|
|
176
|
+
* first. Pure, so it is unit-testable without WebGPU.
|
|
177
|
+
*/
|
|
178
|
+
export function pickSparkFormat(path: string): string[] {
|
|
179
|
+
return RULES.find((rule) => rule.re.test(path))?.chain ?? COLOR;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The first chain entry the device can host, or `null` to decline (raw RGBA8).
|
|
183
|
+
function pickTarget(ctx: SparkContext, path: string): SparkTarget | null {
|
|
184
|
+
for (const name of pickSparkFormat(path)) {
|
|
185
|
+
const target = ctx.targets.find((t) => t.name === name);
|
|
186
|
+
if (target) {
|
|
187
|
+
return target;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
127
193
|
const align256 = (n: number): number => (n + 255) & ~255;
|
|
128
194
|
|
|
129
195
|
// Reads every mip level of `texture` into the container's data region. For
|
|
@@ -139,6 +205,7 @@ async function readInto(
|
|
|
139
205
|
width: number,
|
|
140
206
|
height: number,
|
|
141
207
|
levels: number,
|
|
208
|
+
bytesPerBlock: number,
|
|
142
209
|
): Promise<void> {
|
|
143
210
|
// Where each level sits in the readback buffer. Offsets/`bytesPerRow` must be
|
|
144
211
|
// multiples of the block size; 256 satisfies that and any 4-byte rule.
|
|
@@ -149,7 +216,7 @@ async function readInto(
|
|
|
149
216
|
const cols = Math.ceil(w / 4);
|
|
150
217
|
const rows = Math.ceil(h / 4);
|
|
151
218
|
const offset = align256(total);
|
|
152
|
-
const stride = align256(cols *
|
|
219
|
+
const stride = align256(cols * bytesPerBlock);
|
|
153
220
|
layout.push({ offset, stride, cols, rows });
|
|
154
221
|
total = offset + stride * rows;
|
|
155
222
|
}
|
|
@@ -182,7 +249,7 @@ async function readInto(
|
|
|
182
249
|
const view = new Uint8Array(buffer.getMappedRange());
|
|
183
250
|
let p = dataOffset;
|
|
184
251
|
for (const { offset, stride, cols, rows } of layout) {
|
|
185
|
-
const tight = cols *
|
|
252
|
+
const tight = cols * bytesPerBlock;
|
|
186
253
|
if (stride === tight) {
|
|
187
254
|
// Unpadded (a power-of-two-ish width): the level's blocks are
|
|
188
255
|
// contiguous, so copy the whole level in one go rather than a
|
|
@@ -213,6 +280,7 @@ const encodable = (layer: { width: number; height: number }): boolean =>
|
|
|
213
280
|
async function transcodeLayer(
|
|
214
281
|
ctex: CtexHelpers,
|
|
215
282
|
ctx: SparkContext,
|
|
283
|
+
target: SparkTarget,
|
|
216
284
|
bytes: Uint8Array,
|
|
217
285
|
layer: CtexLayer,
|
|
218
286
|
out: Uint8Array,
|
|
@@ -225,7 +293,7 @@ async function transcodeLayer(
|
|
|
225
293
|
const texture = ctx.device.createTexture({
|
|
226
294
|
size: [width, height, 1],
|
|
227
295
|
mipLevelCount: levels,
|
|
228
|
-
format:
|
|
296
|
+
format: target.webgpu,
|
|
229
297
|
usage: TEXTURE_BINDING | TEXTURE_COPY_DST | TEXTURE_COPY_SRC,
|
|
230
298
|
});
|
|
231
299
|
try {
|
|
@@ -233,13 +301,23 @@ async function transcodeLayer(
|
|
|
233
301
|
throw new Error(`layer decoded as ${bitmap.width}x${bitmap.height}, expected ${width}x${height}`);
|
|
234
302
|
}
|
|
235
303
|
await ctx.spark.encodeTexture(bitmap, {
|
|
236
|
-
format:
|
|
304
|
+
format: target.name,
|
|
237
305
|
mips: true,
|
|
238
306
|
mipmapCount: levels,
|
|
239
307
|
outputTexture: texture,
|
|
240
308
|
outputMipLevel: 0,
|
|
241
309
|
});
|
|
242
|
-
await readInto(
|
|
310
|
+
await readInto(
|
|
311
|
+
ctex,
|
|
312
|
+
ctx.device,
|
|
313
|
+
texture,
|
|
314
|
+
out,
|
|
315
|
+
outOffset,
|
|
316
|
+
width,
|
|
317
|
+
height,
|
|
318
|
+
levels,
|
|
319
|
+
ctex.blockBytes(target.godot),
|
|
320
|
+
);
|
|
243
321
|
} finally {
|
|
244
322
|
bitmap.close();
|
|
245
323
|
texture.destroy();
|
|
@@ -250,7 +328,7 @@ async function transcodeLayer(
|
|
|
250
328
|
// own output texture), so the GPU queue stays fed and the per-layer round trips
|
|
251
329
|
// overlap instead of stacking. Spark encodes 2D textures, so layers are separate
|
|
252
330
|
// encodes; layer `i`'s `.ctex` body lands at `BLOB_TABLE_OFFSET + i * stride`.
|
|
253
|
-
async function transcode(ctex: CtexHelpers, bytes: Uint8Array): Promise<Uint8Array | null> {
|
|
331
|
+
async function transcode(ctex: CtexHelpers, bytes: Uint8Array, path: string): Promise<Uint8Array | null> {
|
|
254
332
|
const layers = ctex.parseCtex(bytes);
|
|
255
333
|
if (!layers || !encodable(layers[0])) {
|
|
256
334
|
return null; // not PNG/WebP-embedded, or a size spark would have to resize
|
|
@@ -259,12 +337,27 @@ async function transcode(ctex: CtexHelpers, bytes: Uint8Array): Promise<Uint8Arr
|
|
|
259
337
|
if (!ctx) {
|
|
260
338
|
return null;
|
|
261
339
|
}
|
|
340
|
+
const target = pickTarget(ctx, path);
|
|
341
|
+
if (!target) {
|
|
342
|
+
return null; // no encodable format for this texture on this device
|
|
343
|
+
}
|
|
262
344
|
const { width, height } = layers[0];
|
|
263
345
|
const levels = ctex.mipChainLength(width, height);
|
|
264
|
-
const { bytes: out, stride } = ctex.allocateCtex(bytes, layers.length, width, height, levels - 1,
|
|
346
|
+
const { bytes: out, stride } = ctex.allocateCtex(bytes, layers.length, width, height, levels - 1, target.godot);
|
|
265
347
|
await Promise.all(
|
|
266
348
|
layers.map((layer, i) =>
|
|
267
|
-
transcodeLayer(
|
|
349
|
+
transcodeLayer(
|
|
350
|
+
ctex,
|
|
351
|
+
ctx,
|
|
352
|
+
target,
|
|
353
|
+
bytes,
|
|
354
|
+
layer,
|
|
355
|
+
out,
|
|
356
|
+
ctex.BLOB_TABLE_OFFSET + i * stride,
|
|
357
|
+
width,
|
|
358
|
+
height,
|
|
359
|
+
levels,
|
|
360
|
+
),
|
|
268
361
|
),
|
|
269
362
|
);
|
|
270
363
|
return out;
|
|
@@ -272,9 +365,10 @@ async function transcode(ctex: CtexHelpers, bytes: Uint8Array): Promise<Uint8Arr
|
|
|
272
365
|
|
|
273
366
|
/**
|
|
274
367
|
* Builds the transcoder that `@ringozz/godot` installs for web `.ctex` staging,
|
|
275
|
-
* given its container helpers.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
368
|
+
* given its container helpers. The block format is chosen from the staged
|
|
369
|
+
* resource path ({@link pickSparkFormat}). Any failure (no WebGPU, no usable
|
|
370
|
+
* format, decode problem) declines so the caller falls back to the raw RGBA8
|
|
371
|
+
* decode; it never throws.
|
|
278
372
|
*/
|
|
279
373
|
export function createSparkTranscoder(ctex: CtexHelpers): SparkTranscoder {
|
|
280
374
|
// Kick off the WebGPU device (and the format probe) as soon as the encoder is
|
|
@@ -283,9 +377,9 @@ export function createSparkTranscoder(ctex: CtexHelpers): SparkTranscoder {
|
|
|
283
377
|
// `.ctex` files behind device creation. `getContext` memoizes, and a failure
|
|
284
378
|
// just declines every transcode (raw RGBA8 fallback).
|
|
285
379
|
void getContext(ctex);
|
|
286
|
-
return async (bytes) => {
|
|
380
|
+
return async (bytes, path) => {
|
|
287
381
|
try {
|
|
288
|
-
return await transcode(ctex, bytes);
|
|
382
|
+
return await transcode(ctex, bytes, path);
|
|
289
383
|
} catch (err) {
|
|
290
384
|
console.warn('[godot] spark transcode failed; using raw RGBA8:', err);
|
|
291
385
|
return null;
|