gputex 0.0.4 → 0.1.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 +19 -2
- package/dist/index.d.ts +50 -3
- package/dist/index.js +223 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,11 +38,28 @@ const { texture, format } = await compressTexture('/cobblestone.avif', {
|
|
|
38
38
|
hint: 'color', // 'color' | 'colorWithAlpha' | 'normal'
|
|
39
39
|
colorSpace: 'srgb',
|
|
40
40
|
mipmaps: true,
|
|
41
|
+
quality: 'fast', // 'fast' (default) | 'high'
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
material.map = texture
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
#### Quality
|
|
48
|
+
|
|
49
|
+
`quality` trades encode speed against compression accuracy:
|
|
50
|
+
|
|
51
|
+
- **`'fast'` (default)** — a bounding-box endpoint seed plus projection-based
|
|
52
|
+
index assignment (each pixel is projected onto the colinear endpoint line in
|
|
53
|
+
O(1) instead of searching every palette entry) with a single fused
|
|
54
|
+
least-squares refit. On GPUs that report the `shader-f16` feature the whole
|
|
55
|
+
fast path runs in f16 (≈2× on Apple) — the f32 path is the automatic
|
|
56
|
+
fallback. Net vs `'high'` on an Apple GPU: **BC7 ~50×**, **ASTC ~9×**,
|
|
57
|
+
**BC5 ~5×** faster, for a PSNR cost of **≤0.45 dB** (imperceptible). BC1 is
|
|
58
|
+
single-pass and unaffected.
|
|
59
|
+
- **`'high'`** — exhaustive endpoint search (farthest-pair seed, full nearest
|
|
60
|
+
search, p-bit search); output is byte-for-byte identical to the CPU reference
|
|
61
|
+
encoders.
|
|
62
|
+
|
|
46
63
|
### `GputexLoader` — Three.js Loader
|
|
47
64
|
|
|
48
65
|
```ts
|
|
@@ -158,9 +175,9 @@ encoder.destroy()
|
|
|
158
175
|
- `texture-compression-bc` (desktop) or `texture-compression-astc` (mobile) for compressed output
|
|
159
176
|
- Falls back to uncompressed RGBA8 when neither is available
|
|
160
177
|
|
|
161
|
-
##
|
|
178
|
+
## Device-specific workarounds
|
|
162
179
|
|
|
163
|
-
- Black texture on Google Pixel 10
|
|
180
|
+
- Black texture on Google Pixel 10: `copyExternalImageToTexture` produces black textures on the Pixel 10's PowerVR DXT GPU (vendor `img-tec`, architecture `d-series`). Worked around by uploading via `writeTexture` with rasterised pixel data instead.
|
|
164
181
|
|
|
165
182
|
## Acknowledgements
|
|
166
183
|
|
package/dist/index.d.ts
CHANGED
|
@@ -45,9 +45,18 @@ interface EncoderOptions {
|
|
|
45
45
|
adapter?: GPUAdapter;
|
|
46
46
|
ownsDevice?: boolean;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Encoder quality level. 'fast' (default) uses the cheaper search paths in the
|
|
50
|
+
* shaders — measured ~2–4× faster for ≤0.36 dB PSNR. 'high' runs the exhaustive
|
|
51
|
+
* search, producing output byte-identical to the CPU reference encoders.
|
|
52
|
+
* No effect on BC1 (already single-pass; both levels are identical).
|
|
53
|
+
*/
|
|
54
|
+
type EncodeQuality = 'fast' | 'high';
|
|
48
55
|
interface EncodeCallOptions {
|
|
49
56
|
/** Tags the output color space. Forced 'linear' for encoders with supportsSrgb=false. */
|
|
50
57
|
colorSpace?: 'srgb' | 'linear';
|
|
58
|
+
/** Encode quality / speed trade-off. Default 'fast'. */
|
|
59
|
+
quality?: EncodeQuality;
|
|
51
60
|
}
|
|
52
61
|
interface EncodeResult {
|
|
53
62
|
width: number;
|
|
@@ -110,9 +119,18 @@ declare abstract class Encoder {
|
|
|
110
119
|
readonly adapter?: GPUAdapter;
|
|
111
120
|
readonly ownsDevice: boolean;
|
|
112
121
|
protected _module: GPUShaderModule;
|
|
122
|
+
protected _moduleF16: GPUShaderModule | null;
|
|
123
|
+
protected _pipelineF16: GPUComputePipeline | null;
|
|
113
124
|
protected _pipeline: GPUComputePipeline;
|
|
125
|
+
protected _pipelineCache: Map<EncodeQuality, GPUComputePipeline>;
|
|
114
126
|
constructor({ device, adapter, ownsDevice }: EncoderOptions);
|
|
115
127
|
protected _buildPipeline(): void;
|
|
128
|
+
/**
|
|
129
|
+
* Pipeline for a given quality level. Encoders that don't declare a
|
|
130
|
+
* `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
|
|
131
|
+
* argument and reuse the single pipeline. Specialised pipelines are cached.
|
|
132
|
+
*/
|
|
133
|
+
protected _getPipeline(quality: EncodeQuality): GPUComputePipeline;
|
|
116
134
|
destroy(): void;
|
|
117
135
|
/** Short lowercase identifier used in GPU object labels and errors. */
|
|
118
136
|
abstract get label(): string;
|
|
@@ -122,6 +140,20 @@ declare abstract class Encoder {
|
|
|
122
140
|
get workgroupSize(): readonly [number, number, number];
|
|
123
141
|
/** Whether this format has an sRGB variant. Default true. */
|
|
124
142
|
get supportsSrgb(): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
|
|
145
|
+
* (i.e. has distinct fast/high search paths). BC1 is already single-pass and
|
|
146
|
+
* leaves this false; BC5/BC7/ASTC override it to true.
|
|
147
|
+
*/
|
|
148
|
+
get supportsQuality(): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Optional f16 WGSL for the 'fast' path. Used only when the device reports the
|
|
151
|
+
* `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
|
|
152
|
+
* `'high'` always uses it. Returns null when there's no f16 variant (BC1).
|
|
153
|
+
*/
|
|
154
|
+
wgslSourceFastF16(): string | null;
|
|
155
|
+
/** Whether the f16 fast path is both available and supported on this device. */
|
|
156
|
+
protected get _useF16(): boolean;
|
|
125
157
|
/** WGSL compute-shader source. */
|
|
126
158
|
abstract wgslSource(): string;
|
|
127
159
|
/** e.g. 'bc1-rgba-unorm-srgb'. */
|
|
@@ -134,7 +166,7 @@ declare abstract class Encoder {
|
|
|
134
166
|
* whether the result can actually be sampled.
|
|
135
167
|
*/
|
|
136
168
|
get supportsSampling(): boolean;
|
|
137
|
-
encode(source: EncoderImageSource, { colorSpace }?: EncodeCallOptions): Promise<EncodeResult>;
|
|
169
|
+
encode(source: EncoderImageSource, { colorSpace, quality }?: EncodeCallOptions): Promise<EncodeResult>;
|
|
138
170
|
/**
|
|
139
171
|
* Encode one image source to raw compressed bytes, skipping the
|
|
140
172
|
* `CompressedTexture` wrap. Used by the public `encode()` above and by
|
|
@@ -145,8 +177,9 @@ declare abstract class Encoder {
|
|
|
145
177
|
* encoder boundary. Still safe to call from outside — it just does
|
|
146
178
|
* less work than `encode()` and the caller assembles the texture.
|
|
147
179
|
*/
|
|
148
|
-
encodeToBytes(source: EncoderImageSource, { flipY }?: {
|
|
180
|
+
encodeToBytes(source: EncoderImageSource, { flipY, quality }?: {
|
|
149
181
|
flipY?: boolean;
|
|
182
|
+
quality?: EncodeQuality;
|
|
150
183
|
}): Promise<EncodeBytesResult>;
|
|
151
184
|
/**
|
|
152
185
|
* Assemble a `CompressedTexture` from pre-encoded mip levels. Called
|
|
@@ -178,7 +211,9 @@ declare class BC5Encoder extends Encoder {
|
|
|
178
211
|
get label(): string;
|
|
179
212
|
get bytesPerBlock(): number;
|
|
180
213
|
get supportsSrgb(): boolean;
|
|
214
|
+
get supportsQuality(): boolean;
|
|
181
215
|
wgslSource(): string;
|
|
216
|
+
wgslSourceFastF16(): string;
|
|
182
217
|
gpuTextureFormat(): GPUTextureFormat;
|
|
183
218
|
threeTextureFormat(): CompressedPixelFormat;
|
|
184
219
|
}
|
|
@@ -189,7 +224,9 @@ declare class BC7Encoder extends Encoder {
|
|
|
189
224
|
get label(): string;
|
|
190
225
|
get bytesPerBlock(): number;
|
|
191
226
|
get supportsSrgb(): boolean;
|
|
227
|
+
get supportsQuality(): boolean;
|
|
192
228
|
wgslSource(): string;
|
|
229
|
+
wgslSourceFastF16(): string;
|
|
193
230
|
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
194
231
|
threeTextureFormat(): CompressedPixelFormat;
|
|
195
232
|
}
|
|
@@ -200,7 +237,9 @@ declare class ASTC4x4Encoder extends Encoder {
|
|
|
200
237
|
get label(): string;
|
|
201
238
|
get bytesPerBlock(): number;
|
|
202
239
|
get supportsSrgb(): boolean;
|
|
240
|
+
get supportsQuality(): boolean;
|
|
203
241
|
wgslSource(): string;
|
|
242
|
+
wgslSourceFastF16(): string;
|
|
204
243
|
gpuTextureFormat({ colorSpace }: FormatVariant): GPUTextureFormat;
|
|
205
244
|
threeTextureFormat(): CompressedPixelFormat;
|
|
206
245
|
}
|
|
@@ -245,6 +284,12 @@ interface CompressOptions {
|
|
|
245
284
|
flipY?: boolean;
|
|
246
285
|
/** Generate a full mip chain down to 1×1 on the CPU, encode every level. */
|
|
247
286
|
mipmaps?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Encode quality / speed trade-off. 'fast' (default) is ~2–4× faster for a
|
|
289
|
+
* ≤0.36 dB PSNR cost; 'high' runs the exhaustive search (output identical to
|
|
290
|
+
* the CPU reference encoders). No effect on BC1.
|
|
291
|
+
*/
|
|
292
|
+
quality?: EncodeQuality;
|
|
248
293
|
/** Reuse an existing device (e.g. Three.js's renderer device) instead
|
|
249
294
|
* of creating a new one. When provided, the encoder never destroys it. */
|
|
250
295
|
device?: GPUDevice;
|
|
@@ -283,6 +328,8 @@ declare class GputexLoader extends Loader<Texture> {
|
|
|
283
328
|
flipY: boolean;
|
|
284
329
|
/** Generate + encode a full mip chain. Default false. */
|
|
285
330
|
mipmaps: boolean;
|
|
331
|
+
/** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
|
|
332
|
+
quality: EncodeQuality;
|
|
286
333
|
/**
|
|
287
334
|
* Optional pre-existing WebGPU device. Reusing the renderer's device
|
|
288
335
|
* avoids spinning up a second WebGPU context for encoding.
|
|
@@ -329,4 +376,4 @@ declare function generateMipChain(level0: MipLevel): MipLevel[];
|
|
|
329
376
|
*/
|
|
330
377
|
declare function padToBlockMultiple(level: MipLevel): MipLevel;
|
|
331
378
|
|
|
332
|
-
export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type SelectFormatOptions, TextureFormat, type TextureHint, WebGPUFeature, compressTexture, detectCapabilities, generateMipChain, padToBlockMultiple, selectFormat };
|
|
379
|
+
export { ASTC4x4Encoder, BC1Encoder, BC5Encoder, BC7Encoder, type Capabilities, type CompressOptions, type CompressResult, type CompressTextureSource, type EncodeBytesResult, type EncodeCallOptions, type EncodeQuality, type EncodeResult, Encoder, type EncoderConstructor, type EncoderImageSource, type EncoderOptions, type FeatureProvider, type FormatSelection, type FormatVariant, GputexLoader, type MipLevel, type SelectFormatOptions, TextureFormat, type TextureHint, WebGPUFeature, compressTexture, detectCapabilities, generateMipChain, padToBlockMultiple, selectFormat };
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,25 @@ import {
|
|
|
50
50
|
SRGBColorSpace,
|
|
51
51
|
RepeatWrapping
|
|
52
52
|
} from "three";
|
|
53
|
+
|
|
54
|
+
// src/workarounds.ts
|
|
55
|
+
function needsWriteTextureWorkaround(adapter) {
|
|
56
|
+
const { vendor, architecture } = adapter.info ?? {};
|
|
57
|
+
return vendor === "img-tec" && architecture === "d-series";
|
|
58
|
+
}
|
|
59
|
+
function uploadSourceTexture(device, srcTex, source, width, height, flipY, useWriteTexture) {
|
|
60
|
+
if (useWriteTexture && source instanceof ImageData) {
|
|
61
|
+
device.queue.writeTexture({ texture: srcTex }, source.data, { bytesPerRow: width * 4 }, [width, height, 1]);
|
|
62
|
+
} else {
|
|
63
|
+
device.queue.copyExternalImageToTexture({ source, flipY }, { texture: srcTex }, [
|
|
64
|
+
width,
|
|
65
|
+
height,
|
|
66
|
+
1
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/Encoder.ts
|
|
53
72
|
var Encoder = class {
|
|
54
73
|
/**
|
|
55
74
|
* Subclasses set this to the WebGPU feature string the output texture
|
|
@@ -77,6 +96,9 @@ var Encoder = class {
|
|
|
77
96
|
if (this.requiredFeature && adapter.features.has(this.requiredFeature)) {
|
|
78
97
|
requiredFeatures.push(this.requiredFeature);
|
|
79
98
|
}
|
|
99
|
+
if (adapter.features.has("shader-f16")) {
|
|
100
|
+
requiredFeatures.push("shader-f16");
|
|
101
|
+
}
|
|
80
102
|
const device = await adapter.requestDevice({ requiredFeatures });
|
|
81
103
|
return new this({ device, adapter, ownsDevice: true });
|
|
82
104
|
}
|
|
@@ -86,7 +108,14 @@ var Encoder = class {
|
|
|
86
108
|
// `!:` because these are set in `_buildPipeline()` which the constructor
|
|
87
109
|
// calls; TypeScript's flow analysis doesn't see through method calls.
|
|
88
110
|
_module;
|
|
111
|
+
// f16 'fast' module — built only when the device supports shader-f16 and the
|
|
112
|
+
// subclass provides an f16 source. null otherwise (falls back to _module).
|
|
113
|
+
_moduleF16 = null;
|
|
114
|
+
_pipelineF16 = null;
|
|
115
|
+
// Default pipeline (fast). Kept as a field for back-compat; the per-quality
|
|
116
|
+
// cache below holds the specialised pipelines for encoders that support it.
|
|
89
117
|
_pipeline;
|
|
118
|
+
_pipelineCache = /* @__PURE__ */ new Map();
|
|
90
119
|
constructor({ device, adapter, ownsDevice = false }) {
|
|
91
120
|
this.device = device;
|
|
92
121
|
this.adapter = adapter;
|
|
@@ -100,11 +129,52 @@ var Encoder = class {
|
|
|
100
129
|
label: `${this.label}-encoder`,
|
|
101
130
|
code
|
|
102
131
|
});
|
|
103
|
-
this.
|
|
104
|
-
|
|
132
|
+
if (this._useF16) {
|
|
133
|
+
this._moduleF16 = device.createShaderModule({
|
|
134
|
+
label: `${this.label}-encoder-f16`,
|
|
135
|
+
code: this.wgslSourceFastF16()
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (this.supportsQuality) {
|
|
139
|
+
this._pipeline = this._getPipeline("fast");
|
|
140
|
+
} else {
|
|
141
|
+
this._pipeline = device.createComputePipeline({
|
|
142
|
+
label: `${this.label}-encoder-pipeline`,
|
|
143
|
+
layout: "auto",
|
|
144
|
+
compute: { module: this._module, entryPoint: "encode" }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pipeline for a given quality level. Encoders that don't declare a
|
|
150
|
+
* `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
|
|
151
|
+
* argument and reuse the single pipeline. Specialised pipelines are cached.
|
|
152
|
+
*/
|
|
153
|
+
_getPipeline(quality) {
|
|
154
|
+
if (!this.supportsQuality) return this._pipeline;
|
|
155
|
+
if (quality === "fast" && this._moduleF16) {
|
|
156
|
+
if (!this._pipelineF16) {
|
|
157
|
+
this._pipelineF16 = this.device.createComputePipeline({
|
|
158
|
+
label: `${this.label}-encoder-pipeline-fast-f16`,
|
|
159
|
+
layout: "auto",
|
|
160
|
+
compute: { module: this._moduleF16, entryPoint: "encode" }
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return this._pipelineF16;
|
|
164
|
+
}
|
|
165
|
+
const cached = this._pipelineCache.get(quality);
|
|
166
|
+
if (cached) return cached;
|
|
167
|
+
const pipeline = this.device.createComputePipeline({
|
|
168
|
+
label: `${this.label}-encoder-pipeline-${quality}`,
|
|
105
169
|
layout: "auto",
|
|
106
|
-
compute: {
|
|
170
|
+
compute: {
|
|
171
|
+
module: this._module,
|
|
172
|
+
entryPoint: "encode",
|
|
173
|
+
constants: { QUALITY_HIGH: quality === "high" ? 1 : 0 }
|
|
174
|
+
}
|
|
107
175
|
});
|
|
176
|
+
this._pipelineCache.set(quality, pipeline);
|
|
177
|
+
return pipeline;
|
|
108
178
|
}
|
|
109
179
|
destroy() {
|
|
110
180
|
if (this.ownsDevice) this.device.destroy();
|
|
@@ -117,6 +187,26 @@ var Encoder = class {
|
|
|
117
187
|
get supportsSrgb() {
|
|
118
188
|
return true;
|
|
119
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Whether the shader declares a `QUALITY_HIGH` pipeline-overridable constant
|
|
192
|
+
* (i.e. has distinct fast/high search paths). BC1 is already single-pass and
|
|
193
|
+
* leaves this false; BC5/BC7/ASTC override it to true.
|
|
194
|
+
*/
|
|
195
|
+
get supportsQuality() {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Optional f16 WGSL for the 'fast' path. Used only when the device reports the
|
|
200
|
+
* `shader-f16` feature; the format's f32 `wgslSource()` is the fallback and
|
|
201
|
+
* `'high'` always uses it. Returns null when there's no f16 variant (BC1).
|
|
202
|
+
*/
|
|
203
|
+
wgslSourceFastF16() {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
/** Whether the f16 fast path is both available and supported on this device. */
|
|
207
|
+
get _useF16() {
|
|
208
|
+
return this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
|
|
209
|
+
}
|
|
120
210
|
/**
|
|
121
211
|
* True if the device reports the feature the output texture needs.
|
|
122
212
|
* The encoder itself only writes to a storage buffer, so this is about
|
|
@@ -129,9 +219,9 @@ var Encoder = class {
|
|
|
129
219
|
// ------------------------------------------------------------------ //
|
|
130
220
|
// Shared encode() — pad, upload, dispatch, readback, wrap. //
|
|
131
221
|
// ------------------------------------------------------------------ //
|
|
132
|
-
async encode(source, { colorSpace = "srgb" } = {}) {
|
|
222
|
+
async encode(source, { colorSpace = "srgb", quality = "fast" } = {}) {
|
|
133
223
|
const effectiveSrgb = colorSpace === "srgb" && this.supportsSrgb;
|
|
134
|
-
const bytes = await this.encodeToBytes(source);
|
|
224
|
+
const bytes = await this.encodeToBytes(source, { quality });
|
|
135
225
|
const threeFormat = this.threeTextureFormat({ colorSpace: effectiveSrgb ? "srgb" : "linear" });
|
|
136
226
|
const mip = {
|
|
137
227
|
data: bytes.data,
|
|
@@ -167,7 +257,7 @@ var Encoder = class {
|
|
|
167
257
|
* encoder boundary. Still safe to call from outside — it just does
|
|
168
258
|
* less work than `encode()` and the caller assembles the texture.
|
|
169
259
|
*/
|
|
170
|
-
async encodeToBytes(source, { flipY = false } = {}) {
|
|
260
|
+
async encodeToBytes(source, { flipY = false, quality = "fast" } = {}) {
|
|
171
261
|
const device = this.device;
|
|
172
262
|
const width = source.width;
|
|
173
263
|
const height = source.height;
|
|
@@ -184,9 +274,11 @@ var Encoder = class {
|
|
|
184
274
|
label: `${this.label}-src`,
|
|
185
275
|
size: [paddedWidth, paddedHeight, 1],
|
|
186
276
|
format: "rgba8unorm",
|
|
277
|
+
// RENDER_ATTACHMENT is required by copyExternalImageToTexture
|
|
278
|
+
// (internally a blit) even though we never render into this texture.
|
|
187
279
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
188
280
|
});
|
|
189
|
-
device
|
|
281
|
+
uploadSourceTexture(device, srcTex, source, width, height, flipY, source instanceof ImageData);
|
|
190
282
|
const dstBuffer = device.createBuffer({
|
|
191
283
|
label: `${this.label}-dst`,
|
|
192
284
|
size: outByteLen,
|
|
@@ -198,9 +290,10 @@ var Encoder = class {
|
|
|
198
290
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
199
291
|
});
|
|
200
292
|
device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY, paddedWidth, paddedHeight]));
|
|
293
|
+
const pipeline = this._getPipeline(quality);
|
|
201
294
|
const bindGroup = device.createBindGroup({
|
|
202
295
|
label: `${this.label}-bg`,
|
|
203
|
-
layout:
|
|
296
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
204
297
|
entries: [
|
|
205
298
|
{ binding: 0, resource: srcTex.createView() },
|
|
206
299
|
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
@@ -211,7 +304,7 @@ var Encoder = class {
|
|
|
211
304
|
const t0 = performance.now();
|
|
212
305
|
const enc = device.createCommandEncoder({ label: `${this.label}-encode` });
|
|
213
306
|
const pass = enc.beginComputePass();
|
|
214
|
-
pass.setPipeline(
|
|
307
|
+
pass.setPipeline(pipeline);
|
|
215
308
|
pass.setBindGroup(0, bindGroup);
|
|
216
309
|
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
217
310
|
pass.end();
|
|
@@ -302,7 +395,10 @@ var BC1Encoder = class extends Encoder {
|
|
|
302
395
|
import { RED_GREEN_RGTC2_Format } from "three";
|
|
303
396
|
|
|
304
397
|
// src/bc5.wgsl
|
|
305
|
-
var bc5_default = "// BC5 (RGTC2) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into a 16-byte BC5 block\n// written as 4 x u32 into the destination storage buffer.\n//\n// BC5 = two BC4 blocks concatenated:\n// block bytes 0..7 : BC4 of R channel (normal.x for tangent-space normals)\n// block bytes 8..15 : BC4 of G channel (normal.y)\n//\n// Each BC4 half-block (8 bytes):\n// byte 0 : red0 (8-bit endpoint)\n// byte 1 : red1 (8-bit endpoint)\n// bytes 2..7 : 16 \xD7 3-bit indices, LSB-first, pixel 0 at bit 0\n//\n// We always produce the 6-interpolation mode (red0 > red1). See\n// `bc4_ref.js` for the reasoning and the CPU reference this shader is\n// ported from \u2014 the algorithm and edge cases mirror it line-for-line.\n//\n// Pipeline per channel:\n// 1. Load 16 single-channel values, find min/max \u2192 initial endpoints.\n// 2. Quantize to 8-bit. Nudge apart if equal (forces 6-interp mode).\n// 3. Build palette, assign each texel its nearest entry (full L2).\n// 4. One-pass least-squares refinement: solve the 2\xD72 normal equations\n// for the (r0, r1) that minimizes \u03A3(palette[i_k] \u2212 v_k)\xB2. Accept\n// only if quantized endpoints still satisfy r0 > r1 AND total\n// squared error decreased.\n// 5. Pack 2 endpoint bytes + 48 bits of indices into the 8-byte block.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// 6-interpolation-mode palette weights. palette[j] = W0_6[j]*r0 + W1_6[j]*r1.\n// Expressed as a switch so we don't rely on module-scope const arrays.\nfn w0_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 6.0 / 7.0; }\n case 3u: { return 5.0 / 7.0; }\n case 4u: { return 4.0 / 7.0; }\n case 5u: { return 3.0 / 7.0; }\n case 6u: { return 2.0 / 7.0; }\n default: { return 1.0 / 7.0; } // case 7u\n }\n}\n\nfn w1_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 7.0; }\n case 3u: { return 2.0 / 7.0; }\n case 4u: { return 3.0 / 7.0; }\n case 5u: { return 4.0 / 7.0; }\n case 6u: { return 5.0 / 7.0; }\n default: { return 6.0 / 7.0; } // case 7u\n }\n}\n\nfn quantize8(v: f32) -> u32 {\n // Round-to-nearest, clamp to [0, 255]. floor(x + 0.5) is the same\n // rounding rule the CPU reference uses.\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n//
|
|
398
|
+
var bc5_default = "// BC5 (RGTC2) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into a 16-byte BC5 block\n// written as 4 x u32 into the destination storage buffer.\n//\n// BC5 = two BC4 blocks concatenated:\n// block bytes 0..7 : BC4 of R channel (normal.x for tangent-space normals)\n// block bytes 8..15 : BC4 of G channel (normal.y)\n//\n// Each BC4 half-block (8 bytes):\n// byte 0 : red0 (8-bit endpoint)\n// byte 1 : red1 (8-bit endpoint)\n// bytes 2..7 : 16 \xD7 3-bit indices, LSB-first, pixel 0 at bit 0\n//\n// We always produce the 6-interpolation mode (red0 > red1). See\n// `bc4_ref.js` for the reasoning and the CPU reference this shader is\n// ported from \u2014 the algorithm and edge cases mirror it line-for-line.\n//\n// Pipeline per channel:\n// 1. Load 16 single-channel values, find min/max \u2192 initial endpoints.\n// 2. Quantize to 8-bit. Nudge apart if equal (forces 6-interp mode).\n// 3. Build palette, assign each texel its nearest entry (full L2).\n// 4. One-pass least-squares refinement: solve the 2\xD72 normal equations\n// for the (r0, r1) that minimizes \u03A3(palette[i_k] \u2212 v_k)\xB2. Accept\n// only if quantized endpoints still satisfy r0 > r1 AND total\n// squared error decreased.\n// 5. Pack 2 endpoint bytes + 48 bits of indices into the 8-byte block.\n//\n// The candidate endpoints/indices/error are tracked in place \u2014 the refit\n// overwrites them only when accepted \u2014 so no 16-entry index array is ever\n// copied across a function return.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): bbox endpoints + a single nearest-search assignment per\n// channel. The LSQ refit pass below is the bulk of the kernel and buys\n// only ~0.36 dB, so it is skipped \u2014 ~3.8\xD7 faster.\n// high (1): runs the refit, byte-for-byte identical to bc4_ref/bc5_ref.\n\n// 0 = fast (default), 1 = exhaustive/high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// 6-interpolation-mode palette weights. palette[j] = W0_6[j]*r0 + W1_6[j]*r1.\n// Expressed as a switch so we don't rely on module-scope const arrays.\nfn w0_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 6.0 / 7.0; }\n case 3u: { return 5.0 / 7.0; }\n case 4u: { return 4.0 / 7.0; }\n case 5u: { return 3.0 / 7.0; }\n case 6u: { return 2.0 / 7.0; }\n default: { return 1.0 / 7.0; } // case 7u\n }\n}\n\nfn w1_6(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 7.0; }\n case 3u: { return 2.0 / 7.0; }\n case 4u: { return 3.0 / 7.0; }\n case 5u: { return 4.0 / 7.0; }\n case 6u: { return 5.0 / 7.0; }\n default: { return 6.0 / 7.0; } // case 7u\n }\n}\n\nfn quantize8(v: f32) -> u32 {\n // Round-to-nearest, clamp to [0, 255]. floor(x + 0.5) is the same\n // rounding rule the CPU reference uses.\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Build the 8-entry palette for endpoints (r0f, r1f) in normalised space.\nfn build_pal(r0f: f32, r1f: f32, pal: ptr<function, array<f32, 8>>) {\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n (*pal)[j] = w0_6(j) * r0f + w1_6(j) * r1f;\n }\n}\n\n// Assign each of the 16 values its nearest palette entry (full 8-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_all(\n values: ptr<function, array<f32, 16>>,\n pal: ptr<function, array<f32, 8>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> f32 {\n var err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = (*values)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e20;\n for (var j: u32 = 0u; j < 8u; j = j + 1u) {\n let d = (*pal)[j] - v;\n let d2 = d * d;\n if (d2 < best_d) {\n best_d = d2;\n best_j = j;\n }\n }\n (*out_idx)[k] = best_j;\n err = err + best_d;\n }\n return err;\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block, packed as\n// two little-endian u32s (u32[0] = bytes 0..3, u32[1] = bytes 4..7).\nfn encode_bc4(values: ptr<function, array<f32, 16>>) -> vec2<u32> {\n // ---------------- 1. Initial endpoints: bbox of input ----------------\n var vmin: f32 = 1.0;\n var vmax: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n vmin = min(vmin, (*values)[k]);\n vmax = max(vmax, (*values)[k]);\n }\n var r0: u32 = quantize8(vmax);\n var r1: u32 = quantize8(vmin);\n // Force 6-interp mode: red0 > red1 strictly.\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; }\n else { r0 = r0 + 1u; }\n }\n\n // ---------------- 2. Initial palette + indices + error --------------\n var pal: array<f32, 8>;\n build_pal(f32(r0) / 255.0, f32(r1) / 255.0, &pal);\n var indices: array<u32, 16>;\n var err = assign_all(values, &pal, &indices);\n\n // ---------------- 3. Refinement: least-squares on (r0, r1) ----------\n // High-quality only \u2014 the refit is the bulk of the per-channel cost and the\n // branch is resolved at pipeline-compile time, so the fast path skips all of\n // it (the sums loop included), not just the acceptance test.\n if (QUALITY_HIGH != 0u) {\n // Normal equations for palette[j] = a_j * r0 + b_j * r1:\n // [\u03A3AA \u03A3AB] [r0] [\u03A3AV]\n // [\u03A3AB \u03A3BB] [r1] = [\u03A3BV]\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: f32 = 0.0; var sBV: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = w0_6(indices[k]);\n let b = w1_6(indices[k]);\n let v = (*values)[k];\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n // Degenerate system \u2192 skip refinement.\n if (abs(det) > 1e-9) {\n let new_r0 = clamp((sBB * sAV - sAB * sBV) / det, 0.0, 1.0);\n let new_r1 = clamp((sAA * sBV - sAB * sAV) / det, 0.0, 1.0);\n let qR0 = quantize8(new_r0);\n let qR1 = quantize8(new_r1);\n // Only accept refinements that stay in 6-interp mode. A refinement\n // that flips or equalizes the endpoints would change decode mode.\n if (qR0 > qR1) {\n build_pal(f32(qR0) / 255.0, f32(qR1) / 255.0, &pal);\n var idx2: array<u32, 16>;\n let err2 = assign_all(values, &pal, &idx2);\n if (err2 < err) {\n r0 = qR0;\n r1 = qR1;\n indices = idx2;\n err = err2;\n }\n }\n }\n }\n\n // ---------------- 4. Pack 48-bit index field + 2 endpoint bytes -----\n // The 48-bit index field spans block bytes 2..7. Split into idx_lo\n // (low 32 bits of the field) and idx_hi (high 16 bits). An index at\n // bit position 3k straddles the 32-bit boundary iff 3k < 32 < 3k+3\n // (only k = 10, 11 straddle: bits 30..32 and 33..35; actually k=10\n // is bits 30..32, k=11 is 33..35 \u2014 so k=10 straddles). We handle\n // straddles by writing to both halves.\n var idx_lo: u32 = 0u;\n var idx_hi: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let bit = 3u * k;\n let v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idx_lo = idx_lo | (v << bit);\n } else if (bit >= 32u) {\n idx_hi = idx_hi | (v << (bit - 32u));\n } else {\n // Straddle: low part into idx_lo's top, high part into idx_hi's bottom.\n idx_lo = idx_lo | (v << bit);\n idx_hi = idx_hi | (v >> (32u - bit));\n }\n }\n\n // Final u32s, both little-endian:\n // u32[0] bytes = red0, red1, idx_lo[7:0], idx_lo[15:8]\n // u32[1] bytes = idx_lo[23:16], idx_lo[31:24], idx_hi[7:0], idx_hi[15:8]\n let out_lo = r0 | (r1 << 8u) | ((idx_lo & 0xFFFFu) << 16u);\n let out_hi = (idx_lo >> 16u) | (idx_hi << 16u);\n\n return vec2<u32>(out_lo, out_hi);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load 4\xD74 RG values, splitting into per-channel arrays so each can\n // be handed to encode_bc4 independently.\n var r_values: array<f32, 16>;\n var g_values: array<f32, 16>;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 input sizes.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n r_values[i] = c.r;\n g_values[i] = c.g;\n }\n\n let r_block = encode_bc4(&r_values);\n let g_block = encode_bc4(&g_values);\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let out = block_index * 4u;\n dst[out + 0u] = r_block.x;\n dst[out + 1u] = r_block.y;\n dst[out + 2u] = g_block.x;\n dst[out + 3u] = g_block.y;\n}\n";
|
|
399
|
+
|
|
400
|
+
// src/bc5_fast_f16.wgsl
|
|
401
|
+
var bc5_fast_f16_default = '// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Identical algorithm to the f32 fast path in bc5.wgsl, but the projection +\n// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput\n// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still\n// quantised to exact 8-bit. The host selects this module only when the device\n// reports shader-f16, falling back to bc5.wgsl otherwise. "high" never uses this.\n//\n// BC5 fast path in f16 (two BC4 halves, no refit). f16 halves the per-channel ALU.\nenable f16;\nalias h = f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\nfn w0(j: u32) -> h { switch j { case 0u:{return h(1.0);} case 1u:{return h(0.0);} case 2u:{return h(6.0/7.0);} case 3u:{return h(5.0/7.0);} case 4u:{return h(4.0/7.0);} case 5u:{return h(3.0/7.0);} case 6u:{return h(2.0/7.0);} default:{return h(1.0/7.0);} } }\nfn w1(j: u32) -> h { switch j { case 0u:{return h(0.0);} case 1u:{return h(1.0);} case 2u:{return h(1.0/7.0);} case 3u:{return h(2.0/7.0);} case 4u:{return h(3.0/7.0);} case 5u:{return h(4.0/7.0);} case 6u:{return h(5.0/7.0);} default:{return h(6.0/7.0);} } }\nfn q8(v: h) -> u32 { return u32(clamp(floor(v*h(255.0)+h(0.5)), h(0.0), h(255.0))); }\nfn encode_bc4(values: ptr<function, array<h,16>>) -> vec2<u32> {\n var vmin=h(1.0); var vmax=h(0.0);\n for(var k:u32=0u;k<16u;k=k+1u){ vmin=min(vmin,(*values)[k]); vmax=max(vmax,(*values)[k]); }\n var r0=q8(vmax); var r1=q8(vmin);\n if(r0==r1){ if(r1>0u){r1=r1-1u;}else{r0=r0+1u;} }\n var pal: array<h,8>; let r0f=h(f32(r0)/255.0); let r1f=h(f32(r1)/255.0);\n for(var j:u32=0u;j<8u;j=j+1u){ pal[j]=w0(j)*r0f+w1(j)*r1f; }\n var indices: array<u32,16>;\n for(var k:u32=0u;k<16u;k=k+1u){ let v=(*values)[k]; var bj=0u; var bd=h(1e4); for(var j:u32=0u;j<8u;j=j+1u){ let d=pal[j]-v; let d2=d*d; if(d2<bd){bd=d2;bj=j;} } indices[k]=bj; }\n var lo=0u; var hi=0u;\n for(var k:u32=0u;k<16u;k=k+1u){ let bit=3u*k; let v=indices[k]&7u;\n if(bit+3u<=32u){ lo=lo|(v<<bit); } else if(bit>=32u){ hi=hi|(v<<(bit-32u)); } else { lo=lo|(v<<bit); hi=hi|(v>>(32u-bit)); } }\n return vec2<u32>(r0 | (r1<<8u) | ((lo&0xFFFFu)<<16u), (lo>>16u) | (hi<<16u));\n}\n@compute @workgroup_size(8,8,1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}\n let bi=gid.y*params.blocks_x+gid.x;\n let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);\n var rv: array<h,16>; var gv: array<h,16>;\n for(var i:u32=0u;i<16u;i=i+1u){ let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx); let c=textureLoad(src_tex,p,0); rv[i]=h(c.r); gv[i]=h(c.g); }\n let rb=encode_bc4(&rv); let gb=encode_bc4(&gv);\n let o=bi*4u; dst[o]=rb.x; dst[o+1u]=rb.y; dst[o+2u]=gb.x; dst[o+3u]=gb.y;\n}\n';
|
|
306
402
|
|
|
307
403
|
// src/BC5Encoder.ts
|
|
308
404
|
var BC5Encoder = class extends Encoder {
|
|
@@ -317,9 +413,15 @@ var BC5Encoder = class extends Encoder {
|
|
|
317
413
|
get supportsSrgb() {
|
|
318
414
|
return false;
|
|
319
415
|
}
|
|
416
|
+
get supportsQuality() {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
320
419
|
wgslSource() {
|
|
321
420
|
return bc5_default;
|
|
322
421
|
}
|
|
422
|
+
wgslSourceFastF16() {
|
|
423
|
+
return bc5_fast_f16_default;
|
|
424
|
+
}
|
|
323
425
|
gpuTextureFormat() {
|
|
324
426
|
return "bc5-rg-unorm";
|
|
325
427
|
}
|
|
@@ -332,7 +434,87 @@ var BC5Encoder = class extends Encoder {
|
|
|
332
434
|
import { RGBA_BPTC_Format } from "three";
|
|
333
435
|
|
|
334
436
|
// src/bc7.wgsl
|
|
335
|
-
var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// This shader mirrors `bc7_ref.ts` function-by-function; see that file for\n// the end-to-end algorithm rationale and the full mode 6 bitstream layout\n// (summarised below).\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit)\n// bits 14..20 R1\n// bits 21..27 G0\n// bits 28..34 G1 \u2190 straddles the word 0 / word 1 boundary\n// bits 35..41 B0\n// bits 42..48 B1\n// bits 49..55 A0\n// bits 56..62 A1\n// bit 63 P0 (shared p-bit for endpoint 0)\n// bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits)\n// ...\n// bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec. Same table as\n// the CPU reference (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\n// Hardware-exact integer interpolation.\nfn interp8(e0: u32, e1: u32, w: u32) -> u32 {\n return ((64u - w) * e0 + w * e1 + 32u) >> 6u;\n}\n\n// f32-normalised [0,1] \u2192 clamped 8-bit.\nfn to8(v: f32) -> u32 {\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// -------------------------- Farthest-pair seed -------------------------- //\n\nstruct PairResult { i0: u32, i1: u32 };\n\n// 4-channel L2 distance squared, u32 domain (bounded by 4 \xD7 255\xB2 = 260 100).\nfn pixel_dist_sq(a: vec4<u32>, b: vec4<u32>) -> u32 {\n let d = vec4<i32>(a) - vec4<i32>(b);\n let d2 = d * d;\n return u32(d2.x + d2.y + d2.z + d2.w);\n}\n\n// O(N\xB2) = 120 comparisons. See bc7_ref.ts `farthestPair` for why bbox\n// corners aren't safe initial endpoints when channels vary in different\n// directions along the data line.\nfn farthest_pair(pixels: ptr<function, array<vec4<u32>, 16>>) -> PairResult {\n var best_d: u32 = 0u;\n var best_i: u32 = 0u;\n var best_j: u32 = 1u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = pixel_dist_sq((*pixels)[i], (*pixels)[j]);\n if (d > best_d) { best_d = d; best_i = i; best_j = j; }\n }\n }\n return PairResult(best_i, best_j);\n}\n\n// -------------------------- Palette + assignment ------------------------ //\n\n// Build the 16-entry RGBA palette from 8-bit endpoints.\nfn build_palette_6(e0: vec4<u32>, e1: vec4<u32>, pal: ptr<function, array<vec4<u32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let w = w4(i);\n (*pal)[i] = vec4<u32>(\n interp8(e0.x, e1.x, w),\n interp8(e0.y, e1.y, w),\n interp8(e0.z, e1.z, w),\n interp8(e0.w, e1.w, w),\n );\n }\n}\n\n// Nearest-palette-entry search for one pixel. Full 16-entry L2 search.\nfn nearest_index_6(pixel: vec4<u32>, pal: ptr<function, array<vec4<u32>, 16>>) -> vec2<u32> {\n var best_i: u32 = 0u;\n var best_d: u32 = 0xFFFFFFFFu;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = pixel_dist_sq(pixel, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n // x = best index, y = its squared error.\n return vec2<u32>(best_i, best_d);\n}\n\n// Assign all 16 pixels to nearest palette entries, accumulate total error.\nstruct AssignResult { indices: array<u32, 16>, err: u32 };\n\nfn assign_all(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n pal: ptr<function, array<vec4<u32>, 16>>,\n) -> AssignResult {\n var out: AssignResult;\n out.err = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index_6((*pixels)[k], pal);\n out.indices[k] = sel.x;\n out.err = out.err + sel.y;\n }\n return out;\n}\n\n// -------------------------- Endpoint quantisation ----------------------- //\n\n// Quantize one 8-bit ideal channel to (7-bit value, reconstructed 8-bit)\n// under a fixed p-bit. Matches the CPU reference.\nfn quantize_ch(ideal8: u32, p: u32) -> vec2<u32> {\n // q7 = round((ideal8 \u2212 p) / 2), clamp to [0, 127].\n let q = u32(clamp(\n floor((f32(ideal8) - f32(p)) / 2.0 + 0.5),\n 0.0, 127.0,\n ));\n let eff = (q << 1u) | p;\n return vec2<u32>(q, eff);\n}\n\nstruct QuantPair { seven: vec4<u32>, eight: vec4<u32> };\n\nfn quantize_endpoint(ideal8: vec4<u32>, p: u32) -> QuantPair {\n let r = quantize_ch(ideal8.x, p);\n let g = quantize_ch(ideal8.y, p);\n let b = quantize_ch(ideal8.z, p);\n let a = quantize_ch(ideal8.w, p);\n return QuantPair(\n vec4<u32>(r.x, g.x, b.x, a.x),\n vec4<u32>(r.y, g.y, b.y, a.y),\n );\n}\n\n// Try all four p-bit combos (p0, p1) \u2208 {0,1}\xB2 and return the best\n// quantised-endpoint-plus-indices triple.\nstruct BestMode6 {\n e0_7: vec4<u32>, e1_7: vec4<u32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: u32,\n};\n\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n ideal0: vec4<u32>,\n ideal1: vec4<u32>,\n) -> BestMode6 {\n var best: BestMode6;\n best.err = 0xFFFFFFFFu;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n var pal: array<vec4<u32>, 16>;\n build_palette_6(q0.eight, q1.eight, &pal);\n let assigned = assign_all(pixels, &pal);\n if (assigned.err < best.err) {\n best.e0_7 = q0.seven;\n best.e1_7 = q1.seven;\n best.p0 = p0;\n best.p1 = p1;\n best.indices = assigned.indices;\n best.err = assigned.err;\n }\n }\n }\n return best;\n}\n\n// ---------------------- Least-squares endpoint refit -------------------- //\n\n// Channel-independent LSQ fit of (e0, e1) given current indices. Normal\n// equations: see bc7_ref.ts `refitEndpointsMode6`. Returns 8-bit ideal\n// endpoints (before p-bit quantisation). `valid` = false for a degenerate\n// system (all texels on one palette entry).\nstruct RefitResult { e0: vec4<u32>, e1: vec4<u32>, valid: bool };\n\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<u32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<u32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\n// Write `n_bits` LSBs of `value` at bit position `pos` in a 128-bit field\n// split across 4 u32s. Straddles the word boundary when necessary.\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // 1. Load 16 RGBA pixels in 8-bit integer domain.\n var pixels: array<vec4<u32>, 16>;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n pixels[i] = vec4<u32>(to8(c.r), to8(c.g), to8(c.b), to8(c.a));\n }\n\n // 2. Farthest-pair \u2192 initial endpoints.\n let fp = farthest_pair(&pixels);\n let ideal0_init = pixels[fp.i0];\n let ideal1_init = pixels[fp.i1];\n\n // 3. First p-bit search over the farthest-pair seed.\n var best = try_pbit_combos(&pixels, ideal0_init, ideal1_init);\n\n // 4. One-pass LSQ refit + second p-bit search; accept if error decreases.\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n let cand = try_pbit_combos(&pixels, refit.e0, refit.e1);\n if (cand.err < best.err) {\n best = cand;\n }\n }\n\n // 5. Anchor rule \u2014 pixel 0's index MSB must be 0. If not, swap endpoints\n // and reflect every index (new_i = 15 \u2212 old_i). The decoded palette\n // reverses, so the reconstructed image is unchanged.\n if ((best.indices[0] & 0x8u) != 0u) {\n let tmp7 = best.e0_7; best.e0_7 = best.e1_7; best.e1_7 = tmp7;\n let tmpP = best.p0; best.p0 = best.p1; best.p1 = tmpP;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n best.indices[k] = 15u - best.indices[k];\n }\n }\n\n // 6. Pack into 128 bits = 4 u32s.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n\n var pos: u32 = 0u;\n // Mode 6: six zero bits followed by a 1 (LSB-first).\n write_bits(&block, pos, 7u, 0x40u); pos = pos + 7u;\n // Endpoints: R0, R1, G0, G1, B0, B1, A0, A1 \u2014 7 bits each.\n write_bits(&block, pos, 7u, best.e0_7.x); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.x); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.y); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.y); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.z); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.z); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e0_7.w); pos = pos + 7u;\n write_bits(&block, pos, 7u, best.e1_7.w); pos = pos + 7u;\n // P-bits.\n write_bits(&block, pos, 1u, best.p0); pos = pos + 1u;\n write_bits(&block, pos, 1u, best.p1); pos = pos + 1u;\n // Pixel 0: 3-bit anchor (MSB implicit 0).\n write_bits(&block, pos, 3u, best.indices[0] & 0x7u); pos = pos + 3u;\n // Pixels 1..15: 4 bits each.\n for (var k: u32 = 1u; k < 16u; k = k + 1u) {\n write_bits(&block, pos, 4u, best.indices[k] & 0xFu);\n pos = pos + 4u;\n }\n\n // 7. Store.\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
|
|
437
|
+
var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 endpoints fitted by a single\n// least-squares pass whose normal-equation sums are accumulated *during* a\n// projection-based index assignment. The 16 palette entries are colinear\n// (pal[i] = lerp(e0,e1,w[i])), so the nearest index is found by projecting\n// each pixel onto the endpoint line \u2014 O(1) per pixel, no palette build and\n// no 16-entry search. Profiled ~20\xD7 faster than `high` for ~0.4 dB PSNR.\n// high (1): farthest-pair seed, exhaustive p-bit search over all four\n// (p0,p1) \u2208 {0,1}\xB2 combos, full 16-entry nearest search, one LSQ refit \u2014\n// byte-for-byte identical to bc7_ref.ts.\n//\n// Both paths run in the i32 domain. The fast path's branch is selected at\n// pipeline-compile time, so the driver eliminates the unused (high) code.\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit) bits 14..20 R1 bits 21..27 G0 bits 28..34 G1\n// bits 35..41 B0 bits 42..48 B1 bits 49..55 A0 bits 56..62 A1\n// bit 63 P0 bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits) ... bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n\n// 0 = fast (default), 1 = exhaustive/high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// Mode 6 interpolation weights (\xD7 1/64), fixed by the spec (`W4` in bc7_ref.ts).\nfn w4(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 4u; }\n case 2u: { return 9u; }\n case 3u: { return 13u; }\n case 4u: { return 17u; }\n case 5u: { return 21u; }\n case 6u: { return 26u; }\n case 7u: { return 30u; }\n case 8u: { return 34u; }\n case 9u: { return 38u; }\n case 10u: { return 43u; }\n case 11u: { return 47u; }\n case 12u: { return 51u; }\n case 13u: { return 55u; }\n case 14u: { return 60u; }\n default: { return 64u; } // case 15u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit) under\n// a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2); used by\n// both paths.\nstruct QuantPair { seven: vec4<i32>, eight: vec4<i32> };\nfn quantize_endpoint(ideal8: vec4<i32>, p: u32) -> QuantPair {\n let q = vec4<i32>(clamp(\n floor((vec4<f32>(ideal8) - f32(p)) / 2.0 + 0.5),\n vec4<f32>(0.0), vec4<f32>(127.0),\n ));\n let eff = (q << vec4<u32>(1u)) | vec4<i32>(i32(p));\n return QuantPair(q, eff);\n}\n\n// ============================ FAST PATH ================================ //\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nstruct Ep { seven: vec4<i32>, eight: vec4<i32>, p: u32 };\nfn pick_ep(ideal: vec4<i32>) -> Ep {\n let a = quantize_endpoint(ideal, 0u);\n let b = quantize_endpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) { return Ep(b.seven, b.eight, 1u); }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Projection index assignment. The palette is colinear, so the nearest entry is\n// found by projecting onto the endpoint line \u2014 O(1) per pixel. When `fit`, the\n// LSQ normal-equation sums are accumulated in the same pass for a fused refit\n// (uniform weight i/15 \u2014 within a fraction of a code of the exact w4 table).\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_assign(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n e0: vec4<i32>, e1: vec4<i32>,\n out_idx: ptr<function, array<u32, 16>>,\n fit: bool,\n) -> Fit {\n var out: Fit;\n let dir = e1 - e0;\n let dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (var k: u32 = 0u; k < 16u; k = k + 1u) { (*out_idx)[k] = 0u; }\n out.valid = false;\n return out;\n }\n let inv = 15.0 / f32(dd);\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let q = (*pixels)[k] - e0;\n let s = clamp(floor(f32(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv + 0.5), 0.0, 15.0);\n (*out_idx)[k] = u32(s);\n if (fit) {\n let v = vec4<f32>((*pixels)[k]);\n let b = s / 15.0; let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b; sAV = sAV + a * v; sBV = sBV + b * v;\n }\n }\n if (!fit) { out.valid = false; return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { out.valid = false; return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette_6(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 16>>) {\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, i32(w4(i)));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 16>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct BestMode6 {\n e0_7: vec4<i32>, e1_7: vec4<i32>,\n p0: u32, p1: u32,\n indices: array<u32, 16>,\n err: i32,\n};\n\n// Exhaustive p-bit search (high path); commits to `*best` only on improvement.\nfn try_pbit_combos(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n ideal0: vec4<i32>,\n ideal1: vec4<i32>,\n best: ptr<function, BestMode6>,\n) {\n var local_best = (*best).err;\n var pal: array<vec4<i32>, 16>;\n var tmp: array<u32, 16>;\n for (var p0: u32 = 0u; p0 < 2u; p0 = p0 + 1u) {\n let q0 = quantize_endpoint(ideal0, p0);\n for (var p1: u32 = 0u; p1 < 2u; p1 = p1 + 1u) {\n let q1 = quantize_endpoint(ideal1, p1);\n build_palette_6(q0.eight, q1.eight, &pal);\n let err = assign_all(pixels, &pal, &tmp);\n if (err < local_best) {\n local_best = err;\n (*best).e0_7 = q0.seven;\n (*best).e1_7 = q1.seven;\n (*best).p0 = p0;\n (*best).p1 = p1;\n (*best).indices = tmp;\n (*best).err = err;\n }\n }\n }\n}\n\n// Exact-weight LSQ refit (high path); matches bc7_ref.ts `refitEndpointsMode6`.\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let i = (*indices)[k];\n let a = f32(64u - w4(i)) / 64.0;\n let b = f32(w4(i)) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load 16 RGBA pixels (8-bit integer domain) and the per-channel bbox.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var indices: array<u32, 16>;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n var best: BestMode6;\n best.err = 2147483647;\n try_pbit_combos(&pixels, fp.a, fp.b, &best);\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n try_pbit_combos(&pixels, refit.e0, refit.e1, &best);\n }\n e0_7 = best.e0_7; e1_7 = best.e1_7; p0 = best.p0; p1 = best.p1; indices = best.indices;\n } else {\n var ep0 = pick_ep(lo);\n var ep1 = pick_ep(hi);\n let r = proj_assign(&pixels, ep0.eight, ep1.eight, &indices, true);\n if (r.valid) {\n ep0 = pick_ep(r.e0);\n ep1 = pick_ep(r.e1);\n proj_assign(&pixels, ep0.eight, ep1.eight, &indices, false);\n }\n e0_7 = ep0.seven; e1_7 = ep1.seven; p0 = ep0.p; p1 = ep1.p;\n }\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. If not, swap endpoints and\n // reflect every index (new_i = 15 \u2212 old_i); decoded image is unchanged.\n if ((indices[0] & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices[k] = 15u - indices[k];\n }\n }\n\n // Pack into 128 bits = 4 u32s.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n var pos: u32 = 0u;\n write_bits(&block, pos, 7u, 0x40u); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.x)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.x)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.y)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.y)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.z)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.z)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e0_7.w)); pos = pos + 7u;\n write_bits(&block, pos, 7u, u32(e1_7.w)); pos = pos + 7u;\n write_bits(&block, pos, 1u, p0); pos = pos + 1u;\n write_bits(&block, pos, 1u, p1); pos = pos + 1u;\n write_bits(&block, pos, 3u, indices[0] & 0x7u); pos = pos + 3u;\n for (var k: u32 = 1u; k < 16u; k = k + 1u) {\n write_bits(&block, pos, 4u, indices[k] & 0xFu);\n pos = pos + 4u;\n }\n\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
|
|
438
|
+
|
|
439
|
+
// src/bc7_fast_f16.wgsl
|
|
440
|
+
var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
441
|
+
// Identical algorithm to the f32 fast path in bc7.wgsl, but the projection +
|
|
442
|
+
// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput
|
|
443
|
+
// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still
|
|
444
|
+
// quantised to exact 8-bit. The host selects this module only when the device
|
|
445
|
+
// reports shader-f16, falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
446
|
+
//
|
|
447
|
+
// BC7 mode 6 fast path in f16 (Apple GPUs run f16 at 2x). All math in the [0,1]
|
|
448
|
+
// domain so dot products stay well under f16's range; endpoints quantised to
|
|
449
|
+
// 8-bit at the end. Same bbox seed + projection + fused LSQ refit + reproject.
|
|
450
|
+
enable f16;
|
|
451
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
452
|
+
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
453
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
454
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
455
|
+
alias h = f16;
|
|
456
|
+
alias h4 = vec4<f16>;
|
|
457
|
+
struct Ep { seven: vec4<i32>, eight: h4, p: u32 };
|
|
458
|
+
// quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit; returns 8-bit eff in [0,1].
|
|
459
|
+
fn pick_ep(ideal01: h4) -> Ep {
|
|
460
|
+
let ideal = ideal01 * h(255.0);
|
|
461
|
+
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
462
|
+
let e0 = q0 * h(2.0);
|
|
463
|
+
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
464
|
+
let e1 = q1 * h(2.0) + h(1.0);
|
|
465
|
+
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
466
|
+
if (dot(d1,d1) < dot(d0,d0)) { return Ep(vec4<i32>(q1), e1 * h(1.0/255.0), 1u); }
|
|
467
|
+
return Ep(vec4<i32>(q0), e0 * h(1.0/255.0), 0u);
|
|
468
|
+
}
|
|
469
|
+
struct Fit { e0: h4, e1: h4, valid: bool };
|
|
470
|
+
fn proj_assign(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {
|
|
471
|
+
var out: Fit; let dir = e1 - e0; let dd = dot(dir,dir);
|
|
472
|
+
if (dd == h(0.0)) { for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }
|
|
473
|
+
let inv = h(15.0) / dd;
|
|
474
|
+
var sAA=h(0.0); var sBB=h(0.0); var sAB=h(0.0); var sAV=h4(0.0); var sBV=h4(0.0);
|
|
475
|
+
for(var k:u32=0u;k<16u;k=k+1u){
|
|
476
|
+
let v=(*pix)[k];
|
|
477
|
+
let s = clamp(floor(dot(v - e0, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
478
|
+
(*out_idx)[k] = u32(s);
|
|
479
|
+
if(fit){ let b=s*h(1.0/15.0); let a=h(1.0)-b; sAA=sAA+a*a; sBB=sBB+b*b; sAB=sAB+a*b; sAV=sAV+a*v; sBV=sBV+b*v; }
|
|
480
|
+
}
|
|
481
|
+
if(!fit){ out.valid=false; return out; }
|
|
482
|
+
let det = sAA*sBB - sAB*sAB; if (abs(det) < h(0.0001)) { out.valid=false; return out; }
|
|
483
|
+
out.e0 = clamp((sBB*sAV - sAB*sBV)/det, h4(0.0), h4(1.0));
|
|
484
|
+
out.e1 = clamp((sAA*sBV - sAB*sAV)/det, h4(0.0), h4(1.0));
|
|
485
|
+
out.valid=true; return out;
|
|
486
|
+
}
|
|
487
|
+
fn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {
|
|
488
|
+
let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);
|
|
489
|
+
let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);
|
|
490
|
+
if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }
|
|
491
|
+
}
|
|
492
|
+
@compute @workgroup_size(8,8,1)
|
|
493
|
+
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
494
|
+
if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}
|
|
495
|
+
let bi=gid.y*params.blocks_x+gid.x;
|
|
496
|
+
let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);
|
|
497
|
+
var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);
|
|
498
|
+
for(var i:u32=0u;i<16u;i=i+1u){
|
|
499
|
+
let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx);
|
|
500
|
+
let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px);
|
|
501
|
+
}
|
|
502
|
+
var ep0=pick_ep(lo); var ep1=pick_ep(hi); var indices: array<u32,16>;
|
|
503
|
+
let r=proj_assign(&pix,ep0.eight,ep1.eight,&indices,true);
|
|
504
|
+
if(r.valid){ ep0=pick_ep(r.e0); ep1=pick_ep(r.e1); proj_assign(&pix,ep0.eight,ep1.eight,&indices,false); }
|
|
505
|
+
if((indices[0]&0x8u)!=0u){ let t=ep0; ep0=ep1; ep1=t; for(var k:u32=0u;k<16u;k=k+1u){indices[k]=15u-indices[k];} }
|
|
506
|
+
var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u; var pos:u32=0u;
|
|
507
|
+
write_bits(&block,pos,7u,0x40u);pos=pos+7u;
|
|
508
|
+
write_bits(&block,pos,7u,u32(ep0.seven.x));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.x));pos=pos+7u;
|
|
509
|
+
write_bits(&block,pos,7u,u32(ep0.seven.y));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.y));pos=pos+7u;
|
|
510
|
+
write_bits(&block,pos,7u,u32(ep0.seven.z));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.z));pos=pos+7u;
|
|
511
|
+
write_bits(&block,pos,7u,u32(ep0.seven.w));pos=pos+7u; write_bits(&block,pos,7u,u32(ep1.seven.w));pos=pos+7u;
|
|
512
|
+
write_bits(&block,pos,1u,ep0.p);pos=pos+1u; write_bits(&block,pos,1u,ep1.p);pos=pos+1u;
|
|
513
|
+
write_bits(&block,pos,3u,indices[0]&0x7u);pos=pos+3u;
|
|
514
|
+
for(var k:u32=1u;k<16u;k=k+1u){write_bits(&block,pos,4u,indices[k]&0xFu);pos=pos+4u;}
|
|
515
|
+
let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
336
518
|
|
|
337
519
|
// src/BC7Encoder.ts
|
|
338
520
|
var BC7Encoder = class extends Encoder {
|
|
@@ -347,9 +529,15 @@ var BC7Encoder = class extends Encoder {
|
|
|
347
529
|
get supportsSrgb() {
|
|
348
530
|
return true;
|
|
349
531
|
}
|
|
532
|
+
get supportsQuality() {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
350
535
|
wgslSource() {
|
|
351
536
|
return bc7_default;
|
|
352
537
|
}
|
|
538
|
+
wgslSourceFastF16() {
|
|
539
|
+
return bc7_fast_f16_default;
|
|
540
|
+
}
|
|
353
541
|
gpuTextureFormat({ colorSpace }) {
|
|
354
542
|
return colorSpace === "srgb" ? "bc7-rgba-unorm-srgb" : "bc7-rgba-unorm";
|
|
355
543
|
}
|
|
@@ -362,7 +550,10 @@ var BC7Encoder = class extends Encoder {
|
|
|
362
550
|
import { RGBA_ASTC_4x4_Format } from "three";
|
|
363
551
|
|
|
364
552
|
// src/astc4x4.wgsl
|
|
365
|
-
var astc4x4_default = "// ASTC 4\xD74 LDR compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// This shader mirrors `astc4x4_ref.ts` function-by-function; see that\n// file for the end-to-end algorithm rationale and the full block layout\n// / block-mode derivation. A short recap follows.\n//\n// RESTRICTED SUBSET (both CPU ref and this shader):\n// \u2022 Single partition, no dual-plane\n// \u2022 CEM 12 (LDR RGBA, direct)\n// \u2022 Weight grid 4\xD74 (no upsampling), 2-bit weights (QUANT_4)\n// \u2022 8-bit endpoints (QUANT_256 \u2014 bit-replication is a no-op)\n//\n// BLOCK LAYOUT (128 bits, LSB-first)\n// bits [10:0] block mode = 0x042\n// bits [12:11] partition count \u2212 1 = 0\n// bits [16:13] CEM = 12\n// bits [80:17] endpoints: R0 R1 G0 G1 B0 B1 A0 A1 (8-bit each)\n// bits [95:81] unused (zero padding)\n// bits [127:96] 16 \xD7 2-bit weights; for weight k \u2208 [0,15]:\n// block_bit(127 \u2212 2k) = weight_k[0] (LSB)\n// block_bit(126 \u2212 2k) = weight_k[1] (MSB)\n//\n// ENDPOINT ORDERING: after fitting, if sum(e0.rgb) > sum(e1.rgb) we swap\n// endpoints and reflect indices (w' = 3 \u2212 w). This keeps the decoder out\n// of the blue-contraction branch (see CPU ref file header for the full\n// decoder behaviour).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\n// QUANT_4 weight unquantisation: q \u2208 [0,3] \u2192 unq \u2208 [0, 21, 43, 64].\n// Switch keeps us off a module-scope const array (some backends reject\n// those inside function-call bodies).\nfn weight_unq(i: u32) -> u32 {\n switch i {\n case 0u: { return 0u; }\n case 1u: { return 21u; }\n case 2u: { return 43u; }\n default: { return 64u; } // case 3u\n }\n}\n\n// Hardware-exact integer interpolation. Identical to BC7's; matches the\n// CPU reference bit-for-bit.\nfn interp8(e0: u32, e1: u32, w: u32) -> u32 {\n return ((64u - w) * e0 + w * e1 + 32u) >> 6u;\n}\n\n// Normalised [0, 1] \u2192 clamped 8-bit. Same rounding rule (floor(v + 0.5))\n// as the CPU reference's Math.round.\nfn to8(v: f32) -> u32 {\n return u32(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// 4-channel L2 distance squared in u32 domain. Bounded by 4 \xB7 255\xB2 = 260,100.\nfn pixel_dist_sq(a: vec4<u32>, b: vec4<u32>) -> u32 {\n let d = vec4<i32>(a) - vec4<i32>(b);\n let d2 = d * d;\n return u32(d2.x + d2.y + d2.z + d2.w);\n}\n\n// -------------------------- Farthest-pair seed -------------------------- //\n\nstruct PairResult { i0: u32, i1: u32 };\n\n// O(N\xB2) = 120 comparisons. Same rationale as BC7's `farthest_pair`:\n// bounding-box corners aren't safe initial endpoints when channels vary\n// in different directions along the data line.\nfn farthest_pair(pixels: ptr<function, array<vec4<u32>, 16>>) -> PairResult {\n var best_d: u32 = 0u;\n var best_i: u32 = 0u;\n var best_j: u32 = 1u;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = pixel_dist_sq((*pixels)[i], (*pixels)[j]);\n if (d > best_d) { best_d = d; best_i = i; best_j = j; }\n }\n }\n return PairResult(best_i, best_j);\n}\n\n// -------------------------- Palette + assignment ------------------------ //\n\n// Build the 4-entry RGBA palette from 8-bit endpoints. Uses the same\n// integer interpolation formula as decode, so assignments made against\n// this palette match the hardware round-trip.\nfn build_palette(\n e0: vec4<u32>, e1: vec4<u32>,\n pal: ptr<function, array<vec4<u32>, 4>>,\n) {\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let w = weight_unq(i);\n (*pal)[i] = vec4<u32>(\n interp8(e0.x, e1.x, w),\n interp8(e0.y, e1.y, w),\n interp8(e0.z, e1.z, w),\n interp8(e0.w, e1.w, w),\n );\n }\n}\n\n// Nearest palette entry for a single RGBA pixel. Full 4-way L2 search.\n// Returns (best_index, its squared error).\nfn nearest_index(pixel: vec4<u32>, pal: ptr<function, array<vec4<u32>, 4>>) -> vec2<u32> {\n var best_i: u32 = 0u;\n var best_d: u32 = 0xFFFFFFFFu;\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let d = pixel_dist_sq(pixel, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n return vec2<u32>(best_i, best_d);\n}\n\n// Assign all 16 texels to nearest palette entries; accumulate squared error.\nstruct AssignResult { indices: array<u32, 16>, err: u32 };\n\nfn assign_all(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n pal: ptr<function, array<vec4<u32>, 4>>,\n) -> AssignResult {\n var out: AssignResult;\n out.err = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let sel = nearest_index((*pixels)[k], pal);\n out.indices[k] = sel.x;\n out.err = out.err + sel.y;\n }\n return out;\n}\n\n// ---------------------- Least-squares endpoint refit -------------------- //\n\n// Given current indices, solve the per-channel 2\xD72 normal equations for\n// (e0, e1). See the CPU reference's `refitEndpoints` for the derivation.\n// `valid = false` signals a degenerate system (all texels on one palette\n// entry) and the caller keeps the farthest-pair seed.\nstruct RefitResult { e0: vec4<u32>, e1: vec4<u32>, valid: bool };\n\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<u32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let unq = weight_unq((*indices)[k]);\n let a = f32(64u - unq) / 64.0;\n let b = f32(unq) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<u32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<u32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\n// Write `n_bits` LSBs of `value` at bit position `pos` of a 128-bit field\n// represented as `array<u32, 4>`. Handles word-boundary straddles.\n// Lifted from the BC7 shader verbatim; the layout contract is identical.\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry ---------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // 1. Load 16 RGBA texels in 8-bit integer domain.\n var pixels: array<vec4<u32>, 16>;\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 input sizes.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0);\n pixels[i] = vec4<u32>(to8(c.r), to8(c.g), to8(c.b), to8(c.a));\n }\n\n // 2. Farthest-pair seed \u2192 initial endpoints.\n let fp = farthest_pair(&pixels);\n var e0 = pixels[fp.i0];\n var e1 = pixels[fp.i1];\n\n // 3. Initial assignment against the seed endpoints.\n var pal: array<vec4<u32>, 4>;\n build_palette(e0, e1, &pal);\n var best = assign_all(&pixels, &pal);\n\n // 4. One LSQ refit pass. Accept only if the squared error strictly\n // decreases \u2014 matches the CPU reference.\n let refit = refit_endpoints(&pixels, &best.indices);\n if (refit.valid) {\n var pal2: array<vec4<u32>, 4>;\n build_palette(refit.e0, refit.e1, &pal2);\n let cand = assign_all(&pixels, &pal2);\n if (cand.err < best.err) {\n e0 = refit.e0;\n e1 = refit.e1;\n best = cand;\n }\n }\n\n // 5. Endpoint ordering so the decoder doesn't apply blue contraction.\n // Strict '>' avoids a gratuitous swap on ties.\n let s0 = e0.x + e0.y + e0.z;\n let s1 = e1.x + e1.y + e1.z;\n if (s0 > s1) {\n let tmp = e0; e0 = e1; e1 = tmp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n // w' = 3 \u2212 w reflects the palette; decoded colour unchanged.\n best.indices[k] = 3u - best.indices[k];\n }\n }\n\n // 6. Pack 128 bits.\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n\n // Config header.\n write_bits(&block, 0u, 11u, 0x042u); // block mode: 4\xD74 grid, QUANT_4 weights\n write_bits(&block, 11u, 2u, 0u); // partition count \u2212 1\n write_bits(&block, 13u, 4u, 12u); // CEM 12: LDR RGBA direct\n\n // Endpoints in the CEM 12 value order: R0 R1 G0 G1 B0 B1 A0 A1.\n write_bits(&block, 17u + 0u * 8u, 8u, e0.x);\n write_bits(&block, 17u + 1u * 8u, 8u, e1.x);\n write_bits(&block, 17u + 2u * 8u, 8u, e0.y);\n write_bits(&block, 17u + 3u * 8u, 8u, e1.y);\n write_bits(&block, 17u + 4u * 8u, 8u, e0.z);\n write_bits(&block, 17u + 5u * 8u, 8u, e1.z);\n write_bits(&block, 17u + 6u * 8u, 8u, e0.w);\n write_bits(&block, 17u + 7u * 8u, 8u, e1.w);\n\n // Weights at the top of the block. Two 1-bit writes per weight keeps\n // the LSB-at-127 convention visible at every call site; the cost over\n // a batched write is negligible next to the full encode.\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = best.indices[k] & 0x3u;\n write_bits(&block, 127u - 2u * k, 1u, w & 1u);\n write_bits(&block, 126u - 2u * k, 1u, (w >> 1u) & 1u);\n }\n\n // 7. Store as 4 u32s.\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
|
|
553
|
+
var astc4x4_default = "// ASTC 4\xD74 LDR compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`.\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): O(N) bounding-box seed \u2192 endpoints fitted by a single\n// least-squares pass whose sums are accumulated during a projection-based\n// weight assignment (the 4 palette entries are colinear, so the nearest is\n// found by projecting onto the endpoint line \u2014 no per-entry search).\n// Profiled ~4\xD7 faster than `high` for ~0.36 dB PSNR.\n// high (1): O(N\xB2) farthest-pair seed, full 4-entry nearest search, one LSQ\n// refit \u2014 byte-for-byte identical to astc4x4_ref.ts.\n// The fast branch is selected at pipeline-compile time; the driver eliminates\n// the unused (high) code.\n//\n// RESTRICTED SUBSET: single partition, no dual-plane, CEM 12 (LDR RGBA direct),\n// 4\xD74 weight grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256).\n//\n// BLOCK LAYOUT (128 bits, LSB-first)\n// bits [10:0] block mode = 0x042\n// bits [12:11] partition count \u2212 1 = 0\n// bits [16:13] CEM = 12\n// bits [80:17] endpoints: R0 R1 G0 G1 B0 B1 A0 A1 (8-bit each)\n// bits [127:96] 16 \xD7 2-bit weights; weight k: bit(127\u22122k)=lsb, bit(126\u22122k)=msb\n//\n// ENDPOINT ORDERING: if sum(e0.rgb) > sum(e1.rgb) swap endpoints and reflect\n// indices (w' = 3 \u2212 w) to keep the decoder out of blue contraction.\n\n// 0 = fast (default), 1 = exhaustive/high-quality. Set via pipeline constants.\noverride QUALITY_HIGH: u32 = 0u;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn weight_unq(i: u32) -> i32 {\n switch i {\n case 0u: { return 0; }\n case 1u: { return 21; }\n case 2u: { return 43; }\n default: { return 64; } // case 3u\n }\n}\n\nfn interp4(e0: vec4<i32>, e1: vec4<i32>, w: i32) -> vec4<i32> {\n return ((64 - w) * e0 + w * e1 + vec4<i32>(32)) >> vec4<u32>(6u);\n}\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// ============================ FAST PATH ================================ //\n\n// Projection weight assignment over 4 levels (QUANT_4 \u2248 thirds), with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_assign(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n e0: vec4<i32>, e1: vec4<i32>,\n out_idx: ptr<function, array<u32, 16>>,\n fit: bool,\n) -> Fit {\n var out: Fit;\n let dir = e1 - e0;\n let dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (var k: u32 = 0u; k < 16u; k = k + 1u) { (*out_idx)[k] = 0u; }\n out.valid = false;\n return out;\n }\n let inv = 3.0 / f32(dd);\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let q = (*pixels)[k] - e0;\n let s = clamp(floor(f32(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv + 0.5), 0.0, 3.0);\n (*out_idx)[k] = u32(s);\n if (fit) {\n let v = vec4<f32>((*pixels)[k]);\n let b = s / 3.0; let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b; sAV = sAV + a * v; sBV = sBV + b * v;\n }\n }\n if (!fit) { out.valid = false; return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { out.valid = false; return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// ============================ HIGH PATH ================================ //\n\nstruct Pair { a: vec4<i32>, b: vec4<i32> };\nfn farthest_pair(pixels: ptr<function, array<vec4<i32>, 16>>) -> Pair {\n var best_d: i32 = 0;\n var pa = (*pixels)[0];\n var pb = (*pixels)[1];\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let xi = (*pixels)[i];\n for (var j: u32 = i + 1u; j < 16u; j = j + 1u) {\n let d = dist2(xi, (*pixels)[j]);\n if (d > best_d) { best_d = d; pa = xi; pb = (*pixels)[j]; }\n }\n }\n return Pair(pa, pb);\n}\n\nfn build_palette(e0: vec4<i32>, e1: vec4<i32>, pal: ptr<function, array<vec4<i32>, 4>>) {\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n (*pal)[i] = interp4(e0, e1, weight_unq(i));\n }\n}\n\nfn assign_all(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n pal: ptr<function, array<vec4<i32>, 4>>,\n out_idx: ptr<function, array<u32, 16>>,\n) -> i32 {\n var err: i32 = 0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let px = (*pixels)[k];\n var best_i: u32 = 0u;\n var best_d: i32 = 2147483647;\n for (var i: u32 = 0u; i < 4u; i = i + 1u) {\n let d = dist2(px, (*pal)[i]);\n if (d < best_d) { best_d = d; best_i = i; }\n }\n (*out_idx)[k] = best_i;\n err = err + best_d;\n }\n return err;\n}\n\nstruct RefitResult { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn refit_endpoints(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0;\n var sBB: f32 = 0.0;\n var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0);\n var sBV: vec4<f32> = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let unq = weight_unq((*indices)[k]);\n let a = f32(64 - unq) / 64.0;\n let b = f32(unq) / 64.0;\n let v = vec4<f32>((*pixels)[k]);\n sAA = sAA + a * a;\n sBB = sBB + b * b;\n sAB = sAB + a * b;\n sAV = sAV + a * v;\n sBV = sBV + b * v;\n }\n let det = sAA * sBB - sAB * sAB;\n var out: RefitResult;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n let e0f = (sBB * sAV - sAB * sBV) / det;\n let e1f = (sAA * sBV - sAB * sAV) / det;\n out.e0 = vec4<i32>(clamp(round(e0f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round(e1f), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// -------------------------- Bit-packing helper -------------------------- //\n\nfn write_bits(block: ptr<function, array<u32, 4>>, pos: u32, n_bits: u32, value: u32) {\n let v = value & ((1u << n_bits) - 1u);\n let word_lo = pos / 32u;\n let bit_lo = pos % 32u;\n let bits_in_lo = min(n_bits, 32u - bit_lo);\n let mask_lo = ((1u << bits_in_lo) - 1u) << bit_lo;\n (*block)[word_lo] = ((*block)[word_lo] & ~mask_lo) | ((v << bit_lo) & mask_lo);\n if (bits_in_lo < n_bits) {\n let bits_in_hi = n_bits - bits_in_lo;\n let mask_hi = (1u << bits_in_hi) - 1u;\n let val_hi = v >> bits_in_lo;\n (*block)[word_lo + 1u] = ((*block)[word_lo + 1u] & ~mask_hi) | (val_hi & mask_hi);\n }\n}\n\n// ------------------------------- Entry ---------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n }\n\n var e0: vec4<i32>;\n var e1: vec4<i32>;\n var indices: array<u32, 16>;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n e0 = fp.a;\n e1 = fp.b;\n var pal: array<vec4<i32>, 4>;\n build_palette(e0, e1, &pal);\n var err = assign_all(&pixels, &pal, &indices);\n let refit = refit_endpoints(&pixels, &indices);\n if (refit.valid) {\n build_palette(refit.e0, refit.e1, &pal);\n var idx2: array<u32, 16>;\n let err2 = assign_all(&pixels, &pal, &idx2);\n if (err2 < err) {\n e0 = refit.e0;\n e1 = refit.e1;\n indices = idx2;\n err = err2;\n }\n }\n } else {\n e0 = lo;\n e1 = hi;\n let r = proj_assign(&pixels, e0, e1, &indices, true);\n if (r.valid) {\n e0 = r.e0;\n e1 = r.e1;\n proj_assign(&pixels, e0, e1, &indices, false);\n }\n }\n\n // Endpoint ordering so the decoder doesn't apply blue contraction.\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n let tmp = e0; e0 = e1; e1 = tmp;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices[k] = 3u - indices[k];\n }\n }\n\n var block: array<u32, 4>;\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n write_bits(&block, 0u, 11u, 0x042u);\n write_bits(&block, 11u, 2u, 0u);\n write_bits(&block, 13u, 4u, 12u);\n write_bits(&block, 17u + 0u * 8u, 8u, u32(e0.x));\n write_bits(&block, 17u + 1u * 8u, 8u, u32(e1.x));\n write_bits(&block, 17u + 2u * 8u, 8u, u32(e0.y));\n write_bits(&block, 17u + 3u * 8u, 8u, u32(e1.y));\n write_bits(&block, 17u + 4u * 8u, 8u, u32(e0.z));\n write_bits(&block, 17u + 5u * 8u, 8u, u32(e1.z));\n write_bits(&block, 17u + 6u * 8u, 8u, u32(e0.w));\n write_bits(&block, 17u + 7u * 8u, 8u, u32(e1.w));\n var w3: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = indices[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * k)) | (((w >> 1u) & 1u) << (30u - 2u * k));\n }\n block[3] = w3;\n\n let out = block_index * 4u;\n dst[out + 0u] = block[0];\n dst[out + 1u] = block[1];\n dst[out + 2u] = block[2];\n dst[out + 3u] = block[3];\n}\n";
|
|
554
|
+
|
|
555
|
+
// src/astc4x4_fast_f16.wgsl
|
|
556
|
+
var astc4x4_fast_f16_default = '// astc4x4 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Identical algorithm to the f32 fast path in astc4x4.wgsl, but the projection +\n// least-squares refit run in f16 ([0,1] domain). On GPUs with 2x f16 throughput\n// (e.g. Apple) this is ~2x faster at the same quality; endpoints are still\n// quantised to exact 8-bit. The host selects this module only when the device\n// reports shader-f16, falling back to astc4x4.wgsl otherwise. "high" never uses this.\n//\nenable f16;\nalias h = f16; alias h4 = vec4<f16>;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\nstruct Fit { e0: h4, e1: h4, valid: bool };\nfn proj(pix: ptr<function, array<h4,16>>, e0: h4, e1: h4, out_idx: ptr<function, array<u32,16>>, fit: bool) -> Fit {\n var out: Fit; let dir=e1-e0; let dd=dot(dir,dir);\n if(dd==h(0.0)){ for(var k:u32=0u;k<16u;k=k+1u){(*out_idx)[k]=0u;} out.valid=false; return out; }\n let inv=h(3.0)/dd;\n var sAA=h(0.0); var sBB=h(0.0); var sAB=h(0.0); var sAV=h4(0.0); var sBV=h4(0.0);\n for(var k:u32=0u;k<16u;k=k+1u){ let v=(*pix)[k]; let s=clamp(floor(dot(v-e0,dir)*inv+h(0.5)),h(0.0),h(3.0)); (*out_idx)[k]=u32(s);\n if(fit){ let b=s*h(1.0/3.0); let a=h(1.0)-b; sAA=sAA+a*a; sBB=sBB+b*b; sAB=sAB+a*b; sAV=sAV+a*v; sBV=sBV+b*v; } }\n if(!fit){ out.valid=false; return out; }\n let det=sAA*sBB-sAB*sAB; if(abs(det)<h(0.0001)){out.valid=false;return out;}\n out.e0=clamp((sBB*sAV-sAB*sBV)/det,h4(0.0),h4(1.0)); out.e1=clamp((sAA*sBV-sAB*sAV)/det,h4(0.0),h4(1.0)); out.valid=true; return out;\n}\nfn write_bits(block: ptr<function, array<u32,4>>, pos: u32, n_bits: u32, value: u32) {\n let v=value&((1u<<n_bits)-1u); let wl=pos/32u; let bl=pos%32u; let il=min(n_bits,32u-bl);\n let ml=((1u<<il)-1u)<<bl; (*block)[wl]=((*block)[wl]&~ml)|((v<<bl)&ml);\n if(il<n_bits){ let ih=n_bits-il; let mh=(1u<<ih)-1u; (*block)[wl+1u]=((*block)[wl+1u]&~mh)|((v>>il)&mh); }\n}\nfn q8(e: h4) -> vec4<i32> { return vec4<i32>(clamp(floor(e*h(255.0)+h(0.5)), h4(0.0), h4(255.0))); }\n@compute @workgroup_size(8,8,1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if(gid.x>=params.blocks_x||gid.y>=params.blocks_y){return;}\n let bi=gid.y*params.blocks_x+gid.x;\n let base=vec2<i32>(i32(gid.x)*4,i32(gid.y)*4); let mx=vec2<i32>(i32(params.width)-1,i32(params.height)-1);\n var pix: array<h4,16>; var lo=h4(1.0); var hi=h4(0.0);\n for(var i:u32=0u;i<16u;i=i+1u){ let p=clamp(base+vec2<i32>(i32(i&3u),i32(i>>2u)),vec2<i32>(0),mx); let px=h4(textureLoad(src_tex,p,0)); pix[i]=px; lo=min(lo,px); hi=max(hi,px); }\n var e0=lo; var e1=hi; var indices: array<u32,16>;\n let r=proj(&pix,e0,e1,&indices,true);\n if(r.valid){ e0=r.e0; e1=r.e1; proj(&pix,e0,e1,&indices,false); }\n var E0=q8(e0); var E1=q8(e1);\n if(E0.x+E0.y+E0.z > E1.x+E1.y+E1.z){ let t=E0; E0=E1; E1=t; for(var k:u32=0u;k<16u;k=k+1u){indices[k]=3u-indices[k];} }\n var block: array<u32,4>; block[0]=0u;block[1]=0u;block[2]=0u;block[3]=0u;\n write_bits(&block,0u,11u,0x042u); write_bits(&block,11u,2u,0u); write_bits(&block,13u,4u,12u);\n write_bits(&block,17u,8u,u32(E0.x)); write_bits(&block,25u,8u,u32(E1.x));\n write_bits(&block,33u,8u,u32(E0.y)); write_bits(&block,41u,8u,u32(E1.y));\n write_bits(&block,49u,8u,u32(E0.z)); write_bits(&block,57u,8u,u32(E1.z));\n write_bits(&block,65u,8u,u32(E0.w)); write_bits(&block,73u,8u,u32(E1.w));\n var w3:u32=0u; for(var k:u32=0u;k<16u;k=k+1u){ let w=indices[k]&3u; w3=w3|((w&1u)<<(31u-2u*k))|(((w>>1u)&1u)<<(30u-2u*k)); } block[3]=w3;\n let o=bi*4u; dst[o]=block[0];dst[o+1u]=block[1];dst[o+2u]=block[2];dst[o+3u]=block[3];\n}\n';
|
|
366
557
|
|
|
367
558
|
// src/ASTC4x4Encoder.ts
|
|
368
559
|
var ASTC4x4Encoder = class extends Encoder {
|
|
@@ -377,9 +568,15 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
377
568
|
get supportsSrgb() {
|
|
378
569
|
return true;
|
|
379
570
|
}
|
|
571
|
+
get supportsQuality() {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
380
574
|
wgslSource() {
|
|
381
575
|
return astc4x4_default;
|
|
382
576
|
}
|
|
577
|
+
wgslSourceFastF16() {
|
|
578
|
+
return astc4x4_fast_f16_default;
|
|
579
|
+
}
|
|
383
580
|
gpuTextureFormat({ colorSpace }) {
|
|
384
581
|
return colorSpace === "srgb" ? "astc-4x4-unorm-srgb" : "astc-4x4-unorm";
|
|
385
582
|
}
|
|
@@ -540,6 +737,7 @@ async function compressTexture(source, options = {}) {
|
|
|
540
737
|
colorSpace = "srgb",
|
|
541
738
|
flipY = true,
|
|
542
739
|
mipmaps = false,
|
|
740
|
+
quality = "fast",
|
|
543
741
|
device: providedDevice,
|
|
544
742
|
adapter: providedAdapter
|
|
545
743
|
} = options;
|
|
@@ -608,8 +806,16 @@ async function compressTexture(source, options = {}) {
|
|
|
608
806
|
encoder = await selection.encoderClass.create();
|
|
609
807
|
}
|
|
610
808
|
try {
|
|
809
|
+
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
611
810
|
if (!mipmaps) {
|
|
612
|
-
|
|
811
|
+
let bytes;
|
|
812
|
+
if (needsWriteTexture) {
|
|
813
|
+
const level02 = bitmapToMipLevel(bitmap, flipY);
|
|
814
|
+
const imageData = mipLevelToImageData(level02);
|
|
815
|
+
bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
816
|
+
} else {
|
|
817
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY, quality });
|
|
818
|
+
}
|
|
613
819
|
const tex2 = encoder.buildMippedTexture([bytes], { colorSpace });
|
|
614
820
|
return {
|
|
615
821
|
texture: tex2,
|
|
@@ -633,7 +839,7 @@ async function compressTexture(source, options = {}) {
|
|
|
633
839
|
for (const level of chain) {
|
|
634
840
|
const padded = padToBlockMultiple(level);
|
|
635
841
|
const imageData = mipLevelToImageData(padded);
|
|
636
|
-
const bytes = await encoder.encodeToBytes(imageData);
|
|
842
|
+
const bytes = await encoder.encodeToBytes(imageData, { quality });
|
|
637
843
|
encodedLevels.push(bytes);
|
|
638
844
|
totalEncodeMs += bytes.encodeMs;
|
|
639
845
|
}
|
|
@@ -669,6 +875,8 @@ var GputexLoader = class extends Loader {
|
|
|
669
875
|
flipY = true;
|
|
670
876
|
/** Generate + encode a full mip chain. Default false. */
|
|
671
877
|
mipmaps = false;
|
|
878
|
+
/** Encode quality / speed trade-off. Default 'fast' (~2–4× faster, ≤0.36 dB). */
|
|
879
|
+
quality = "fast";
|
|
672
880
|
/**
|
|
673
881
|
* Optional pre-existing WebGPU device. Reusing the renderer's device
|
|
674
882
|
* avoids spinning up a second WebGPU context for encoding.
|
|
@@ -695,6 +903,7 @@ var GputexLoader = class extends Loader {
|
|
|
695
903
|
colorSpace: this.colorSpace,
|
|
696
904
|
flipY: this.flipY,
|
|
697
905
|
mipmaps: this.mipmaps,
|
|
906
|
+
quality: this.quality,
|
|
698
907
|
device: this.device,
|
|
699
908
|
adapter: this.adapter
|
|
700
909
|
}).then(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gputex",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@webgpu/types": "^0.1.69",
|
|
24
24
|
"three": "^0.183.2",
|
|
25
25
|
"tsup": "8.5.1",
|
|
26
|
-
"typescript": "
|
|
26
|
+
"typescript": "6.0.3"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"three": ">=0.170"
|