gputex 0.4.0 → 0.5.0
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 +121 -31
- package/dist/index.d.ts +193 -29
- package/dist/index.js +1318 -521
- package/dist/testing.d.ts +52 -15
- package/dist/testing.js +935 -109
- package/dist/three.d.ts +79 -8
- package/dist/three.js +1646 -558
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# gputex | GPU texture encoding
|
|
2
2
|
|
|
3
|
-
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF — or an SVG, rasterised on the fly — and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, or
|
|
4
|
-
|
|
5
|
-
⚠️ 100% vibe-coded. The code is completely unreviewed and under-tested. Do not use for anything important.
|
|
3
|
+
Runtime GPU texture compression via WebGPU compute shaders, with a WebGL2 fragment-shader fallback. Feed it a PNG/JPG/WebP/AVIF — or an SVG, rasterised on the fly — and get back a GPU-compressed texture (BC7, BC5, ASTC 4x4, BC1, or ETC2) ready for Three.js or React Three Fiber.
|
|
6
4
|
|
|
7
5
|
🚀 Used in production on [Mana Blade](https://manablade.com).
|
|
8
6
|
|
|
@@ -28,21 +26,26 @@ bun add gputex
|
|
|
28
26
|
|
|
29
27
|
## Formats
|
|
30
28
|
|
|
31
|
-
| Format
|
|
32
|
-
|
|
|
33
|
-
| **BC7**
|
|
34
|
-
| **BC5**
|
|
35
|
-
| **ASTC 4x4**
|
|
36
|
-
| **BC1**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
| Format | Bytes / 4x4 block | Use case |
|
|
30
|
+
| ------------- | ----------------- | ------------------------------------------------------------------------------- |
|
|
31
|
+
| **BC7** | 16 (8 bpp) | Color / RGBA on desktop (`texture-compression-bc`) |
|
|
32
|
+
| **BC5** | 16 (8 bpp) | Normal maps — RG only (`texture-compression-bc`) |
|
|
33
|
+
| **ASTC 4x4** | 16 (8 bpp) | Color / RGBA on mobile / iOS (`texture-compression-astc`) |
|
|
34
|
+
| **BC1** | 8 (4 bpp) | Opaque color at half BC7's size (`quality: 'low'`) |
|
|
35
|
+
| **ETC2 RGB8** | 8 (4 bpp) | Opaque color at half ASTC's size (`texture-compression-etc2`, `quality: 'low'`) |
|
|
36
|
+
|
|
37
|
+
Format selection is automatic: BC7/BC5 on desktop, ASTC on mobile, ETC2 as the
|
|
38
|
+
last-resort compressed format for opaque colour, uncompressed RGBA8 fallback
|
|
39
|
+
otherwise.
|
|
40
|
+
|
|
41
|
+
The 4-bpp formats are never picked by default — half the memory of BC7/ASTC
|
|
42
|
+
but visibly lower quality, a trade-off only the application can make. Opt in
|
|
43
|
+
with `quality: 'low'`: opaque colour textures then encode as BC1 on BC-capable
|
|
44
|
+
devices and as ETC2 RGB8 on ETC2-capable ones (most mobile GPUs), while
|
|
45
|
+
`'colorWithAlpha'` and `'normal'` hints keep the high-quality formats (the
|
|
46
|
+
4-bpp formats can't carry them). Per-texture, `preferredFormat: 'bc1'` forces
|
|
47
|
+
BC1 on BC hardware the same way; both knobs fall back to the normal selection
|
|
48
|
+
when unsupported, and both apply to `hint: 'color'` only.
|
|
46
49
|
|
|
47
50
|
## WebGL fallback
|
|
48
51
|
|
|
@@ -52,7 +55,7 @@ The fallback chain is **WebGPU → WebGL2 → uncompressed RGBA8**. The `backend
|
|
|
52
55
|
|
|
53
56
|
Notes on the WebGL path:
|
|
54
57
|
|
|
55
|
-
- It needs the matching WebGL2 compressed-texture extension to be sampleable: `EXT_texture_compression_bptc` (BC7), `EXT_texture_compression_rgtc` (BC5), `WEBGL_compressed_texture_astc` (ASTC), or `WEBGL_compressed_texture_s3tc` (BC1). Selection mirrors the WebGPU side, with BC1 added as a broadly-available last resort for **opaque** colour when neither BPTC nor ASTC is present.
|
|
58
|
+
- It needs the matching WebGL2 compressed-texture extension to be sampleable: `EXT_texture_compression_bptc` (BC7), `EXT_texture_compression_rgtc` (BC5), `WEBGL_compressed_texture_astc` (ASTC), or `WEBGL_compressed_texture_s3tc` (BC1). Selection mirrors the WebGPU side, with BC1 added as a broadly-available last resort for **opaque** colour when neither BPTC nor ASTC is present. ETC2 is WebGPU-only (no WebGL fragment encoder), so `quality: 'low'` on the WebGL tier can only deliver BC1.
|
|
56
59
|
- The `device` / `adapter` options apply to the WebGPU path only.
|
|
57
60
|
- All encoding happens on one shared, off-screen WebGL2 context; nothing is drawn to a visible canvas.
|
|
58
61
|
|
|
@@ -88,6 +91,24 @@ it skips it and stays the cheapest per pixel. On GPUs that report the
|
|
|
88
91
|
`shader-f16` feature everything runs in f16 — the f32 shaders are the
|
|
89
92
|
automatic fallback.
|
|
90
93
|
|
|
94
|
+
ETC2 is the exception to the endpoint-line story: its blocks are per-subblock
|
|
95
|
+
base colours shifted by scalar modifier tables. The encoder exploits the
|
|
96
|
+
algebra of that scalar shift — table and index selection depend only on each
|
|
97
|
+
texel's luma-sum difference from the base, exactly (modulo decode clamping) —
|
|
98
|
+
so the whole 8-table × 4-modifier search collapses to a handful of scalar
|
|
99
|
+
threshold tests against a two-candidate table shortlist, with subblock error
|
|
100
|
+
constants and the flip preselect computed O(1) from quadrant sums. A gated
|
|
101
|
+
base-colour refit and a closed-form least-squares fit of ETC2's planar mode
|
|
102
|
+
(which rescues the smooth gradients ETC1-style blocks band on) complete the
|
|
103
|
+
block, all driven by the same estimates. The rewrite took the GPU pass
|
|
104
|
+
from 6.0 ms to ~0.2 ms at 2048² (30×, within ~0.2 dB of the exhaustive
|
|
105
|
+
search on photographic content — only the base refit was traded for
|
|
106
|
+
speed). Its f16 module is EXACT-VALUE: lumas, D values and thresholds
|
|
107
|
+
are integers f16 represents exactly, while the sums-of-squares estimates
|
|
108
|
+
stay f32 (they overflow f16), so the two modules produce byte-identical
|
|
109
|
+
output — f16 buys register pressure on mobile GPUs, not different
|
|
110
|
+
results.
|
|
111
|
+
|
|
91
112
|
On the repo's test cards this lands within **≤0.1 dB** of the exhaustive
|
|
92
113
|
per-block reference encoders (BC5 matches the reference exactly; ASTC and
|
|
93
114
|
BC1-on-normal-maps measure slightly above it), trailing only on adversarial
|
|
@@ -219,6 +240,38 @@ const { data, width, height, paddedWidth, paddedHeight } = await encoder.encodeT
|
|
|
219
240
|
encoder.destroy()
|
|
220
241
|
```
|
|
221
242
|
|
|
243
|
+
For mip chains, `encodeMipChainToBytes()` encodes every level in a **single
|
|
244
|
+
GPU submission** — one compute pass and one readback instead of a full
|
|
245
|
+
CPU↔GPU round trip per level (an 11-level 1024² chain is one `mapAsync`
|
|
246
|
+
wait instead of eleven):
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { BC7Encoder, generateMipChain } from 'gputex'
|
|
250
|
+
|
|
251
|
+
const encoder = await BC7Encoder.create()
|
|
252
|
+
// level0 = { data: Uint8ClampedArray (RGBA8), width, height }
|
|
253
|
+
const { levels, encodeMs } = await encoder.encodeMipChainToBytes(generateMipChain(level0))
|
|
254
|
+
// levels[i] = { data, width, height, paddedWidth, paddedHeight }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
When the source is an image (not raw pixels), skip the CPU entirely:
|
|
258
|
+
`generateGpuMipChain()` uploads it once and box-filters the whole chain on
|
|
259
|
+
the GPU in one compute pass, and `encodeMipChainFromTexture()` encodes
|
|
260
|
+
straight from the texture's mip views — no `getImageData` readback, no JS
|
|
261
|
+
filter, no per-level uploads. This is what `compressTexture()` uses for
|
|
262
|
+
`mipmaps: true` (mipped BC7: 28 → 7.5 ms at 2048², 110 → 23 ms at 4096²),
|
|
263
|
+
and its box filter is integer-exact against the CPU one, so both paths emit
|
|
264
|
+
identical bytes:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { BC7Encoder, generateGpuMipChain } from 'gputex'
|
|
268
|
+
|
|
269
|
+
const encoder = await BC7Encoder.create()
|
|
270
|
+
const chainTex = await generateGpuMipChain(encoder.device, imageBitmap, { flipY: true })
|
|
271
|
+
const { levels, encodeMs } = await encoder.encodeMipChainFromTexture(chainTex)
|
|
272
|
+
chainTex.destroy()
|
|
273
|
+
```
|
|
274
|
+
|
|
222
275
|
To turn an encoder's output into a Three.js `CompressedTexture` directly, use the helpers in `gputex/three`:
|
|
223
276
|
|
|
224
277
|
```ts
|
|
@@ -239,15 +292,42 @@ const tex = buildCompressedTexture([bytes], TextureFormat.BC7_SRGB)
|
|
|
239
292
|
|
|
240
293
|
### `compressTexture` options
|
|
241
294
|
|
|
242
|
-
| Option | Type | Default | Description
|
|
243
|
-
| ----------------- | ----------------------------- | --------- |
|
|
244
|
-
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'`
|
|
245
|
-
| `
|
|
246
|
-
| `
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
250
|
-
| `
|
|
295
|
+
| Option | Type | Default | Description |
|
|
296
|
+
| ----------------- | ----------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
297
|
+
| `hint` | `TextureHint` | `'color'` | `'color'`, `'colorWithAlpha'`, or `'normal'` |
|
|
298
|
+
| `quality` | `'high' \| 'low'` | `'high'` | `'low'` picks the 4-bpp formats (BC1 on desktop, ETC2 RGB8 on mobile) for opaque colour — half the memory, lower quality |
|
|
299
|
+
| `preferredFormat` | `'bc1'` | — | Prefer BC1 (half of BC7's size) when supported; normal selection otherwise. `hint: 'color'` only |
|
|
300
|
+
| `colorSpace` | `'srgb' \| 'linear'` | `'srgb'` | Use the sRGB or linear variant of the chosen format |
|
|
301
|
+
| `svgSize` | `number \| { width, height }` | intrinsic | Raster size for SVG sources: longest side (aspect preserved) or exact size |
|
|
302
|
+
| `flipY` | `boolean` | `true` | Flip vertically (matches Three.js convention) |
|
|
303
|
+
| `mipmaps` | `boolean` | `false` | Generate full mip chain down to 1x1 |
|
|
304
|
+
| `cache` | `boolean` | `false` | Session-scoped in-memory cache; repeat calls skip decode + encode (see below) |
|
|
305
|
+
| `cacheKey` | `string` | derived | Explicit cache identity (skips content hashing; makes pixel sources cacheable) |
|
|
306
|
+
| `device` | `GPUDevice` | — | Reuse an existing WebGPU device instead of creating one |
|
|
307
|
+
|
|
308
|
+
#### In-memory transcode cache
|
|
309
|
+
|
|
310
|
+
With `cache: true`, the compressed bytes are kept in a session-scoped
|
|
311
|
+
in-memory LRU keyed by source identity (URL, or a content hash for
|
|
312
|
+
Blobs/Files/data URLs) plus the selected format and encode options. Loading
|
|
313
|
+
the same texture again later in the session — say, two worlds sharing an
|
|
314
|
+
atlas — skips **both** the image decode and the encode, the two dominant
|
|
315
|
+
costs: a 4K PNG that takes ~220 ms to decode + encode comes back in ~30 ms
|
|
316
|
+
(content-hashed) or ~2 ms (URL-keyed). Nothing touches persistent storage;
|
|
317
|
+
the cache dies with the page. Total compressed payload is capped at 256 MiB
|
|
318
|
+
with LRU eviction — `setTranscodeCacheLimit(bytes)` tunes it (0 disables),
|
|
319
|
+
`clearTranscodeCache()` empties it (e.g. on world unload). Pixel sources
|
|
320
|
+
(ImageBitmap, canvas, ImageData) are only cached when you pass a `cacheKey`.
|
|
321
|
+
|
|
322
|
+
When neither `device` nor `adapter` is passed, `compressTexture()` shares one
|
|
323
|
+
WebGPU device and one encoder per format across calls: the first call pays the
|
|
324
|
+
adapter/device request and pipeline compile, subsequent calls skip straight to
|
|
325
|
+
the encode and reuse the encoder's cached GPU resources. The result's
|
|
326
|
+
`destroy()` only disposes that call's texture; call `releaseSharedGpuResources()`
|
|
327
|
+
(also exported from `gputex/three`) to tear down the shared device — the next
|
|
328
|
+
`compressTexture()` call transparently recreates it. With `mipmaps: true` the
|
|
329
|
+
whole chain is encoded in a single GPU submission (one compute pass, one
|
|
330
|
+
readback) rather than a round trip per level.
|
|
251
331
|
|
|
252
332
|
## Benchmarks
|
|
253
333
|
|
|
@@ -270,6 +350,16 @@ end-to-end wall time by ~10% at 512², ~20% at 1024–2048² and ~35% at 4096².
|
|
|
270
350
|
| BC7 | f32 | 0.59 ms |
|
|
271
351
|
| ASTC 4×4 | f16 (default) | **0.26 ms** |
|
|
272
352
|
| ASTC 4×4 | f32 | 0.56 ms |
|
|
353
|
+
| ETC2 | f16 + f32 | 0.20 ms |
|
|
354
|
+
|
|
355
|
+
The ETC2 figure is the interleaved `/ab` harness measurement (batched
|
|
356
|
+
dispatches, clock-stable). On a 100 GB/s part just reading the 2048² RGBA8
|
|
357
|
+
source costs ~0.15 ms, so the entire selection algorithm adds ~30% on top
|
|
358
|
+
of touching the bytes. Two faster variants live in git history and were
|
|
359
|
+
deliberately not shipped: a two-pass 2 B/px prepared source (encode pass
|
|
360
|
+
0.115 ms, but the prep pass is also bandwidth-bound and cannot overlap, so
|
|
361
|
+
the per-texture total regressed) and an O(1) hedged table pick (−3% for
|
|
362
|
+
−0.5 dB — a poor trade against the scored search).
|
|
273
363
|
|
|
274
364
|
Timestamps are quantised to 100 µs by Chrome and Apple GPU clock states swing
|
|
275
365
|
timings by ~2×, so sub-millisecond figures are indicative (±0.1 ms); compare
|
|
@@ -319,7 +409,7 @@ consumer can run the same validation.
|
|
|
319
409
|
|
|
320
410
|
- WebGPU (primary) **or** WebGL2 (fallback) — almost every current browser has at least one
|
|
321
411
|
- A compressed-texture capability for compressed output:
|
|
322
|
-
- WebGPU: `texture-compression-bc` (desktop) or `texture-compression-
|
|
412
|
+
- WebGPU: `texture-compression-bc` (desktop), `texture-compression-astc` (mobile), or `texture-compression-etc2` (mobile)
|
|
323
413
|
- WebGL2: `EXT_texture_compression_bptc` / `_rgtc`, `WEBGL_compressed_texture_astc`, or `WEBGL_compressed_texture_s3tc`
|
|
324
414
|
- Falls back to uncompressed RGBA8 when no compressed format is available on either backend
|
|
325
415
|
|
|
@@ -329,4 +419,4 @@ consumer can run the same validation.
|
|
|
329
419
|
|
|
330
420
|
## Acknowledgements
|
|
331
421
|
|
|
332
|
-
The concept of encoding images on the GPU on the fly via compute shaders was first introduced by [spark.js](https://ludicon.com/sparkjs/).
|
|
422
|
+
The concept of encoding images on the GPU on the fly via compute shaders was first introduced by [spark.js](https://ludicon.com/sparkjs/). gputex is not derived from Spark. Its encoders have been implemented from scratch using official references, which have been ported to TypeScript, and then converted to WGSL and GLSL via AI. Spark was never mentioned or used as reference at any point of the implementation, and multiple reviews have found the implementations to be completely independent. For any serious production use of GPU-compressed textures, Spark is the recommended choice over gputex.
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ declare const TextureFormat: {
|
|
|
6
6
|
readonly BC7_SRGB: "BC7_SRGB";
|
|
7
7
|
readonly ASTC_4x4: "ASTC_4x4";
|
|
8
8
|
readonly ASTC_4x4_SRGB: "ASTC_4x4_SRGB";
|
|
9
|
+
readonly ETC2_RGB8: "ETC2_RGB8";
|
|
10
|
+
readonly ETC2_RGB8_SRGB: "ETC2_RGB8_SRGB";
|
|
9
11
|
};
|
|
10
12
|
type TextureFormat = (typeof TextureFormat)[keyof typeof TextureFormat];
|
|
11
13
|
declare const WebGPUFeature: {
|
|
@@ -33,6 +35,31 @@ interface Capabilities {
|
|
|
33
35
|
}
|
|
34
36
|
declare function detectCapabilities(adapter: FeatureProvider): Capabilities;
|
|
35
37
|
|
|
38
|
+
/** One level of a mip chain. 4 bytes per pixel (RGBA8). */
|
|
39
|
+
interface MipLevel {
|
|
40
|
+
data: Uint8ClampedArray;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Produce the full mip chain from a level-0 image. The chain goes down
|
|
46
|
+
* to a 1×1 level — the standard OpenGL / WebGPU convention — so the
|
|
47
|
+
* caller gets `floor(log2(max(w, h))) + 1` levels total.
|
|
48
|
+
*
|
|
49
|
+
* Levels whose logical dimensions are below the encoder's 4×4 block
|
|
50
|
+
* grid are still produced here at their true logical size; padding up
|
|
51
|
+
* to a single block is the encoder's job, not ours.
|
|
52
|
+
*/
|
|
53
|
+
declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
54
|
+
/**
|
|
55
|
+
* Pad a mip level up to a multiple of 4 in each dimension using clamp-
|
|
56
|
+
* to-edge sampling. Used before handing sub-4×4 levels to a block-
|
|
57
|
+
* compression encoder, which requires at least one full block per level.
|
|
58
|
+
*
|
|
59
|
+
* If the input is already block-aligned this returns the input unchanged.
|
|
60
|
+
*/
|
|
61
|
+
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
62
|
+
|
|
36
63
|
/**
|
|
37
64
|
* Anything `GPUQueue.copyExternalImageToTexture` accepts. Matches the
|
|
38
65
|
* WebGPU spec's CopyExternalImageSource set.
|
|
@@ -54,17 +81,21 @@ interface EncodeCallOptions {
|
|
|
54
81
|
colorSpace?: 'srgb' | 'linear';
|
|
55
82
|
}
|
|
56
83
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* you like, or use
|
|
84
|
+
* One level of encoded output: the compressed block bytes plus logical and
|
|
85
|
+
* block-aligned dimensions. This is the encoder's native output shape, with
|
|
86
|
+
* no Three.js (or any engine) involvement — feed `data` into whatever
|
|
87
|
+
* renderer's compressed-texture upload you like, or use
|
|
88
|
+
* `buildCompressedTexture()` from `gputex/three`.
|
|
61
89
|
*/
|
|
62
|
-
interface
|
|
90
|
+
interface EncodedLevelBytes {
|
|
63
91
|
width: number;
|
|
64
92
|
height: number;
|
|
65
93
|
paddedWidth: number;
|
|
66
94
|
paddedHeight: number;
|
|
67
95
|
data: Uint8Array;
|
|
96
|
+
}
|
|
97
|
+
/** Result of a raw bytes-only single-image encode. */
|
|
98
|
+
interface EncodeBytesResult extends EncodedLevelBytes {
|
|
68
99
|
encodeMs: number;
|
|
69
100
|
/**
|
|
70
101
|
* GPU-side compute-pass time in ms, measured with timestamp queries.
|
|
@@ -75,6 +106,16 @@ interface EncodeBytesResult {
|
|
|
75
106
|
*/
|
|
76
107
|
gpuMs?: number;
|
|
77
108
|
}
|
|
109
|
+
/** Result of a whole-chain encode — see `Encoder.encodeMipChainToBytes()`. */
|
|
110
|
+
interface EncodeMipChainResult {
|
|
111
|
+
/** Encoded levels in the order given; `levels[0]` is the base level. */
|
|
112
|
+
levels: EncodedLevelBytes[];
|
|
113
|
+
/** Wall-clock time for the whole chain: uploads → dispatches → readback. */
|
|
114
|
+
encodeMs: number;
|
|
115
|
+
/** GPU compute time for the whole chain's single compute pass (see
|
|
116
|
+
* `EncodeBytesResult.gpuMs` for caveats). */
|
|
117
|
+
gpuMs?: number;
|
|
118
|
+
}
|
|
78
119
|
interface FormatVariant {
|
|
79
120
|
colorSpace: 'srgb' | 'linear';
|
|
80
121
|
}
|
|
@@ -116,16 +157,34 @@ declare abstract class Encoder {
|
|
|
116
157
|
readonly adapter?: GPUAdapter;
|
|
117
158
|
readonly ownsDevice: boolean;
|
|
118
159
|
readonly disableF16: boolean;
|
|
119
|
-
protected
|
|
160
|
+
protected _pipelineReady: Promise<GPUComputePipeline>;
|
|
161
|
+
protected _prepPipelineReady: Promise<GPUComputePipeline> | null;
|
|
120
162
|
private _cachedSrcTex;
|
|
121
163
|
private _cachedSrcW;
|
|
122
164
|
private _cachedSrcH;
|
|
165
|
+
private _cachedSrcSource;
|
|
166
|
+
private _cachedSrcFlipY;
|
|
123
167
|
private _cachedDst;
|
|
124
168
|
private _cachedStaging;
|
|
125
169
|
private _cachedParams;
|
|
126
170
|
private _lastParams;
|
|
127
171
|
private _cachedBindGroup;
|
|
172
|
+
private _cachedPrepPlanes;
|
|
173
|
+
private _cachedPrepBindGroup;
|
|
128
174
|
private _resourcesBusy;
|
|
175
|
+
/** Set when the active shader declares a @binding(3) sampler (the BC5
|
|
176
|
+
* kernels read texels through textureGather + clamp-to-edge). */
|
|
177
|
+
private _usesSampler;
|
|
178
|
+
private _sampler;
|
|
179
|
+
private _chainSig;
|
|
180
|
+
private _chainTextures;
|
|
181
|
+
private _chainPrepPlanes;
|
|
182
|
+
private _chainPrepBindGroups;
|
|
183
|
+
private _chainParams;
|
|
184
|
+
private _chainBindGroups;
|
|
185
|
+
private _chainDst;
|
|
186
|
+
private _chainStaging;
|
|
187
|
+
private _chainBusy;
|
|
129
188
|
constructor({ device, adapter, ownsDevice, disableF16 }: EncoderOptions);
|
|
130
189
|
protected _buildPipeline(): void;
|
|
131
190
|
destroy(): void;
|
|
@@ -133,10 +192,27 @@ declare abstract class Encoder {
|
|
|
133
192
|
abstract get label(): string;
|
|
134
193
|
/** 8 for BC1/BC4, 16 for BC5/BC7/ASTC 4×4. */
|
|
135
194
|
abstract get bytesPerBlock(): number;
|
|
195
|
+
/**
|
|
196
|
+
* Pipeline-creation override constants for the active shader, or
|
|
197
|
+
* undefined for none. Subclasses expose quality/behaviour toggles this
|
|
198
|
+
* way so the OFF state is dead-coded by the shader compiler instead of
|
|
199
|
+
* branched at runtime.
|
|
200
|
+
*/
|
|
201
|
+
protected pipelineConstants(): Record<string, number> | undefined;
|
|
136
202
|
/** WGSL `@workgroup_size` dimensions. Default 8×8×1. */
|
|
137
203
|
get workgroupSize(): readonly [number, number, number];
|
|
138
204
|
/** Whether this format has an sRGB variant. Default true. */
|
|
139
205
|
get supportsSrgb(): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Format of the single-shot source texture the encode pass samples.
|
|
208
|
+
* Encoders that read only a channel subset can narrow it to cut DRAM
|
|
209
|
+
* traffic on the bandwidth-bound compute pass (BC5 reads rg8unorm — half
|
|
210
|
+
* the bytes of rgba8). Must be valid as a copyExternalImageToTexture
|
|
211
|
+
* destination and renderable. Chain encodes keep rgba8unorm regardless
|
|
212
|
+
* (their inputs are RGBA mip levels / texture views); the WGSL is
|
|
213
|
+
* format-agnostic (`texture_2d<f32>`), so mixing is byte-identical.
|
|
214
|
+
*/
|
|
215
|
+
protected get srcTextureFormat(): GPUTextureFormat;
|
|
140
216
|
/**
|
|
141
217
|
* Optional f16 WGSL variant. Used only when the device reports the
|
|
142
218
|
* `shader-f16` feature; the format's f32 `wgslSource()` is the automatic
|
|
@@ -147,6 +223,21 @@ declare abstract class Encoder {
|
|
|
147
223
|
protected get _useF16(): boolean;
|
|
148
224
|
/** WGSL compute-shader source (f32; the fallback when f16 is unavailable). */
|
|
149
225
|
abstract wgslSource(): string;
|
|
226
|
+
/** WGSL for the preparation pass, or null when the encoder reads the
|
|
227
|
+
* RGBA8 source directly. */
|
|
228
|
+
protected wgslPrepSource(): string | null;
|
|
229
|
+
/** Formats and sizes of the prepared planes for a padded source size. */
|
|
230
|
+
protected prepPlanes(paddedWidth: number, paddedHeight: number): {
|
|
231
|
+
format: GPUTextureFormat;
|
|
232
|
+
width: number;
|
|
233
|
+
height: number;
|
|
234
|
+
}[];
|
|
235
|
+
/** Workgroup counts for the prep dispatch. */
|
|
236
|
+
protected prepDispatch(blocksX: number, blocksY: number): [number, number];
|
|
237
|
+
/** Create the prepared-plane textures for one padded source size. */
|
|
238
|
+
private _createPrepPlanes;
|
|
239
|
+
/** Bind group for one prep dispatch. */
|
|
240
|
+
private _createPrepBindGroup;
|
|
150
241
|
/** e.g. 'bc1-rgba-unorm-srgb'. */
|
|
151
242
|
abstract gpuTextureFormat(opts: FormatVariant): GPUTextureFormat;
|
|
152
243
|
/**
|
|
@@ -166,6 +257,55 @@ declare abstract class Encoder {
|
|
|
166
257
|
flipY?: boolean;
|
|
167
258
|
withGpuTime?: boolean;
|
|
168
259
|
}): Promise<EncodeBytesResult>;
|
|
260
|
+
/**
|
|
261
|
+
* Encode a whole mip chain in ONE GPU submission. A per-level
|
|
262
|
+
* `encodeToBytes()` loop costs a full CPU↔GPU round trip per level (a
|
|
263
|
+
* 1024² chain is 11 levels → 11 `mapAsync` waits with the GPU idle in
|
|
264
|
+
* between); this path uploads every level, records one dispatch per level
|
|
265
|
+
* into a single compute pass, copies all outputs into one staging buffer
|
|
266
|
+
* and maps it once.
|
|
267
|
+
*
|
|
268
|
+
* `levels` are raw RGBA8 pixels in base-to-tail order; sizes don't have to
|
|
269
|
+
* halve level-to-level (each level is padded and clamped independently,
|
|
270
|
+
* exactly like `encodeToBytes`). Uploads use `writeTexture` — the direct
|
|
271
|
+
* raw-bytes path, which also sidesteps the broken
|
|
272
|
+
* `copyExternalImageToTexture` devices (see workarounds.ts). There is no
|
|
273
|
+
* flip option: bake any vertical flip into level 0 before generating the
|
|
274
|
+
* chain, as `compressTexture()` does.
|
|
275
|
+
*/
|
|
276
|
+
encodeMipChainToBytes(levels: readonly MipLevel[], { withGpuTime }?: {
|
|
277
|
+
withGpuTime?: boolean;
|
|
278
|
+
}): Promise<EncodeMipChainResult>;
|
|
279
|
+
/**
|
|
280
|
+
* Encode every mip level of a GPU-resident texture in one submission —
|
|
281
|
+
* the zero-CPU-pixels counterpart of `encodeMipChainToBytes()`. Pair it
|
|
282
|
+
* with `generateGpuMipChain()` (gpuMipgen.ts): upload the image once,
|
|
283
|
+
* box-filter the chain on the GPU, then encode straight from the
|
|
284
|
+
* texture's mip views. Pixels never transit the CPU between the source
|
|
285
|
+
* image and the compressed-bytes readback.
|
|
286
|
+
*
|
|
287
|
+
* `srcTex` must be `rgba8unorm` with TEXTURE_BINDING usage; level 0's
|
|
288
|
+
* dimensions are taken from the texture and lower levels follow the
|
|
289
|
+
* standard floor-halving chain. Encoded output is identical to feeding
|
|
290
|
+
* the equivalent CPU chain to `encodeMipChainToBytes()`.
|
|
291
|
+
*/
|
|
292
|
+
encodeMipChainFromTexture(srcTex: GPUTexture, { withGpuTime }?: {
|
|
293
|
+
withGpuTime?: boolean;
|
|
294
|
+
}): Promise<EncodeMipChainResult>;
|
|
295
|
+
/** Block-grid geometry + packed output offsets for a chain of levels.
|
|
296
|
+
* `byteSpan` is both the dst buffer size and the readback copy size (a
|
|
297
|
+
* multiple of 4: byteLen is a multiple of bytesPerBlock ≥ 8, offsets are
|
|
298
|
+
* CHAIN_ALIGN-ed). */
|
|
299
|
+
private _chainGeometry;
|
|
300
|
+
/** Shared chain-encode tail: one compute pass with a dispatch per level,
|
|
301
|
+
* one submit, one staging readback sliced into per-level byte arrays. */
|
|
302
|
+
private _submitChainAndRead;
|
|
303
|
+
/** Create the query set + resolve/staging buffers for one timed
|
|
304
|
+
* submission, or null when the device lacks 'timestamp-query'. */
|
|
305
|
+
private _createTiming;
|
|
306
|
+
/** Read back a timed submission's pass duration (ms) and destroy the
|
|
307
|
+
* timing objects. Timestamps are u64 nanoseconds. */
|
|
308
|
+
private _readTimingMs;
|
|
169
309
|
}
|
|
170
310
|
|
|
171
311
|
declare class BC1Encoder extends Encoder {
|
|
@@ -185,14 +325,26 @@ declare class BC5Encoder extends Encoder {
|
|
|
185
325
|
get label(): string;
|
|
186
326
|
get bytesPerBlock(): number;
|
|
187
327
|
get supportsSrgb(): boolean;
|
|
328
|
+
protected get srcTextureFormat(): GPUTextureFormat;
|
|
188
329
|
wgslSource(): string;
|
|
189
330
|
wgslSourceFastF16(): string;
|
|
190
331
|
gpuTextureFormat(): GPUTextureFormat;
|
|
191
332
|
}
|
|
192
333
|
|
|
334
|
+
interface BC7EncoderOptions extends EncoderOptions {
|
|
335
|
+
/**
|
|
336
|
+
* Emit BC7 mode 4 on decorrelated blocks (normal maps, channel-packed
|
|
337
|
+
* atlases): a large quality win there, at up to ~1.5× encode time on
|
|
338
|
+
* exactly that content. See the header note for the measurements.
|
|
339
|
+
*/
|
|
340
|
+
adaptiveMode4?: boolean;
|
|
341
|
+
}
|
|
193
342
|
declare class BC7Encoder extends Encoder {
|
|
194
343
|
static readonly requiredFeature: GPUFeatureName;
|
|
195
344
|
static readonly textureFormats: readonly TextureFormat[];
|
|
345
|
+
private readonly adaptiveMode4;
|
|
346
|
+
constructor(opts: BC7EncoderOptions);
|
|
347
|
+
protected pipelineConstants(): Record<string, number> | undefined;
|
|
196
348
|
get label(): string;
|
|
197
349
|
get bytesPerBlock(): number;
|
|
198
350
|
get supportsSrgb(): boolean;
|
|
@@ -212,6 +364,17 @@ declare class ASTC4x4Encoder extends Encoder {
|
|
|
212
364
|
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
213
365
|
}
|
|
214
366
|
|
|
367
|
+
declare class ETC2Encoder extends Encoder {
|
|
368
|
+
static readonly requiredFeature: GPUFeatureName;
|
|
369
|
+
static readonly textureFormats: readonly TextureFormat[];
|
|
370
|
+
get label(): string;
|
|
371
|
+
get bytesPerBlock(): number;
|
|
372
|
+
get supportsSrgb(): boolean;
|
|
373
|
+
wgslSource(): string;
|
|
374
|
+
wgslSourceFastF16(): string;
|
|
375
|
+
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
376
|
+
}
|
|
377
|
+
|
|
215
378
|
/** Raw RGBA8 pixel data (e.g. a CPU-generated mip level). */
|
|
216
379
|
interface RawPixelSource {
|
|
217
380
|
data: ArrayBufferView;
|
|
@@ -358,16 +521,26 @@ type TextureHint = 'color' | 'colorWithAlpha' | 'normal';
|
|
|
358
521
|
/**
|
|
359
522
|
* Optional format preference — a wish, not a demand. Applied when the
|
|
360
523
|
* device supports the format and the hint is compatible; otherwise
|
|
361
|
-
* selection proceeds normally (BC7 → ASTC → null). Currently only
|
|
524
|
+
* selection proceeds normally (BC7 → ASTC → ETC2 → null). Currently only
|
|
362
525
|
* 'bc1': half the memory of BC7 (0.5 vs 1 byte/pixel) for opaque
|
|
363
526
|
* colour, at visibly lower quality on smooth content.
|
|
364
527
|
*/
|
|
365
528
|
type PreferredFormat = 'bc1';
|
|
529
|
+
/**
|
|
530
|
+
* Memory/fidelity trade-off for opaque colour textures. 'high' (default)
|
|
531
|
+
* picks the 8-bpp formats (BC7 / ASTC 4×4); 'low' picks the 4-bpp ones
|
|
532
|
+
* (BC1 / ETC2 RGB8) when the adapter supports one. Hints that the 4-bpp
|
|
533
|
+
* formats can't carry ('colorWithAlpha', 'normal') ignore this and keep
|
|
534
|
+
* the 'high' selection.
|
|
535
|
+
*/
|
|
536
|
+
type FormatQuality = 'high' | 'low';
|
|
366
537
|
interface SelectFormatOptions {
|
|
367
538
|
/** Pick the sRGB variant when the format has one. Default 'srgb'. */
|
|
368
539
|
colorSpace?: 'srgb' | 'linear';
|
|
369
540
|
/** Prefer a specific format when supported. See `PreferredFormat`. */
|
|
370
541
|
preferredFormat?: PreferredFormat;
|
|
542
|
+
/** Memory/fidelity trade-off. See `FormatQuality`. Default 'high'. */
|
|
543
|
+
quality?: FormatQuality;
|
|
371
544
|
}
|
|
372
545
|
interface FormatSelection {
|
|
373
546
|
/** null = no compressed path on this adapter; caller should fall back. */
|
|
@@ -393,30 +566,21 @@ interface WebGLFormatSelection {
|
|
|
393
566
|
}
|
|
394
567
|
declare function selectWebGLFormat(caps: WebGLCapabilities, hint: TextureHint, options?: SelectFormatOptions): WebGLFormatSelection;
|
|
395
568
|
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
data: Uint8ClampedArray;
|
|
399
|
-
width: number;
|
|
400
|
-
height: number;
|
|
401
|
-
}
|
|
569
|
+
/** Matches mipgen's chain length: floor(log2(max(w, h))) + 1 levels. */
|
|
570
|
+
declare function gpuMipLevelCount(width: number, height: number): number;
|
|
402
571
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
572
|
+
* Upload `source` and generate its full mip chain on the GPU. Returns an
|
|
573
|
+
* `rgba8unorm` texture with `gpuMipLevelCount` levels whose dimensions
|
|
574
|
+
* follow the standard floor-halving chain; feed it to
|
|
575
|
+
* `Encoder.encodeMipChainFromTexture()`. The caller owns the texture —
|
|
576
|
+
* destroy it after encoding.
|
|
406
577
|
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* to a single block is the encoder's job, not ours.
|
|
578
|
+
* The returned promise resolves once the work is submitted (not completed);
|
|
579
|
+
* queue ordering makes the levels visible to any later submission.
|
|
410
580
|
*/
|
|
411
|
-
declare function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
* to-edge sampling. Used before handing sub-4×4 levels to a block-
|
|
415
|
-
* compression encoder, which requires at least one full block per level.
|
|
416
|
-
*
|
|
417
|
-
* If the input is already block-aligned this returns the input unchanged.
|
|
418
|
-
*/
|
|
419
|
-
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
581
|
+
declare function generateGpuMipChain(device: GPUDevice, source: ImageBitmap | ImageData | HTMLCanvasElement | OffscreenCanvas, { flipY }?: {
|
|
582
|
+
flipY?: boolean;
|
|
583
|
+
}): Promise<GPUTexture>;
|
|
420
584
|
|
|
421
585
|
/**
|
|
422
586
|
* Target raster size for an SVG source. A number scales the SVG so its
|
|
@@ -441,4 +605,4 @@ interface RasterizeSvgOptions {
|
|
|
441
605
|
*/
|
|
442
606
|
declare function rasterizeSvg(source: string | Blob, options?: RasterizeSvgOptions): Promise<ImageBitmap>;
|
|
443
607
|
|
|
444
|
-
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, BC7WebGLEncoder, type Capabilities, type EncodeBytesResult, type EncodeCallOptions, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateMipChain, getSharedWebGLContext, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
|
|
608
|
+
export { ASTC4x4Encoder, ASTC4x4WebGLEncoder, BC1Encoder, BC1WebGLEncoder, BC5Encoder, BC5WebGLEncoder, BC7Encoder, type BC7EncoderOptions, BC7WebGLEncoder, type Capabilities, ETC2Encoder, type EncodeBytesResult, type EncodeCallOptions, type EncodeMipChainResult, type EncodedLevelBytes, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type ExtensionProvider, type FeatureProvider, type FormatQuality, type FormatSelection, type FormatVariant, type MipLevel, type PreferredFormat, type RasterizeSvgOptions, type RawPixelSource, type SelectFormatOptions, type SvgRasterSize, TextureFormat, type TextureHint, WebGLBlockEncoder, type WebGLCapabilities, type WebGLEncodeBytesResult, type WebGLEncoderConstructor, type WebGLEncoderImageSource, type WebGLEncoderOptions, type WebGLFormatSelection, WebGPUFeature, createWebGLContext, detectCapabilities, detectWebGLCapabilities, generateGpuMipChain, generateMipChain, getSharedWebGLContext, gpuMipLevelCount, isWebGLAvailable, padToBlockMultiple, rasterizeSvg, selectFormat, selectWebGLFormat };
|