gputex 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -93
- package/dist/index.d.ts +198 -67
- package/dist/index.js +1406 -672
- package/dist/testing.d.ts +52 -15
- package/dist/testing.js +935 -109
- package/dist/three.d.ts +80 -21
- package/dist/three.js +1740 -719
- package/package.json +1 -1
package/dist/three.js
CHANGED
|
@@ -6,7 +6,9 @@ var TextureFormat = {
|
|
|
6
6
|
BC7: "BC7",
|
|
7
7
|
BC7_SRGB: "BC7_SRGB",
|
|
8
8
|
ASTC_4x4: "ASTC_4x4",
|
|
9
|
-
ASTC_4x4_SRGB: "ASTC_4x4_SRGB"
|
|
9
|
+
ASTC_4x4_SRGB: "ASTC_4x4_SRGB",
|
|
10
|
+
ETC2_RGB8: "ETC2_RGB8",
|
|
11
|
+
ETC2_RGB8_SRGB: "ETC2_RGB8_SRGB"
|
|
10
12
|
};
|
|
11
13
|
var WebGPUFeature = {
|
|
12
14
|
BC: "texture-compression-bc",
|
|
@@ -24,8 +26,7 @@ var FORMATS_BY_FEATURE = {
|
|
|
24
26
|
TextureFormat.BC7_SRGB
|
|
25
27
|
],
|
|
26
28
|
[WebGPUFeature.ASTC]: [TextureFormat.ASTC_4x4, TextureFormat.ASTC_4x4_SRGB],
|
|
27
|
-
|
|
28
|
-
[WebGPUFeature.ETC2]: []
|
|
29
|
+
[WebGPUFeature.ETC2]: [TextureFormat.ETC2_RGB8, TextureFormat.ETC2_RGB8_SRGB]
|
|
29
30
|
};
|
|
30
31
|
function detectCapabilities(adapter) {
|
|
31
32
|
if (!adapter || !adapter.features || typeof adapter.features.has !== "function") {
|
|
@@ -38,6 +39,7 @@ function detectCapabilities(adapter) {
|
|
|
38
39
|
const supportedFormats = [];
|
|
39
40
|
if (bc) supportedFormats.push(...FORMATS_BY_FEATURE[WebGPUFeature.BC]);
|
|
40
41
|
if (astc) supportedFormats.push(...FORMATS_BY_FEATURE[WebGPUFeature.ASTC]);
|
|
42
|
+
if (etc2) supportedFormats.push(...FORMATS_BY_FEATURE[WebGPUFeature.ETC2]);
|
|
41
43
|
return { bc, astc, etc2, supportedFormats };
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -46,8 +48,18 @@ function needsWriteTextureWorkaround(adapter) {
|
|
|
46
48
|
const { vendor, architecture } = adapter.info ?? {};
|
|
47
49
|
return vendor === "img-tec" && architecture === "d-series";
|
|
48
50
|
}
|
|
49
|
-
function uploadSourceTexture(device, srcTex, source, width, height, flipY
|
|
50
|
-
if (
|
|
51
|
+
function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
|
|
52
|
+
if (source instanceof ImageData && !flipY) {
|
|
53
|
+
if (srcTex.format === "rg8unorm") {
|
|
54
|
+
const rgba = source.data;
|
|
55
|
+
const rg = new Uint8Array(width * height * 2);
|
|
56
|
+
for (let i = 0; i < width * height; i++) {
|
|
57
|
+
rg[i * 2] = rgba[i * 4];
|
|
58
|
+
rg[i * 2 + 1] = rgba[i * 4 + 1];
|
|
59
|
+
}
|
|
60
|
+
device.queue.writeTexture({ texture: srcTex }, rg, { bytesPerRow: width * 2 }, [width, height, 1]);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
51
63
|
device.queue.writeTexture({ texture: srcTex }, source.data, { bytesPerRow: width * 4 }, [width, height, 1]);
|
|
52
64
|
} else {
|
|
53
65
|
device.queue.copyExternalImageToTexture({ source, flipY }, { texture: srcTex }, [
|
|
@@ -59,6 +71,7 @@ function uploadSourceTexture(device, srcTex, source, width, height, flipY, useWr
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
// src/Encoder.ts
|
|
74
|
+
var CHAIN_ALIGN = 256;
|
|
62
75
|
var Encoder = class {
|
|
63
76
|
/**
|
|
64
77
|
* Subclasses set this to the WebGPU feature string the output texture
|
|
@@ -99,20 +112,16 @@ var Encoder = class {
|
|
|
99
112
|
adapter;
|
|
100
113
|
ownsDevice;
|
|
101
114
|
disableF16;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Default pipeline (fast). Kept as a field for back-compat; the per-quality
|
|
113
|
-
// cache below holds the specialised pipelines for encoders that support it.
|
|
114
|
-
_pipeline;
|
|
115
|
-
_pipelineCache = /* @__PURE__ */ new Map();
|
|
115
|
+
// The single compute pipeline: built from the f16 module when the device
|
|
116
|
+
// supports shader-f16 and the subclass provides an f16 source, from the
|
|
117
|
+
// f32 module otherwise. Both implement the same algorithm. Created with
|
|
118
|
+
// `createComputePipelineAsync` so shader compilation overlaps whatever
|
|
119
|
+
// follows construction (image decode, first upload) instead of stalling
|
|
120
|
+
// the first dispatch; encodes await readiness.
|
|
121
|
+
_pipelineReady;
|
|
122
|
+
// Source-preparation pipeline, when the subclass declares one (ETC2's
|
|
123
|
+
// packed-luma + quadrant-average split). Null for direct-source encoders.
|
|
124
|
+
_prepPipelineReady = null;
|
|
116
125
|
// -------------------------------------------------------------------- //
|
|
117
126
|
// Per-encoder GPU resource cache. Creating the source texture, output/
|
|
118
127
|
// staging buffers and bind group on every encode costs ~1ms of host time
|
|
@@ -122,17 +131,45 @@ var Encoder = class {
|
|
|
122
131
|
// these; concurrent encodes on the same encoder see `_resourcesBusy` and
|
|
123
132
|
// fall back to transient resources, keeping the API contract unchanged.
|
|
124
133
|
// Buffers are grow-only, the texture is recreated on size change, and the
|
|
125
|
-
// bind group is
|
|
126
|
-
// has its own layout) until any bound resource is recreated.
|
|
134
|
+
// bind group is kept until any bound resource is recreated.
|
|
127
135
|
_cachedSrcTex = null;
|
|
128
136
|
_cachedSrcW = 0;
|
|
129
137
|
_cachedSrcH = 0;
|
|
138
|
+
// Upload memoisation: the ImageBitmap whose pixels the cached source
|
|
139
|
+
// texture currently holds. ImageBitmaps are immutable, so encoding the
|
|
140
|
+
// same bitmap again (benchmark loops, quality-ladder re-encodes, format
|
|
141
|
+
// A/B) can skip the copyExternalImageToTexture entirely — at 4096² that
|
|
142
|
+
// upload is ~9 ms, dominating the whole encode. Mutable sources
|
|
143
|
+
// (ImageData, canvases, video) are never memoised.
|
|
144
|
+
_cachedSrcSource = null;
|
|
145
|
+
_cachedSrcFlipY = false;
|
|
130
146
|
_cachedDst = null;
|
|
131
147
|
_cachedStaging = null;
|
|
132
148
|
_cachedParams = null;
|
|
133
149
|
_lastParams = null;
|
|
134
|
-
|
|
150
|
+
_cachedBindGroup = null;
|
|
151
|
+
_cachedPrepPlanes = null;
|
|
152
|
+
_cachedPrepBindGroup = null;
|
|
135
153
|
_resourcesBusy = false;
|
|
154
|
+
/** Set when the active shader declares a @binding(3) sampler (the BC5
|
|
155
|
+
* kernels read texels through textureGather + clamp-to-edge). */
|
|
156
|
+
_usesSampler = false;
|
|
157
|
+
_sampler = null;
|
|
158
|
+
// Mip-chain cache — the `encodeMipChainToBytes()` counterpart of the
|
|
159
|
+
// single-shot cache above: per-level source textures + bind groups keyed
|
|
160
|
+
// on the exact level-size signature, one params buffer holding every
|
|
161
|
+
// level's uniforms at 256-byte offsets, grow-only output/staging buffers.
|
|
162
|
+
// Pays off when consecutive chains share dimensions (bulk-loading
|
|
163
|
+
// same-sized textures through `compressTexture()`).
|
|
164
|
+
_chainSig = null;
|
|
165
|
+
_chainTextures = [];
|
|
166
|
+
_chainPrepPlanes = [];
|
|
167
|
+
_chainPrepBindGroups = [];
|
|
168
|
+
_chainParams = null;
|
|
169
|
+
_chainBindGroups = [];
|
|
170
|
+
_chainDst = null;
|
|
171
|
+
_chainStaging = null;
|
|
172
|
+
_chainBusy = false;
|
|
136
173
|
constructor({ device, adapter, ownsDevice = false, disableF16 = false }) {
|
|
137
174
|
this.device = device;
|
|
138
175
|
this.adapter = adapter;
|
|
@@ -142,77 +179,71 @@ var Encoder = class {
|
|
|
142
179
|
}
|
|
143
180
|
_buildPipeline() {
|
|
144
181
|
const device = this.device;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
const useF16 = this._useF16;
|
|
183
|
+
const code = useF16 ? this.wgslSourceFastF16() : this.wgslSource();
|
|
184
|
+
this._usesSampler = /@binding\(3\)\s+var\s+\w+\s*:\s*sampler\s*;/.test(code);
|
|
185
|
+
const module = device.createShaderModule({
|
|
186
|
+
label: `${this.label}-encoder${useF16 ? "-f16" : ""}`,
|
|
187
|
+
code
|
|
188
|
+
});
|
|
189
|
+
const constants = this.pipelineConstants();
|
|
190
|
+
this._pipelineReady = device.createComputePipelineAsync({
|
|
191
|
+
label: `${this.label}-encoder-pipeline${useF16 ? "-f16" : ""}`,
|
|
192
|
+
layout: "auto",
|
|
193
|
+
compute: { module, entryPoint: "encode", ...constants ? { constants } : {} }
|
|
194
|
+
});
|
|
195
|
+
const prepCode = this.wgslPrepSource();
|
|
196
|
+
if (prepCode) {
|
|
197
|
+
const prepModule = device.createShaderModule({ label: `${this.label}-prep`, code: prepCode });
|
|
198
|
+
this._prepPipelineReady = device.createComputePipelineAsync({
|
|
199
|
+
label: `${this.label}-prep-pipeline`,
|
|
158
200
|
layout: "auto",
|
|
159
|
-
compute: { module:
|
|
201
|
+
compute: { module: prepModule, entryPoint: "encode" }
|
|
160
202
|
});
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
/** The f32 module, parsed on first use (see `_module`). */
|
|
164
|
-
_ensureModule() {
|
|
165
|
-
if (!this._module) {
|
|
166
|
-
this._module = this.device.createShaderModule({
|
|
167
|
-
label: `${this.label}-encoder`,
|
|
168
|
-
code: this.wgslSource()
|
|
203
|
+
this._prepPipelineReady.catch(() => {
|
|
169
204
|
});
|
|
170
205
|
}
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Pipeline for a given quality level. Encoders that don't declare a
|
|
175
|
-
* `QUALITY_HIGH` override (`supportsQuality === false`, e.g. BC1) ignore the
|
|
176
|
-
* argument and reuse the single pipeline. Specialised pipelines are cached.
|
|
177
|
-
*/
|
|
178
|
-
_getPipeline(quality) {
|
|
179
|
-
if (!this.supportsQuality) return this._pipeline;
|
|
180
|
-
if (quality === "fast" && this._moduleF16) {
|
|
181
|
-
if (!this._pipelineF16) {
|
|
182
|
-
this._pipelineF16 = this.device.createComputePipeline({
|
|
183
|
-
label: `${this.label}-encoder-pipeline-fast-f16`,
|
|
184
|
-
layout: "auto",
|
|
185
|
-
compute: { module: this._moduleF16, entryPoint: "encode" }
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return this._pipelineF16;
|
|
189
|
-
}
|
|
190
|
-
const cached = this._pipelineCache.get(quality);
|
|
191
|
-
if (cached) return cached;
|
|
192
|
-
const pipeline = this.device.createComputePipeline({
|
|
193
|
-
label: `${this.label}-encoder-pipeline-${quality}`,
|
|
194
|
-
layout: "auto",
|
|
195
|
-
compute: {
|
|
196
|
-
module: this._ensureModule(),
|
|
197
|
-
entryPoint: "encode",
|
|
198
|
-
constants: { QUALITY_HIGH: quality === "high" ? 1 : 0 }
|
|
199
|
-
}
|
|
206
|
+
this._pipelineReady.catch(() => {
|
|
200
207
|
});
|
|
201
|
-
this._pipelineCache.set(quality, pipeline);
|
|
202
|
-
return pipeline;
|
|
203
208
|
}
|
|
204
209
|
destroy() {
|
|
205
210
|
this._cachedSrcTex?.destroy();
|
|
211
|
+
if (this._cachedPrepPlanes) for (const t of this._cachedPrepPlanes) t.destroy();
|
|
212
|
+
this._cachedPrepPlanes = null;
|
|
213
|
+
this._cachedPrepBindGroup = null;
|
|
206
214
|
this._cachedDst?.destroy();
|
|
207
215
|
this._cachedStaging?.destroy();
|
|
208
216
|
this._cachedParams?.destroy();
|
|
209
217
|
this._cachedSrcTex = null;
|
|
218
|
+
this._cachedSrcSource = null;
|
|
210
219
|
this._cachedDst = null;
|
|
211
220
|
this._cachedStaging = null;
|
|
212
221
|
this._cachedParams = null;
|
|
213
|
-
this.
|
|
222
|
+
this._cachedBindGroup = null;
|
|
223
|
+
for (const tex of this._chainTextures) tex.destroy();
|
|
224
|
+
this._chainTextures = [];
|
|
225
|
+
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
226
|
+
this._chainPrepPlanes = [];
|
|
227
|
+
this._chainPrepBindGroups = [];
|
|
228
|
+
this._chainParams?.destroy();
|
|
229
|
+
this._chainDst?.destroy();
|
|
230
|
+
this._chainStaging?.destroy();
|
|
231
|
+
this._chainParams = null;
|
|
232
|
+
this._chainDst = null;
|
|
233
|
+
this._chainStaging = null;
|
|
234
|
+
this._chainBindGroups = [];
|
|
235
|
+
this._chainSig = null;
|
|
214
236
|
if (this.ownsDevice) this.device.destroy();
|
|
215
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Pipeline-creation override constants for the active shader, or
|
|
240
|
+
* undefined for none. Subclasses expose quality/behaviour toggles this
|
|
241
|
+
* way so the OFF state is dead-coded by the shader compiler instead of
|
|
242
|
+
* branched at runtime.
|
|
243
|
+
*/
|
|
244
|
+
pipelineConstants() {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
216
247
|
/** WGSL `@workgroup_size` dimensions. Default 8×8×1. */
|
|
217
248
|
get workgroupSize() {
|
|
218
249
|
return [8, 8, 1];
|
|
@@ -222,25 +253,76 @@ var Encoder = class {
|
|
|
222
253
|
return true;
|
|
223
254
|
}
|
|
224
255
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
256
|
+
* Format of the single-shot source texture the encode pass samples.
|
|
257
|
+
* Encoders that read only a channel subset can narrow it to cut DRAM
|
|
258
|
+
* traffic on the bandwidth-bound compute pass (BC5 reads rg8unorm — half
|
|
259
|
+
* the bytes of rgba8). Must be valid as a copyExternalImageToTexture
|
|
260
|
+
* destination and renderable. Chain encodes keep rgba8unorm regardless
|
|
261
|
+
* (their inputs are RGBA mip levels / texture views); the WGSL is
|
|
262
|
+
* format-agnostic (`texture_2d<f32>`), so mixing is byte-identical.
|
|
228
263
|
*/
|
|
229
|
-
get
|
|
230
|
-
return
|
|
264
|
+
get srcTextureFormat() {
|
|
265
|
+
return "rgba8unorm";
|
|
231
266
|
}
|
|
232
267
|
/**
|
|
233
|
-
* Optional f16 WGSL
|
|
234
|
-
* `shader-f16` feature; the format's f32 `wgslSource()` is the
|
|
235
|
-
*
|
|
268
|
+
* Optional f16 WGSL variant. Used only when the device reports the
|
|
269
|
+
* `shader-f16` feature; the format's f32 `wgslSource()` is the automatic
|
|
270
|
+
* fallback. Returns null when there's no f16 variant.
|
|
236
271
|
*/
|
|
237
272
|
wgslSourceFastF16() {
|
|
238
273
|
return null;
|
|
239
274
|
}
|
|
240
|
-
/** Whether the f16
|
|
275
|
+
/** Whether the f16 shader is both available and supported on this device. */
|
|
241
276
|
get _useF16() {
|
|
242
277
|
return !this.disableF16 && this.wgslSourceFastF16() !== null && this.device.features.has("shader-f16");
|
|
243
278
|
}
|
|
279
|
+
// ------------------------------------------------------------------ //
|
|
280
|
+
// Optional source-preparation pass (null/empty for direct encoders). //
|
|
281
|
+
// A subclass returning a prep shader gets a two-pass encode: the prep
|
|
282
|
+
// pass reads the RGBA8 source (binding 0) and writes the prepared
|
|
283
|
+
// planes as storage textures (plane 0 at binding 1, plane 1 at
|
|
284
|
+
// binding 3, params at binding 2); the encode pass then reads plane 0
|
|
285
|
+
// at binding 0 and plane 1 at binding 3 instead of the source.
|
|
286
|
+
// ------------------------------------------------------------------ //
|
|
287
|
+
/** WGSL for the preparation pass, or null when the encoder reads the
|
|
288
|
+
* RGBA8 source directly. */
|
|
289
|
+
wgslPrepSource() {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
/** Formats and sizes of the prepared planes for a padded source size. */
|
|
293
|
+
prepPlanes(paddedWidth, paddedHeight) {
|
|
294
|
+
void paddedWidth;
|
|
295
|
+
void paddedHeight;
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
/** Workgroup counts for the prep dispatch. */
|
|
299
|
+
prepDispatch(blocksX, blocksY) {
|
|
300
|
+
return [blocksX, blocksY];
|
|
301
|
+
}
|
|
302
|
+
/** Create the prepared-plane textures for one padded source size. */
|
|
303
|
+
_createPrepPlanes(paddedWidth, paddedHeight) {
|
|
304
|
+
return this.prepPlanes(paddedWidth, paddedHeight).map(
|
|
305
|
+
(p, i) => this.device.createTexture({
|
|
306
|
+
label: `${this.label}-prep-${i}`,
|
|
307
|
+
size: [p.width, p.height, 1],
|
|
308
|
+
format: p.format,
|
|
309
|
+
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
/** Bind group for one prep dispatch. */
|
|
314
|
+
_createPrepBindGroup(prepPipeline, srcView, planes, params, paramsOffset = 0) {
|
|
315
|
+
return this.device.createBindGroup({
|
|
316
|
+
label: `${this.label}-prep-bg`,
|
|
317
|
+
layout: prepPipeline.getBindGroupLayout(0),
|
|
318
|
+
entries: [
|
|
319
|
+
{ binding: 0, resource: srcView },
|
|
320
|
+
{ binding: 1, resource: planes[0].createView() },
|
|
321
|
+
{ binding: 2, resource: { buffer: params, offset: paramsOffset, size: 16 } },
|
|
322
|
+
{ binding: 3, resource: planes[1].createView() }
|
|
323
|
+
]
|
|
324
|
+
});
|
|
325
|
+
}
|
|
244
326
|
/**
|
|
245
327
|
* True if the device reports the feature the output texture needs.
|
|
246
328
|
* The encoder itself only writes to a storage buffer, so this is about
|
|
@@ -260,11 +342,7 @@ var Encoder = class {
|
|
|
260
342
|
* bytes into a `CompressedTexture`; callers targeting another engine feed
|
|
261
343
|
* `data` into that engine's compressed-texture upload directly.
|
|
262
344
|
*/
|
|
263
|
-
async encodeToBytes(source, {
|
|
264
|
-
flipY = false,
|
|
265
|
-
quality = "fast",
|
|
266
|
-
withGpuTime = false
|
|
267
|
-
} = {}) {
|
|
345
|
+
async encodeToBytes(source, { flipY = false, withGpuTime = false } = {}) {
|
|
268
346
|
const device = this.device;
|
|
269
347
|
const width = source.width;
|
|
270
348
|
const height = source.height;
|
|
@@ -277,12 +355,15 @@ var Encoder = class {
|
|
|
277
355
|
const blocksY = paddedHeight >> 2;
|
|
278
356
|
const blockCount = blocksX * blocksY;
|
|
279
357
|
const outByteLen = blockCount * this.bytesPerBlock;
|
|
358
|
+
const pipeline = await this._pipelineReady;
|
|
359
|
+
const prepPipeline = this._prepPipelineReady ? await this._prepPipelineReady : null;
|
|
280
360
|
const useCache = !this._resourcesBusy;
|
|
281
361
|
if (useCache) this._resourcesBusy = true;
|
|
282
362
|
let srcTex;
|
|
283
363
|
let dstBuffer;
|
|
284
364
|
let paramsBuffer;
|
|
285
365
|
let staging;
|
|
366
|
+
let transientPrepPlanes = null;
|
|
286
367
|
try {
|
|
287
368
|
let srcTexIsNew = true;
|
|
288
369
|
if (useCache && this._cachedSrcTex && this._cachedSrcW === paddedWidth && this._cachedSrcH === paddedHeight) {
|
|
@@ -292,7 +373,7 @@ var Encoder = class {
|
|
|
292
373
|
srcTex = device.createTexture({
|
|
293
374
|
label: `${this.label}-src`,
|
|
294
375
|
size: [paddedWidth, paddedHeight, 1],
|
|
295
|
-
format:
|
|
376
|
+
format: this.srcTextureFormat,
|
|
296
377
|
// RENDER_ATTACHMENT is required by copyExternalImageToTexture
|
|
297
378
|
// (internally a blit) even though we never render into this texture.
|
|
298
379
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
@@ -304,7 +385,31 @@ var Encoder = class {
|
|
|
304
385
|
this._cachedSrcH = paddedHeight;
|
|
305
386
|
}
|
|
306
387
|
}
|
|
307
|
-
|
|
388
|
+
let prepPlanes = null;
|
|
389
|
+
let prepPlanesNew = false;
|
|
390
|
+
if (prepPipeline) {
|
|
391
|
+
if (useCache && !srcTexIsNew && this._cachedPrepPlanes) {
|
|
392
|
+
prepPlanes = this._cachedPrepPlanes;
|
|
393
|
+
} else {
|
|
394
|
+
prepPlanes = this._createPrepPlanes(paddedWidth, paddedHeight);
|
|
395
|
+
prepPlanesNew = true;
|
|
396
|
+
if (useCache) {
|
|
397
|
+
if (this._cachedPrepPlanes) for (const t of this._cachedPrepPlanes) t.destroy();
|
|
398
|
+
this._cachedPrepPlanes = prepPlanes;
|
|
399
|
+
this._cachedPrepBindGroup = null;
|
|
400
|
+
} else {
|
|
401
|
+
transientPrepPlanes = prepPlanes;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const uploadSkippable = !srcTexIsNew && source instanceof ImageBitmap && this._cachedSrcSource === source && this._cachedSrcFlipY === flipY;
|
|
406
|
+
if (!uploadSkippable) {
|
|
407
|
+
uploadSourceTexture(device, srcTex, source, width, height, flipY);
|
|
408
|
+
}
|
|
409
|
+
if (useCache) {
|
|
410
|
+
this._cachedSrcSource = source instanceof ImageBitmap ? source : null;
|
|
411
|
+
this._cachedSrcFlipY = flipY;
|
|
412
|
+
}
|
|
308
413
|
let dstIsNew = true;
|
|
309
414
|
if (useCache && this._cachedDst && this._cachedDst.size >= outByteLen) {
|
|
310
415
|
dstBuffer = this._cachedDst;
|
|
@@ -356,63 +461,74 @@ var Encoder = class {
|
|
|
356
461
|
});
|
|
357
462
|
device.queue.writeBuffer(paramsBuffer, 0, new Uint32Array([blocksX, blocksY, width, height]));
|
|
358
463
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
let bindGroup = useCache ? this._bindGroupCache.get(pipeline) : void 0;
|
|
464
|
+
if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this._cachedBindGroup = null;
|
|
465
|
+
let bindGroup = useCache ? this._cachedBindGroup : null;
|
|
362
466
|
if (!bindGroup) {
|
|
467
|
+
const entries2 = [
|
|
468
|
+
{ binding: 0, resource: prepPlanes ? prepPlanes[0].createView() : srcTex.createView() },
|
|
469
|
+
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
470
|
+
{ binding: 2, resource: { buffer: paramsBuffer } }
|
|
471
|
+
];
|
|
472
|
+
if (prepPlanes) {
|
|
473
|
+
entries2.push({ binding: 3, resource: prepPlanes[1].createView() });
|
|
474
|
+
} else if (this._usesSampler) {
|
|
475
|
+
this._sampler ??= device.createSampler({
|
|
476
|
+
label: `${this.label}-clamp-sampler`,
|
|
477
|
+
addressModeU: "clamp-to-edge",
|
|
478
|
+
addressModeV: "clamp-to-edge"
|
|
479
|
+
});
|
|
480
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
481
|
+
}
|
|
363
482
|
bindGroup = device.createBindGroup({
|
|
364
483
|
label: `${this.label}-bg`,
|
|
365
484
|
layout: pipeline.getBindGroupLayout(0),
|
|
366
|
-
entries:
|
|
367
|
-
{ binding: 0, resource: srcTex.createView() },
|
|
368
|
-
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
369
|
-
{ binding: 2, resource: { buffer: paramsBuffer } }
|
|
370
|
-
]
|
|
485
|
+
entries: entries2
|
|
371
486
|
});
|
|
372
|
-
if (useCache) this.
|
|
487
|
+
if (useCache) this._cachedBindGroup = bindGroup;
|
|
373
488
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}) : null;
|
|
489
|
+
let prepBindGroup = useCache && !prepPlanesNew && !srcTexIsNew ? this._cachedPrepBindGroup : null;
|
|
490
|
+
if (prepPipeline && prepPlanes && !prepBindGroup) {
|
|
491
|
+
prepBindGroup = this._createPrepBindGroup(prepPipeline, srcTex.createView(), prepPlanes, paramsBuffer);
|
|
492
|
+
if (useCache) this._cachedPrepBindGroup = prepBindGroup;
|
|
493
|
+
}
|
|
494
|
+
const timing = withGpuTime ? this._createTiming() : null;
|
|
381
495
|
const [wgX, wgY] = this.workgroupSize;
|
|
382
496
|
const t0 = performance.now();
|
|
383
497
|
const enc = device.createCommandEncoder({ label: `${this.label}-encode` });
|
|
498
|
+
if (prepPipeline && prepBindGroup) {
|
|
499
|
+
const prepPass = enc.beginComputePass(
|
|
500
|
+
timing ? { timestampWrites: { querySet: timing.querySet, beginningOfPassWriteIndex: 0 } } : void 0
|
|
501
|
+
);
|
|
502
|
+
prepPass.setPipeline(prepPipeline);
|
|
503
|
+
prepPass.setBindGroup(0, prepBindGroup);
|
|
504
|
+
const [px, py] = this.prepDispatch(blocksX, blocksY);
|
|
505
|
+
prepPass.dispatchWorkgroups(Math.ceil(px / wgX), Math.ceil(py / wgY), 1);
|
|
506
|
+
prepPass.end();
|
|
507
|
+
}
|
|
384
508
|
const pass = enc.beginComputePass(
|
|
385
|
-
|
|
509
|
+
timing ? {
|
|
510
|
+
timestampWrites: {
|
|
511
|
+
querySet: timing.querySet,
|
|
512
|
+
...prepPipeline ? {} : { beginningOfPassWriteIndex: 0 },
|
|
513
|
+
endOfPassWriteIndex: 1
|
|
514
|
+
}
|
|
515
|
+
} : void 0
|
|
386
516
|
);
|
|
387
517
|
pass.setPipeline(pipeline);
|
|
388
518
|
pass.setBindGroup(0, bindGroup);
|
|
389
519
|
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
390
520
|
pass.end();
|
|
391
|
-
if (querySet && queryBuffer) enc.resolveQuerySet(querySet, 0, 2, queryBuffer, 0);
|
|
392
521
|
enc.copyBufferToBuffer(dstBuffer, 0, staging, 0, outByteLen);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}) : null;
|
|
398
|
-
if (tsStaging && queryBuffer) enc.copyBufferToBuffer(queryBuffer, 0, tsStaging, 0, 16);
|
|
522
|
+
if (timing) {
|
|
523
|
+
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
524
|
+
enc.copyBufferToBuffer(timing.resolve, 0, timing.staging, 0, 16);
|
|
525
|
+
}
|
|
399
526
|
device.queue.submit([enc.finish()]);
|
|
400
527
|
await staging.mapAsync(GPUMapMode.READ, 0, outByteLen);
|
|
401
528
|
const data = new Uint8Array(staging.getMappedRange(0, outByteLen).slice(0));
|
|
402
529
|
staging.unmap();
|
|
403
530
|
const encodeMs = performance.now() - t0;
|
|
404
|
-
|
|
405
|
-
if (tsStaging) {
|
|
406
|
-
await tsStaging.mapAsync(GPUMapMode.READ);
|
|
407
|
-
const [begin, end] = new BigUint64Array(tsStaging.getMappedRange().slice(0));
|
|
408
|
-
tsStaging.unmap();
|
|
409
|
-
tsStaging.destroy();
|
|
410
|
-
if (end !== void 0 && begin !== void 0 && end > begin) {
|
|
411
|
-
gpuMs = Number(end - begin) / 1e6;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
querySet?.destroy();
|
|
415
|
-
queryBuffer?.destroy();
|
|
531
|
+
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
416
532
|
return { width, height, paddedWidth, paddedHeight, data, encodeMs, gpuMs };
|
|
417
533
|
} finally {
|
|
418
534
|
if (useCache) {
|
|
@@ -422,13 +538,441 @@ var Encoder = class {
|
|
|
422
538
|
dstBuffer?.destroy();
|
|
423
539
|
staging?.destroy();
|
|
424
540
|
paramsBuffer?.destroy();
|
|
541
|
+
if (transientPrepPlanes) for (const t of transientPrepPlanes) t.destroy();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// ------------------------------------------------------------------ //
|
|
546
|
+
// Whole-chain encode — every level in one submission. //
|
|
547
|
+
// ------------------------------------------------------------------ //
|
|
548
|
+
/**
|
|
549
|
+
* Encode a whole mip chain in ONE GPU submission. A per-level
|
|
550
|
+
* `encodeToBytes()` loop costs a full CPU↔GPU round trip per level (a
|
|
551
|
+
* 1024² chain is 11 levels → 11 `mapAsync` waits with the GPU idle in
|
|
552
|
+
* between); this path uploads every level, records one dispatch per level
|
|
553
|
+
* into a single compute pass, copies all outputs into one staging buffer
|
|
554
|
+
* and maps it once.
|
|
555
|
+
*
|
|
556
|
+
* `levels` are raw RGBA8 pixels in base-to-tail order; sizes don't have to
|
|
557
|
+
* halve level-to-level (each level is padded and clamped independently,
|
|
558
|
+
* exactly like `encodeToBytes`). Uploads use `writeTexture` — the direct
|
|
559
|
+
* raw-bytes path, which also sidesteps the broken
|
|
560
|
+
* `copyExternalImageToTexture` devices (see workarounds.ts). There is no
|
|
561
|
+
* flip option: bake any vertical flip into level 0 before generating the
|
|
562
|
+
* chain, as `compressTexture()` does.
|
|
563
|
+
*/
|
|
564
|
+
async encodeMipChainToBytes(levels, { withGpuTime = false } = {}) {
|
|
565
|
+
const device = this.device;
|
|
566
|
+
if (levels.length === 0) {
|
|
567
|
+
throw new Error(`${this.label}Encoder: encodeMipChainToBytes needs at least one level`);
|
|
568
|
+
}
|
|
569
|
+
const pipeline = await this._pipelineReady;
|
|
570
|
+
const prepPipeline = this._prepPipelineReady ? await this._prepPipelineReady : null;
|
|
571
|
+
const t0 = performance.now();
|
|
572
|
+
levels.forEach((level, i) => {
|
|
573
|
+
if (!level.width || !level.height) {
|
|
574
|
+
throw new Error(`${this.label}Encoder: mip level ${i} has no dimensions`);
|
|
575
|
+
}
|
|
576
|
+
if (level.data.length < level.width * level.height * 4) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`${this.label}Encoder: mip level ${i} has ${level.data.length} bytes, expected ${level.width * level.height * 4}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
const { geoms, byteSpan } = this._chainGeometry(levels);
|
|
583
|
+
const sig = geoms.map((g) => `${g.width}x${g.height}`).join();
|
|
584
|
+
const useCache = !this._chainBusy;
|
|
585
|
+
if (useCache) this._chainBusy = true;
|
|
586
|
+
let textures;
|
|
587
|
+
let prepPlaneSets;
|
|
588
|
+
let prepBindGroups;
|
|
589
|
+
let params;
|
|
590
|
+
let bindGroups;
|
|
591
|
+
let dst;
|
|
592
|
+
let staging;
|
|
593
|
+
let transientLevelSet = false;
|
|
594
|
+
try {
|
|
595
|
+
let dstIsNew = true;
|
|
596
|
+
if (useCache && this._chainDst && this._chainDst.size >= byteSpan) {
|
|
597
|
+
dst = this._chainDst;
|
|
598
|
+
dstIsNew = false;
|
|
599
|
+
} else {
|
|
600
|
+
dst = device.createBuffer({
|
|
601
|
+
label: `${this.label}-chain-dst`,
|
|
602
|
+
size: byteSpan,
|
|
603
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
604
|
+
});
|
|
605
|
+
if (useCache) {
|
|
606
|
+
this._chainDst?.destroy();
|
|
607
|
+
this._chainDst = dst;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (useCache && !dstIsNew && this._chainSig === sig && this._chainParams) {
|
|
611
|
+
textures = this._chainTextures;
|
|
612
|
+
params = this._chainParams;
|
|
613
|
+
bindGroups = this._chainBindGroups;
|
|
614
|
+
prepPlaneSets = this._chainPrepPlanes;
|
|
615
|
+
prepBindGroups = this._chainPrepBindGroups;
|
|
616
|
+
} else {
|
|
617
|
+
const texs = geoms.map(
|
|
618
|
+
(g, i) => device.createTexture({
|
|
619
|
+
label: `${this.label}-chain-src-${i}`,
|
|
620
|
+
size: [g.paddedWidth, g.paddedHeight, 1],
|
|
621
|
+
format: "rgba8unorm",
|
|
622
|
+
// No RENDER_ATTACHMENT: writeTexture is a plain copy, not the
|
|
623
|
+
// copyExternalImageToTexture blit.
|
|
624
|
+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
textures = texs;
|
|
628
|
+
params = device.createBuffer({
|
|
629
|
+
label: `${this.label}-chain-params`,
|
|
630
|
+
size: geoms.length * CHAIN_ALIGN,
|
|
631
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
632
|
+
});
|
|
633
|
+
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
634
|
+
geoms.forEach((g, i) => {
|
|
635
|
+
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
636
|
+
});
|
|
637
|
+
device.queue.writeBuffer(params, 0, paramsData);
|
|
638
|
+
if (this._usesSampler) {
|
|
639
|
+
this._sampler ??= device.createSampler({
|
|
640
|
+
label: `${this.label}-clamp-sampler`,
|
|
641
|
+
addressModeU: "clamp-to-edge",
|
|
642
|
+
addressModeV: "clamp-to-edge"
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const dstBuf = dst;
|
|
646
|
+
const paramsBuf = params;
|
|
647
|
+
const planeSets = prepPipeline ? geoms.map((g) => this._createPrepPlanes(g.paddedWidth, g.paddedHeight)) : [];
|
|
648
|
+
prepPlaneSets = planeSets;
|
|
649
|
+
prepBindGroups = prepPipeline ? geoms.map(
|
|
650
|
+
(g, i) => this._createPrepBindGroup(prepPipeline, texs[i].createView(), planeSets[i], paramsBuf, i * CHAIN_ALIGN)
|
|
651
|
+
) : [];
|
|
652
|
+
bindGroups = geoms.map((g, i) => {
|
|
653
|
+
const entries2 = [
|
|
654
|
+
{
|
|
655
|
+
binding: 0,
|
|
656
|
+
resource: prepPipeline ? planeSets[i][0].createView() : texs[i].createView()
|
|
657
|
+
},
|
|
658
|
+
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
659
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
|
|
660
|
+
];
|
|
661
|
+
if (prepPipeline) {
|
|
662
|
+
entries2.push({ binding: 3, resource: planeSets[i][1].createView() });
|
|
663
|
+
} else if (this._usesSampler) {
|
|
664
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
665
|
+
}
|
|
666
|
+
return device.createBindGroup({
|
|
667
|
+
label: `${this.label}-chain-bg-${i}`,
|
|
668
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
669
|
+
entries: entries2
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
if (useCache) {
|
|
673
|
+
for (const tex of this._chainTextures) tex.destroy();
|
|
674
|
+
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
675
|
+
this._chainParams?.destroy();
|
|
676
|
+
this._chainTextures = textures;
|
|
677
|
+
this._chainPrepPlanes = planeSets;
|
|
678
|
+
this._chainPrepBindGroups = prepBindGroups;
|
|
679
|
+
this._chainParams = params;
|
|
680
|
+
this._chainBindGroups = bindGroups;
|
|
681
|
+
this._chainSig = sig;
|
|
682
|
+
} else {
|
|
683
|
+
transientLevelSet = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (useCache && this._chainStaging && this._chainStaging.size >= byteSpan) {
|
|
687
|
+
staging = this._chainStaging;
|
|
688
|
+
} else {
|
|
689
|
+
staging = device.createBuffer({
|
|
690
|
+
label: `${this.label}-chain-staging`,
|
|
691
|
+
size: byteSpan,
|
|
692
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
693
|
+
});
|
|
694
|
+
if (useCache) {
|
|
695
|
+
this._chainStaging?.destroy();
|
|
696
|
+
this._chainStaging = staging;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (let i = 0; i < levels.length; i++) {
|
|
700
|
+
const level = levels[i];
|
|
701
|
+
device.queue.writeTexture({ texture: textures[i] }, level.data, { bytesPerRow: level.width * 4 }, [
|
|
702
|
+
level.width,
|
|
703
|
+
level.height,
|
|
704
|
+
1
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
return await this._submitChainAndRead(
|
|
708
|
+
pipeline,
|
|
709
|
+
geoms,
|
|
710
|
+
byteSpan,
|
|
711
|
+
bindGroups,
|
|
712
|
+
dst,
|
|
713
|
+
staging,
|
|
714
|
+
withGpuTime,
|
|
715
|
+
t0,
|
|
716
|
+
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
717
|
+
);
|
|
718
|
+
} finally {
|
|
719
|
+
if (useCache) {
|
|
720
|
+
this._chainBusy = false;
|
|
721
|
+
} else {
|
|
722
|
+
dst?.destroy();
|
|
723
|
+
staging?.destroy();
|
|
724
|
+
}
|
|
725
|
+
if (transientLevelSet) {
|
|
726
|
+
if (textures) for (const tex of textures) tex.destroy();
|
|
727
|
+
if (prepPlaneSets) for (const planes of prepPlaneSets) for (const t of planes) t.destroy();
|
|
728
|
+
params?.destroy();
|
|
425
729
|
}
|
|
426
730
|
}
|
|
427
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Encode every mip level of a GPU-resident texture in one submission —
|
|
734
|
+
* the zero-CPU-pixels counterpart of `encodeMipChainToBytes()`. Pair it
|
|
735
|
+
* with `generateGpuMipChain()` (gpuMipgen.ts): upload the image once,
|
|
736
|
+
* box-filter the chain on the GPU, then encode straight from the
|
|
737
|
+
* texture's mip views. Pixels never transit the CPU between the source
|
|
738
|
+
* image and the compressed-bytes readback.
|
|
739
|
+
*
|
|
740
|
+
* `srcTex` must be `rgba8unorm` with TEXTURE_BINDING usage; level 0's
|
|
741
|
+
* dimensions are taken from the texture and lower levels follow the
|
|
742
|
+
* standard floor-halving chain. Encoded output is identical to feeding
|
|
743
|
+
* the equivalent CPU chain to `encodeMipChainToBytes()`.
|
|
744
|
+
*/
|
|
745
|
+
async encodeMipChainFromTexture(srcTex, { withGpuTime = false } = {}) {
|
|
746
|
+
const device = this.device;
|
|
747
|
+
const pipeline = await this._pipelineReady;
|
|
748
|
+
const prepPipeline = this._prepPipelineReady ? await this._prepPipelineReady : null;
|
|
749
|
+
const t0 = performance.now();
|
|
750
|
+
const dims = [];
|
|
751
|
+
for (let i = 0; i < srcTex.mipLevelCount; i++) {
|
|
752
|
+
dims.push({ width: Math.max(1, srcTex.width >> i), height: Math.max(1, srcTex.height >> i) });
|
|
753
|
+
}
|
|
754
|
+
const { geoms, byteSpan } = this._chainGeometry(dims);
|
|
755
|
+
const useCache = !this._chainBusy;
|
|
756
|
+
if (useCache) this._chainBusy = true;
|
|
757
|
+
let dst;
|
|
758
|
+
let staging;
|
|
759
|
+
let params;
|
|
760
|
+
let planeSets = null;
|
|
761
|
+
try {
|
|
762
|
+
if (useCache && this._chainDst && this._chainDst.size >= byteSpan) {
|
|
763
|
+
dst = this._chainDst;
|
|
764
|
+
} else {
|
|
765
|
+
dst = device.createBuffer({
|
|
766
|
+
label: `${this.label}-chain-dst`,
|
|
767
|
+
size: byteSpan,
|
|
768
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
769
|
+
});
|
|
770
|
+
if (useCache) {
|
|
771
|
+
this._chainDst?.destroy();
|
|
772
|
+
this._chainDst = dst;
|
|
773
|
+
this._chainSig = null;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (useCache && this._chainStaging && this._chainStaging.size >= byteSpan) {
|
|
777
|
+
staging = this._chainStaging;
|
|
778
|
+
} else {
|
|
779
|
+
staging = device.createBuffer({
|
|
780
|
+
label: `${this.label}-chain-staging`,
|
|
781
|
+
size: byteSpan,
|
|
782
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
783
|
+
});
|
|
784
|
+
if (useCache) {
|
|
785
|
+
this._chainStaging?.destroy();
|
|
786
|
+
this._chainStaging = staging;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
params = device.createBuffer({
|
|
790
|
+
label: `${this.label}-chain-params`,
|
|
791
|
+
size: geoms.length * CHAIN_ALIGN,
|
|
792
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
793
|
+
});
|
|
794
|
+
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
795
|
+
geoms.forEach((g, i) => {
|
|
796
|
+
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
797
|
+
});
|
|
798
|
+
device.queue.writeBuffer(params, 0, paramsData);
|
|
799
|
+
if (this._usesSampler) {
|
|
800
|
+
this._sampler ??= device.createSampler({
|
|
801
|
+
label: `${this.label}-clamp-sampler`,
|
|
802
|
+
addressModeU: "clamp-to-edge",
|
|
803
|
+
addressModeV: "clamp-to-edge"
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const dstBuf = dst;
|
|
807
|
+
const paramsBuf = params;
|
|
808
|
+
planeSets = prepPipeline ? geoms.map((g) => this._createPrepPlanes(g.paddedWidth, g.paddedHeight)) : null;
|
|
809
|
+
const planes = planeSets;
|
|
810
|
+
const prepBindGroups = prepPipeline && planes ? geoms.map(
|
|
811
|
+
(_, i) => this._createPrepBindGroup(
|
|
812
|
+
prepPipeline,
|
|
813
|
+
srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 }),
|
|
814
|
+
planes[i],
|
|
815
|
+
paramsBuf,
|
|
816
|
+
i * CHAIN_ALIGN
|
|
817
|
+
)
|
|
818
|
+
) : null;
|
|
819
|
+
const bindGroups = geoms.map((g, i) => {
|
|
820
|
+
const entries2 = [
|
|
821
|
+
{
|
|
822
|
+
binding: 0,
|
|
823
|
+
resource: planes ? planes[i][0].createView() : srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 })
|
|
824
|
+
},
|
|
825
|
+
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
826
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: i * CHAIN_ALIGN, size: 16 } }
|
|
827
|
+
];
|
|
828
|
+
if (planes) {
|
|
829
|
+
entries2.push({ binding: 3, resource: planes[i][1].createView() });
|
|
830
|
+
} else if (this._usesSampler) {
|
|
831
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
832
|
+
}
|
|
833
|
+
return device.createBindGroup({
|
|
834
|
+
label: `${this.label}-chain-bg-${i}`,
|
|
835
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
836
|
+
entries: entries2
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
return await this._submitChainAndRead(
|
|
840
|
+
pipeline,
|
|
841
|
+
geoms,
|
|
842
|
+
byteSpan,
|
|
843
|
+
bindGroups,
|
|
844
|
+
dst,
|
|
845
|
+
staging,
|
|
846
|
+
withGpuTime,
|
|
847
|
+
t0,
|
|
848
|
+
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
849
|
+
);
|
|
850
|
+
} finally {
|
|
851
|
+
if (useCache) {
|
|
852
|
+
this._chainBusy = false;
|
|
853
|
+
} else {
|
|
854
|
+
dst?.destroy();
|
|
855
|
+
staging?.destroy();
|
|
856
|
+
}
|
|
857
|
+
params?.destroy();
|
|
858
|
+
if (planeSets) for (const planes of planeSets) for (const t of planes) t.destroy();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/** Block-grid geometry + packed output offsets for a chain of levels.
|
|
862
|
+
* `byteSpan` is both the dst buffer size and the readback copy size (a
|
|
863
|
+
* multiple of 4: byteLen is a multiple of bytesPerBlock ≥ 8, offsets are
|
|
864
|
+
* CHAIN_ALIGN-ed). */
|
|
865
|
+
_chainGeometry(dims) {
|
|
866
|
+
let dstCursor = 0;
|
|
867
|
+
const geoms = dims.map(({ width, height }) => {
|
|
868
|
+
const paddedWidth = width + 3 & ~3;
|
|
869
|
+
const paddedHeight = height + 3 & ~3;
|
|
870
|
+
const blocksX = paddedWidth >> 2;
|
|
871
|
+
const blocksY = paddedHeight >> 2;
|
|
872
|
+
const byteLen = blocksX * blocksY * this.bytesPerBlock;
|
|
873
|
+
const dstOffset = dstCursor;
|
|
874
|
+
dstCursor = Math.ceil((dstCursor + byteLen) / CHAIN_ALIGN) * CHAIN_ALIGN;
|
|
875
|
+
return { width, height, paddedWidth, paddedHeight, blocksX, blocksY, byteLen, dstOffset };
|
|
876
|
+
});
|
|
877
|
+
const last = geoms[geoms.length - 1];
|
|
878
|
+
return { geoms, byteSpan: last.dstOffset + last.byteLen };
|
|
879
|
+
}
|
|
880
|
+
/** Shared chain-encode tail: one compute pass with a dispatch per level,
|
|
881
|
+
* one submit, one staging readback sliced into per-level byte arrays. */
|
|
882
|
+
async _submitChainAndRead(pipeline, geoms, byteSpan, bindGroups, dst, staging, withGpuTime, t0, prep = null) {
|
|
883
|
+
const device = this.device;
|
|
884
|
+
const timing = withGpuTime ? this._createTiming() : null;
|
|
885
|
+
const [wgX, wgY] = this.workgroupSize;
|
|
886
|
+
const enc = device.createCommandEncoder({ label: `${this.label}-encode-chain` });
|
|
887
|
+
if (prep) {
|
|
888
|
+
const prepPass = enc.beginComputePass(
|
|
889
|
+
timing ? { timestampWrites: { querySet: timing.querySet, beginningOfPassWriteIndex: 0 } } : void 0
|
|
890
|
+
);
|
|
891
|
+
prepPass.setPipeline(prep.pipeline);
|
|
892
|
+
for (let i = 0; i < geoms.length; i++) {
|
|
893
|
+
const g = geoms[i];
|
|
894
|
+
prepPass.setBindGroup(0, prep.bindGroups[i]);
|
|
895
|
+
const [px, py] = this.prepDispatch(g.blocksX, g.blocksY);
|
|
896
|
+
prepPass.dispatchWorkgroups(Math.ceil(px / wgX), Math.ceil(py / wgY), 1);
|
|
897
|
+
}
|
|
898
|
+
prepPass.end();
|
|
899
|
+
}
|
|
900
|
+
const pass = enc.beginComputePass(
|
|
901
|
+
timing ? {
|
|
902
|
+
timestampWrites: {
|
|
903
|
+
querySet: timing.querySet,
|
|
904
|
+
...prep ? {} : { beginningOfPassWriteIndex: 0 },
|
|
905
|
+
endOfPassWriteIndex: 1
|
|
906
|
+
}
|
|
907
|
+
} : void 0
|
|
908
|
+
);
|
|
909
|
+
pass.setPipeline(pipeline);
|
|
910
|
+
for (let i = 0; i < geoms.length; i++) {
|
|
911
|
+
const g = geoms[i];
|
|
912
|
+
pass.setBindGroup(0, bindGroups[i]);
|
|
913
|
+
pass.dispatchWorkgroups(Math.ceil(g.blocksX / wgX), Math.ceil(g.blocksY / wgY), 1);
|
|
914
|
+
}
|
|
915
|
+
pass.end();
|
|
916
|
+
enc.copyBufferToBuffer(dst, 0, staging, 0, byteSpan);
|
|
917
|
+
if (timing) {
|
|
918
|
+
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
919
|
+
enc.copyBufferToBuffer(timing.resolve, 0, timing.staging, 0, 16);
|
|
920
|
+
}
|
|
921
|
+
device.queue.submit([enc.finish()]);
|
|
922
|
+
await staging.mapAsync(GPUMapMode.READ, 0, byteSpan);
|
|
923
|
+
const mapped = staging.getMappedRange(0, byteSpan);
|
|
924
|
+
const out = geoms.map((g) => ({
|
|
925
|
+
width: g.width,
|
|
926
|
+
height: g.height,
|
|
927
|
+
paddedWidth: g.paddedWidth,
|
|
928
|
+
paddedHeight: g.paddedHeight,
|
|
929
|
+
data: new Uint8Array(mapped.slice(g.dstOffset, g.dstOffset + g.byteLen))
|
|
930
|
+
}));
|
|
931
|
+
staging.unmap();
|
|
932
|
+
const encodeMs = performance.now() - t0;
|
|
933
|
+
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
934
|
+
return { levels: out, encodeMs, gpuMs };
|
|
935
|
+
}
|
|
936
|
+
// ------------------------------------------------------------------ //
|
|
937
|
+
// GPU timing plumbing (timestamp queries) //
|
|
938
|
+
// ------------------------------------------------------------------ //
|
|
939
|
+
/** Create the query set + resolve/staging buffers for one timed
|
|
940
|
+
* submission, or null when the device lacks 'timestamp-query'. */
|
|
941
|
+
_createTiming() {
|
|
942
|
+
const device = this.device;
|
|
943
|
+
if (!device.features.has("timestamp-query")) return null;
|
|
944
|
+
return {
|
|
945
|
+
querySet: device.createQuerySet({ type: "timestamp", count: 2 }),
|
|
946
|
+
resolve: device.createBuffer({
|
|
947
|
+
label: `${this.label}-ts-resolve`,
|
|
948
|
+
size: 16,
|
|
949
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
|
|
950
|
+
}),
|
|
951
|
+
staging: device.createBuffer({
|
|
952
|
+
label: `${this.label}-ts-staging`,
|
|
953
|
+
size: 16,
|
|
954
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
955
|
+
})
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/** Read back a timed submission's pass duration (ms) and destroy the
|
|
959
|
+
* timing objects. Timestamps are u64 nanoseconds. */
|
|
960
|
+
async _readTimingMs(timing) {
|
|
961
|
+
await timing.staging.mapAsync(GPUMapMode.READ);
|
|
962
|
+
const [begin, end] = new BigUint64Array(timing.staging.getMappedRange().slice(0));
|
|
963
|
+
timing.staging.unmap();
|
|
964
|
+
timing.staging.destroy();
|
|
965
|
+
timing.resolve.destroy();
|
|
966
|
+
timing.querySet.destroy();
|
|
967
|
+
if (end !== void 0 && begin !== void 0 && end > begin) {
|
|
968
|
+
return Number(end - begin) / 1e6;
|
|
969
|
+
}
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
428
972
|
};
|
|
429
973
|
|
|
430
974
|
// src/bc1.wgsl
|
|
431
|
-
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// QUALITY LEVELS (pipeline-overridable constant `QUALITY_HIGH`)\n// fast (0, default): principal-axis endpoint seed (the high path's\n// covariance power-iteration; inset bbox on degenerate blocks), inset by\n// ~half a 565 cell along the axis, then ONE fused pass that projects\n// every pixel onto the decoded-endpoint line (the 4 palette entries are\n// colinear and evenly spaced, so the nearest entry is the rounded\n// projection \u2014 no 4-entry search) while accumulating the least-squares\n// refit sums; the refit endpoints are re-quantised and a final projection\n// pass assigns the indices, packed on the fly.\n// high (1): endpoints are seeded from the block's principal colour axis\n// (covariance power-iteration) as well as the bbox diagonal, each refined by\n// several least-squares passes with full 4-entry searches; the lower-error\n// family wins. Mirrors bc1_ref.ts.\n\n// 0 = fast (default), 1 = 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 to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = (c >> 11u) & 31u;\n let g = (c >> 5u) & 63u;\n let b = c & 31u;\n // 5/6-bit -> 8-bit: (x*527+23)>>6 (6-bit: 259/33) \u2014 round-to-nearest\n // scaling, matching bc1_ref.ts and typical hardware decoders (white ->\n // 255). Integer u32 math is exact. NOTE: this is NOT plain bit-replication\n // ((x<<3)|(x>>2)) \u2014 they differ for some codes (e.g. 5-bit 3 -> 25 vs 24).\n // Selecting indices against this palette is what makes the encoder agree\n // with what the GPU will actually sample.\n let r8 = (r * 527u + 23u) >> 6u;\n let g8 = (g * 259u + 33u) >> 6u;\n let b8 = (b * 527u + 23u) >> 6u;\n return vec3<f32>(vec3<u32>(r8, g8, b8)) / 255.0;\n}\n\n// 4-color-mode interpolation weights: palette[j] = wa(j)*c0 + wb(j)*c1.\nfn wa(j: u32) -> f32 {\n switch j {\n case 0u: { return 1.0; }\n case 1u: { return 0.0; }\n case 2u: { return 2.0 / 3.0; }\n default: { return 1.0 / 3.0; } // case 3u\n }\n}\nfn wb(j: u32) -> f32 {\n switch j {\n case 0u: { return 0.0; }\n case 1u: { return 1.0; }\n case 2u: { return 1.0 / 3.0; }\n default: { return 2.0 / 3.0; } // case 3u\n }\n}\n\nfn build_palette(c0: u32, c1: u32, pal: ptr<function, array<vec3<f32>, 4>>) {\n let p0 = from565(c0);\n let p1 = from565(c1);\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n (*pal)[j] = wa(j) * p0 + wb(j) * p1;\n }\n}\n\n// Assign each of the 16 pixels its nearest palette entry (full 4-entry L2),\n// writing indices into `out_idx` and returning the total squared error.\nfn assign_indices(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n pal: ptr<function, array<vec3<f32>, 4>>,\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 c = (*pixels)[k];\n var best_j: u32 = 0u;\n var best_d: f32 = 1e30;\n for (var j: u32 = 0u; j < 4u; j = j + 1u) {\n let d = (*pal)[j] - c;\n let d2 = dot(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// One least-squares refit pass: solve the 2x2 normal equations for the endpoint\n// colours that minimise \u03A3\u2016wa\xB7e0 + wb\xB7e1 \u2212 c\u2016\xB2 under the current indices. The\n// three channels share the scalar sums, so it's one 2x2 solve with vec3 RHS.\nstruct RefitResult { e0: vec3<f32>, e1: vec3<f32>, valid: bool };\nfn refit(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n indices: ptr<function, array<u32, 16>>,\n) -> RefitResult {\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec3<f32> = vec3<f32>(0.0);\n var sBV: vec3<f32> = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let a = wa((*indices)[k]);\n let b = wb((*indices)[k]);\n let v = (*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 var out: RefitResult;\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) {\n out.valid = false;\n return out;\n }\n out.e0 = clamp((sBB * sAV - sAB * sBV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.e1 = clamp((sAA * sBV - sAB * sAV) / det, vec3<f32>(0.0), vec3<f32>(1.0));\n out.valid = true;\n return out;\n}\n\n// Candidate solution tracked across endpoint seeds / refit passes.\nstruct Best { c0: u32, c1: u32, indices: array<u32, 16>, err: f32 };\n\n// Quantize (hi, lo) to 565, force 4-color mode, assign indices, then refine with\n// up to `max_refits` least-squares passes. Commits to `*best` only on strict\n// improvement.\nfn fit_from_endpoints(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n hi: vec3<f32>,\n lo: vec3<f32>,\n max_refits: u32,\n best: ptr<function, Best>,\n) {\n var c0 = to565(hi);\n var c1 = to565(lo);\n // 4-color mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n var pal: array<vec3<f32>, 4>;\n var idx: array<u32, 16>;\n build_palette(c0, c1, &pal);\n var err = assign_indices(pixels, &pal, &idx);\n if (err < (*best).err) {\n (*best).c0 = c0; (*best).c1 = c1; (*best).indices = idx; (*best).err = err;\n }\n\n for (var rp: u32 = 0u; rp < max_refits; rp = rp + 1u) {\n let r = refit(pixels, &idx);\n if (!r.valid) { break; }\n var nc0 = to565(r.e0);\n var nc1 = to565(r.e1);\n // A refit that flips/equalises the endpoints would change decode mode;\n // keep 4-color mode, and stop once it stops moving.\n if (nc0 < nc1) { let t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1) { break; }\n if (nc0 == c0 && nc1 == c1) { break; }\n build_palette(nc0, nc1, &pal);\n let nerr = assign_indices(pixels, &pal, &idx);\n c0 = nc0; c1 = nc1; err = nerr;\n if (nerr < (*best).err) {\n (*best).c0 = nc0; (*best).c1 = nc1; (*best).indices = idx; (*best).err = nerr;\n }\n }\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant) block.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\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 var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n\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 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n }\n mean = mean * (1.0 / 16.0);\n\n // Inset the bounding box by ~half an RGB565 cell (1/16) so the quantized\n // 4-color palette covers the real data range more tightly (stb_dxt heuristic).\n let inset = (bb_max - bb_min) / 16.0;\n let bbox_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n let bbox_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n\n if (QUALITY_HIGH == 0u) {\n // -------- fast: PCA seed + projection + fused LSQ refit + reprojection\n // Seed endpoints from the block's principal colour axis (same\n // power-iteration as the high path). The bbox diagonal is sign-blind: on\n // anti-correlated channels (normal maps, hue edges) it points across the\n // data instead of along it, and the LSQ refit \u2014 which fits endpoints\n // GIVEN the projection indices \u2014 can't recover from a wrong axis.\n // Degenerate (near-flat) blocks keep the inset-bbox seed.\n var seed_hi = bbox_hi;\n var seed_lo = bbox_lo;\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n // Inset along the axis by ~half a 565 cell (stb_dxt heuristic,\n // matching the bbox inset).\n let pad = (t_max - t_min) / 16.0;\n seed_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n seed_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n }\n var c0 = to565(seed_hi);\n var c1 = to565(seed_lo);\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n let p0 = from565(c0);\n let p1 = from565(c1);\n\n // Fused pass: projection assignment + LSQ sums + the seed solution's\n // packed indices and squared error. Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922,\n // 2\u21923, 3\u21921 (c1); as a packed LUT: (0x78 >> 2L) & 3.\n var idx_bits: u32 = 0u;\n let dir = p1 - p0;\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let inv = 3.0 / dd;\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV = vec3<f32>(0.0); var sBV = vec3<f32>(0.0);\n var s_min = 3.0; var s_max = 0.0;\n var seed_err: f32 = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = pixels[k];\n let s = clamp(floor(dot(v - p0, dir) * inv + 0.5), 0.0, 3.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n let e = v - (p0 + b * dir);\n seed_err = seed_err + dot(e, e);\n idx_bits = idx_bits | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n let det = sAA * sBB - sAB * sAB;\n // Refit only on a well-conditioned system: when every pixel lands on\n // ONE level (flat blocks \u2014 the 4-colour nudge forces c0 \u2260 c1 even\n // then) the system is rank-1 and det/numerators are pure float noise;\n // the solve would return garbage endpoints. With \u22652 levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 ~1.67, so 1e-3 is a safe guard.\n if (s_min < s_max && abs(det) > 1e-3) {\n // Clamp the refit to the block bbox (not [0,1]): on multi-cluster\n // blocks the unconstrained solve extrapolates far outside the block's\n // colours and the per-channel clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to\n // the bbox also measures better in plain SSE (+1.6 dB on the colour\n // test card), so the accept-if-better guard below keeps more refits.\n let e0 = clamp((sBB * sAV - sAB * sBV) / det, bb_min, bb_max);\n let e1 = clamp((sAA * sBV - sAB * sAV) / det, bb_min, bb_max);\n var nc0 = to565(e0);\n var nc1 = to565(e1);\n if (nc0 == nc1) {\n if (nc1 > 0u) { nc1 = nc1 - 1u; } else { nc0 = nc0 + 1u; }\n } else if (nc0 < nc1) {\n let t = nc0; nc0 = nc1; nc1 = t;\n }\n let np0 = from565(nc0);\n let np1 = from565(nc1);\n let ndir = np1 - np0;\n let ndd = dot(ndir, ndir);\n if (ndd > 0.0 && !(nc0 == c0 && nc1 == c1)) {\n // Reproject against the refit endpoints and accept them only if\n // the block error actually decreases (the refit minimises a\n // continuous objective; after 565 quantisation it can lose).\n let ninv = 3.0 / ndd;\n var refit_err: f32 = 0.0;\n var nidx_bits: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = pixels[k];\n let s = clamp(floor(dot(v - np0, ndir) * ninv + 0.5), 0.0, 3.0);\n let e = v - (np0 + s * (1.0 / 3.0) * ndir);\n refit_err = refit_err + dot(e, e);\n nidx_bits = nidx_bits | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n if (refit_err < seed_err) {\n c0 = nc0; c1 = nc1;\n idx_bits = nidx_bits;\n }\n }\n }\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = idx_bits;\n return;\n }\n\n // ------------------------------ high --------------------------------- //\n var best: Best;\n best.err = 1e30;\n\n // Seed from the principal colour axis: project all texels onto it, take the\n // extreme projections as endpoints, inset along the axis. Then also try the\n // bbox seed and keep whichever family yields the lower error.\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n let pca_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n let pca_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n fit_from_endpoints(&pixels, pca_hi, pca_lo, 3u, &best);\n }\n fit_from_endpoints(&pixels, bbox_hi, bbox_lo, 3u, &best);\n\n var indices: u32 = 0u;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n indices = indices | ((best.indices[k] & 3u) << (k * 2u));\n }\n\n let out = block_index * 2u;\n dst[out] = best.c0 | (best.c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
975
|
+
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer. This is the f32\n// fallback; bc1_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// ALGORITHM: principal-axis endpoint seed (covariance power-iteration; inset\n// bbox on degenerate blocks), inset by ~half a 565 cell along the axis, then\n// a fused pass that projects every pixel onto the decoded-endpoint line (the\n// 4 palette entries are colinear and evenly spaced, so the nearest entry is\n// the rounded projection \u2014 no 4-entry search) while accumulating the\n// least-squares refit sums, followed by up to TWO refit rounds (re-quantise,\n// reproject with indices packed on the fly, accept only on lower block\n// error).\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 to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = (c >> 11u) & 31u;\n let g = (c >> 5u) & 63u;\n let b = c & 31u;\n // 5/6-bit -> 8-bit: (x*527+23)>>6 (6-bit: 259/33) \u2014 round-to-nearest\n // scaling, matching bc1_ref.ts and typical hardware decoders (white ->\n // 255). Integer u32 math is exact. NOTE: this is NOT plain bit-replication\n // ((x<<3)|(x>>2)) \u2014 they differ for some codes (e.g. 5-bit 3 -> 25 vs 24).\n // Selecting indices against this palette is what makes the encoder agree\n // with what the GPU will actually sample.\n let r8 = (r * 527u + 23u) >> 6u;\n let g8 = (g * 259u + 33u) >> 6u;\n let b8 = (b * 527u + 23u) >> 6u;\n return vec3<f32>(vec3<u32>(r8, g8, b8)) / 255.0;\n}\n\n// One projection pass against the decoded endpoints of (c0,c1): the packed\n// 2-bit indices, the block's squared error, and the LSQ normal-equation sums\n// of the resulting assignment \u2014 so an accepted refit can seed the next\n// round. Levels s run 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014\n// colinear, evenly spaced, so rounding the projection IS the nearest-entry\n// search). Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a\n// packed LUT: (0x78 >> 2L) & 3.\nstruct ProjStats {\n indices: u32,\n err: f32,\n sAA: f32, sBB: f32, sAB: f32,\n sAV: vec3<f32>, sBV: vec3<f32>,\n s_min: f32, s_max: f32,\n};\nfn project_stats(pix: ptr<function, array<vec3<f32>, 16>>, c0: u32, c1: u32) -> ProjStats {\n var out: ProjStats;\n out.indices = 0u;\n out.err = 0.0;\n out.sAA = 0.0; out.sBB = 0.0; out.sAB = 0.0;\n out.sAV = vec3<f32>(0.0); out.sBV = vec3<f32>(0.0);\n out.s_min = 3.0; out.s_max = 0.0;\n let p0 = from565(c0);\n let p1 = from565(c1);\n let dir = p1 - p0;\n let dd = dot(dir, dir);\n if (dd == 0.0) {\n // Unreachable for distinct 565 codes (the decode is injective); kept so\n // a degenerate call still returns a consistent error.\n out.s_min = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let e = (*pix)[k] - p0;\n out.err = out.err + dot(e, e);\n }\n return out;\n }\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = (*pix)[k];\n let s = clamp(floor(dot(v - p0, dir) * inv + 0.5), 0.0, 3.0);\n out.s_min = min(out.s_min, s); out.s_max = max(out.s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n out.sAA = out.sAA + a * a; out.sBB = out.sBB + b * b; out.sAB = out.sAB + a * b;\n out.sAV = out.sAV + a * v; out.sBV = out.sBV + b * v;\n let e = v - (p0 + b * dir);\n out.err = out.err + dot(e, e);\n out.indices = out.indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n return out;\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant)\n// block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\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 var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n var gd = 0.0;\n\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 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n gd = max(gd, max(abs(c.x - c.y), abs(c.x - c.z)));\n }\n mean = mean * (1.0 / 16.0);\n // Exactly-gray blocks free the refit from the bbox clamp (no hue to\n // protect; smooth gradients want endpoints outside the data range) \u2014\n // see bc1_fast_f16.wgsl.\n let gray = gd == 0.0;\n let lim_lo = select(bb_min, vec3<f32>(0.0), gray);\n let lim_hi = select(bb_max, vec3<f32>(1.0), gray);\n\n // Seed endpoints from the block's principal colour axis at the exact\n // projection extents, inset by ~half a 565 cell along the axis (stb_dxt\n // heuristic). Degenerate (near-flat) blocks keep the inset-bbox seed.\n var seed_hi: vec3<f32>;\n var seed_lo: vec3<f32>;\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n seed_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n seed_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n } else {\n let inset = (bb_max - bb_min) / 16.0;\n seed_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n seed_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n }\n var c0 = to565(seed_hi);\n var c1 = to565(seed_lo);\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n // Fused seed pass, then up to TWO least-squares refit rounds, each\n // accepted only if the block's squared error actually decreases \u2014 the\n // refit minimises a continuous objective and can lose after 565\n // quantisation. Every pass re-accumulates the normal-equation sums, so an\n // accepted round seeds the next.\n var cur = project_stats(&pixels, c0, c1);\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n // Refit only on a well-conditioned system: when every pixel lands on\n // ONE level (flat blocks \u2014 the 4-colour nudge forces c0 \u2260 c1 even\n // then) the system is rank-1 and det/numerators are pure float noise;\n // the solve would return garbage endpoints. With \u22652 levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 ~1.67, so 1e-3 is a safe guard.\n if (cur.s_min >= cur.s_max) { break; }\n let det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;\n if (abs(det) <= 1e-3) { break; }\n // Clamp the refit to the block bbox (not [0,1]): on multi-cluster\n // blocks the unconstrained solve extrapolates far outside the block's\n // colours and the per-channel clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to\n // the bbox also measures better in plain SSE (+1.6 dB on the colour\n // test card), so the accept-if-better guard below keeps more refits.\n let e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, lim_lo, lim_hi);\n let e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, lim_lo, lim_hi);\n var nc0 = to565(e0);\n var nc1 = to565(e1);\n if (nc0 == nc1) {\n if (nc1 > 0u) { nc1 = nc1 - 1u; } else { nc0 = nc0 + 1u; }\n } else if (nc0 < nc1) {\n let t = nc0; nc0 = nc1; nc1 = t;\n }\n if (nc0 == c0 && nc1 == c1) { break; }\n let nxt = project_stats(&pixels, nc0, nc1);\n if (nxt.err >= cur.err) { break; }\n c0 = nc0;\n c1 = nc1;\n cur = nxt;\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = cur.indices;\n}\n";
|
|
432
976
|
|
|
433
977
|
// src/bc1_fast_f16.wgsl
|
|
434
978
|
var bc1_fast_f16_default = `// bc1 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -444,11 +988,13 @@ var bc1_fast_f16_default = `// bc1 "fast" encoder \u2014 f16 variant (requires t
|
|
|
444
988
|
// 3. ONE fused pass: project every pixel onto the decoded-endpoint line
|
|
445
989
|
// (the 4 palette entries are colinear and evenly spaced, so the nearest
|
|
446
990
|
// entry is the rounded projection \u2014 no 4-entry search) while
|
|
447
|
-
// accumulating the least-squares refit sums, the
|
|
448
|
-
//
|
|
449
|
-
// 4.
|
|
450
|
-
//
|
|
451
|
-
//
|
|
991
|
+
// accumulating the least-squares refit sums, the packed indices and the
|
|
992
|
+
// squared error
|
|
993
|
+
// 4. up to TWO refit rounds (mirroring the high path's iterated refits):
|
|
994
|
+
// re-quantise the refit endpoints, reproject (indices packed on the
|
|
995
|
+
// fly, sums re-accumulated to seed the next round), and accept each
|
|
996
|
+
// round only if the block error decreases \u2014 flat/single-level blocks
|
|
997
|
+
// skip these passes entirely
|
|
452
998
|
//
|
|
453
999
|
// vs the pre-projection fast branch (build palette + full 4-entry search \xD7 3
|
|
454
1000
|
// passes + refit sums pass) this does roughly half the ALU per block. The
|
|
@@ -496,6 +1042,56 @@ fn order565(a: u32, b: u32) -> vec2<u32> {
|
|
|
496
1042
|
return vec2<u32>(c0, c1);
|
|
497
1043
|
}
|
|
498
1044
|
|
|
1045
|
+
// One projection pass against the decoded endpoints of (c0,c1): the packed
|
|
1046
|
+
// 2-bit indices, the block's squared error, and the LSQ normal-equation sums
|
|
1047
|
+
// of the resulting assignment \u2014 so an accepted refit can seed the next
|
|
1048
|
+
// round. Levels s run 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014
|
|
1049
|
+
// colinear, evenly spaced, so rounding the projection IS the nearest-entry
|
|
1050
|
+
// search). Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a
|
|
1051
|
+
// packed LUT: (0x78 >> 2L) & 3.
|
|
1052
|
+
struct Proj {
|
|
1053
|
+
indices: u32,
|
|
1054
|
+
err: h,
|
|
1055
|
+
sAA: h, sBB: h, sAB: h,
|
|
1056
|
+
sAV: h3, sBV: h3,
|
|
1057
|
+
s_min: h, s_max: h,
|
|
1058
|
+
};
|
|
1059
|
+
fn project_stats(pix: ptr<function, array<h3, 16>>, c0: u32, c1: u32) -> Proj {
|
|
1060
|
+
var out: Proj;
|
|
1061
|
+
out.indices = 0u;
|
|
1062
|
+
out.err = h(0.0);
|
|
1063
|
+
out.sAA = h(0.0); out.sBB = h(0.0); out.sAB = h(0.0);
|
|
1064
|
+
out.sAV = h3(0.0); out.sBV = h3(0.0);
|
|
1065
|
+
out.s_min = h(3.0); out.s_max = h(0.0);
|
|
1066
|
+
let p0 = from565(c0);
|
|
1067
|
+
let p1 = from565(c1);
|
|
1068
|
+
let dir = p1 - p0;
|
|
1069
|
+
let dd = dot(dir, dir);
|
|
1070
|
+
if (dd == h(0.0)) {
|
|
1071
|
+
// Unreachable for distinct 565 codes (the decode is injective); kept so
|
|
1072
|
+
// a degenerate call still returns a consistent error.
|
|
1073
|
+
out.s_min = h(0.0);
|
|
1074
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1075
|
+
let e = (*pix)[k] - p0;
|
|
1076
|
+
out.err = out.err + dot(e, e);
|
|
1077
|
+
}
|
|
1078
|
+
return out;
|
|
1079
|
+
}
|
|
1080
|
+
let inv = h(3.0) / dd;
|
|
1081
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1082
|
+
let v = (*pix)[k];
|
|
1083
|
+
let s = clamp(floor(dot(v - p0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
1084
|
+
out.s_min = min(out.s_min, s); out.s_max = max(out.s_max, s);
|
|
1085
|
+
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
1086
|
+
out.sAA = out.sAA + a * a; out.sBB = out.sBB + b * b; out.sAB = out.sAB + a * b;
|
|
1087
|
+
out.sAV = out.sAV + a * v; out.sBV = out.sBV + b * v;
|
|
1088
|
+
let e = v - (p0 + b * dir);
|
|
1089
|
+
out.err = out.err + dot(e, e);
|
|
1090
|
+
out.indices = out.indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
1091
|
+
}
|
|
1092
|
+
return out;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
499
1095
|
@compute @workgroup_size(8, 8, 1)
|
|
500
1096
|
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
501
1097
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
@@ -507,13 +1103,23 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
507
1103
|
var mn = h3(1.0);
|
|
508
1104
|
var mxv = h3(0.0);
|
|
509
1105
|
var mean = h3(0.0);
|
|
1106
|
+
var gd = h(0.0);
|
|
510
1107
|
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
511
1108
|
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
512
1109
|
let px = h3(textureLoad(src_tex, p, 0).rgb);
|
|
513
1110
|
pix[i] = px; mn = min(mn, px); mxv = max(mxv, px);
|
|
514
1111
|
mean = mean + px;
|
|
1112
|
+
gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));
|
|
515
1113
|
}
|
|
516
1114
|
mean = mean * h(1.0 / 16.0);
|
|
1115
|
+
// Exactly-gray blocks free the refit from the bbox clamp below: a gray
|
|
1116
|
+
// block has no hue to bend (the clamp's whole purpose), and on smooth
|
|
1117
|
+
// gradients the LSQ optimum often lies OUTSIDE the data range \u2014 endpoints
|
|
1118
|
+
// spread wider than the block so the 1/3-2/3 interpolants land on the
|
|
1119
|
+
// values. Same rationale as the BC5 scalar channels (+0.32 dB there).
|
|
1120
|
+
let gray = gd == h(0.0);
|
|
1121
|
+
let lim_lo = select(mn, h3(0.0), gray);
|
|
1122
|
+
let lim_hi = select(mxv, h3(1.0), gray);
|
|
517
1123
|
|
|
518
1124
|
// Seed endpoints from the block's principal colour axis (covariance
|
|
519
1125
|
// power-iteration, seeded with the bbox diagonal \u2014 same family as the
|
|
@@ -568,35 +1174,15 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
568
1174
|
let seed = order565(to565(seed_hi), to565(seed_lo));
|
|
569
1175
|
var c0 = seed.x;
|
|
570
1176
|
var c1 = seed.y;
|
|
571
|
-
let p0 = from565(c0);
|
|
572
|
-
let p1 = from565(c1);
|
|
573
1177
|
|
|
574
|
-
// Fused pass
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (dd > h(0.0)) {
|
|
583
|
-
let inv = h(3.0) / dd;
|
|
584
|
-
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
585
|
-
var sAV = h3(0.0); var sBV = h3(0.0);
|
|
586
|
-
var s_min = h(3.0); var s_max = h(0.0);
|
|
587
|
-
var seed_err = h(0.0);
|
|
588
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
589
|
-
let v = pix[k];
|
|
590
|
-
let s = clamp(floor(dot(v - p0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
591
|
-
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
592
|
-
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
593
|
-
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
594
|
-
sAV = sAV + a * v; sBV = sBV + b * v;
|
|
595
|
-
let e = v - (p0 + b * dir);
|
|
596
|
-
seed_err = seed_err + dot(e, e);
|
|
597
|
-
indices = indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
598
|
-
}
|
|
599
|
-
let det = sAA * sBB - sAB * sAB;
|
|
1178
|
+
// Fused seed pass, then up to TWO least-squares refit rounds (mirroring
|
|
1179
|
+
// the high path's iterated refits, at projection cost), each accepted only
|
|
1180
|
+
// if the block's squared error actually decreases \u2014 the refit minimises a
|
|
1181
|
+
// continuous objective and can lose after 565 quantisation. Every pass
|
|
1182
|
+
// re-accumulates the normal-equation sums, so an accepted round seeds the
|
|
1183
|
+
// next.
|
|
1184
|
+
var cur = project_stats(&pix, c0, c1);
|
|
1185
|
+
for (var it: u32 = 0u; it < 2u; it = it + 1u) {
|
|
600
1186
|
// Refit only on a well-conditioned system. When every pixel lands on ONE
|
|
601
1187
|
// level (flat / near-flat blocks \u2014 note the 4-colour-mode nudge forces
|
|
602
1188
|
// c0 \u2260 c1 even for perfectly flat blocks) the system is rank-1: det is 0
|
|
@@ -604,45 +1190,30 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
604
1190
|
// noise, so the solve returns garbage endpoints. With \u22652 distinct levels
|
|
605
1191
|
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15\xB7(1/3)\xB2 \u2248 1.67, far above the ~0.05 f16
|
|
606
1192
|
// noise floor \u2014 0.5 separates the two regimes cleanly.
|
|
607
|
-
if (s_min
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
var refit_err = h(0.0);
|
|
627
|
-
var nindices: u32 = 0u;
|
|
628
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
629
|
-
let v = pix[k];
|
|
630
|
-
let s = clamp(floor(dot(v - np0, ndir) * ninv + h(0.5)), h(0.0), h(3.0));
|
|
631
|
-
let e = v - (np0 + s * h(1.0 / 3.0) * ndir);
|
|
632
|
-
refit_err = refit_err + dot(e, e);
|
|
633
|
-
nindices = nindices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
634
|
-
}
|
|
635
|
-
if (refit_err < seed_err) {
|
|
636
|
-
c0 = refit.x; c1 = refit.y;
|
|
637
|
-
indices = nindices;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
1193
|
+
if (cur.s_min >= cur.s_max) { break; }
|
|
1194
|
+
let det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;
|
|
1195
|
+
if (abs(det) <= h(0.5)) { break; }
|
|
1196
|
+
// Clamp the refit to the block bbox (not [0,1]) \u2014 except for exactly
|
|
1197
|
+
// gray blocks, see the load pass: on multi-cluster blocks the
|
|
1198
|
+
// unconstrained solve extrapolates far outside the block's colours
|
|
1199
|
+
// and the per-channel clamp then bends the hue \u2014 fringe pixels decode to
|
|
1200
|
+
// colours that exist nowhere in the block. Constraining to the bbox also
|
|
1201
|
+
// measures better in plain SSE (+1.6 dB on the colour test card), so the
|
|
1202
|
+
// accept-if-better guard below keeps more refits.
|
|
1203
|
+
let e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, lim_lo, lim_hi);
|
|
1204
|
+
let e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, lim_lo, lim_hi);
|
|
1205
|
+
let rq = order565(to565(e0), to565(e1));
|
|
1206
|
+
if (rq.x == c0 && rq.y == c1) { break; }
|
|
1207
|
+
let nxt = project_stats(&pix, rq.x, rq.y);
|
|
1208
|
+
if (nxt.err >= cur.err) { break; }
|
|
1209
|
+
c0 = rq.x;
|
|
1210
|
+
c1 = rq.y;
|
|
1211
|
+
cur = nxt;
|
|
641
1212
|
}
|
|
642
1213
|
|
|
643
1214
|
let o = bi * 2u;
|
|
644
1215
|
dst[o] = c0 | (c1 << 16u);
|
|
645
|
-
dst[o + 1u] = indices;
|
|
1216
|
+
dst[o + 1u] = cur.indices;
|
|
646
1217
|
}
|
|
647
1218
|
`;
|
|
648
1219
|
|
|
@@ -659,9 +1230,6 @@ var BC1Encoder = class extends Encoder {
|
|
|
659
1230
|
get supportsSrgb() {
|
|
660
1231
|
return true;
|
|
661
1232
|
}
|
|
662
|
-
get supportsQuality() {
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
1233
|
wgslSource() {
|
|
666
1234
|
return bc1_default;
|
|
667
1235
|
}
|
|
@@ -674,12 +1242,12 @@ var BC1Encoder = class extends Encoder {
|
|
|
674
1242
|
};
|
|
675
1243
|
|
|
676
1244
|
// src/bc5.wgsl
|
|
677
|
-
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 + O(1) projection assignment per texel.\n// The 8-entry palette in 6-interp mode is colinear and EVENLY spaced from\n// r0 to r1 (levels 0..7 in palette order 0,2,3,4,5,6,7,1), so the nearest\n// entry is the rounded projection onto the r0\u2192r1 axis \u2014 no 8-entry\n// search, and the 3-bit indices are packed on the fly. The LSQ refit sums\n// are accumulated in the same fused pass; the requantised refit is\n// accepted only if it lowers the block error (worth ~1.3 dB on the\n// normal-map card).\n// high (1): full nearest search + refit, matches 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).\n// vmin/vmax are the channel's min/max, computed in the caller's load loop \u2014\n// fusing that scan there saves a 16-value pass per channel.\nfn encode_bc4(values: ptr<function, array<f32, 16>>, vmin: f32, vmax: f32) -> vec2<u32> {\n // ---------------- 1. Initial endpoints: bbox of input ----------------\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 if (QUALITY_HIGH == 0u) {\n // -------- fast: fused projection + LSQ refit, accept-if-better -------\n // ONE fused pass: level = round(7\xB7(v \u2212 r0)/(r1 \u2212 r0)) projection\n // assignment (the 8-entry 6-interp palette is colinear and evenly\n // spaced, so the rounded projection IS the nearest-entry search), the\n // seed solution's packed indices and squared error, and the least-\n // squares normal-equation sums. The refit endpoints are re-quantised,\n // reprojected, and accepted only if the block error decreases \u2014 worth\n // ~1.3 dB on the normal-map card over the refit-free seed.\n // Level \u2192 BC4 index LUT (0,2,3,4,5,6,7,1) packed as 3-bit entries in\n // 0x3F58D0. Pixel k's 3 bits start at bit 3k+16 of the (w0,w1) pair\n // (bytes 0..1 are the endpoints); k = 5 straddles the word boundary.\n let r0f = f32(r0) / 255.0;\n let dir = f32(r1) / 255.0 - r0f;\n let scale = 7.0 / dir;\n var w0 = r0 | (r1 << 8u);\n var w1 = 0u;\n var sAA = 0.0; var sBB = 0.0; var sAB = 0.0;\n var sAV = 0.0; var sBV = 0.0;\n var s_min = 7.0; var s_max = 0.0;\n var seed_err = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let vr = (*values)[k] - r0f;\n let L = clamp(floor(vr * scale + 0.5), 0.0, 7.0);\n s_min = min(s_min, L); s_max = max(s_max, L);\n let b = L * (1.0 / 7.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * vr; sBV = sBV + b * vr;\n let e = vr - b * dir;\n seed_err = seed_err + e * e;\n let idx = (0x3F58D0u >> (u32(L) * 3u)) & 7u;\n let bit = 3u * k + 16u;\n if (bit <= 29u) {\n w0 = w0 | (idx << bit);\n } else if (bit >= 32u) {\n w1 = w1 | (idx << (bit - 32u));\n } else {\n w0 = w0 | (idx << bit);\n w1 = w1 | (idx >> (32u - bit));\n }\n }\n\n // Rank-1 guard: with every pixel on ONE level the system is singular\n // (det is float rounding noise); with \u22652 distinct levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15/49 \u2248 0.306.\n if (s_min < s_max) {\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) > 1e-3) {\n // Clamp the refit to the block's value range (a strict-SSE win vs\n // clamping to [0,1], same as the other formats' fast paths).\n let e0 = clamp(r0f + (sBB * sAV - sAB * sBV) / det, vmin, vmax);\n let e1 = clamp(r0f + (sAA * sBV - sAB * sAV) / det, vmin, vmax);\n let n0 = quantize8(e0);\n let n1 = quantize8(e1);\n // Keep 6-interp mode (r0 > r1 strictly); skip the no-op refit.\n if (n0 > n1 && !(n0 == r0 && n1 == r1)) {\n let n0f = f32(n0) / 255.0;\n let ndir = f32(n1) / 255.0 - n0f;\n let nscale = 7.0 / ndir;\n var nw0 = n0 | (n1 << 8u);\n var nw1 = 0u;\n var refit_err = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let vr = (*values)[k] - n0f;\n let L = clamp(floor(vr * nscale + 0.5), 0.0, 7.0);\n let e = vr - L * (1.0 / 7.0) * ndir;\n refit_err = refit_err + e * e;\n let idx = (0x3F58D0u >> (u32(L) * 3u)) & 7u;\n let bit = 3u * k + 16u;\n if (bit <= 29u) {\n nw0 = nw0 | (idx << bit);\n } else if (bit >= 32u) {\n nw1 = nw1 | (idx << (bit - 32u));\n } else {\n nw0 = nw0 | (idx << bit);\n nw1 = nw1 | (idx >> (32u - bit));\n }\n }\n if (refit_err < seed_err) {\n return vec2<u32>(nw0, nw1);\n }\n }\n }\n }\n return vec2<u32>(w0, w1);\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; the per-channel min/max scan is\n // fused into the same loop.\n var r_values: array<f32, 16>;\n var g_values: array<f32, 16>;\n var r_min: f32 = 1.0; var r_max: f32 = 0.0;\n var g_min: f32 = 1.0; var g_max: f32 = 0.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 // 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 r_min = min(r_min, c.r); r_max = max(r_max, c.r);\n g_min = min(g_min, c.g); g_max = max(g_max, c.g);\n }\n\n let r_block = encode_bc4(&r_values, r_min, r_max);\n let g_block = encode_bc4(&g_values, g_min, g_max);\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";
|
|
1245
|
+
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. This is the f32\n// fallback; bc5_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\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 `bc4_ref.ts`\n// for the reference this encoder is validated against.\n//\n// Same algorithm as bc5_fast_f16.wgsl (see that file for the full notes):\n// \u2022 both channels processed as vec2 lanes of the fused passes;\n// \u2022 pass 1 accumulates MOMENTS (\u03A3L, \u03A3L\xB2, \u03A3\u03C1, \u03A3L\u03C1) from which every LSQ\n// normal-equation sum is an O(1) per-block expression; the rank guard\n// is the exact 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2 test;\n// \u2022 the closed-form refit prices the nearest rounding of the solve\n// through E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB\n// + \u03B41\xB2sBB, accept-if-better;\n// \u2022 pass 2 packs the indices ONCE, against the FINAL endpoints \u2014 full\n// reprojection quality at parity cost;\n// \u2022 the 16 texel reads are 8 textureGather fetches (4 quads \xD7 R,G)\n// through a clamp-to-edge sampler, byte-identical to per-texel loads;\n// \u2022 3-bit indices accumulate branch-free into two 24-bit words (pixels\n// 0..7 and 8..15) recombined with constant shifts \u2014 no per-pixel\n// straddle branches.\n// Values are kept in the [0,255] f32 domain throughout.\n//\n// Level \u2192 BC4 index LUT (0,2,3,4,5,6,7,1) packed as 3-bit entries in\n// 0x3F58D0.\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@group(0) @binding(3) var smp: sampler;\n\nconst IDX_LUT: u32 = 0x3F58D0u;\n\n// Closed-form accept-if-better endpoint refinement for one channel \u2014 same\n// as the f16 module's `refine` (both run this per-block step in f32).\n// Endpoints clamp to [0,255], NOT the block's value range: for a scalar\n// channel, endpoints beyond the data range are often genuinely optimal and\n// there is no colour axis to bend.\nfn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, spread: bool) -> vec2<u32> {\n var out = vec2<u32>(b0, b1);\n let det = sAA * sBB - sAB * sAB;\n if (!spread || abs(det) <= 1e-3) { return out; }\n let b0f = f32(b0);\n let b1f = f32(b1);\n let e0 = clamp(b0f + (sBB * sAR - sAB * sBR) / det, 0.0, 255.0);\n let e1 = clamp(b1f + (sAA * sBR - sAB * sAR) / det, 0.0, 255.0);\n let q0f = floor(e0 + 0.5);\n let q1f = floor(e1 + 0.5);\n let q0 = u32(q0f);\n let q1 = u32(q1f);\n if (q0 > q1 && !(q0 == b0 && q1 == b1)) {\n let dd0 = q0f - b0f;\n let dd1 = q1f - b1f;\n let eNew = -2.0 * (dd0 * sAR + dd1 * sBR)\n + dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;\n if (eNew < 0.0) {\n out = vec2<u32>(q0, q1);\n }\n }\n return out;\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 let bi = gid.y * params.blocks_x + gid.x;\n let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n\n // Load 4\xD74 R/G pairs (x = R, y = G throughout), min/max fused in.\n // Interior blocks read via 8 gathers normalised by the PHYSICAL (padded)\n // texture size; blocks straddling the source edge of a non-multiple-of-4\n // image fall back to per-texel loads clamped to the last real texel (the\n // padding strip is zero-initialised \u2014 see bc5_fast_f16.wgsl).\n // Gather components: w=(0,0) z=(1,0) x=(0,1) y=(1,1) within each quad.\n var v: array<vec2<f32>, 16>;\n var vmin = vec2<f32>(255.0);\n var vmax = vec2<f32>(0.0);\n if (u32(base.x) + 4u <= params.width && u32(base.y) + 4u <= params.height) {\n let inv_size = vec2<f32>(1.0, 1.0) / vec2<f32>(textureDimensions(src_tex));\n for (var q: u32 = 0u; q < 4u; q = q + 1u) {\n let qo = vec2<u32>((q & 1u) * 2u, (q >> 1u) * 2u);\n let cc = (vec2<f32>(base) + vec2<f32>(qo) + vec2<f32>(1.0, 1.0)) * inv_size;\n let r4 = textureGather(0, src_tex, smp, cc) * 255.0;\n let g4 = textureGather(1, src_tex, smp, cc) * 255.0;\n let i = qo.y * 4u + qo.x;\n let vw = vec2<f32>(r4.w, g4.w);\n let vz = vec2<f32>(r4.z, g4.z);\n let vx = vec2<f32>(r4.x, g4.x);\n let vy = vec2<f32>(r4.y, g4.y);\n v[i] = vw; v[i + 1u] = vz; v[i + 4u] = vx; v[i + 5u] = vy;\n vmin = min(min(vmin, min(vw, vz)), min(vx, vy));\n vmax = max(max(vmax, max(vw, vz)), max(vx, vy));\n }\n } else {\n let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\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), mx);\n let c = textureLoad(src_tex, p, 0);\n let val = vec2<f32>(c.r, c.g) * 255.0;\n v[i] = val; vmin = min(vmin, val); vmax = max(vmax, val);\n }\n }\n\n // Seed endpoints at the exact per-channel extremes (round-to-nearest, the\n // same rule the CPU reference uses). Flat blocks get nudged apart to keep\n // the 6-interp mode (r0 > r1 strictly).\n var r0 = vec2<u32>(clamp(floor(vmax + 0.5), vec2<f32>(0.0), vec2<f32>(255.0)));\n var r1 = vec2<u32>(clamp(floor(vmin + 0.5), vec2<f32>(0.0), vec2<f32>(255.0)));\n if (r0.x == r1.x) { if (r1.x > 0u) { r1.x = r1.x - 1u; } else { r0.x = r0.x + 1u; } }\n if (r0.y == r1.y) { if (r1.y > 0u) { r1.y = r1.y - 1u; } else { r0.y = r0.y + 1u; } }\n\n let r0f = vec2<f32>(r0);\n let r1f = vec2<f32>(r1);\n let dir = r1f - r0f;\n let scale = vec2<f32>(7.0) / dir;\n\n // Pass 1, both channels \u2014 MOMENTS only. t = 7(v\u2212r0)/(r1\u2212r0) \u2208 [0,7]\n // (the seed covers the data), L = round(t), \u03C1 = t \u2212 L.\n var sL = vec2<f32>(0.0); var sLL = vec2<f32>(0.0);\n var pR = vec2<f32>(0.0); var pLR = vec2<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = (v[k] - r0f) * scale;\n let L = clamp(floor(t + 0.5), vec2<f32>(0.0), vec2<f32>(7.0));\n let rho = t - L;\n sL = sL + L; sLL = sLL + L * L;\n pR = pR + rho; pLR = pLR + L * rho;\n }\n\n // Per-block refit off the moments (see bc5_fast_f16.wgsl for the\n // identities).\n let sBB = sLL * (1.0 / 49.0);\n let sAB = sL * (1.0 / 7.0) - sBB;\n let sAA = vec2<f32>(16.0) - 2.0 * sL * (1.0 / 7.0) + sBB;\n let sBR = pLR * dir * (1.0 / 49.0);\n let sAR = (pR - pLR * (1.0 / 7.0)) * dir * (1.0 / 7.0);\n let spread = 16.0 * sLL != sL * sL;\n\n let fx = refine(sAA.x, sBB.x, sAB.x, sAR.x, sBR.x, r0.x, r1.x, spread.x);\n let fy = refine(sAA.y, sBB.y, sAB.y, sAR.y, sBR.y, r0.y, r1.y, spread.y);\n let n0 = vec2<u32>(fx.x, fy.x);\n let n1 = vec2<u32>(fx.y, fy.y);\n\n // Pass 2, both channels \u2014 pack the shipped indices against the FINAL\n // endpoints (rejected channels re-derive their seed assignment). iA\n // holds pixels 0..7 (3 bits each), iB pixels 8..15.\n let n0f = vec2<f32>(n0);\n let n1f = vec2<f32>(n1);\n let scale2 = vec2<f32>(7.0) / (n1f - n0f);\n var iAx = 0u; var iBx = 0u; var iAy = 0u; var iBy = 0u;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let L = clamp(floor((v[k] - n0f) * scale2 + 0.5), vec2<f32>(0.0), vec2<f32>(7.0));\n iAx = iAx | (((IDX_LUT >> (u32(L.x) * 3u)) & 7u) << (k * 3u));\n iAy = iAy | (((IDX_LUT >> (u32(L.y) * 3u)) & 7u) << (k * 3u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let L = clamp(floor((v[k] - n0f) * scale2 + 0.5), vec2<f32>(0.0), vec2<f32>(7.0));\n iBx = iBx | (((IDX_LUT >> (u32(L.x) * 3u)) & 7u) << ((k - 8u) * 3u));\n iBy = iBy | (((IDX_LUT >> (u32(L.y) * 3u)) & 7u) << ((k - 8u) * 3u));\n }\n\n // BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.\n let o = bi * 4u;\n dst[o] = n0.x | (n1.x << 8u) | (iAx << 16u);\n dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);\n dst[o + 2u] = n0.y | (n1.y << 8u) | (iAy << 16u);\n dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);\n}\n";
|
|
678
1246
|
|
|
679
1247
|
// src/bc5_fast_f16.wgsl
|
|
680
1248
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
681
|
-
// Two BC4 halves (R and G) \u2014 same output family as bc5.wgsl
|
|
682
|
-
//
|
|
1249
|
+
// Two BC4 halves (R and G) \u2014 same output family as bc5.wgsl, tuned for
|
|
1250
|
+
// throughput:
|
|
683
1251
|
//
|
|
684
1252
|
// \u2022 The 8-entry palette in 6-interpolation mode is COLINEAR and EVENLY
|
|
685
1253
|
// spaced from r0 to r1 (levels 0..7 in palette order 0,2,3,4,5,6,7,1),
|
|
@@ -688,162 +1256,218 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
688
1256
|
// \u2022 Math runs in the exact-integer [0,255] f16 domain: endpoints and pixel
|
|
689
1257
|
// values are whole numbers \u2264 255 (exact in f16), so the only rounding is
|
|
690
1258
|
// the single 1/(r1\u2212r0) division.
|
|
691
|
-
// \u2022
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
1259
|
+
// \u2022 BOTH channels ride the same fused passes as vec2<f16> lanes \u2014 each
|
|
1260
|
+
// loop computes projections and moments for R and G at once instead of
|
|
1261
|
+
// two scalar encode_bc4 calls.
|
|
1262
|
+
// \u2022 The 16 texel reads are 8 textureGather fetches (4 quads \xD7 R,G) for
|
|
1263
|
+
// interior blocks \u2014 byte-identical output to per-texel loads, \u22123.6%
|
|
1264
|
+
// GPU on 4096\xB2 (/ab, 2026-07). Blocks straddling the source edge of a
|
|
1265
|
+
// non-multiple-of-4 image use clamped per-texel loads instead: the
|
|
1266
|
+
// upload pads the texture with ZEROS, so a normalised-coordinate
|
|
1267
|
+
// gather there would read padding (or mis-scale against the padded
|
|
1268
|
+
// size) instead of replicating the last real texel.
|
|
1269
|
+
// \u2022 Pass 1 accumulates MOMENTS, not normal-equation sums. With b = L/7
|
|
1270
|
+
// and the level-space residual \u03C1 = t \u2212 L (t = 7(v\u2212r0)/(r1\u2212r0), so the
|
|
1271
|
+
// value-space residual is r = \u03C1\xB7dir/7), every LSQ sum is an O(1)
|
|
1272
|
+
// per-block function of four accumulators:
|
|
1273
|
+
// sBB = \u03A3L\xB2/49 sAB = \u03A3L/7 \u2212 \u03A3L\xB2/49 sAA = 16 \u2212 2\u03A3L/7 + \u03A3L\xB2/49
|
|
1274
|
+
// sBR = \u03A3L\u03C1\xB7dir/49 sAR = (\u03A3\u03C1 \u2212 \u03A3L\u03C1/7)\xB7dir/7
|
|
1275
|
+
// Per-pixel work drops from 5 product-accumulates + 3 temporaries to 4
|
|
1276
|
+
// cheap accumulates (\u03A3L, \u03A3L\xB2, \u03A3\u03C1, \u03A3L\u03C1), and the f16 range analysis
|
|
1277
|
+
// becomes trivial: \u03A3L \u2264 112 and \u03A3L\xB2 \u2264 784 are exact f16 integers,
|
|
1278
|
+
// |\u03C1| \u2264 ~0.5 keeps \u03A3\u03C1/\u03A3L\u03C1 tiny. The per-BLOCK refit math (including
|
|
1279
|
+
// the solve and E(\u03B4) pricing) runs in f32 \u2014 free at block granularity,
|
|
1280
|
+
// and it retires the sum-cancellation worries the old residual-sum
|
|
1281
|
+
// scheme was built around.
|
|
1282
|
+
// \u2022 The rank guard is EXACT: all pixels on one level \u27FA 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2
|
|
1283
|
+
// (integers, so the comparison is precise in f32) \u2014 no lmin/lmax
|
|
1284
|
+
// tracking in the loop.
|
|
1285
|
+
// \u2022 The refit is accepted or rejected CLOSED-FORM, with no trial
|
|
1286
|
+
// projection pass: the solve is e = seed + M\u207B\xB9(sAR,sBR), and the error
|
|
1287
|
+
// of re-quantised endpoints ON THE CURRENT INDICES is
|
|
1288
|
+
// E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB + \u03B41\xB2sBB
|
|
1289
|
+
// with \u03B4 = quantised endpoint \u2212 base endpoint, compared as the delta
|
|
1290
|
+
// form E \u2212 err < 0. Only the NEAREST rounding of the fractional solve
|
|
1291
|
+
// is priced: pricing all four floor/ceil combinations (the correlated
|
|
1292
|
+
// 2-D quadratic's integer optimum isn't always the nearest rounding)
|
|
1293
|
+
// measured \u22640.015 dB on every content class but ~8% GPU on smooth
|
|
1294
|
+
// content, where Apple's lossless texture compression collapses the
|
|
1295
|
+
// read cost and leaves the kernel ALU-bound (/ab 2026-07, rock 4K
|
|
1296
|
+
// displacement: floor 0.61\xD7 of the rgba8-noise floor).
|
|
1297
|
+
// \u2022 Pass 2 packs the indices ONCE, against the FINAL endpoints \u2014 full
|
|
1298
|
+
// reprojection quality. The previous single-pass scheme shipped the
|
|
1299
|
+
// SEED indices with accepted refit endpoints, giving up 0.08..0.19 dB
|
|
1300
|
+
// because a third reprojection loop cost +14% GPU; with the moment
|
|
1301
|
+
// form paying for the second loop, reprojection now measures at PARITY
|
|
1302
|
+
// with that scheme (/ab 2026-07, interleaved: proc 2048\xB2/4096\xB2 and the
|
|
1303
|
+
// rock 4K normal map, all within \xB11%, vs read floor ~2% below).
|
|
1304
|
+
// Measured and NOT taken: a second refit round off pass-2 moments
|
|
1305
|
+
// (+14%, register cliff, \u22480 gain on smooth content \u2014 round 2 only pays
|
|
1306
|
+
// on noise); full normal-equation sums in both passes (+49%).
|
|
1307
|
+
// \u2022 3-bit indices are packed BRANCH-FREE: pixels 0..7 accumulate into a
|
|
1308
|
+
// 24-bit word, pixels 8..15 into another, recombined with constant
|
|
1309
|
+
// shifts into the 48-bit field (w0 gets field bits 0..15 above the two
|
|
1310
|
+
// endpoint bytes, w1 gets field bits 16..47) \u2014 no per-pixel straddle
|
|
1311
|
+
// branches.
|
|
705
1312
|
//
|
|
706
1313
|
// Level \u2192 BC4 index (0\u2192r0 ... 7\u2192r1): 0,2,3,4,5,6,7,1 \u2014 packed 3-bit LUT
|
|
707
1314
|
// 0x3F58D0 = sum(idx[L] << 3L).
|
|
708
1315
|
//
|
|
709
1316
|
// The host selects this module only when the device reports shader-f16,
|
|
710
|
-
// falling back to bc5.wgsl otherwise.
|
|
1317
|
+
// falling back to bc5.wgsl otherwise.
|
|
711
1318
|
enable f16;
|
|
712
1319
|
alias h = f16;
|
|
1320
|
+
alias h2 = vec2<f16>;
|
|
713
1321
|
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
714
1322
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
715
1323
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
716
1324
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
out
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1325
|
+
@group(0) @binding(3) var smp: sampler;
|
|
1326
|
+
|
|
1327
|
+
const IDX_LUT: u32 = 0x3F58D0u;
|
|
1328
|
+
|
|
1329
|
+
// Closed-form accept-if-better endpoint refinement for one channel (f32:
|
|
1330
|
+
// per-block O(1) work, so precision is free here). Takes the LSQ sums for
|
|
1331
|
+
// the current indices, the current integer endpoints b0 > b1, and the rank
|
|
1332
|
+
// guard; prices the nearest rounding of the fractional solve via E(\u03B4) and
|
|
1333
|
+
// returns it when it strictly improves and stays in 6-interp mode
|
|
1334
|
+
// (q0 > q1), or (b0,b1) unchanged. Endpoints clamp to [0,255], NOT the
|
|
1335
|
+
// block's value range: for a scalar channel, endpoints beyond the data
|
|
1336
|
+
// range are often genuinely optimal and there is no colour axis to bend
|
|
1337
|
+
// (the bbox clamp the colour formats need costs ~0.3 dB here).
|
|
1338
|
+
fn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, spread: bool) -> vec2<u32> {
|
|
1339
|
+
var out = vec2<u32>(b0, b1);
|
|
1340
|
+
let det = sAA * sBB - sAB * sAB;
|
|
1341
|
+
if (!spread || abs(det) <= 1e-3) { return out; }
|
|
1342
|
+
let b0f = f32(b0);
|
|
1343
|
+
let b1f = f32(b1);
|
|
1344
|
+
let e0 = clamp(b0f + (sBB * sAR - sAB * sBR) / det, 0.0, 255.0);
|
|
1345
|
+
let e1 = clamp(b1f + (sAA * sBR - sAB * sAR) / det, 0.0, 255.0);
|
|
1346
|
+
let q0f = floor(e0 + 0.5);
|
|
1347
|
+
let q1f = floor(e1 + 0.5);
|
|
1348
|
+
let q0 = u32(q0f);
|
|
1349
|
+
let q1 = u32(q1f);
|
|
1350
|
+
if (q0 > q1 && !(q0 == b0 && q1 == b1)) {
|
|
1351
|
+
let dd0 = q0f - b0f;
|
|
1352
|
+
let dd1 = q1f - b1f;
|
|
1353
|
+
let eNew = -2.0 * (dd0 * sAR + dd1 * sBR)
|
|
1354
|
+
+ dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;
|
|
1355
|
+
if (eNew < 0.0) {
|
|
1356
|
+
out = vec2<u32>(q0, q1);
|
|
746
1357
|
}
|
|
747
1358
|
}
|
|
748
1359
|
return out;
|
|
749
1360
|
}
|
|
750
1361
|
|
|
751
|
-
// Encode one channel (16 values in exact-integer [0,255] f16) to a BC4 half.
|
|
752
|
-
// vmin/vmax are the channel's min/max, computed in the caller's load loop \u2014
|
|
753
|
-
// fusing that scan there saves a 16-value pass per channel.
|
|
754
|
-
fn encode_bc4(values: ptr<function, array<h, 16>>, vmin: h, vmax: h) -> vec2<u32> {
|
|
755
|
-
var r0 = u32(vmax); // values are exact integers \u2014 no rounding needed
|
|
756
|
-
var r1 = u32(vmin);
|
|
757
|
-
if (r0 == r1) {
|
|
758
|
-
// Flat block: nudge to keep 6-interp mode (r0 > r1 strictly).
|
|
759
|
-
if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Fused seed pass: projection assignment (level = round(7\xB7(v \u2212 r0)/
|
|
763
|
-
// (r1 \u2212 r0)), clamped \u2014 |v \u2212 r0| \u2264 r0 \u2212 r1 for in-block values) + packed
|
|
764
|
-
// indices + seed error + the LSQ normal-equation sums. Value sums
|
|
765
|
-
// accumulate (v \u2212 r0)/16: the shift keeps the accumulators proportional to
|
|
766
|
-
// the block's span, the exact power-of-two scale keeps products \u2264 4080.
|
|
767
|
-
let r0f = h(f32(r0));
|
|
768
|
-
let dir = h(f32(r1)) - r0f;
|
|
769
|
-
let scale = h(7.0) / dir;
|
|
770
|
-
var seed: Proj;
|
|
771
|
-
seed.w0 = r0 | (r1 << 8u);
|
|
772
|
-
seed.w1 = 0u;
|
|
773
|
-
seed.err = h(0.0);
|
|
774
|
-
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
775
|
-
var sAV = h(0.0); var sBV = h(0.0);
|
|
776
|
-
var s_min = h(7.0); var s_max = h(0.0);
|
|
777
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
778
|
-
let vr = (*values)[k] - r0f;
|
|
779
|
-
let L = clamp(floor(vr * scale + h(0.5)), h(0.0), h(7.0));
|
|
780
|
-
s_min = min(s_min, L); s_max = max(s_max, L);
|
|
781
|
-
let b = L * h(1.0 / 7.0); let a = h(1.0) - b;
|
|
782
|
-
let vr16 = vr * h(1.0 / 16.0);
|
|
783
|
-
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
784
|
-
sAV = sAV + a * vr16; sBV = sBV + b * vr16;
|
|
785
|
-
let e = vr16 - b * dir * h(1.0 / 16.0);
|
|
786
|
-
seed.err = seed.err + e * e;
|
|
787
|
-
let idx = (0x3F58D0u >> (u32(L) * 3u)) & 7u;
|
|
788
|
-
let bit = 3u * k + 16u;
|
|
789
|
-
if (bit <= 29u) {
|
|
790
|
-
seed.w0 = seed.w0 | (idx << bit);
|
|
791
|
-
} else if (bit >= 32u) {
|
|
792
|
-
seed.w1 = seed.w1 | (idx << (bit - 32u));
|
|
793
|
-
} else {
|
|
794
|
-
// k = 5 straddles the word boundary (bits 31..33).
|
|
795
|
-
seed.w0 = seed.w0 | (idx << bit);
|
|
796
|
-
seed.w1 = seed.w1 | (idx >> (32u - bit));
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// LSQ refit, accepted only if the requantised endpoints lower the block
|
|
801
|
-
// error. Rank-1 guard: with every pixel on ONE level the system is
|
|
802
|
-
// singular and det is pure f16 rounding noise (\u22720.03); with \u22652 distinct
|
|
803
|
-
// levels det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15/49 \u2248 0.306 \u2014 0.1 separates cleanly.
|
|
804
|
-
if (s_min < s_max) {
|
|
805
|
-
let det = sAA * sBB - sAB * sAB;
|
|
806
|
-
if (abs(det) > h(0.1)) {
|
|
807
|
-
// \xD716 undoes the accumulator scale; clamp to the block's value range
|
|
808
|
-
// (a strict-SSE win vs clamping to [0,255], same as the other formats).
|
|
809
|
-
let e0 = clamp(r0f + (sBB * sAV - sAB * sBV) * h(16.0) / det, vmin, vmax);
|
|
810
|
-
let e1 = clamp(r0f + (sAA * sBV - sAB * sAV) * h(16.0) / det, vmin, vmax);
|
|
811
|
-
let n0 = u32(floor(e0 + h(0.5)));
|
|
812
|
-
let n1 = u32(floor(e1 + h(0.5)));
|
|
813
|
-
// Keep 6-interp mode (r0 > r1 strictly); skip the no-op refit.
|
|
814
|
-
if (n0 > n1 && !(n0 == r0 && n1 == r1)) {
|
|
815
|
-
let refit = project_pack(values, n0, n1);
|
|
816
|
-
if (refit.err < seed.err) {
|
|
817
|
-
return vec2<u32>(refit.w0, refit.w1);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
return vec2<u32>(seed.w0, seed.w1);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
1362
|
@compute @workgroup_size(8, 8, 1)
|
|
826
1363
|
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
827
1364
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
828
1365
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
829
1366
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1367
|
+
|
|
1368
|
+
// Load 4\xD74 R/G pairs (x = R, y = G throughout), min/max fused in.
|
|
1369
|
+
// INTERIOR blocks \u2014 every block when the source is a multiple of 4, so
|
|
1370
|
+
// the branch is wavefront-uniform on benchmark-shaped content \u2014 read via
|
|
1371
|
+
// 8 gathers (4 quads \xD7 R,G); the gather point (base+quad+1) normalised by
|
|
1372
|
+
// the PHYSICAL (padded) texture size sits exactly between the quad's
|
|
1373
|
+
// texel centers, and interior quads never touch the zero-initialised
|
|
1374
|
+
// padding strip. Blocks straddling the source edge of a
|
|
1375
|
+
// non-multiple-of-4 image fall back to per-texel loads clamped to the
|
|
1376
|
+
// last real texel (gather cannot replicate an edge texel mid-quad).
|
|
1377
|
+
// Gather components: w=(0,0) z=(1,0) x=(0,1) y=(1,1) within each quad.
|
|
1378
|
+
var v: array<h2, 16>;
|
|
1379
|
+
var vmin = h2(255.0);
|
|
1380
|
+
var vmax = h2(0.0);
|
|
1381
|
+
if (u32(base.x) + 4u <= params.width && u32(base.y) + 4u <= params.height) {
|
|
1382
|
+
let inv_size = vec2<f32>(1.0, 1.0) / vec2<f32>(textureDimensions(src_tex));
|
|
1383
|
+
for (var q: u32 = 0u; q < 4u; q = q + 1u) {
|
|
1384
|
+
let qo = vec2<u32>((q & 1u) * 2u, (q >> 1u) * 2u);
|
|
1385
|
+
let cc = (vec2<f32>(base) + vec2<f32>(qo) + vec2<f32>(1.0, 1.0)) * inv_size;
|
|
1386
|
+
let r4 = textureGather(0, src_tex, smp, cc) * 255.0;
|
|
1387
|
+
let g4 = textureGather(1, src_tex, smp, cc) * 255.0;
|
|
1388
|
+
let i = qo.y * 4u + qo.x;
|
|
1389
|
+
let vw = h2(h(r4.w), h(g4.w));
|
|
1390
|
+
let vz = h2(h(r4.z), h(g4.z));
|
|
1391
|
+
let vx = h2(h(r4.x), h(g4.x));
|
|
1392
|
+
let vy = h2(h(r4.y), h(g4.y));
|
|
1393
|
+
v[i] = vw; v[i + 1u] = vz; v[i + 4u] = vx; v[i + 5u] = vy;
|
|
1394
|
+
vmin = min(min(vmin, min(vw, vz)), min(vx, vy));
|
|
1395
|
+
vmax = max(max(vmax, max(vw, vz)), max(vx, vy));
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
1399
|
+
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
1400
|
+
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
1401
|
+
let c = textureLoad(src_tex, p, 0);
|
|
1402
|
+
let val = h2(h(c.r * 255.0), h(c.g * 255.0));
|
|
1403
|
+
v[i] = val; vmin = min(vmin, val); vmax = max(vmax, val);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Seed endpoints at the exact per-channel extremes (values are exact
|
|
1408
|
+
// integers \u2014 no rounding needed). Flat blocks get nudged apart to keep
|
|
1409
|
+
// the 6-interp mode (r0 > r1 strictly).
|
|
1410
|
+
var r0 = vec2<u32>(vmax);
|
|
1411
|
+
var r1 = vec2<u32>(vmin);
|
|
1412
|
+
if (r0.x == r1.x) { if (r1.x > 0u) { r1.x = r1.x - 1u; } else { r0.x = r0.x + 1u; } }
|
|
1413
|
+
if (r0.y == r1.y) { if (r1.y > 0u) { r1.y = r1.y - 1u; } else { r0.y = r0.y + 1u; } }
|
|
1414
|
+
|
|
1415
|
+
let r0f = h2(vec2<f32>(r0));
|
|
1416
|
+
let r1f = h2(vec2<f32>(r1));
|
|
1417
|
+
let dir = r1f - r0f;
|
|
1418
|
+
let scale = h2(7.0) / dir;
|
|
1419
|
+
|
|
1420
|
+
// Pass 1, both channels \u2014 MOMENTS only. t = 7(v\u2212r0)/(r1\u2212r0) \u2208 [0,7]
|
|
1421
|
+
// (the seed covers the data), L = round(t), \u03C1 = t \u2212 L.
|
|
1422
|
+
var sL = h2(0.0); var sLL = h2(0.0); var pR = h2(0.0); var pLR = h2(0.0);
|
|
1423
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1424
|
+
let t = (v[k] - r0f) * scale;
|
|
1425
|
+
let L = clamp(floor(t + h2(0.5)), h2(0.0), h2(7.0));
|
|
1426
|
+
let rho = t - L;
|
|
1427
|
+
sL = sL + L; sLL = sLL + L * L;
|
|
1428
|
+
pR = pR + rho; pLR = pLR + L * rho;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Per-block refit in f32 off the moments (see header for the identities).
|
|
1432
|
+
let sLf = vec2<f32>(sL);
|
|
1433
|
+
let sLLf = vec2<f32>(sLL);
|
|
1434
|
+
let dirf = vec2<f32>(r1) - vec2<f32>(r0);
|
|
1435
|
+
let sBB = sLLf * (1.0 / 49.0);
|
|
1436
|
+
let sAB = sLf * (1.0 / 7.0) - sBB;
|
|
1437
|
+
let sAA = vec2<f32>(16.0) - 2.0 * sLf * (1.0 / 7.0) + sBB;
|
|
1438
|
+
let sBR = vec2<f32>(pLR) * dirf * (1.0 / 49.0);
|
|
1439
|
+
let sAR = (vec2<f32>(pR) - vec2<f32>(pLR) * (1.0 / 7.0)) * dirf * (1.0 / 7.0);
|
|
1440
|
+
let spread = 16.0 * sLLf != sLf * sLf;
|
|
1441
|
+
|
|
1442
|
+
let fx = refine(sAA.x, sBB.x, sAB.x, sAR.x, sBR.x, r0.x, r1.x, spread.x);
|
|
1443
|
+
let fy = refine(sAA.y, sBB.y, sAB.y, sAR.y, sBR.y, r0.y, r1.y, spread.y);
|
|
1444
|
+
let n0 = vec2<u32>(fx.x, fy.x);
|
|
1445
|
+
let n1 = vec2<u32>(fx.y, fy.y);
|
|
1446
|
+
|
|
1447
|
+
// Pass 2, both channels \u2014 pack the shipped indices against the FINAL
|
|
1448
|
+
// endpoints (rejected channels re-derive their seed assignment). iA
|
|
1449
|
+
// holds pixels 0..7 (3 bits each), iB pixels 8..15.
|
|
1450
|
+
let n0f = h2(vec2<f32>(n0));
|
|
1451
|
+
let n1f = h2(vec2<f32>(n1));
|
|
1452
|
+
let scale2 = h2(7.0) / (n1f - n0f);
|
|
1453
|
+
var iAx = 0u; var iBx = 0u; var iAy = 0u; var iBy = 0u;
|
|
1454
|
+
for (var k: u32 = 0u; k < 8u; k = k + 1u) {
|
|
1455
|
+
let L = clamp(floor((v[k] - n0f) * scale2 + h2(0.5)), h2(0.0), h2(7.0));
|
|
1456
|
+
iAx = iAx | (((IDX_LUT >> (u32(L.x) * 3u)) & 7u) << (k * 3u));
|
|
1457
|
+
iAy = iAy | (((IDX_LUT >> (u32(L.y) * 3u)) & 7u) << (k * 3u));
|
|
1458
|
+
}
|
|
1459
|
+
for (var k: u32 = 8u; k < 16u; k = k + 1u) {
|
|
1460
|
+
let L = clamp(floor((v[k] - n0f) * scale2 + h2(0.5)), h2(0.0), h2(7.0));
|
|
1461
|
+
iBx = iBx | (((IDX_LUT >> (u32(L.x) * 3u)) & 7u) << ((k - 8u) * 3u));
|
|
1462
|
+
iBy = iBy | (((IDX_LUT >> (u32(L.y) * 3u)) & 7u) << ((k - 8u) * 3u));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// BC5 block = R half (bytes 0..7) || G half (bytes 8..15) = 4 u32s.
|
|
845
1466
|
let o = bi * 4u;
|
|
846
|
-
dst[o] =
|
|
1467
|
+
dst[o] = n0.x | (n1.x << 8u) | (iAx << 16u);
|
|
1468
|
+
dst[o + 1u] = (iAx >> 16u) | (iBx << 8u);
|
|
1469
|
+
dst[o + 2u] = n0.y | (n1.y << 8u) | (iAy << 16u);
|
|
1470
|
+
dst[o + 3u] = (iAy >> 16u) | (iBy << 8u);
|
|
847
1471
|
}
|
|
848
1472
|
`;
|
|
849
1473
|
|
|
@@ -860,8 +1484,13 @@ var BC5Encoder = class extends Encoder {
|
|
|
860
1484
|
get supportsSrgb() {
|
|
861
1485
|
return false;
|
|
862
1486
|
}
|
|
863
|
-
|
|
864
|
-
|
|
1487
|
+
// BC5 stores only R and G — a two-channel source texture halves the
|
|
1488
|
+
// encode pass's DRAM reads. Measured on the pass (/ab 2026-07): −4% on a
|
|
1489
|
+
// real 4K normal map, −8..−12% on procedural 2048²/4096², and the raw
|
|
1490
|
+
// read floor itself drops 35%, so the win grows as the kernel's ALU
|
|
1491
|
+
// shrinks. Also halves source-texture memory during encodes.
|
|
1492
|
+
get srcTextureFormat() {
|
|
1493
|
+
return "rg8unorm";
|
|
865
1494
|
}
|
|
866
1495
|
wgslSource() {
|
|
867
1496
|
return bc5_default;
|
|
@@ -875,202 +1504,25 @@ var BC5Encoder = class extends Encoder {
|
|
|
875
1504
|
};
|
|
876
1505
|
|
|
877
1506
|
// src/bc7.wgsl
|
|
878
|
-
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): principal-axis seed (covariance power-iteration; bbox\n// on degenerate blocks) at the exact projection extents, quantised\n// directly \u2014 no LSQ refit; with the seed on the principal axis, mode 6's\n// 16-level palette leaves the refit under 0.15 dB, unlike the 4-level\n// BC1/ASTC fast paths which keep theirs \u2014 then one pass that projects\n// each pixel onto the endpoint line (the 16 palette entries are\n// colinear, so the nearest index is the rounded projection \u2014 no palette\n// build, no 16-entry search), packed on the fly into two nibble words.\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// matches bc7_ref.ts up to FP tie-breaks.\n//\n// The fast/high branch is selected at pipeline-compile time, so the driver\n// eliminates the unused code entirely.\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// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\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// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0) for\n// a degenerate (constant) block. Same family as bc1.wgsl's principal_axis;\n// used by the fast path to seed the LSQ fit \u2014 the bbox diagonal is\n// sign-blind and points across anti-correlated data (normal maps, hue edges)\n// instead of along it.\nfn principal_axis4(\n c0v: vec4<f32>,\n c1v: vec4<f32>,\n c2v: vec4<f32>,\n c3v: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\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// ------------------------------- 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 // with the covariance moments FUSED in: d = px \u2212 pixel0 (first-pixel-\n // relative, so the sums scale with the block's span; d is integer-valued\n // and \u2264255, exact in f32). Only the fast branch consumes the moment\n // accumulators \u2014 the QUALITY_HIGH pipeline dead-code-eliminates them.\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n var p0f = vec4<f32>(0.0);\n var sd = vec4<f32>(0.0);\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.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 if (i == 0u) { p0f = vec4<f32>(px); }\n let d = vec4<f32>(px) - p0f;\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0f + sd * (1.0 / 16.0);\n\n // Both branches produce: 7-bit endpoints + p-bits, and the 16 4-bit indices\n // packed LSB-first into two nibble words (pixel k \u2192 bits 4k..4k+3).\n var e0_7: vec4<i32>;\n var e1_7: vec4<i32>;\n var p0: u32;\n var p1: u32;\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\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;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n ilo = ilo | (best.indices[k] << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n ihi = ihi | (best.indices[k] << ((k - 8u) * 4u));\n }\n } else {\n // Seed endpoints from the block's principal colour axis (covariance\n // power-iteration; bbox on degenerate blocks) at the exact projection\n // extents, quantise, and assign indices in one projection pass. No LSQ\n // refit: with the seed already on the principal axis, mode 6's 16-level\n // palette leaves the refit \u22640.05 dB on the colour card and \u22640.15 dB on\n // the normal card \u2014 not worth its two extra 16-pixel passes (the coarse\n // 4-level BC1/ASTC fast paths DO keep theirs).\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n let sd16 = sd * (1.0 / 16.0);\n let r0v = c0v - sd.x * sd16;\n let r1v = c1v - sd.y * sd16;\n let r2v = c2v - sd.z * sd16;\n let r3v = c3v - sd.w * sd16;\n var seed0 = lo;\n var seed1 = hi;\n let axis = principal_axis4(r0v, r1v, r2v, r3v, vec4<f32>(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but\n // costs 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3\n // misjudges two-cluster and outlier blocks. The pass stays.)\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n let ep0 = pick_ep(seed0);\n let ep1 = pick_ep(seed1);\n let dir = vec4<f32>(ep1.eight - ep0.eight);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(ep0.eight);\n let inv = 15.0 / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\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. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout at the top of the file).\n let e0 = vec4<u32>(e0_7);\n let e1 = vec4<u32>(e1_7);\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u);\n let w2 = p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
1507
|
+
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]`. This is the f32 fallback;\n// bc7_fast_f16.wgsl is the same algorithm and is preferred when the device\n// reports shader-f16.\n//\n// ALGORITHM: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents, quantised directly \u2014\n// no LSQ refit; with the seed on the principal axis, mode 6's 16-level\n// palette leaves the refit under 0.15 dB, unlike the 4-level BC1/ASTC\n// encoders which keep theirs \u2014 then one pass that projects each pixel onto\n// the endpoint line (the 16 palette entries are colinear, so the nearest\n// index is the rounded projection \u2014 no palette build, no 16-entry search),\n// packed on the fly into two nibble words.\n//\n// A MODE 1 (2-subset) candidate was built and evaluated (2026-07) and\n// dropped: ~+1.3 dB on multi-modal content but up to ~3\xD7 the pass cost on\n// exactly that content \u2014 see bc7_fast_f16.wgsl. The CPU reference decoder\n// keeps mode 1 support (bc7_ref.ts).\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// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\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 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)\n// under a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2).\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// 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// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0) for\n// a degenerate (constant) block. Same family as bc1.wgsl's principal_axis \u2014\n// the bbox diagonal alone is sign-blind and points across anti-correlated\n// data (normal maps, hue edges) instead of along it.\nfn principal_axis4(\n c0v: vec4<f32>,\n c1v: vec4<f32>,\n c2v: vec4<f32>,\n c3v: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\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 // with the covariance moments FUSED in: d = px \u2212 pixel0 (first-pixel-\n // relative, so the sums scale with the block's span; d is integer-valued\n // and \u2264255, exact in f32).\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n var gd = 0;\n var p0f = vec4<f32>(0.0);\n var sd = vec4<f32>(0.0);\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.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 gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0u) { p0f = vec4<f32>(px); }\n let d = vec4<f32>(px) - p0f;\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0f + sd * (1.0 / 16.0);\n\n // Seed endpoints from the block's principal colour axis at the exact\n // projection extents (see header), quantise, and assign indices in one\n // projection pass.\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n let sd16 = sd * (1.0 / 16.0);\n let r0v = c0v - sd.x * sd16;\n let r1v = c1v - sd.y * sd16;\n let r2v = c2v - sd.z * sd16;\n let r3v = c3v - sd.w * sd16;\n var seed0 = lo;\n var seed1 = hi;\n // Gray + opaque blocks: the axis is analytically (1,1,1,0)/\u221A3 with\n // extents at the luma min/max \u2014 skip iteration + extents pass entirely\n // (see bc7_fast_f16.wgsl).\n let gray = lo.w == 255 && gd == 0;\n if (gray) {\n seed0 = vec4<i32>(lo.x, lo.x, lo.x, 255);\n seed1 = vec4<i32>(hi.x, hi.x, hi.x, 255);\n } else {\n let axis = principal_axis4(r0v, r1v, r2v, r3v, vec4<f32>(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but\n // costs 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3\n // misjudges two-cluster and outlier blocks. The pass stays.)\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n }\n\n // The 16 4-bit indices, packed LSB-first into two nibble words\n // (pixel k \u2192 bits 4k..4k+3).\n var ilo: u32 = 0u;\n var ihi: u32 = 0u;\n var ep0 = pick_ep(seed0);\n var ep1 = pick_ep(seed1);\n let dir = vec4<f32>(ep1.eight - ep0.eight);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(ep0.eight);\n let inv = 15.0 / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ilo = ilo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 15.0);\n ihi = ihi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n var e0_7 = ep0.seven;\n var e1_7 = ep1.seven;\n var p0 = ep0.p;\n var p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects\n // every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.\n if ((ilo & 0x8u) != 0u) {\n let t7 = e0_7; e0_7 = e1_7; e1_7 = t7;\n let tp = p0; p0 = p1; p1 = tp;\n ilo = ~ilo; ihi = ~ihi;\n }\n\n // Straight-line mode-6 packing (see layout at the top of the file).\n let e0 = vec4<u32>(e0_7);\n let e1 = vec4<u32>(e1_7);\n let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (p0 << 31u);\n let w2 = p1 | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n let w3 = ihi;\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
879
1508
|
|
|
880
1509
|
// src/bc7_fast_f16.wgsl
|
|
881
|
-
var bc7_fast_f16_default = `// bc7 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
882
|
-
// Same algorithm family as the f32 fast path in bc7.wgsl (principal-axis
|
|
883
|
-
// seed at the exact projection extents \u2192 quantise \u2192 one projection-based
|
|
884
|
-
// index-assignment pass), tuned for throughput:
|
|
885
|
-
//
|
|
886
|
-
// \u2022 All projection math in f16 ([0,1] domain). ~2\xD7 ALU throughput on
|
|
887
|
-
// f16-capable GPUs. The projection direction is pre-scaled by 32:
|
|
888
|
-
// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,
|
|
889
|
-
// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products
|
|
890
|
-
// inside the projection dot are subnormal \u2014 the indices turn to garbage
|
|
891
|
-
// (visible as banding on smooth gradients). Scaling dir by 32 multiplies
|
|
892
|
-
// the dots by 32 and dd by 1024; s = dot\xB7(32\xB715/dd\u2083\u2082) is the same
|
|
893
|
-
// quantity with every intermediate in f16's normal range (worst case
|
|
894
|
-
// inv = 480/0.0157 \u2248 3.0e4 < 65504).
|
|
895
|
-
// \u2022 NO least-squares refit, unlike the BC1/BC5/ASTC fast paths: with the
|
|
896
|
-
// seed already on the principal axis at the exact projection extents,
|
|
897
|
-
// mode 6's fine 16-level palette leaves the refit \u22640.05 dB on the colour
|
|
898
|
-
// card and \u22640.15 dB on the normal card \u2014 not worth its two extra
|
|
899
|
-
// 16-pixel passes. The coarse 4-level formats DO need it (dropping it
|
|
900
|
-
// there costs 0.5\u20131.3 dB).
|
|
901
|
-
// \u2022 Indices are packed into two u32 nibble words ON THE FLY during the
|
|
902
|
-
// projection pass \u2014 no array<u32,16> private array. The BC7 anchor
|
|
903
|
-
// reflection (i \u2192 15\u2212i) is then just a bitwise NOT of both words.
|
|
904
|
-
// \u2022 The 128-bit block is assembled with straight-line constant shifts
|
|
905
|
-
// instead of a generic write_bits() helper (whose dynamic word indexing
|
|
906
|
-
// defeats register promotion of the output array).
|
|
907
|
-
//
|
|
908
|
-
// The host selects this module only when the device reports shader-f16,
|
|
909
|
-
// falling back to bc7.wgsl otherwise. "high" never uses this.
|
|
910
|
-
//
|
|
911
|
-
// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:
|
|
912
|
-
// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]
|
|
913
|
-
// w1: G1[6:4] B0 B1 A0 A1 P0
|
|
914
|
-
// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)
|
|
915
|
-
// w3: pixels 8..15 (4 bits each)
|
|
916
|
-
enable f16;
|
|
917
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
918
|
-
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
919
|
-
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
920
|
-
@group(0) @binding(2) var<uniform> params: Params;
|
|
921
|
-
alias h = f16;
|
|
922
|
-
alias h4 = vec4<f16>;
|
|
923
|
-
|
|
924
|
-
// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the
|
|
925
|
-
// p-bit with the lower quantisation error. \`eight\` is the decoded value the
|
|
926
|
-
// hardware will interpolate with, back in [0,1].
|
|
927
|
-
struct Ep { seven: vec4<u32>, eight: h4, p: u32 };
|
|
928
|
-
fn pick_ep(ideal01: h4) -> Ep {
|
|
929
|
-
let ideal = ideal01 * h(255.0);
|
|
930
|
-
let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0
|
|
931
|
-
let e0 = q0 * h(2.0);
|
|
932
|
-
let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1
|
|
933
|
-
let e1 = q1 * h(2.0) + h(1.0);
|
|
934
|
-
let d0 = e0 - ideal; let d1 = e1 - ideal;
|
|
935
|
-
if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }
|
|
936
|
-
return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
@compute @workgroup_size(8, 8, 1)
|
|
940
|
-
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
941
|
-
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
942
|
-
let bi = gid.y * params.blocks_x + gid.x;
|
|
943
|
-
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
944
|
-
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
945
|
-
|
|
946
|
-
// Load pass, with the covariance moments FUSED in (no separate 16-pixel
|
|
947
|
-
// pass): d = (px \u2212 pixel0)\xB716, relative to the block's first pixel so the
|
|
948
|
-
// accumulators scale with the block's span \u2014 raw \u03A3v\xB7v\u1D40 moments would
|
|
949
|
-
// cancel catastrophically in f16 \u2014 and pre-scaled \xD716 so shallow blocks
|
|
950
|
-
// (span ~1/255 \u2192 d\xB2 \u2248 1e-3) clear the subnormal floor while full-range
|
|
951
|
-
// sums stay \u22644096. C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16 is the \xD7256-scaled covariance.
|
|
952
|
-
var pix: array<h4, 16>;
|
|
953
|
-
var lo = h4(1.0);
|
|
954
|
-
var hi = h4(0.0);
|
|
955
|
-
var p0v = h4(0.0);
|
|
956
|
-
var sd = h4(0.0);
|
|
957
|
-
var c0v = h4(0.0);
|
|
958
|
-
var c1v = h4(0.0);
|
|
959
|
-
var c2v = h4(0.0);
|
|
960
|
-
var c3v = h4(0.0);
|
|
961
|
-
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
962
|
-
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
963
|
-
let px = h4(textureLoad(src_tex, p, 0));
|
|
964
|
-
pix[i] = px; lo = min(lo, px); hi = max(hi, px);
|
|
965
|
-
if (i == 0u) { p0v = px; }
|
|
966
|
-
let d = (px - p0v) * h(16.0);
|
|
967
|
-
sd = sd + d;
|
|
968
|
-
c0v = c0v + d.x * d;
|
|
969
|
-
c1v = c1v + d.y * d;
|
|
970
|
-
c2v = c2v + d.z * d;
|
|
971
|
-
c3v = c3v + d.w * d;
|
|
972
|
-
}
|
|
973
|
-
let mean = p0v + sd * h(1.0 / 256.0);
|
|
974
|
-
// Mean-correction via sd4\xB7sd4\u1D40 with sd4 = \u03A3d/4: (\u03A3d)(\u03A3d)\u1D40/16 with every
|
|
975
|
-
// product \u22644096 (a direct \u03A3d\xB7\u03A3d\u1D40 could hit 65536 and overflow f16).
|
|
976
|
-
let sd4 = sd * h(0.25);
|
|
977
|
-
c0v = c0v - sd4.x * sd4;
|
|
978
|
-
c1v = c1v - sd4.y * sd4;
|
|
979
|
-
c2v = c2v - sd4.z * sd4;
|
|
980
|
-
c3v = c3v - sd4.w * sd4;
|
|
981
|
-
|
|
982
|
-
// Seed endpoints from the block's principal colour axis (covariance
|
|
983
|
-
// power-iteration, seeded with the bbox diagonal \u2014 same family as the BC1
|
|
984
|
-
// 'high' path). The bbox diagonal is sign-blind: on anti-correlated
|
|
985
|
-
// channels (normal maps, hue edges) it points across the data instead of
|
|
986
|
-
// along it, and the LSQ refit \u2014 which fits endpoints GIVEN the projection
|
|
987
|
-
// indices \u2014 can't recover from a wrong axis. The iteration renormalises by
|
|
988
|
-
// the max component (a plain length() of the matvec output could overflow
|
|
989
|
-
// f16), so only the direction survives.
|
|
990
|
-
var seed_lo = lo;
|
|
991
|
-
var seed_hi = hi;
|
|
992
|
-
var axis = hi - lo;
|
|
993
|
-
var axis_ok = true;
|
|
994
|
-
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
995
|
-
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
996
|
-
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
997
|
-
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
998
|
-
axis = nv / m;
|
|
999
|
-
}
|
|
1000
|
-
if (axis_ok) {
|
|
1001
|
-
axis = axis / length(axis);
|
|
1002
|
-
// Exact projection extents along the axis. (A Rayleigh-quotient span
|
|
1003
|
-
// estimate was tried in place of this pass \u2014 it saves 16 dots but costs
|
|
1004
|
-
// 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3 misjudges
|
|
1005
|
-
// two-cluster and outlier blocks and the quantised weight grid can't
|
|
1006
|
-
// recover. The pass stays.)
|
|
1007
|
-
var t_min = h(4.0);
|
|
1008
|
-
var t_max = h(-4.0);
|
|
1009
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1010
|
-
let t = dot(pix[k] - mean, axis);
|
|
1011
|
-
t_min = min(t_min, t);
|
|
1012
|
-
t_max = max(t_max, t);
|
|
1013
|
-
}
|
|
1014
|
-
seed_lo = clamp(mean + t_min * axis, h4(0.0), h4(1.0));
|
|
1015
|
-
seed_hi = clamp(mean + t_max * axis, h4(0.0), h4(1.0));
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Fit from the principal-axis seed (bbox on degenerate blocks), then
|
|
1019
|
-
// quantise the refit endpoints. The refit is clamped to the block bbox: on
|
|
1020
|
-
// multi-cluster blocks the unconstrained solve extrapolates far outside
|
|
1021
|
-
// the block's colours and the per-channel [0,1] clamp then bends the hue \u2014
|
|
1022
|
-
// fringe pixels decode to colours that exist nowhere in the block.
|
|
1023
|
-
// Constraining to the bbox also measures better in plain SSE (+1.3 dB on
|
|
1024
|
-
// the colour test card).
|
|
1025
|
-
// Quantise the PCA-extents seed directly \u2014 no LSQ refit (see header).
|
|
1026
|
-
var ep0 = pick_ep(seed_lo);
|
|
1027
|
-
var ep1 = pick_ep(seed_hi);
|
|
1028
|
-
|
|
1029
|
-
// Final projection against the decoded endpoints, packing the 4-bit indices
|
|
1030
|
-
// into two nibble words as we go (pixel k \u2192 bits 4k..4k+3 of ilo/ihi).
|
|
1031
|
-
var ilo: u32 = 0u;
|
|
1032
|
-
var ihi: u32 = 0u;
|
|
1033
|
-
// Same \xD732 pre-scale as proj_fit; distinct quantised endpoints are \u22651/255
|
|
1034
|
-
// apart, i.e. dd\u2083\u2082 \u2265 0.0157, so the flat-block threshold only catches
|
|
1035
|
-
// truly identical endpoints.
|
|
1036
|
-
let dir = (ep1.eight - ep0.eight) * h(32.0);
|
|
1037
|
-
let dd = dot(dir, dir);
|
|
1038
|
-
if (dd >= h(0.008)) {
|
|
1039
|
-
let inv = h(480.0) / dd;
|
|
1040
|
-
for (var k: u32 = 0u; k < 8u; k = k + 1u) {
|
|
1041
|
-
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
1042
|
-
ilo = ilo | (u32(s) << (k * 4u));
|
|
1043
|
-
}
|
|
1044
|
-
for (var k: u32 = 8u; k < 16u; k = k + 1u) {
|
|
1045
|
-
let s = clamp(floor(dot(pix[k] - ep0.eight, dir) * inv + h(0.5)), h(0.0), h(15.0));
|
|
1046
|
-
ihi = ihi | (u32(s) << ((k - 8u) * 4u));
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Anchor rule \u2014 pixel 0's index MSB must be 0. Swapping endpoints reflects
|
|
1051
|
-
// every index (i \u2192 15\u2212i), which on packed nibbles is a bitwise NOT.
|
|
1052
|
-
if ((ilo & 0x8u) != 0u) {
|
|
1053
|
-
let t = ep0; ep0 = ep1; ep1 = t;
|
|
1054
|
-
ilo = ~ilo; ihi = ~ihi;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Straight-line mode-6 packing (see layout above).
|
|
1058
|
-
let e0 = ep0.seven;
|
|
1059
|
-
let e1 = ep1.seven;
|
|
1060
|
-
let w0 = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);
|
|
1061
|
-
let w1 = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);
|
|
1062
|
-
let w2 = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);
|
|
1063
|
-
let w3 = ihi;
|
|
1064
|
-
|
|
1065
|
-
let o = bi * 4u;
|
|
1066
|
-
dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;
|
|
1067
|
-
}
|
|
1068
|
-
`;
|
|
1510
|
+
var bc7_fast_f16_default = "// bc7 \"fast\" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Same algorithm family as the f32 fast path in bc7.wgsl (principal-axis\n// seed at the exact projection extents \u2192 quantise \u2192 one projection-based\n// index-assignment pass), tuned for throughput:\n//\n// \u2022 All projection math in f16 ([0,1] domain). ~2\xD7 ALU throughput on\n// f16-capable GPUs. The projection direction is pre-scaled by 32:\n// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,\n// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products\n// inside the projection dot are subnormal \u2014 the indices turn to garbage\n// (visible as banding on smooth gradients). Scaling dir by 32 multiplies\n// the dots by 32 and dd by 1024; s = dot\xB7(32\xB7L/dd\u2083\u2082) is the same\n// quantity with every intermediate in f16's normal range (worst case\n// inv = 480/0.0157 \u2248 3.0e4 < 65504).\n// \u2022 TWO MODES (mode 4 OPT-IN via the enable_mode4 override constant,\n// default off and dead-coded at pipeline creation \u2014 see the constant's\n// comment for the measured cost/benefit), decided per block BEFORE\n// encoding \u2014 never encoded both:\n// mode 6 (single RGBA line, 4-bit indices) by default, mode 4 (rotation:\n// one channel split into its own scalar plane with 3-bit indices, the\n// remaining three on a 2-bit line) when the principal axis leaves a\n// large share of the block's variance unexplained \u2014 decorrelated data\n// (normal maps, channel-packed atlases) where any single 4-D line fails.\n// The decision reads the covariance already in registers (\u03BB = axis\u1D40Ca,\n// residual = trace \u2212 \u03BB) and costs no extra pass. An encode-both-and-\n// compare trial was priced at ~2\xD7 on exactly this content (see the mode\n// 1 postmortem below) \u2014 deciding first keeps it at ~1.2\xD7.\n// \u2022 The two modes SHARE the per-pixel passes (axis matvecs, projection\n// extents, the index/weight pass runs once with per-thread level count,\n// index width and packing split) so warps holding a mix of mode-4 and\n// mode-6 blocks do not execute two disjoint kernels back to back \u2014 a\n// first cut with separate per-mode passes measured 1.77\xD7 on normal maps\n// from exactly that divergence; the only mode-4-extra 16-pixel work is\n// the cheap scalar-plane pass.\n// \u2022 GRAY + opaque blocks (every texel R == G == B, A == 1) have their\n// principal axis analytically: (1,1,1,0)/\u221A3, with projection extents at\n// the luma min/max. They skip the power iteration AND the extents pass\n// (\u221234% GPU on roughness/AO/displacement content) and always take\n// mode 6 \u2014 a gray single line fits gray data exactly.\n// \u2022 NO least-squares refit, unlike the BC1/BC5/ASTC fast paths: with the\n// seed already on the principal axis at the exact projection extents,\n// mode 6's fine 16-level palette leaves the refit \u22640.05 dB on the colour\n// card, \u22640.15 dB on the normal card and +0.03 dB on the channel-packed\n// packed-materials atlas \u2014 not worth its two extra 16-pixel passes. The\n// coarse 4-level formats DO need it (dropping it there costs 0.5\u20131.3 dB).\n// \u2022 A MODE 1 (2-subset) candidate was built and evaluated (2026-07): it\n// buys ~+1.3 dB on multi-modal content but its candidate evaluation\n// costs up to ~3\xD7 the mode-6 pass on exactly that content \u2014 dropped in\n// favour of the decided (not compared) mode 4 above, which covers the\n// decorrelated-channel share of that content at a fraction of the cost.\n// The CPU reference decoder keeps mode 1 support (bc7_ref.ts).\n// \u2022 Indices are packed into two u32 words ON THE FLY during the\n// projection pass \u2014 no array<u32,16> private array. The BC7 anchor\n// reflection is then just a bitwise NOT of the packed words.\n// \u2022 The 128-bit block is assembled with straight-line constant shifts\n// instead of a generic write_bits() helper (whose dynamic word indexing\n// defeats register promotion of the output array).\n//\n// The host selects this module only when the device reports shader-f16,\n// falling back to bc7.wgsl otherwise.\n//\n// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:\n// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]\n// w1: G1[6:4] B0 B1 A0 A1 P0\n// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)\n// w3: pixels 8..15 (4 bits each)\n// MODE 4 BIT LAYOUT (LSB-first): mode 0b00001, rotation @5 (channel swapped\n// with alpha), idxMode @7 (0 = colour \u2192 2-bit set, scalar \u2192 3-bit set),\n// colour endpoints 6\xD75 bits @8, alpha endpoints 2\xD76 @38, 31-bit 2-bit index\n// field @50 (pixel 0 anchored to 1 bit), 47-bit 3-bit index field @81\n// (pixel 0 anchored to 2 bits). Validated bit-exact against hardware\n// bc7-rgba-unorm sampling; decode reference in bc7_ref.ts.\nenable 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;\nalias h = f16;\nalias h4 = vec4<f16>;\n\n// Mode-4 gate: encode mode 4 when the principal axis leaves more than\n// MODE4_THETA of the (\xD7256-scaled) total variance unexplained and the block\n// isn't near-flat. Tuned against per-content mode histograms and PSNR.\nconst MODE4_THETA: f16 = 0.2;\nconst MODE4_FLOOR: f16 = 1.0;\nconst MODE4_CONC: f16 = 0.5;\n\n// OPT-IN adaptive mode 4, folded at pipeline creation (WebGPU override\n// constant; default OFF dead-codes the whole path \u2014 measured at exact par\n// with the mode-6-only kernel). Rationale: the quality is real (+2.5\u20132.9 dB\n// on normal maps, +1.9\u20132.4 on channel-packed atlases) but any warp holding\n// one mode-4 block executes both modes' passes, and content that benefits\n// runs 1.4\u20131.5\xD7; a \u03B8 sweep showed quality and warp-poisoning scale together\n// (no per-block middle ground without subgroup ballots). So the trade is\n// the CALLER's: BC7Encoder({ adaptiveMode4: true }).\noverride enable_mode4: bool = false;\n\n// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the\n// p-bit with the lower quantisation error. `eight` is the decoded value the\n// hardware will interpolate with, back in [0,1].\nstruct Ep { seven: vec4<u32>, eight: h4, p: u32 };\nfn pick_ep(ideal01: h4) -> Ep {\n let ideal = ideal01 * h(255.0);\n let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0\n let e0 = q0 * h(2.0);\n let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1\n let e1 = q1 * h(2.0) + h(1.0);\n let d0 = e0 - ideal; let d1 = e1 - ideal;\n if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }\n return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);\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) { return; }\n let bi = gid.y * params.blocks_x + gid.x;\n let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load pass, with the covariance moments FUSED in (no separate 16-pixel\n // pass): d = (px \u2212 pixel0)\xB716, relative to the block's first pixel so the\n // accumulators scale with the block's span \u2014 raw \u03A3v\xB7v\u1D40 moments would\n // cancel catastrophically in f16 \u2014 and pre-scaled \xD716 so shallow blocks\n // (span ~1/255 \u2192 d\xB2 \u2248 1e-3) clear the subnormal floor while full-range\n // sums stay \u22644096. C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16 is the \xD7256-scaled covariance.\n var pix: array<h4, 16>;\n var lo = h4(1.0);\n var hi = h4(0.0);\n var gd = h(0.0);\n var p0v = h4(0.0);\n var sd = h4(0.0);\n var c0v = h4(0.0);\n var c1v = h4(0.0);\n var c2v = h4(0.0);\n var c3v = h4(0.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), mx);\n let px = h4(textureLoad(src_tex, p, 0));\n pix[i] = px; lo = min(lo, px); hi = max(hi, px);\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0u) { p0v = px; }\n let d = (px - p0v) * h(16.0);\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0v + sd * h(1.0 / 256.0);\n // Mean-correction via sd4\xB7sd4\u1D40 with sd4 = \u03A3d/4: (\u03A3d)(\u03A3d)\u1D40/16 with every\n // product \u22644096 (a direct \u03A3d\xB7\u03A3d\u1D40 could hit 65536 and overflow f16).\n let sd4 = sd * h(0.25);\n c0v = c0v - sd4.x * sd4;\n c1v = c1v - sd4.y * sd4;\n c2v = c2v - sd4.z * sd4;\n c3v = c3v - sd4.w * sd4;\n\n // Seed endpoints + per-block mode decision (see header).\n var seed_lo = lo;\n var seed_hi = hi;\n var use4 = false;\n var cmask = h4(1.0);\n var ch = 0u;\n if (lo.w == h(1.0) && gd == h(0.0)) {\n // GRAY + opaque: analytic axis (1,1,1,0)/\u221A3, extents at luma min/max,\n // always mode 6 \u2014 and a fully specialised tail: gray textures are\n // warp-uniform, and routing them through the parametric shared loop\n // below (runtime index width/split) measured +28% on displacement\n // content purely from the lost constant-shift codegen.\n var ep0g = pick_ep(h4(lo.x, lo.x, lo.x, h(1.0)));\n var ep1g = pick_ep(h4(hi.x, hi.x, hi.x, h(1.0)));\n var ilo = 0u;\n var ihi = 0u;\n let dirg = (ep1g.eight - ep0g.eight) * h(32.0);\n let ddg = dot(dirg, dirg);\n if (ddg >= h(0.008)) {\n let invg = h(480.0) / ddg;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ilo = ilo | (u32(sg) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ihi = ihi | (u32(sg) << ((k - 8u) * 4u));\n }\n }\n if ((ilo & 0x8u) != 0u) {\n let t = ep0g; ep0g = ep1g; ep1g = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0g = ep0g.seven;\n let e1g = ep1g.seven;\n let og = bi * 4u;\n dst[og] = 0x40u | (e0g.x << 7u) | (e1g.x << 14u) | (e0g.y << 21u) | (e1g.y << 28u);\n dst[og + 1u] = (e1g.y >> 4u) | (e0g.z << 3u) | (e1g.z << 10u) | (e0g.w << 17u) | (e1g.w << 24u) | (ep0g.p << 31u);\n dst[og + 2u] = ep1g.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[og + 3u] = ihi;\n return;\n }\n {\n var axis = hi - lo;\n var axis_ok = true;\n // 8 iterations: 4 was under-converged on noisy 4-D blocks (heavily\n // downscaled photographic/channel-packed content) \u2014 going to 8 measured\n // +0.75 dB on the normal card, +0.12 colour, +0.08 packed-materials, and\n // matches the f32 fallback's iteration count. Four extra 4-dot matvecs\n // per block are noise next to the index pass.\n for (var it: u32 = 0u; it < 8u; it = it + 1u) {\n let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { axis_ok = false; break; }\n axis = nv / m;\n }\n if (axis_ok) {\n axis = axis / length(axis);\n var axisF = axis;\n\n // Mode decision from the covariance already in registers: \u03BB is the\n // variance the mode-6 line explains, trace \u2212 \u03BB what it cannot.\n let Ca = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let lam = dot(Ca, axis);\n let diag = h4(c0v.x, c1v.y, c2v.z, c3v.w);\n let trace = diag.x + diag.y + diag.z + diag.w;\n let resid = trace - lam;\n let rc = diag - lam * axis * axis;\n var rbest = rc.x;\n if (rc.y > rbest) { ch = 1u; rbest = rc.y; }\n if (rc.z > rbest) { ch = 2u; rbest = rc.z; }\n if (rc.w > rbest) { ch = 3u; rbest = rc.w; }\n use4 = enable_mode4 && resid > MODE4_THETA * trace && trace > MODE4_FLOOR && rbest > MODE4_CONC * resid;\n if (use4) {\n // The colour plane is the remaining three channels, handled as\n // masked 4-vectors so every vec4 pass below applies unchanged.\n // Branchless mask build \u2014 a dynamic component store spills the\n // vector to scratch on some compilers.\n cmask = h4(1.0) - h4(h(f32(u32(ch == 0u))), h(f32(u32(ch == 1u))), h(f32(u32(ch == 2u))), h(f32(u32(ch == 3u))));\n var a3 = (hi - lo) * cmask;\n var ok3 = true;\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n let nv = h4(dot(c0v, a3), dot(c1v, a3), dot(c2v, a3), dot(c3v, a3)) * cmask;\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { ok3 = false; break; }\n a3 = nv / m;\n }\n if (ok3) {\n axisF = a3 / length(a3);\n } else {\n use4 = false;\n cmask = h4(1.0);\n }\n }\n\n // Exact projection extents along the fit axis \u2014 ONE shared pass for\n // both modes (for mode 4 axisF[ch] = 0, so the scalar plane is\n // invisible to it). (A Rayleigh-quotient span estimate was tried in\n // place of this pass \u2014 it saves 16 dots but costs 0.1\u20130.8 dB and\n // 4\u201310\xD7 on the worst-easy-block gate.)\n var t_min = h(4.0);\n var t_max = h(-4.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pix[k] - mean, axisF);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed_lo = clamp(mean + t_min * axisF, h4(0.0), h4(1.0));\n seed_hi = clamp(mean + t_max * axisF, h4(0.0), h4(1.0));\n }\n }\n\n // Endpoints, per mode. d0/d1 are the DECODED values the weight pass\n // projects against.\n var ep0: Ep;\n var ep1: Ep;\n var q0c = vec4<u32>(0u);\n var q1c = vec4<u32>(0u);\n var A0 = 0u;\n var A1 = 0u;\n var iA = 0u;\n var iB = 0u;\n var d0: h4;\n var d1: h4;\n var d0a = h(0.0);\n var sca = h(0.0);\n let chs = h4(1.0) - cmask;\n ep0 = pick_ep(seed_lo);\n ep1 = pick_ep(seed_hi);\n d0 = ep0.eight;\n d1 = ep1.eight;\n if (use4) {\n // Scalar plane (3-bit index set): 6-bit endpoints at the channel's\n // exact extremes. Its projection is FUSED into the shared weight pass\n // below \u2014 a separate 16-pixel pass here measured +46% on normal maps\n // (mixed warps paid it wholesale); fused, the marginal cost is one dot\n // per pixel under a warp-uniform predicate.\n let a0q = u32(floor(dot(lo, chs) * h(63.0) + h(0.5)));\n let a1q = u32(floor(dot(hi, chs) * h(63.0) + h(0.5)));\n A0 = a0q;\n A1 = a1q;\n let d0av = h(f32((a0q << 2u) | (a0q >> 4u))) * h(1.0 / 255.0);\n let d1av = h(f32((a1q << 2u) | (a1q >> 4u))) * h(1.0 / 255.0);\n let aspan = d1av - d0av;\n if (aspan > h(0.001)) {\n d0a = d0av;\n sca = h(7.0) / aspan;\n }\n // Colour plane: 5-bit endpoints from the masked extents seed.\n q0c = vec4<u32>(clamp(floor(seed_lo * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n q1c = vec4<u32>(clamp(floor(seed_hi * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n d0 = h4(vec4<f32>((q0c << vec4<u32>(3u)) | (q0c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n d1 = h4(vec4<f32>((q1c << vec4<u32>(3u)) | (q1c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n }\n\n // Index/weight pass: per-mode SPECIALISED loops (constant level counts\n // and shifts, so each unrolls cleanly \u2014 a single parametric loop with\n // runtime width/split measured +22% on pure mode-6 photo content).\n // Mixed warps execute both loops; the mode-4 one carries the fused\n // scalar-plane projection. For mode 4 pix[ch]\xB7dir[ch] = 0, so the\n // scalar plane never perturbs the colour projection.\n var a_lo = 0u;\n var a_hi = 0u;\n // Same \xD732 pre-scale as the extents math; distinct quantised endpoints\n // are \u22651/255 apart (dd\u2083\u2082 \u2265 0.0157), so the flat-block threshold only\n // catches truly identical ones.\n let dir = (d1 - d0) * h(32.0);\n let dd = dot(dir, dir);\n let live = dd >= h(0.008);\n if (use4) {\n if (live) {\n let inv = h(96.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iA = iA | (u32(sv) << (k * 3u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iB = iB | (u32(sv) << ((k - 8u) * 3u));\n }\n }\n } else if (live) {\n let inv = h(480.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_lo = a_lo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_hi = a_hi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n\n // Anchors + packing. Mode 6 packs unconditionally (one-sided branches\n // compile better than two-sided divergence); mode-4 threads overwrite.\n let o = bi * 4u;\n {\n var ilo = a_lo;\n var ihi = a_hi;\n if ((ilo & 0x8u) != 0u) {\n let t = ep0; ep0 = ep1; ep1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0 = ep0.seven;\n let e1 = ep1.seven;\n dst[o] = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n dst[o + 1u] = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);\n dst[o + 2u] = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[o + 3u] = ihi;\n }\n if (use4) {\n // 3-bit anchor: pixel 0's MSB must be 0; reflect = bitwise NOT.\n if ((iA & 4u) != 0u) {\n let tA = A0;\n A0 = A1;\n A1 = tA;\n iA = ~iA & 0xFFFFFFu;\n iB = ~iB & 0xFFFFFFu;\n }\n var c2 = a_lo;\n // 2-bit anchor: pixel 0's MSB must be 0.\n if ((c2 & 2u) != 0u) {\n let tq = q0c;\n q0c = q1c;\n q1c = tq;\n c2 = ~c2;\n }\n // Rotated-space RGB: position ch carries the original alpha.\n let R0 = select(q0c.x, q0c.w, ch == 0u);\n let G0 = select(q0c.y, q0c.w, ch == 1u);\n let B0 = select(q0c.z, q0c.w, ch == 2u);\n let R1 = select(q1c.x, q1c.w, ch == 0u);\n let G1 = select(q1c.y, q1c.w, ch == 1u);\n let B1 = select(q1c.z, q1c.w, ch == 2u);\n let rot = (ch + 1u) & 3u;\n // Index fields drop the anchors' MSBs: 31 bits (2-bit set) and 47 bits\n // (3-bit set).\n let field2 = (c2 & 1u) | ((c2 >> 2u) << 1u);\n let f_lo = (iA & 3u) | ((iA >> 3u) << 2u) | (iB << 23u);\n let f_hi = iB >> 9u;\n dst[o] = 0x10u | (rot << 5u) | (R0 << 8u) | (R1 << 13u) | (G0 << 18u) | (G1 << 23u) | (B0 << 28u);\n dst[o + 1u] = (B0 >> 4u) | (B1 << 1u) | (A0 << 6u) | (A1 << 12u) | ((field2 & 0x3FFFu) << 18u);\n dst[o + 2u] = (field2 >> 14u) | (f_lo << 17u);\n dst[o + 3u] = (f_lo >> 15u) | (f_hi << 17u);\n }\n}\n";
|
|
1069
1511
|
|
|
1070
1512
|
// src/BC7Encoder.ts
|
|
1071
1513
|
var BC7Encoder = class extends Encoder {
|
|
1072
1514
|
static requiredFeature = WebGPUFeature.BC;
|
|
1073
1515
|
static textureFormats = [TextureFormat.BC7, TextureFormat.BC7_SRGB];
|
|
1516
|
+
adaptiveMode4;
|
|
1517
|
+
constructor(opts) {
|
|
1518
|
+
super(opts);
|
|
1519
|
+
this.adaptiveMode4 = opts.adaptiveMode4 === true;
|
|
1520
|
+
if (this.adaptiveMode4) this._buildPipeline();
|
|
1521
|
+
}
|
|
1522
|
+
pipelineConstants() {
|
|
1523
|
+
if (this.adaptiveMode4 && this._useF16) return { enable_mode4: 1 };
|
|
1524
|
+
return void 0;
|
|
1525
|
+
}
|
|
1074
1526
|
get label() {
|
|
1075
1527
|
return "bc7";
|
|
1076
1528
|
}
|
|
@@ -1080,9 +1532,6 @@ var BC7Encoder = class extends Encoder {
|
|
|
1080
1532
|
get supportsSrgb() {
|
|
1081
1533
|
return true;
|
|
1082
1534
|
}
|
|
1083
|
-
get supportsQuality() {
|
|
1084
|
-
return true;
|
|
1085
|
-
}
|
|
1086
1535
|
wgslSource() {
|
|
1087
1536
|
return bc7_default;
|
|
1088
1537
|
}
|
|
@@ -1095,35 +1544,60 @@ var BC7Encoder = class extends Encoder {
|
|
|
1095
1544
|
};
|
|
1096
1545
|
|
|
1097
1546
|
// src/astc4x4.wgsl
|
|
1098
|
-
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): principal-axis seed (covariance power-iteration; bbox\n// on degenerate blocks) \u2192 one fused pass that projects each pixel onto\n// the endpoint line (the 4 palette entries are colinear, so the nearest\n// is the rounded projection \u2014 no per-entry search) while accumulating the\n// least-squares refit sums, then a reprojection against the quantised\n// refit endpoints with the weights packed on the fly. The endpoint\n// ordering rule is applied before the weight pass, so no reflection is\n// needed.\n// high (1): O(N\xB2) farthest-pair seed, full 4-entry nearest search, one LSQ\n// refit \u2014 matches astc4x4_ref.ts up to FP tie-breaks.\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// One pass over the block: project every pixel onto the e0\u2192e1 line (4 levels,\n// QUANT_4 \u2248 thirds) and accumulate the least-squares normal-equation sums;\n// solve for the refit endpoints. Weights are not produced here \u2014 the caller\n// reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / 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 var s_min = 3.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 3.0);\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15\xB7(1/3)\xB2 \u2248 1.67.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { 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// Principal colour axis via covariance power-iteration (RGBA, 8-bit integer\n// pixel domain), seeded with the bbox diagonal. Returns a unit axis, or\n// vec4(0) for a degenerate (constant) block. Used by the fast path to seed\n// the LSQ fit \u2014 the bbox diagonal is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nfn principal_axis4(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n mean: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = vec4<f32>((*pixels)[k]) - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\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// ------------------------------- 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 var isum = 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 isum = isum + px;\n }\n let mean = vec4<f32>(isum) * (1.0 / 16.0);\n\n // Both branches produce the final endpoints (already ordered so the\n // decoder doesn't apply blue contraction) and the packed weight word\n // (weight k's lsb at bit 31\u22122k, msb at bit 30\u22122k).\n var e0: vec4<i32>;\n var e1: vec4<i32>;\n var w3: u32 = 0u;\n\n if (QUALITY_HIGH != 0u) {\n let fp = farthest_pair(&pixels);\n e0 = fp.a;\n e1 = fp.b;\n var indices: array<u32, 16>;\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 // Endpoint ordering, reflecting the assigned weights.\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 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 } else {\n // Fused LSQ fit seeded from the block's principal colour axis\n // (covariance power-iteration; bbox on degenerate blocks), quantised\n // refit endpoints, ordering applied BEFORE the weight pass so no\n // reflection is needed.\n // The refit is clamped to the block bbox: on multi-cluster blocks the\n // unconstrained solve extrapolates far outside the block's colours and the\n // per-channel [0,255] clamp then bends the hue \u2014 fringe pixels decode to\n // colours that exist nowhere in the block. Constraining to the bbox also\n // measures better in plain SSE (+1.8 dB on the colour test card).\n var seed0 = lo;\n var seed1 = hi;\n let axis = principal_axis4(&pixels, mean, vec4<f32>(hi - lo));\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n let r = proj_fit(&pixels, seed0, seed1);\n e0 = lo;\n e1 = hi;\n if (r.valid) { e0 = clamp(r.e0, lo, hi); e1 = clamp(r.e1, lo, hi); }\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n let tmp = e0; e0 = e1; e1 = tmp;\n }\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd > 0.0) {\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let s = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 3.0));\n w3 = w3 | ((s & 1u) << (31u - 2u * k)) | (((s >> 1u) & 1u) << (30u - 2u * k));\n }\n }\n }\n\n // Straight-line packing: block mode 0x042 @0, partitions\u22121=0 @11, CEM 12\n // @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 (8 bits each) from bit 17,\n // weights in the last word.\n let E0 = vec4<u32>(e0);\n let E1 = vec4<u32>(e1);\n let w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n let w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n let w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
1547
|
+
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]`. This is the f32 fallback;\n// astc4x4_fast_f16.wgsl is the same algorithm and is preferred when the\n// device reports shader-f16.\n//\n// ALGORITHM: per-block class selection, then a single-line fit:\n// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, block mode 0x253 \u2014\n// scalar path: exact min/max endpoints, 32-level weight\n// assignment, no covariance / iteration / refit needed\n// opaque \u2192 CEM 8 (RGB), 3-bit weights, block mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, block mode 0x042\n// Colour paths: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents \u2192 one fused pass that\n// projects each pixel onto the endpoint line (the palette entries are\n// colinear, so the nearest is the rounded projection \u2014 no per-entry search)\n// while accumulating the least-squares refit sums, then a reprojection\n// against the quantised refit endpoints with the weights packed on the fly.\n// The endpoint ordering rule is applied before the weight pass, so no\n// reflection is needed.\n//\n// RESTRICTED SUBSET + BLOCK LAYOUT: see astc4x4_ref.ts (single partition,\n// no dual-plane, CEM 0/8/12, 8-bit endpoints, plain-bit weight ISE; block\n// mode derivations and the weight-stream bit order are documented there).\n//\n// WEIGHT PLACEMENT: stream bit q (bit j of weight k, q = nBits\xB7k + j) lives\n// at block bit 127 \u2212 q, so a stream word assembled LSB-first maps onto a\n// block word with a single reverseBits().\n//\n// ENDPOINT ORDERING: CEM 8/12 decoders branch into blue contraction when\n// sum(e0.rgb) > sum(e1.rgb); the encoder swaps endpoints up front (weights\n// are assigned after the swap, so no reflection pass). CEM 0 has no rule\n// (L0 \u2264 L1 by construction from min/max).\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 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\n// One pass over the block: project every pixel onto the e0\u2192e1 line\n// (lmax + 1 colinear levels, so the nearest entry is the rounded\n// projection) and accumulate the least-squares normal-equation sums;\n// solve for the refit endpoints. Weights are not produced here \u2014 the\n// caller reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool, wstream: u32 };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n out.wstream = 0u;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / 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 var s_min = 3.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 3.0);\n out.wstream = out.wstream | (u32(s) << (2u * k));\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15\xB7(1/3)\xB2 \u2248 1.67.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { 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// Principal colour axis via covariance power-iteration (RGBA, 8-bit integer\n// pixel domain), seeded with the bbox diagonal. Returns a unit axis, or\n// vec4(0) for a degenerate (constant) block. Used to seed the LSQ fit \u2014 the\n// bbox diagonal is sign-blind and points across anti-correlated data (normal\n// maps, hue edges) instead of along it.\nfn principal_axis4(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n mean: vec4<f32>,\n seed: vec4<f32>,\n iters: u32,\n) -> vec4<f32> {\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = vec4<f32>((*pixels)[k]) - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < iters; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\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 var isum = vec4<i32>(0);\n var gd = 0; // max |R\u2212G|, |R\u2212B| over the block; 0 \u21D4 exactly grayscale\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 isum = isum + px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n let opaque = lo.w == 255;\n\n var w0: u32; var w1: u32; var w2: u32; var w3: u32;\n\n if (opaque && gd == 0) {\n // ---------------- Luminance path: CEM 0, 5-bit weights ----------------\n // Endpoints at the exact extremes; 32 palette levels make an LSQ refit\n // unnecessary.\n let L0 = u32(lo.x);\n let L1 = u32(hi.x);\n var s0 = 0u; var s1 = 0u; var s2 = 0u;\n if (L1 > L0) {\n let sc = 64.0 / f32(hi.x - lo.x);\n // Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,\n // 2w + 2 for w \u2265 16 (4-wide gap at the middle, so uniform rounding\n // is wrong there). Best candidate of each half, keep the closer.\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let u = clamp(f32(pixels[k].x - lo.x) * sc, 0.0, 64.0);\n let wlo = clamp(floor(u * 0.5 + 0.5), 0.0, 15.0);\n let whi = clamp(floor((u - 2.0) * 0.5 + 0.5), 16.0, 31.0);\n let pick = abs(u - wlo * 2.0) <= abs(u - (whi * 2.0 + 2.0));\n let w = u32(select(whi, wlo, pick));\n // Stream bit q = 5k + j; straddles handled with constant shifts.\n let off = 5u * k;\n if (off < 28u) { s0 = s0 | (w << off); }\n else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }\n else if (off < 60u) { s1 = s1 | (w << (off - 32u)); }\n else if (off == 60u) { s1 = s1 | (w << 28u); s2 = s2 | (w >> 4u); }\n else { s2 = s2 | (w << (off - 64u)); }\n }\n }\n // Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills\n // into w1 bit 0); stream words map onto block words via reverseBits.\n w0 = 0x253u | (L0 << 17u) | (L1 << 25u);\n w1 = (L1 >> 7u) | reverseBits(s2);\n w2 = reverseBits(s1);\n w3 = reverseBits(s0);\n } else {\n // ------------- Colour paths: shared PCA seed ---------------------------\n let mean = vec4<f32>(isum) * (1.0 / 16.0);\n\n // Fused LSQ fit seeded from the block's principal colour axis at the\n // exact projection extents, quantised refit endpoints, ordering applied\n // BEFORE the weight pass so no reflection is needed.\n // The refit is clamped to the block bbox: on multi-cluster blocks the\n // unconstrained solve extrapolates far outside the block's colours and\n // the per-channel [0,255] clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to\n // the bbox also measures better in plain SSE (+1.8 dB on the colour\n // test card).\n var seed0 = lo;\n var seed1 = hi;\n // 8 iterations for opaque blocks (the axis is the endpoint quality\n // there), 4 for translucent ones whose LSQ refit absorbs residual\n // axis error (see astc4x4_fast_f16.wgsl).\n let axis = principal_axis4(&pixels, mean, vec4<f32>(hi - lo), select(4u, 8u, opaque));\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n // Opaque blocks (CEM 8, 8-level weights) ship the quantised PCA\n // extents directly; only the translucent CEM 12 path refits its coarse\n // 4-level grid (see astc4x4_fast_f16.wgsl for the measured trade).\n var e0 = lo;\n var e1 = hi;\n var fitStream = 0u;\n var haveFitWeights = false;\n if (opaque) {\n // Bbox-clamped like the fit output (see astc4x4_fast_f16.wgsl).\n e0 = clamp(seed0, lo, hi);\n e1 = clamp(seed1, lo, hi);\n } else {\n let r = proj_fit(&pixels, seed0, seed1);\n if (r.valid) {\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\n fitStream = r.wstream;\n haveFitWeights = true;\n }\n }\n var swapped = false;\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n let tmp = e0; e0 = e1; e1 = tmp;\n swapped = true;\n }\n let E0 = vec4<u32>(e0);\n let E1 = vec4<u32>(e1);\n\n // Weight pass against the final endpoints, packing on the fly.\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n let e0f = vec4<f32>(e0);\n if (opaque) {\n // CEM 8: 3-bit weights, stream bit q = 3k.\n var s0 = 0u; var s1 = 0u;\n if (dd > 0.0) {\n let inv = 7.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 7.0));\n let off = 3u * k;\n if (off < 30u) { s0 = s0 | (w << off); }\n else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }\n else { s1 = s1 | (w << (off - 32u)); }\n }\n }\n // Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.\n w0 = 0x053u | (8u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n w2 = (E1.z >> 7u) | reverseBits(s1);\n w3 = reverseBits(s0);\n } else {\n // CEM 12: 2-bit weights, stream bit q = 2k (single stream word).\n // Valid fits ship the fit-pass weights; the blue-contraction swap is\n // a full reflection w \u2192 3\u2212w = bitwise NOT of the packed stream (see\n // astc4x4_fast_f16.wgsl for the measured trade).\n var s0 = 0u;\n if (haveFitWeights) {\n s0 = select(fitStream, ~fitStream, swapped);\n } else if (dd > 0.0) {\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 3.0));\n s0 = s0 | (w << (2u * k));\n }\n }\n // Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.\n w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);\n w3 = reverseBits(s0);\n }\n }\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
1099
1548
|
|
|
1100
1549
|
// src/astc4x4_fast_f16.wgsl
|
|
1101
1550
|
var astc4x4_fast_f16_default = `// astc4x4 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
1102
|
-
// Same algorithm family as the f32
|
|
1103
|
-
// seed \u2192 projection weight assignment with
|
|
1104
|
-
// reproject), tuned for throughput:
|
|
1551
|
+
// Same algorithm family as the f32 fallback in astc4x4.wgsl (per-block
|
|
1552
|
+
// class selection, principal-axis seed \u2192 projection weight assignment with
|
|
1553
|
+
// a fused least-squares refit \u2192 reproject), tuned for throughput:
|
|
1105
1554
|
//
|
|
1106
|
-
// \u2022
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1555
|
+
// \u2022 THREE block classes, picked per block from the loaded pixels (the
|
|
1556
|
+
// ASTC bit budget trades endpoint bits against weight bits, so a block
|
|
1557
|
+
// only pays for the channels it uses):
|
|
1558
|
+
// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253
|
|
1559
|
+
// opaque \u2192 CEM 8 (RGB), 3-bit weights, mode 0x053
|
|
1560
|
+
// translucent \u2192 CEM 12 (RGBA), 2-bit weights, mode 0x042
|
|
1561
|
+
// "gray" = every texel R == G == B exactly (f16 equality is exact for
|
|
1562
|
+
// 8-bit sources), "opaque" = every texel A == 1. The gray path is
|
|
1563
|
+
// scalar \u2014 no covariance, no power iteration, no 4-D fit \u2014 and runs
|
|
1564
|
+
// [0,255]-integer f16 math like the BC5 kernel (exact endpoints, and
|
|
1565
|
+
// 64/span \u2264 64 never overflows f16, unlike a [0,1]-domain 1/dd).
|
|
1566
|
+
// \u2022 All projection / refit math in f16 ([0,1] domain, colour paths).
|
|
1567
|
+
// The projection direction is pre-scaled by 32 \u2014 a shallow block
|
|
1568
|
+
// (endpoints ~1/255 apart) has dd \u2248 1.5e-5, where 3/dd \u2248 2e5 overflows
|
|
1569
|
+
// f16 (max 65504) to +inf and the projection dots go subnormal,
|
|
1570
|
+
// turning weights and the LSQ refit to garbage (banding on smooth
|
|
1571
|
+
// gradients). Scaling dir by 32 puts every intermediate in f16's
|
|
1572
|
+
// normal range; s = dot\xB7(32\xB7L/dd\u2083\u2082) is the same quantity (worst case
|
|
1573
|
+
// for L = 7 levels: inv = 224/0.0157 \u2248 1.4e4).
|
|
1574
|
+
// \u2022 Opaque blocks ship the quantised PCA extents directly (no LSQ fit
|
|
1575
|
+
// pass, 8 power iterations \u2014 see the endpoint-selection comment); only
|
|
1576
|
+
// the translucent CEM 12 path still refits, and its seed pass only
|
|
1577
|
+
// accumulates the LSQ sums \u2014 final weights always come from a
|
|
1578
|
+
// reprojection against the final endpoints.
|
|
1115
1579
|
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
1116
1580
|
// exceed sum(e1.rgb)) is applied BEFORE the final projection, so no
|
|
1117
1581
|
// weight-reflection pass is needed.
|
|
1118
|
-
// \u2022
|
|
1119
|
-
// the
|
|
1120
|
-
//
|
|
1582
|
+
// \u2022 Weight streams are accumulated LSB-first into u32 words and placed
|
|
1583
|
+
// into the block's reversed-bit-order field with reverseBits() \u2014
|
|
1584
|
+
// stream bit q lives at block bit 127 \u2212 q, so a whole stream word
|
|
1585
|
+
// maps onto a block word with a single bit reversal.
|
|
1586
|
+
// \u2022 The covariance moments stay in their OWN pass, deliberately: fusing
|
|
1587
|
+
// them into the load loop bc7-style (pixel-0 residuals, with or without
|
|
1588
|
+
// hoisting pixel 0 out of the loop) measured +5% GPU time on 4096\xB2
|
|
1589
|
+
// (/ab A/B, 2026-07, M3) \u2014 the separate loop overlaps the 16 texture
|
|
1590
|
+
// loads' latency better than a longer in-loop dependency chain does.
|
|
1591
|
+
// Per-pass cost on the same rig, for future tuning: covariance+power-
|
|
1592
|
+
// iteration \u2248 18%, LSQ fit pass \u2248 24%, extents pass \u2248 6% of the kernel;
|
|
1593
|
+
// @workgroup_size 16\xD78 measured exactly at par with 8\xD78.
|
|
1121
1594
|
//
|
|
1122
|
-
// RESTRICTED SUBSET + BLOCK LAYOUT: see
|
|
1123
|
-
// CEM 12, 8-bit endpoints,
|
|
1595
|
+
// RESTRICTED SUBSET + BLOCK LAYOUT: see astc4x4_ref.ts (single partition,
|
|
1596
|
+
// CEM 0/8/12, 8-bit endpoints, plain-bit weight ISE, block modes
|
|
1597
|
+
// 0x253/0x053/0x042).
|
|
1124
1598
|
//
|
|
1125
1599
|
// The host selects this module only when the device reports shader-f16,
|
|
1126
|
-
// falling back to astc4x4.wgsl otherwise.
|
|
1600
|
+
// falling back to astc4x4.wgsl otherwise.
|
|
1127
1601
|
enable f16;
|
|
1128
1602
|
alias h = f16;
|
|
1129
1603
|
alias h4 = vec4<f16>;
|
|
@@ -1132,10 +1606,15 @@ struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
|
1132
1606
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1133
1607
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
1134
1608
|
|
|
1135
|
-
|
|
1609
|
+
// Fused projection + least-squares refit against the 4-level QUANT_4
|
|
1610
|
+
// palette \u2014 only the translucent (CEM 12) path still refits: the 2-bit
|
|
1611
|
+
// weight grid is coarse enough to need it, while the opaque paths get
|
|
1612
|
+
// more from spending the same time elsewhere (see header).
|
|
1613
|
+
struct Fit { e0: h4, e1: h4, valid: bool, wstream: u32 };
|
|
1136
1614
|
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
1137
1615
|
var out: Fit;
|
|
1138
1616
|
out.valid = false;
|
|
1617
|
+
out.wstream = 0u;
|
|
1139
1618
|
// dir pre-scaled by 32 to keep dd and the projection dots in f16's normal
|
|
1140
1619
|
// range (see header). Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008,
|
|
1141
1620
|
// possible only for non-8-bit sources) are treated as flat.
|
|
@@ -1153,8 +1632,10 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1153
1632
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1154
1633
|
let vr = (*pix)[k] - e0;
|
|
1155
1634
|
let s = clamp(floor(dot(vr, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
1635
|
+
out.wstream = out.wstream | (u32(s) << (2u * k));
|
|
1156
1636
|
s_min = min(s_min, s); s_max = max(s_max, s);
|
|
1157
|
-
let b = s * h(1.0 / 3.0);
|
|
1637
|
+
let b = s * h(1.0 / 3.0);
|
|
1638
|
+
let a = h(1.0) - b;
|
|
1158
1639
|
sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;
|
|
1159
1640
|
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
1160
1641
|
}
|
|
@@ -1182,102 +1663,218 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1182
1663
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
1183
1664
|
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
1184
1665
|
|
|
1666
|
+
// Load pass. gd tracks the largest chroma deviation \u2014 0 iff the block is
|
|
1667
|
+
// exactly grayscale (equal 8-bit channels convert to identical f16s).
|
|
1185
1668
|
var pix: array<h4, 16>;
|
|
1186
1669
|
var lo = h4(1.0);
|
|
1187
1670
|
var hi = h4(0.0);
|
|
1188
1671
|
var mean = h4(0.0);
|
|
1672
|
+
var gd = h(0.0);
|
|
1189
1673
|
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
1190
1674
|
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
1191
1675
|
let px = h4(textureLoad(src_tex, p, 0));
|
|
1192
1676
|
pix[i] = px; lo = min(lo, px); hi = max(hi, px);
|
|
1193
1677
|
mean = mean + px;
|
|
1678
|
+
gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));
|
|
1194
1679
|
}
|
|
1195
1680
|
mean = mean * h(1.0 / 16.0);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
var t_min = h(4.0);
|
|
1231
|
-
var t_max = h(-4.0);
|
|
1232
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1233
|
-
let t = dot(pix[k] - mean, axis);
|
|
1234
|
-
t_min = min(t_min, t);
|
|
1235
|
-
t_max = max(t_max, t);
|
|
1681
|
+
let opaque = lo.w == h(1.0);
|
|
1682
|
+
|
|
1683
|
+
var w0: u32; var w1: u32; var w2: u32; var w3: u32;
|
|
1684
|
+
|
|
1685
|
+
if (opaque && gd == h(0.0)) {
|
|
1686
|
+
// ---------------- Luminance path: CEM 0, 5-bit weights ----------------
|
|
1687
|
+
// Scalar [0,255]-integer domain. Endpoints at the exact extremes (the
|
|
1688
|
+
// 8-bit values round-trip f16 exactly); 32 palette levels make an LSQ
|
|
1689
|
+
// refit unnecessary.
|
|
1690
|
+
let L0 = u32(floor(lo.x * h(255.0) + h(0.5)));
|
|
1691
|
+
let L1 = u32(floor(hi.x * h(255.0) + h(0.5)));
|
|
1692
|
+
var s0 = 0u; var s1 = 0u; var s2 = 0u;
|
|
1693
|
+
if (L1 > L0) {
|
|
1694
|
+
let l0f = h(f32(L0));
|
|
1695
|
+
let sc = h(64.0) / h(f32(L1 - L0)); // span \u2265 1 \u2192 sc \u2264 64, no overflow
|
|
1696
|
+
// Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,
|
|
1697
|
+
// 2w + 2 for w \u2265 16 (the grid has a 4-wide gap at the middle, so
|
|
1698
|
+
// uniform rounding is wrong there). Evaluate the best candidate of
|
|
1699
|
+
// each half and keep the closer.
|
|
1700
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1701
|
+
let v = floor(pix[k].x * h(255.0) + h(0.5)) - l0f; // exact integer
|
|
1702
|
+
let u = clamp(v * sc, h(0.0), h(64.0));
|
|
1703
|
+
let wlo = clamp(floor(u * h(0.5) + h(0.5)), h(0.0), h(15.0));
|
|
1704
|
+
let whi = clamp(floor((u - h(2.0)) * h(0.5) + h(0.5)), h(16.0), h(31.0));
|
|
1705
|
+
let pick = abs(u - wlo * h(2.0)) <= abs(u - (whi * h(2.0) + h(2.0)));
|
|
1706
|
+
let w = u32(select(whi, wlo, pick));
|
|
1707
|
+
// Stream bit q = 5k + j; straddles handled with constant shifts.
|
|
1708
|
+
let off = 5u * k;
|
|
1709
|
+
if (off < 28u) { s0 = s0 | (w << off); }
|
|
1710
|
+
else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }
|
|
1711
|
+
else if (off < 60u) { s1 = s1 | (w << (off - 32u)); }
|
|
1712
|
+
else if (off == 60u) { s1 = s1 | (w << 28u); s2 = s2 | (w >> 4u); }
|
|
1713
|
+
else { s2 = s2 | (w << (off - 64u)); }
|
|
1714
|
+
}
|
|
1236
1715
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
// word, msb at bit 30\u22122k.
|
|
1263
|
-
var w3: u32 = 0u;
|
|
1264
|
-
// Same \xD732 pre-scale as proj_fit; distinct 8-bit endpoints are \u22651/255
|
|
1265
|
-
// apart (dd\u2083\u2082 \u2265 0.0157), so the threshold only catches identical ones.
|
|
1266
|
-
let dir = (d1 - d0) * h(32.0);
|
|
1267
|
-
let dd = dot(dir, dir);
|
|
1268
|
-
if (dd >= h(0.008)) {
|
|
1269
|
-
let inv = h(96.0) / dd;
|
|
1716
|
+
// Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills
|
|
1717
|
+
// into w1 bit 0); stream word q\u2208[0,31] \u2192 block bits 127\u202696 via
|
|
1718
|
+
// reverseBits, q\u2208[32,63] \u2192 95\u202664, q\u2208[64,79] \u2192 63\u202648.
|
|
1719
|
+
w0 = 0x253u | (L0 << 17u) | (L1 << 25u);
|
|
1720
|
+
w1 = (L1 >> 7u) | reverseBits(s2);
|
|
1721
|
+
w2 = reverseBits(s1);
|
|
1722
|
+
w3 = reverseBits(s0);
|
|
1723
|
+
} else {
|
|
1724
|
+
// ------------- Colour paths: shared PCA seed + LSQ refit --------------
|
|
1725
|
+
// Seed endpoints from the block's principal colour axis (covariance
|
|
1726
|
+
// power-iteration, seeded with the bbox diagonal). The bbox diagonal is
|
|
1727
|
+
// sign-blind: on anti-correlated channels (normal maps, hue edges) it
|
|
1728
|
+
// points across the data instead of along it, and the LSQ refit \u2014 which
|
|
1729
|
+
// fits endpoints GIVEN the projection weights \u2014 can't recover from a
|
|
1730
|
+
// wrong axis. Deviations are pre-scaled \xD716 so covariance entries for
|
|
1731
|
+
// shallow blocks stay in f16's normal range (span ~1/255 \u2192 d\xB2 \u2248 1e-3)
|
|
1732
|
+
// while full-range sums stay \u22644096; the iteration renormalises by the
|
|
1733
|
+
// max component (a plain length() of the matvec output could overflow
|
|
1734
|
+
// f16), so only the direction survives.
|
|
1735
|
+
var seed_lo = lo;
|
|
1736
|
+
var seed_hi = hi;
|
|
1737
|
+
var c0v = h4(0.0);
|
|
1738
|
+
var c1v = h4(0.0);
|
|
1739
|
+
var c2v = h4(0.0);
|
|
1740
|
+
var c3v = h4(0.0);
|
|
1270
1741
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1271
|
-
let
|
|
1272
|
-
|
|
1742
|
+
let d = (pix[k] - mean) * h(16.0);
|
|
1743
|
+
c0v = c0v + d.x * d;
|
|
1744
|
+
c1v = c1v + d.y * d;
|
|
1745
|
+
c2v = c2v + d.z * d;
|
|
1746
|
+
c3v = c3v + d.w * d;
|
|
1747
|
+
}
|
|
1748
|
+
var axis = hi - lo;
|
|
1749
|
+
var axis_ok = true;
|
|
1750
|
+
// 4 shared iterations, then 4 more for opaque blocks only \u2014 two
|
|
1751
|
+
// FIXED-bound loops rather than one divergent trip count, so both
|
|
1752
|
+
// unroll. Opaque blocks need the converged axis (it IS the endpoint
|
|
1753
|
+
// quality there; 4 was under-converged on noisy 4-D content), while
|
|
1754
|
+
// translucent blocks' LSQ refit absorbs residual axis error \u2014 their
|
|
1755
|
+
// extra 4 steps measured exactly 0.000 dB on the alpha card for
|
|
1756
|
+
// ~5% GPU.
|
|
1757
|
+
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1758
|
+
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1759
|
+
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1760
|
+
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1761
|
+
axis = nv / m;
|
|
1762
|
+
}
|
|
1763
|
+
if (axis_ok && opaque) {
|
|
1764
|
+
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1765
|
+
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1766
|
+
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1767
|
+
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1768
|
+
axis = nv / m;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (axis_ok) {
|
|
1772
|
+
axis = axis / length(axis);
|
|
1773
|
+
var t_min = h(4.0);
|
|
1774
|
+
var t_max = h(-4.0);
|
|
1775
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1776
|
+
let t = dot(pix[k] - mean, axis);
|
|
1777
|
+
t_min = min(t_min, t);
|
|
1778
|
+
t_max = max(t_max, t);
|
|
1779
|
+
}
|
|
1780
|
+
seed_lo = clamp(mean + t_min * axis, h4(0.0), h4(1.0));
|
|
1781
|
+
seed_hi = clamp(mean + t_max * axis, h4(0.0), h4(1.0));
|
|
1273
1782
|
}
|
|
1274
|
-
}
|
|
1275
1783
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1784
|
+
// Endpoint selection. OPAQUE blocks (CEM 8, 8-level weights) quantise
|
|
1785
|
+
// the PCA-extent seed directly, BC7-style \u2014 the LSQ fit pass measured
|
|
1786
|
+
// +26% GPU for \u22120.06..+0.31 dB against the converged 8-step axis
|
|
1787
|
+
// (/ab + PSNR A/B, 2026-07): at 8 weight levels the projection is fine
|
|
1788
|
+
// enough that a good axis, not a refit, carries the quality.
|
|
1789
|
+
// TRANSLUCENT blocks (CEM 12) keep the fit: 4 levels are coarse enough
|
|
1790
|
+
// that dropping it costs 0.5+ dB. Its result is clamped to the block
|
|
1791
|
+
// bbox: on multi-cluster blocks the unconstrained solve extrapolates
|
|
1792
|
+
// far outside the block's colours and the per-channel [0,1] clamp then
|
|
1793
|
+
// bends the hue \u2014 fringe pixels decode to colours that exist nowhere
|
|
1794
|
+
// in the block (and the bbox constraint also measures better in plain
|
|
1795
|
+
// SSE, +1.8 dB on the colour test card).
|
|
1796
|
+
var e0 = lo;
|
|
1797
|
+
var e1 = hi;
|
|
1798
|
+
var fitStream = 0u;
|
|
1799
|
+
var haveFitWeights = false;
|
|
1800
|
+
if (opaque) {
|
|
1801
|
+
// Bbox-clamped like the fit output: on multi-cluster blocks the axis
|
|
1802
|
+
// extents overshoot the data per-channel and decode to colours that
|
|
1803
|
+
// exist nowhere in the block (the odd-size padding gate caught a
|
|
1804
|
+
// \u22123.9 dB crop without this).
|
|
1805
|
+
e0 = clamp(seed_lo, lo, hi);
|
|
1806
|
+
e1 = clamp(seed_hi, lo, hi);
|
|
1807
|
+
} else {
|
|
1808
|
+
let r = proj_fit(&pix, seed_lo, seed_hi);
|
|
1809
|
+
if (r.valid) {
|
|
1810
|
+
e0 = clamp(r.e0, lo, hi);
|
|
1811
|
+
e1 = clamp(r.e1, lo, hi);
|
|
1812
|
+
fitStream = r.wstream;
|
|
1813
|
+
haveFitWeights = true;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
var E0 = q8(e0);
|
|
1817
|
+
var E1 = q8(e1);
|
|
1818
|
+
|
|
1819
|
+
// Blue-contraction ordering, applied before the weight pass so weights
|
|
1820
|
+
// are already oriented (no reflection needed).
|
|
1821
|
+
var swapped = false;
|
|
1822
|
+
if (E0.x + E0.y + E0.z > E1.x + E1.y + E1.z) {
|
|
1823
|
+
let t = E0; E0 = E1; E1 = t;
|
|
1824
|
+
swapped = true;
|
|
1825
|
+
}
|
|
1826
|
+
let d0 = h4(vec4<f32>(E0)) * h(1.0 / 255.0);
|
|
1827
|
+
let d1 = h4(vec4<f32>(E1)) * h(1.0 / 255.0);
|
|
1828
|
+
|
|
1829
|
+
// Weight pass against the decoded endpoints. Same \xD732 pre-scale as
|
|
1830
|
+
// proj_fit; distinct 8-bit endpoints are \u22651/255 apart (dd\u2083\u2082 \u2265 0.0157),
|
|
1831
|
+
// so the flat threshold only catches identical ones.
|
|
1832
|
+
let dir = (d1 - d0) * h(32.0);
|
|
1833
|
+
let dd = dot(dir, dir);
|
|
1834
|
+
if (opaque) {
|
|
1835
|
+
// CEM 8: 3-bit weights, stream bit q = 3k.
|
|
1836
|
+
var s0 = 0u; var s1 = 0u;
|
|
1837
|
+
if (dd >= h(0.008)) {
|
|
1838
|
+
let inv = h(224.0) / dd;
|
|
1839
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1840
|
+
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(7.0)));
|
|
1841
|
+
let off = 3u * k;
|
|
1842
|
+
if (off < 30u) { s0 = s0 | (w << off); }
|
|
1843
|
+
else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }
|
|
1844
|
+
else { s1 = s1 | (w << (off - 32u)); }
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
// Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.
|
|
1848
|
+
w0 = 0x053u | (8u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1849
|
+
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1850
|
+
w2 = (E1.z >> 7u) | reverseBits(s1);
|
|
1851
|
+
w3 = reverseBits(s0);
|
|
1852
|
+
} else {
|
|
1853
|
+
// CEM 12: 2-bit weights, stream bit q = 2k (single stream word).
|
|
1854
|
+
// Valid fits ship the FIT-PASS weights instead of reprojecting \u2014
|
|
1855
|
+
// worth a whole 16-pixel pass for \u22120.09 dB on the alpha card (/ab +
|
|
1856
|
+
// PSNR A/B, 2026-07; the pre-adaptive-CEM encoder rejected this same
|
|
1857
|
+
// trade when EVERY block was CEM 12 \u2014 now only translucent blocks
|
|
1858
|
+
// pay it). The blue-contraction swap is a full reflection w \u2192 3\u2212w,
|
|
1859
|
+
// i.e. bitwise NOT of the packed stream. Invalid fits (rank-1 /
|
|
1860
|
+
// degenerate) fall back to reprojection against the bbox endpoints.
|
|
1861
|
+
var s0 = 0u;
|
|
1862
|
+
if (haveFitWeights) {
|
|
1863
|
+
s0 = select(fitStream, ~fitStream, swapped);
|
|
1864
|
+
} else if (dd >= h(0.008)) {
|
|
1865
|
+
let inv = h(96.0) / dd;
|
|
1866
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1867
|
+
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
1868
|
+
s0 = s0 | (w << (2u * k));
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
// Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.
|
|
1872
|
+
w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1873
|
+
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1874
|
+
w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);
|
|
1875
|
+
w3 = reverseBits(s0);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1281
1878
|
|
|
1282
1879
|
let o = bi * 4u;
|
|
1283
1880
|
dst[o] = w0; dst[o + 1u] = w1; dst[o + 2u] = w2; dst[o + 3u] = w3;
|
|
@@ -1297,9 +1894,6 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1297
1894
|
get supportsSrgb() {
|
|
1298
1895
|
return true;
|
|
1299
1896
|
}
|
|
1300
|
-
get supportsQuality() {
|
|
1301
|
-
return true;
|
|
1302
|
-
}
|
|
1303
1897
|
wgslSource() {
|
|
1304
1898
|
return astc4x4_default;
|
|
1305
1899
|
}
|
|
@@ -1311,6 +1905,36 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1311
1905
|
}
|
|
1312
1906
|
};
|
|
1313
1907
|
|
|
1908
|
+
// src/etc2.wgsl
|
|
1909
|
+
var etc2_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. The f16 module (etc2_fast_f16.wgsl) is an\n// EXACT-VALUE port \u2014 byte-identical output; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\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\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\nfn sb_table_score(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let thr = THR[t];\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = select(a3, b3, ad > thr);\n acc = acc + m3 * (m3 - 2.0 * ad);\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32) -> SearchOut {\n var mx = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let cover = min(\n u32(mx > 24.0) + u32(mx > 51.0) + u32(mx > 87.0) + u32(mx > 126.0) +\n u32(mx > 180.0) + u32(mx > 240.0) + u32(mx > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f32, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, out.lb0);\n let s1 = sb_search(luma, flip, 1u, out.lb1);\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> u32 {\n let thr = THR[t];\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var luma: array<f32, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\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_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = l;\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, sel.lb0, t0);\n let fit1 = sb_indices(&luma, bflip, 1u, sel.lb1, t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = qv.b;\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1910
|
+
|
|
1911
|
+
// src/etc2_fast_f16.wgsl
|
|
1912
|
+
var etc2_fast_f16_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is that f16 module.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), every f16 value in this shader is an\n// integer that f16 represents exactly \u2014 lumas and bases (<= 765), D values\n// (|D| <= 765) and thresholds (<= 549) all sit below f16's 2048 integer-\n// exactness limit. Sums of squares, scores and estimates stay f32 (they\n// reach +-5e5..9e6, far past f16's 65504 max). The output is therefore\n// BYTE-IDENTICAL to the f32 module \u2014 verified per-block on the suite\n// textures \u2014 and the two modules share every pin and every test gate.\n//\n// What f16 buys here is register pressure (the luma array halves), not\n// arithmetic rate: on Apple/metal-3 the two modules measure identical\n// (the shader is DRAM-read-bound), but on the mobile GPUs where ETC2 is\n// actually the target format, occupancy from smaller registers is the\n// cheapest speed there is. The COLOUR accumulators deliberately stay f32\n// even though quadrant/pair sums (<= 2040) would be exact in f16: porting\n// them measured 15% SLOWER on Apple (conversion traffic outweighs the\n// register saving). Luma + the table search are the f16 surface.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\n\nenable f16;\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\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// D-domain values (|D| <= 765, thresholds <= 549) are exact in f16; the\n// score PRODUCTS reach +-5e5 and must be f32.\nfn sb_table_score(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> f32 {\n let a3 = f16(A3[t]);\n let b3 = f16(B3[t]);\n let thr = f16(THR[t]);\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = f32(select(a3, b3, ad > thr));\n acc = acc + m3 * (m3 - 2.0 * f32(ad));\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16) -> SearchOut {\n var mx: f16 = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let mxf = f32(mx);\n let cover = min(\n u32(mxf > 24.0) + u32(mxf > 51.0) + u32(mxf > 87.0) + u32(mxf > 126.0) +\n u32(mxf > 180.0) + u32(mxf > 240.0) + u32(mxf > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f16, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, f16(out.lb0));\n let s1 = sb_search(luma, flip, 1u, f16(out.lb1));\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> u32 {\n let thr = f16(THR[t]);\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\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 block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Luma lives in f16: every value is an integer <= 765, exact in f16.\n var luma: array<f16, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\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_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = f16(l);\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, f16(sel.lb0), t0);\n let fit1 = sb_indices(&luma, bflip, 1u, f16(sel.lb1), t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = qv.b;\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1913
|
+
|
|
1914
|
+
// src/ETC2Encoder.ts
|
|
1915
|
+
var ETC2Encoder = class extends Encoder {
|
|
1916
|
+
static requiredFeature = WebGPUFeature.ETC2;
|
|
1917
|
+
static textureFormats = [TextureFormat.ETC2_RGB8, TextureFormat.ETC2_RGB8_SRGB];
|
|
1918
|
+
get label() {
|
|
1919
|
+
return "etc2";
|
|
1920
|
+
}
|
|
1921
|
+
get bytesPerBlock() {
|
|
1922
|
+
return 8;
|
|
1923
|
+
}
|
|
1924
|
+
get supportsSrgb() {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
wgslSource() {
|
|
1928
|
+
return etc2_default;
|
|
1929
|
+
}
|
|
1930
|
+
wgslSourceFastF16() {
|
|
1931
|
+
return etc2_fast_f16_default;
|
|
1932
|
+
}
|
|
1933
|
+
gpuTextureFormat({ colorSpace }) {
|
|
1934
|
+
return colorSpace === "srgb" ? "etc2-rgb8unorm-srgb" : "etc2-rgb8unorm";
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1314
1938
|
// src/webgl/glsl/fullscreen.vert.glsl
|
|
1315
1939
|
var fullscreen_vert_default = "#version 300 es\n// Fullscreen-triangle vertex shader for the WebGL block encoders.\n//\n// Draws a single oversized triangle covering the viewport from gl_VertexID\n// alone \u2014 no vertex buffers / attributes needed (drawArrays(TRIANGLES, 0, 3)).\n// The encoder sets the viewport to (blocks_x \xD7 blocks_y), so each rasterised\n// fragment corresponds to exactly one 4\xD74 output block.\n//\n// id 0 -> (-1,-1) id 1 -> ( 3,-1) id 2 -> (-1, 3)\n\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
1316
1940
|
|
|
@@ -1522,7 +2146,7 @@ var WebGLBlockEncoder = class {
|
|
|
1522
2146
|
};
|
|
1523
2147
|
|
|
1524
2148
|
// src/webgl/glsl/bc1.frag.glsl
|
|
1525
|
-
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl (fast path).\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block.
|
|
2149
|
+
var bc1_frag_default = "#version 300 es\n// BC1 (DXT1) fragment-shader encoder \u2014 WebGL2 port of bc1.wgsl (fast path).\n//\n// One fragment per 4\xD74 block. Output is the 8-byte BC1 block as 2 \xD7 u32 in\n// outColor.rg (outColor.ba unused); the encoder reads back RGBA32UI and keeps\n// the low two words per block. Same algorithm as bc1.wgsl:\n// principal-axis endpoint seed (covariance\n// power-iteration; inset bbox on degenerate blocks), RGB565 quantisation,\n// forced 4-colour mode, full 4-entry L2 index search, then up to TWO\n// least-squares endpoint refit rounds, each accepted only when it lowers the\n// block's error. See bc1.wgsl for the full derivation.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize; // original (unpadded) width, height\nuniform int uFlipY; // 1 = sample bottom-up (matches Three.js flipY)\n\nlayout(location = 0) out uvec4 outColor;\n\n// 4-colour-mode interpolation weights: pal[j] = WA[j]*c0 + WB[j]*c1.\nconst float WA[4] = float[4](1.0, 0.0, 2.0 / 3.0, 1.0 / 3.0);\nconst float WB[4] = float[4](0.0, 1.0, 1.0 / 3.0, 2.0 / 3.0);\n\nuint to565(vec3 c) {\n uint r = uint(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n uint g = uint(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n uint b = uint(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11) | (g << 5) | b;\n}\n\nvec3 from565(uint c) {\n float r = float((c >> 11) & 31u);\n float g = float((c >> 5) & 63u);\n float b = float(c & 31u);\n // 5/6-bit \u2192 8-bit. floor((x*527+23)/64) == (x<<3)|(x>>2): exact hardware\n // bit-replication (white \u2192 255), so index selection matches the GPU decode.\n float r8 = floor((r * 527.0 + 23.0) / 64.0);\n float g8 = floor((g * 259.0 + 33.0) / 64.0);\n float b8 = floor((b * 527.0 + 23.0) / 64.0);\n return vec3(r8, g8, b8) / 255.0;\n}\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by\n// ptr; GLSL would copy array parameters by value).\nvec3 gPixels[16];\nuint gIdx[16];\n\n// Nearest-palette assignment of gPixels for the decoded palette of (c0,c1),\n// with the block's squared error and the LSQ normal-equation sums of the\n// resulting assignment accumulated in the same pass \u2014 so an accepted refit\n// can seed the next round.\nstruct Assign { float err; float sAA; float sBB; float sAB; vec3 sAV; vec3 sBV; };\nAssign assignStats(uint c0, uint c1, out uint indices[16]) {\n vec3 p0 = from565(c0);\n vec3 p1 = from565(c1);\n vec3 pal[4];\n for (int j = 0; j < 4; j++) pal[j] = WA[j] * p0 + WB[j] * p1;\n Assign r = Assign(0.0, 0.0, 0.0, 0.0, vec3(0.0), vec3(0.0));\n for (int k = 0; k < 16; k++) {\n vec3 c = gPixels[k];\n uint bestJ = 0u;\n float bestD = 1e30;\n for (int j = 0; j < 4; j++) {\n vec3 d = pal[j] - c;\n float d2 = dot(d, d);\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n indices[k] = bestJ;\n r.err += bestD;\n float a = WA[int(bestJ)];\n float b = WB[int(bestJ)];\n r.sAA += a * a; r.sBB += b * b; r.sAB += a * b; r.sAV += a * c; r.sBV += b * c;\n }\n return r;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n float gd = 0.0;\n vec3 bbMin = vec3(1.0);\n vec3 bbMax = vec3(0.0);\n vec3 mean = vec3(0.0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec3 c = texelFetch(uSrc, ivec2(p.x, sy), 0).rgb;\n gPixels[i] = c;\n bbMin = min(bbMin, c);\n gd = max(gd, max(abs(c.x - c.y), abs(c.x - c.z)));\n bbMax = max(bbMax, c);\n mean += c;\n }\n mean /= 16.0;\n\n // Seed endpoints from the block's principal colour axis (covariance\n // power-iteration, seeded with the bbox diagonal \u2014 mirrors bc1.wgsl). The\n // bbox diagonal is sign-blind: on anti-correlated channels (normal maps,\n // hue edges) it points across the data instead of along it, and the LSQ\n // refit below \u2014 which fits endpoints GIVEN the indices \u2014 can't recover.\n // Degenerate (near-flat) blocks keep the inset-bbox seed. Both seeds inset\n // by ~half an RGB565 cell (1/16) to tighten the quantised palette.\n vec3 c0v = vec3(0.0);\n vec3 c1v = vec3(0.0);\n vec3 c2v = vec3(0.0);\n for (int k = 0; k < 16; k++) {\n vec3 d = gPixels[k] - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n }\n vec3 hi;\n vec3 lo;\n vec3 axis = bbMax - bbMin;\n float alen = length(axis);\n bool axisOk = alen > 1e-9;\n if (axisOk) {\n axis /= alen;\n for (int it = 0; it < 8; it++) {\n vec3 nv = vec3(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis));\n float nlen = length(nv);\n if (nlen < 1e-12) { axisOk = false; break; }\n axis = nv / nlen;\n }\n }\n if (axisOk) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(gPixels[k] - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n float pad = (tMax - tMin) / 16.0;\n hi = clamp(mean + (tMax - pad) * axis, vec3(0.0), vec3(1.0));\n lo = clamp(mean + (tMin + pad) * axis, vec3(0.0), vec3(1.0));\n } else {\n vec3 inset = (bbMax - bbMin) / 16.0;\n hi = clamp(bbMax - inset, vec3(0.0), vec3(1.0));\n lo = clamp(bbMin + inset, vec3(0.0), vec3(1.0));\n }\n\n uint c0 = to565(hi);\n uint c1 = to565(lo);\n // 4-colour mode requires color0 > color1.\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n uint tmp = c0; c0 = c1; c1 = tmp;\n }\n\n // Seed assignment, then up to TWO least-squares refit rounds (mirroring\n // bc1.wgsl's fast path), each accepted only if the block's squared error\n // drops \u2014 the refit minimises a continuous objective and can lose after\n // 565 quantisation. Every assignment pass re-accumulates the sums, so an\n // accepted round seeds the next.\n // Exactly-gray blocks free the refit from the bbox clamp (no hue to\n // protect; smooth gradients want endpoints outside the data range) \u2014\n // see bc1_fast_f16.wgsl.\n vec3 limLo = gd == 0.0 ? vec3(0.0) : bbMin;\n vec3 limHi = gd == 0.0 ? vec3(1.0) : bbMax;\n Assign cur = assignStats(c0, c1, gIdx);\n for (int it = 0; it < 2; it++) {\n float det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;\n if (abs(det) <= 1e-9) { break; }\n // Clamp the refit to the block bbox (not [0,1]): on multi-cluster blocks\n // the unconstrained LSQ solve extrapolates far outside the block's\n // colours and the per-channel clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to the\n // bbox also measures better in plain SSE (+1.6 dB on the colour test\n // card), so the accept-if-better guard below keeps more refits.\n vec3 e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, limLo, limHi);\n vec3 e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, limLo, limHi);\n uint nc0 = to565(e0);\n uint nc1 = to565(e1);\n if (nc0 < nc1) { uint t = nc0; nc0 = nc1; nc1 = t; }\n if (nc0 == nc1 || (nc0 == c0 && nc1 == c1)) { break; }\n uint idx2[16];\n Assign nxt = assignStats(nc0, nc1, idx2);\n if (nxt.err >= cur.err) { break; }\n c0 = nc0; c1 = nc1;\n cur = nxt;\n for (int k = 0; k < 16; k++) gIdx[k] = idx2[k];\n }\n\n uint indices = 0u;\n for (int k = 0; k < 16; k++) indices |= (gIdx[k] & 3u) << (uint(k) * 2u);\n\n outColor = uvec4(c0 | (c1 << 16), indices, 0u, 0u);\n}\n";
|
|
1526
2150
|
|
|
1527
2151
|
// src/webgl/BC1WebGLEncoder.ts
|
|
1528
2152
|
var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -1541,7 +2165,7 @@ var BC1WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
1541
2165
|
};
|
|
1542
2166
|
|
|
1543
2167
|
// src/webgl/glsl/bc5.frag.glsl
|
|
1544
|
-
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl (fast path).\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G). This is the *fast* path only: bbox\n// endpoints + a full-L2 index assignment per channel with the least-squares\n// refit sums accumulated in the same pass, then one refit accepted
|
|
2168
|
+
var bc5_frag_default = "#version 300 es\n// BC5 (RGTC2) fragment-shader encoder \u2014 WebGL2 port of bc5.wgsl (fast path).\n//\n// One fragment per 4\xD74 block \u2192 16-byte BC5 block as 4 \xD7 u32 in outColor.\n// BC5 = two BC4 halves (R then G). This is the *fast* path only: bbox\n// endpoints + a full-L2 index assignment per channel with the least-squares\n// refit sums accumulated in the same pass, then one refit accepted\n// CLOSED-FORM when it lowers the block's error on the seed indices \u2014 the\n// accepted endpoints ship with the seed indices, no second assignment pass\n// (mirrors bc5.wgsl; see bc5_fast_f16.wgsl for the measured trade). Always\n// emits 6-interpolation mode (red0 > red1).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// 6-interpolation-mode palette weights: pal[j] = W0_6[j]*r0 + W1_6[j]*r1.\nconst float W0_6[8] = float[8](1.0, 0.0, 6.0 / 7.0, 5.0 / 7.0, 4.0 / 7.0, 3.0 / 7.0, 2.0 / 7.0, 1.0 / 7.0);\nconst float W1_6[8] = float[8](0.0, 1.0, 1.0 / 7.0, 2.0 / 7.0, 3.0 / 7.0, 4.0 / 7.0, 5.0 / 7.0, 6.0 / 7.0);\n\nuint quantize8(float v) {\n return uint(clamp(floor(v * 255.0 + 0.5), 0.0, 255.0));\n}\n\n// Nearest-palette assignment with the LSQ normal-equation sums and total\n// squared error accumulated in the same pass. Sums are only consumed by the\n// caller's refit; err drives the accept-if-better test.\nstruct Assign { float err; float sAA; float sBB; float sAB; float sAV; float sBV; };\nAssign assignAll(float values[16], float pal[8], out uint indices[16]) {\n Assign r = Assign(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);\n for (int k = 0; k < 16; k++) {\n float v = values[k];\n uint bestJ = 0u;\n float bestD = 1e20;\n for (int j = 0; j < 8; j++) {\n float d = pal[j] - v;\n float d2 = d * d;\n if (d2 < bestD) { bestD = d2; bestJ = uint(j); }\n }\n indices[k] = bestJ;\n r.err += bestD;\n float a = W0_6[int(bestJ)];\n float b = W1_6[int(bestJ)];\n r.sAA += a * a; r.sBB += b * b; r.sAB += a * b; r.sAV += a * v; r.sBV += b * v;\n }\n return r;\n}\n\n// Encode 16 single-channel values into an 8-byte BC4 block (two little-endian\n// u32s). Mirrors encode_bc4() in bc5.wgsl's fast branch: bbox seed, fused\n// assignment + LSQ sums, refit accepted only if the error drops. vmin/vmax\n// are the channel's min/max, computed in the caller's load loop \u2014 fusing\n// that scan there saves a 16-value pass per channel.\nuvec2 encodeBC4(float values[16], float vmin, float vmax) {\n uint r0 = quantize8(vmax);\n uint r1 = quantize8(vmin);\n if (r0 == r1) {\n if (r1 > 0u) { r1 = r1 - 1u; } else { r0 = r0 + 1u; }\n }\n\n float pal[8];\n float r0f = float(r0) / 255.0;\n float r1f = float(r1) / 255.0;\n for (int j = 0; j < 8; j++) {\n pal[j] = W0_6[j] * r0f + W1_6[j] * r1f;\n }\n\n uint indices[16];\n Assign seed = assignAll(values, pal, indices);\n\n // One least-squares refit, accepted CLOSED-FORM: with the seed indices\n // kept, the block error for endpoints (e0, e1) is\n // E(e0, e1) = sAA\xB7e0\xB2 + sBB\xB7e1\xB2 + 2(sAB\xB7e0\xB7e1 \u2212 e0\xB7sAV \u2212 e1\xB7sBV) + \u03A3V\xB2\n // and \u03A3V\xB2 cancels out of the accept comparison, so no second assignment\n // pass is needed. Clamp to [0,1], NOT the block's value range: for a\n // scalar channel, endpoints beyond the data range are often genuinely\n // optimal and there is no colour axis to bend \u2014 the bbox clamp the colour\n // formats need costs ~0.3 dB here. Keep 6-interp mode (r0 > r1 strictly).\n float det = seed.sAA * seed.sBB - seed.sAB * seed.sAB;\n if (abs(det) > 1e-9) {\n float e0 = clamp((seed.sBB * seed.sAV - seed.sAB * seed.sBV) / det, 0.0, 1.0) * 255.0;\n float e1 = clamp((seed.sAA * seed.sBV - seed.sAB * seed.sAV) / det, 0.0, 1.0) * 255.0;\n // Price all four floor/ceil roundings of the fractional solve\n // closed-form (see bc5_fast_f16.wgsl); keep the best that stays in\n // 6-interp mode and beats the seed.\n float quadSeed = seed.sAA * r0f * r0f + seed.sBB * r1f * r1f\n + 2.0 * (seed.sAB * r0f * r1f - r0f * seed.sAV - r1f * seed.sBV);\n float bestQuad = quadSeed;\n uint seed0 = r0;\n uint seed1 = r1;\n for (uint m = 0u; m < 4u; m++) {\n float q0f = clamp(floor(e0) + float(m & 1u), 0.0, 255.0);\n float q1f = clamp(floor(e1) + float(m >> 1u), 0.0, 255.0);\n uint n0 = uint(q0f);\n uint n1 = uint(q1f);\n if (n0 > n1 && !(n0 == seed0 && n1 == seed1)) {\n float n0f = q0f / 255.0;\n float n1f = q1f / 255.0;\n float quadNew = seed.sAA * n0f * n0f + seed.sBB * n1f * n1f\n + 2.0 * (seed.sAB * n0f * n1f - n0f * seed.sAV - n1f * seed.sBV);\n if (quadNew < bestQuad) {\n bestQuad = quadNew;\n r0 = n0; r1 = n1;\n }\n }\n }\n }\n\n // Pack the 48-bit index field (bytes 2..7) split across two u32 halves.\n uint idxLo = 0u;\n uint idxHi = 0u;\n for (int k = 0; k < 16; k++) {\n uint bit = 3u * uint(k);\n uint v = indices[k] & 7u;\n if (bit + 3u <= 32u) {\n idxLo = idxLo | (v << bit);\n } else if (bit >= 32u) {\n idxHi = idxHi | (v << (bit - 32u));\n } else {\n idxLo = idxLo | (v << bit);\n idxHi = idxHi | (v >> (32u - bit));\n }\n }\n\n uint outLo = r0 | (r1 << 8) | ((idxLo & 0xFFFFu) << 16);\n uint outHi = (idxLo >> 16) | (idxHi << 16);\n return uvec2(outLo, outHi);\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n float rValues[16];\n float gValues[16];\n float rMin = 1.0; float rMax = 0.0;\n float gMin = 1.0; float gMax = 0.0;\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n vec4 c = texelFetch(uSrc, ivec2(p.x, sy), 0);\n rValues[i] = c.r;\n gValues[i] = c.g;\n rMin = min(rMin, c.r); rMax = max(rMax, c.r);\n gMin = min(gMin, c.g); gMax = max(gMax, c.g);\n }\n\n uvec2 rBlock = encodeBC4(rValues, rMin, rMax);\n uvec2 gBlock = encodeBC4(gValues, gMin, gMax);\n outColor = uvec4(rBlock.x, rBlock.y, gBlock.x, gBlock.y);\n}\n";
|
|
1545
2169
|
|
|
1546
2170
|
// src/webgl/BC5WebGLEncoder.ts
|
|
1547
2171
|
var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -1560,7 +2184,7 @@ var BC5WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
1560
2184
|
};
|
|
1561
2185
|
|
|
1562
2186
|
// src/webgl/glsl/bc7.frag.glsl
|
|
1563
|
-
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: principal-axis seed (covariance power-iteration; bbox on degenerate\n// blocks) at the exact projection extents, quantised directly (no LSQ refit \u2014\n// mode 6's 16-level palette leaves it under 0.15 dB) \u2192 one projection-based\n// index-assignment pass (palette is colinear, so the nearest entry is found\n// by projecting onto the endpoint line \u2014 O(1) per pixel).
|
|
2187
|
+
var bc7_frag_default = "#version 300 es\n// BC7 (BPTC) mode-6 fragment-shader encoder \u2014 WebGL2 port of bc7.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Fast path\n// only: principal-axis seed (covariance power-iteration; bbox on degenerate\n// blocks) at the exact projection extents, quantised directly (no LSQ refit \u2014\n// mode 6's 16-level palette leaves it under 0.15 dB) \u2192 one projection-based\n// index-assignment pass (palette is colinear, so the nearest entry is found\n// by projecting onto the endpoint line \u2014 O(1) per pixel). Same algorithm as\n// bc7.wgsl; see that file for the mode-6 bit layout and rationale.\n//\n// Determinism note: the WGSL refit uses round() (half-to-even); here we use\n// floor(x + 0.5) for portability. The two differ only at exact .5 ties, a\n// sub-LSB endpoint nudge that is visually identical.\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\n// Per-invocation scratch (mirrors the WGSL function-scope arrays passed by ptr).\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct QuantPair { ivec4 seven; ivec4 eight; };\nstruct Ep { ivec4 seven; ivec4 eight; uint p; };\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\nint dist2(ivec4 a, ivec4 b) {\n ivec4 d = a - b;\n ivec4 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.\nQuantPair quantizeEndpoint(ivec4 ideal8, uint p) {\n ivec4 q = ivec4(clamp(floor((vec4(ideal8) - float(p)) / 2.0 + 0.5), vec4(0.0), vec4(127.0)));\n // eff = (q << 1) | p. q*2 is even and p \u2208 {0,1}, so q*2 + p is identical and\n // avoids any vector-shift-by-scalar portability question.\n ivec4 eff = q * 2 + ivec4(int(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nEp pickEp(ivec4 ideal) {\n QuantPair a = quantizeEndpoint(ideal, 0u);\n QuantPair b = quantizeEndpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) {\n return Ep(b.seven, b.eight, 1u);\n }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0.0)\n// for a degenerate (constant) block. The bbox diagonal alone is sign-blind\n// and points across anti-correlated data (normal maps, hue edges) instead of\n// along it.\nvec4 principalAxis(vec4 c0v, vec4 c1v, vec4 c2v, vec4 c3v, vec4 seed) {\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < 8; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Projection index assignment over gPixels \u2192 gIdx. When `fit`, accumulate the\n// LSQ normal-equation sums in the same pass and return refitted endpoints.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 15.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 15.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 15.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n // Load pass with the covariance moments FUSED in: d = px \u2212 pixel0\n // (first-pixel-relative, so the sums scale with the block's span).\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n vec4 p0f = vec4(0.0);\n vec4 sd = vec4(0.0);\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n int gd = 0;\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0) { p0f = vec4(px); }\n vec4 d = vec4(px) - p0f;\n sd += d;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 mean = p0f + sd / 16.0;\n // Mean-correct the fused moments: C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16.\n vec4 sd16 = sd / 16.0;\n c0v -= sd.x * sd16;\n c1v -= sd.y * sd16;\n c2v -= sd.z * sd16;\n c3v -= sd.w * sd16;\n\n ivec4 seed0 = lo;\n ivec4 seed1 = hi;\n // Gray + opaque blocks: axis is analytically (1,1,1,0)/\u221A3 with extents\n // at the luma min/max \u2014 skip iteration + extents (see bc7_fast_f16.wgsl).\n if (lo.w == 255 && gd == 0) {\n seed0 = ivec4(lo.x, lo.x, lo.x, 255);\n seed1 = ivec4(hi.x, hi.x, hi.x, 255);\n } else {\n vec4 axis = principalAxis(c0v, c1v, c2v, c3v, vec4(hi - lo));\n if (dot(axis, axis) > 0.0) {\n // Exact projection extents along the axis. (A Rayleigh-quotient span\n // estimate was tried in place of this pass \u2014 it saves 16 dots but costs\n // 0.1\u20130.8 dB and 4\u201310\xD7 on the worst-easy-block gate: \u03C3 misjudges\n // two-cluster and outlier blocks. The pass stays.)\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n seed0 = ivec4(clamp(floor(mean + tMin * axis + 0.5), vec4(0.0), vec4(255.0)));\n seed1 = ivec4(clamp(floor(mean + tMax * axis + 0.5), vec4(0.0), vec4(255.0)));\n }\n }\n\n // Quantise the PCA-extents seed directly and assign indices in one\n // projection pass \u2014 no LSQ refit: with the seed already on the principal\n // axis, mode 6's 16-level palette leaves the refit under 0.15 dB (the\n // coarse 4-level BC1/ASTC fast paths DO keep theirs).\n Ep ep0 = pickEp(seed0);\n Ep ep1 = pickEp(seed1);\n projAssign(ep0.eight, ep1.eight, false);\n ivec4 e0_7 = ep0.seven;\n ivec4 e1_7 = ep1.seven;\n uint p0 = ep0.p;\n uint p1 = ep1.p;\n\n // Anchor rule \u2014 pixel 0's index MSB must be 0; otherwise swap endpoints and\n // reflect every index (decoded image unchanged).\n if ((gIdx[0] & 0x8u) != 0u) {\n ivec4 t = e0_7; e0_7 = e1_7; e1_7 = t;\n uint tp = p0; p0 = p1; p1 = tp;\n for (int k = 0; k < 16; k++) { gIdx[k] = 15u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n uint pos = 0u;\n writeBits(block, pos, 7u, 0x40u); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.x)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.y)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.z)); pos += 7u;\n writeBits(block, pos, 7u, uint(e0_7.w)); pos += 7u;\n writeBits(block, pos, 7u, uint(e1_7.w)); pos += 7u;\n writeBits(block, pos, 1u, p0); pos += 1u;\n writeBits(block, pos, 1u, p1); pos += 1u;\n writeBits(block, pos, 3u, gIdx[0] & 0x7u); pos += 3u;\n for (int k = 1; k < 16; k++) {\n writeBits(block, pos, 4u, gIdx[k] & 0xFu);\n pos += 4u;\n }\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
1564
2188
|
|
|
1565
2189
|
// src/webgl/BC7WebGLEncoder.ts
|
|
1566
2190
|
var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -1579,7 +2203,7 @@ var BC7WebGLEncoder = class extends WebGLBlockEncoder {
|
|
|
1579
2203
|
};
|
|
1580
2204
|
|
|
1581
2205
|
// src/webgl/glsl/astc4x4.frag.glsl
|
|
1582
|
-
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor. Restricted\n// subset: single partition, no dual-plane, CEM 12 (LDR RGBA direct), 4\xD74 weight\n// grid with 2-bit weights (QUANT_4), 8-bit endpoints (QUANT_256). Fast path:\n// principal-axis seed (covariance power-iteration; bbox on degenerate blocks)\n// \u2192 one LSQ refit fused into a projection weight assignment (4 colinear\n// levels). Mirrors the `QUALITY_HIGH == 0` branch of astc4x4.wgsl; see that file\n// for the 128-bit block layout.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// Principal colour axis of gPixels via covariance power-iteration, seeded\n// with the bbox diagonal. Returns a unit axis, or vec4(0.0) for a degenerate\n// (constant) block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nvec4 principalAxis(vec4 mean, vec4 seed) {\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n vec4 d = vec4(gPixels[k]) - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < 8; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\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.\nFit projAssign(ivec4 pe0, ivec4 pe1, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = 3.0 / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, 3.0);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / 3.0;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid writeBits(inout uint block[4], uint pos, uint nbits, uint value) {\n uint v = value & ((1u << nbits) - 1u);\n uint wordLo = pos / 32u;\n uint bitLo = pos % 32u;\n uint bitsInLo = min(nbits, 32u - bitLo);\n uint maskLo = ((1u << bitsInLo) - 1u) << bitLo;\n block[wordLo] = (block[wordLo] & ~maskLo) | ((v << bitLo) & maskLo);\n if (bitsInLo < nbits) {\n uint bitsInHi = nbits - bitsInLo;\n uint maskHi = (1u << bitsInHi) - 1u;\n uint valHi = v >> bitsInLo;\n block[wordLo + 1u] = (block[wordLo + 1u] & ~maskHi) | (valHi & maskHi);\n }\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n ivec4 isum = ivec4(0);\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n isum += px;\n }\n vec4 mean = vec4(isum) / 16.0;\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n vec4 axis = principalAxis(mean, vec4(hi - lo));\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n e0 = ivec4(clamp(floor(mean + tMin * axis + 0.5), vec4(0.0), vec4(255.0)));\n e1 = ivec4(clamp(floor(mean + tMax * axis + 0.5), vec4(0.0), vec4(255.0)));\n }\n Fit r = projAssign(e0, e1, true);\n if (r.valid) {\n // Clamp the refit to the block bbox: on multi-cluster blocks the\n // unconstrained LSQ solve extrapolates far outside the block's colours and\n // the per-channel [0,255] clamp then bends the hue \u2014 fringe pixels decode\n // to colours that exist nowhere in the block. Constraining to the bbox\n // also measures better in plain SSE (+1.8 dB on the colour test card).\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\n projAssign(e0, e1, false);\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 ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = 3u - gIdx[k]; }\n }\n\n uint block[4];\n block[0] = 0u; block[1] = 0u; block[2] = 0u; block[3] = 0u;\n writeBits(block, 0u, 11u, 0x042u);\n writeBits(block, 11u, 2u, 0u);\n writeBits(block, 13u, 4u, 12u);\n writeBits(block, 17u + 0u * 8u, 8u, uint(e0.x));\n writeBits(block, 17u + 1u * 8u, 8u, uint(e1.x));\n writeBits(block, 17u + 2u * 8u, 8u, uint(e0.y));\n writeBits(block, 17u + 3u * 8u, 8u, uint(e1.y));\n writeBits(block, 17u + 4u * 8u, 8u, uint(e0.z));\n writeBits(block, 17u + 5u * 8u, 8u, uint(e1.z));\n writeBits(block, 17u + 6u * 8u, 8u, uint(e0.w));\n writeBits(block, 17u + 7u * 8u, 8u, uint(e1.w));\n\n uint w3 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k] & 0x3u;\n w3 = w3 | ((w & 1u) << (31u - 2u * uint(k))) | (((w >> 1u) & 1u) << (30u - 2u * uint(k)));\n }\n block[3] = w3;\n\n outColor = uvec4(block[0], block[1], block[2], block[3]);\n}\n";
|
|
2206
|
+
var astc4x4_frag_default = "#version 300 es\n// ASTC 4\xD74 LDR fragment-shader encoder \u2014 WebGL2 port of astc4x4.wgsl (fast).\n//\n// One fragment per 4\xD74 block \u2192 16-byte block as 4 \xD7 u32 in outColor.\n// Restricted subset: single partition, no dual-plane, 8-bit endpoints, with\n// the block class picked per block from the content (see astc4x4_ref.ts for\n// layouts and block-mode derivations):\n// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253\n// opaque \u2192 CEM 8 (RGB), 3-bit weights, mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, mode 0x042\n// Colour paths: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) \u2192 one LSQ refit fused into a projection weight\n// assignment. Mirrors astc4x4.wgsl.\n//\n// Determinism note: floor(x + 0.5) replaces WGSL round() for the refit endpoints\n// (sub-LSB difference at exact .5 ties only).\n\nprecision highp float;\nprecision highp int;\n\nuniform sampler2D uSrc;\nuniform ivec2 uSrcSize;\nuniform int uFlipY;\n\nlayout(location = 0) out uvec4 outColor;\n\nivec4 gPixels[16];\nuint gIdx[16];\n\nstruct Fit { ivec4 e0; ivec4 e1; bool valid; };\n\nivec4 to8(vec4 v) {\n return ivec4(clamp(floor(v * 255.0 + 0.5), vec4(0.0), vec4(255.0)));\n}\n\n// GLSL ES 3.00 has no bitfieldReverse; classic 5-step swap. Weight-stream\n// bit q lives at block bit 127 \u2212 q, so a stream word assembled LSB-first\n// maps onto a block word with one reversal (see astc4x4.wgsl).\nuint rev32(uint x) {\n uint v = x;\n v = ((v & 0x55555555u) << 1) | ((v >> 1) & 0x55555555u);\n v = ((v & 0x33333333u) << 2) | ((v >> 2) & 0x33333333u);\n v = ((v & 0x0F0F0F0Fu) << 4) | ((v >> 4) & 0x0F0F0F0Fu);\n v = ((v & 0x00FF00FFu) << 8) | ((v >> 8) & 0x00FF00FFu);\n return (v << 16) | (v >> 16);\n}\n\n// Principal colour axis of gPixels via covariance power-iteration, seeded\n// with the bbox diagonal. Returns a unit axis, or vec4(0.0) for a degenerate\n// (constant) block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nvec4 principalAxis(vec4 mean, vec4 seed, int iters) {\n vec4 c0v = vec4(0.0);\n vec4 c1v = vec4(0.0);\n vec4 c2v = vec4(0.0);\n vec4 c3v = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n vec4 d = vec4(gPixels[k]) - mean;\n c0v += d.x * d;\n c1v += d.y * d;\n c2v += d.z * d;\n c3v += d.w * d;\n }\n vec4 v = seed;\n float len = length(v);\n if (len < 1e-9) { return vec4(0.0); }\n v /= len;\n for (int it = 0; it < iters; it++) {\n vec4 nv = vec4(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// Projection weight assignment over lmax + 1 colinear levels, with the LSQ\n// normal-equation sums accumulated in the same pass for a fused refit.\nFit projAssign(ivec4 pe0, ivec4 pe1, float lmax, bool fit) {\n Fit res;\n res.e0 = ivec4(0);\n res.e1 = ivec4(0);\n res.valid = false;\n ivec4 dir = pe1 - pe0;\n int dd = dir.x * dir.x + dir.y * dir.y + dir.z * dir.z + dir.w * dir.w;\n if (dd == 0) {\n for (int k = 0; k < 16; k++) { gIdx[k] = 0u; }\n return res;\n }\n float inv = lmax / float(dd);\n float sAA = 0.0, sBB = 0.0, sAB = 0.0;\n vec4 sAV = vec4(0.0), sBV = vec4(0.0);\n for (int k = 0; k < 16; k++) {\n ivec4 q = gPixels[k] - pe0;\n float proj = float(q.x * dir.x + q.y * dir.y + q.z * dir.z + q.w * dir.w) * inv;\n float s = clamp(floor(proj + 0.5), 0.0, lmax);\n gIdx[k] = uint(s);\n if (fit) {\n vec4 v = vec4(gPixels[k]);\n float b = s / lmax;\n float a = 1.0 - b;\n sAA += a * a; sBB += b * b; sAB += a * b; sAV += a * v; sBV += b * v;\n }\n }\n if (!fit) { return res; }\n float det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-9) { return res; }\n res.e0 = ivec4(clamp(floor((sBB * sAV - sAB * sBV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.e1 = ivec4(clamp(floor((sAA * sBV - sAB * sAV) / det + 0.5), vec4(0.0), vec4(255.0)));\n res.valid = true;\n return res;\n}\n\nvoid main() {\n ivec2 base = ivec2(gl_FragCoord.xy) * 4;\n ivec2 maxXY = uSrcSize - ivec2(1);\n\n ivec4 lo = ivec4(255);\n ivec4 hi = ivec4(0);\n ivec4 isum = ivec4(0);\n int gd = 0; // max |R\u2212G|, |R\u2212B| over the block; 0 \u21D4 exactly grayscale\n for (int i = 0; i < 16; i++) {\n ivec2 p = clamp(base + ivec2(i & 3, i >> 2), ivec2(0), maxXY);\n int sy = (uFlipY != 0) ? (uSrcSize.y - 1 - p.y) : p.y;\n ivec4 px = to8(texelFetch(uSrc, ivec2(p.x, sy), 0));\n gPixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n isum += px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n bool opaque = lo.w == 255;\n\n uint w0 = 0u; uint w1 = 0u; uint w2 = 0u; uint w3 = 0u;\n\n if (opaque && gd == 0) {\n // ---------------- Luminance path: CEM 0, 5-bit weights ----------------\n // Endpoints at the exact extremes; 32 palette levels make an LSQ refit\n // unnecessary.\n uint L0 = uint(lo.x);\n uint L1 = uint(hi.x);\n uint s0 = 0u; uint s1 = 0u; uint s2 = 0u;\n if (L1 > L0) {\n float sc = 64.0 / float(hi.x - lo.x);\n // Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,\n // 2w + 2 for w \u2265 16 (4-wide gap at the middle, so uniform rounding\n // is wrong there). Best candidate of each half, keep the closer.\n for (int k = 0; k < 16; k++) {\n float u = clamp(float(gPixels[k].x - lo.x) * sc, 0.0, 64.0);\n float wlo = clamp(floor(u * 0.5 + 0.5), 0.0, 15.0);\n float whi = clamp(floor((u - 2.0) * 0.5 + 0.5), 16.0, 31.0);\n bool pick = abs(u - wlo * 2.0) <= abs(u - (whi * 2.0 + 2.0));\n uint w = uint(pick ? wlo : whi);\n // Stream bit q = 5k + j; straddles handled with constant shifts.\n uint off = 5u * uint(k);\n if (off < 28u) { s0 |= (w << off); }\n else if (off == 30u) { s0 |= (w << 30u); s1 |= (w >> 2u); }\n else if (off < 60u) { s1 |= (w << (off - 32u)); }\n else if (off == 60u) { s1 |= (w << 28u); s2 |= (w >> 4u); }\n else { s2 |= (w << (off - 64u)); }\n }\n }\n // Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills\n // into word 1 bit 0); stream words map onto block words via rev32.\n w0 = 0x253u | (L0 << 17u) | (L1 << 25u);\n w1 = (L1 >> 7u) | rev32(s2);\n w2 = rev32(s1);\n w3 = rev32(s0);\n } else {\n // ------------- Colour paths: shared PCA seed ---------------------------\n vec4 mean = vec4(isum) / 16.0;\n float lmax = opaque ? 7.0 : 3.0;\n uint wmax = opaque ? 7u : 3u;\n\n ivec4 e0 = lo;\n ivec4 e1 = hi;\n // 8 iterations for opaque blocks, 4 for translucent (their refit\n // absorbs residual axis error \u2014 see astc4x4_fast_f16.wgsl).\n vec4 axis = principalAxis(mean, vec4(hi - lo), opaque ? 8 : 4);\n if (dot(axis, axis) > 0.0) {\n float tMin = 1e30;\n float tMax = -1e30;\n for (int k = 0; k < 16; k++) {\n float t = dot(vec4(gPixels[k]) - mean, axis);\n tMin = min(tMin, t);\n tMax = max(tMax, t);\n }\n e0 = ivec4(clamp(floor(mean + tMin * axis + 0.5), vec4(0.0), vec4(255.0)));\n e1 = ivec4(clamp(floor(mean + tMax * axis + 0.5), vec4(0.0), vec4(255.0)));\n }\n if (opaque) {\n // CEM 8 ships the quantised PCA extents directly (no LSQ fit \u2014 see\n // astc4x4_fast_f16.wgsl for the measured trade), bbox-clamped like\n // the fit output; one assignment pass fills gIdx for the packer.\n e0 = clamp(e0, lo, hi);\n e1 = clamp(e1, lo, hi);\n projAssign(e0, e1, lmax, false);\n } else {\n Fit r = projAssign(e0, e1, lmax, true);\n if (r.valid) {\n // Clamp the refit to the block bbox: on multi-cluster blocks the\n // unconstrained LSQ solve extrapolates far outside the block's\n // colours and the per-channel [0,255] clamp then bends the hue \u2014\n // fringe pixels decode to colours that exist nowhere in the block.\n // gIdx keeps the fit-pass weights (assigned against the seed line)\n // rather than reassigning against the refit endpoints \u2014 see\n // astc4x4_fast_f16.wgsl for the measured trade.\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\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 ivec4 t = e0; e0 = e1; e1 = t;\n for (int k = 0; k < 16; k++) { gIdx[k] = wmax - gIdx[k]; }\n }\n\n if (opaque) {\n // CEM 8: 3-bit weights, stream bit q = 3k.\n uint s0 = 0u; uint s1 = 0u;\n for (int k = 0; k < 16; k++) {\n uint w = gIdx[k];\n uint off = 3u * uint(k);\n if (off < 30u) { s0 |= (w << off); }\n else if (off == 30u) { s0 |= (w << 30u); s1 |= (w >> 2u); }\n else { s1 |= (w << (off - 32u)); }\n }\n // Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.\n w0 = 0x053u | (8u << 13u) | (uint(e0.x) << 17u) | (uint(e1.x) << 25u);\n w1 = (uint(e1.x) >> 7u) | (uint(e0.y) << 1u) | (uint(e1.y) << 9u)\n | (uint(e0.z) << 17u) | (uint(e1.z) << 25u);\n w2 = (uint(e1.z) >> 7u) | rev32(s1);\n w3 = rev32(s0);\n } else {\n // CEM 12: 2-bit weights, stream bit q = 2k (single stream word).\n uint s0 = 0u;\n for (int k = 0; k < 16; k++) {\n s0 |= (gIdx[k] << (2u * uint(k)));\n }\n // Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.\n w0 = 0x042u | (12u << 13u) | (uint(e0.x) << 17u) | (uint(e1.x) << 25u);\n w1 = (uint(e1.x) >> 7u) | (uint(e0.y) << 1u) | (uint(e1.y) << 9u)\n | (uint(e0.z) << 17u) | (uint(e1.z) << 25u);\n w2 = (uint(e1.z) >> 7u) | (uint(e0.w) << 1u) | (uint(e1.w) << 9u);\n w3 = rev32(s0);\n }\n }\n\n outColor = uvec4(w0, w1, w2, w3);\n}\n";
|
|
1583
2207
|
|
|
1584
2208
|
// src/webgl/ASTC4x4WebGLEncoder.ts
|
|
1585
2209
|
var ASTC4x4WebGLEncoder = class extends WebGLBlockEncoder {
|
|
@@ -1615,26 +2239,30 @@ function detectWebGLCapabilities(gl) {
|
|
|
1615
2239
|
// src/webgl/selectWebGLFormat.ts
|
|
1616
2240
|
var NONE = { format: null, encoderClass: null, astcNormalRemap: false };
|
|
1617
2241
|
function selectWebGLFormat(caps, hint, options = {}) {
|
|
1618
|
-
const { colorSpace = "srgb", preferredFormat } = options;
|
|
2242
|
+
const { colorSpace = "srgb", preferredFormat, quality = "high" } = options;
|
|
1619
2243
|
const srgb = colorSpace === "srgb";
|
|
1620
2244
|
const astc = (astcNormalRemap) => ({
|
|
1621
2245
|
format: srgb ? TextureFormat.ASTC_4x4_SRGB : TextureFormat.ASTC_4x4,
|
|
1622
2246
|
encoderClass: ASTC4x4WebGLEncoder,
|
|
1623
2247
|
astcNormalRemap
|
|
1624
2248
|
});
|
|
2249
|
+
const bc1 = () => ({
|
|
2250
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
2251
|
+
encoderClass: BC1WebGLEncoder,
|
|
2252
|
+
astcNormalRemap: false
|
|
2253
|
+
});
|
|
1625
2254
|
if (preferredFormat === "bc1") {
|
|
1626
2255
|
if (hint !== "color") {
|
|
1627
2256
|
console.warn(
|
|
1628
2257
|
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1629
2258
|
);
|
|
1630
2259
|
} else if (srgb ? caps.s3tcSrgb : caps.s3tc) {
|
|
1631
|
-
return
|
|
1632
|
-
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1633
|
-
encoderClass: BC1WebGLEncoder,
|
|
1634
|
-
astcNormalRemap: false
|
|
1635
|
-
};
|
|
2260
|
+
return bc1();
|
|
1636
2261
|
}
|
|
1637
2262
|
}
|
|
2263
|
+
if (quality === "low" && hint === "color" && (srgb ? caps.s3tcSrgb : caps.s3tc)) {
|
|
2264
|
+
return bc1();
|
|
2265
|
+
}
|
|
1638
2266
|
if (hint === "normal") {
|
|
1639
2267
|
if (caps.rgtc) return { format: TextureFormat.BC5, encoderClass: BC5WebGLEncoder, astcNormalRemap: false };
|
|
1640
2268
|
if (caps.astc) return astc(true);
|
|
@@ -1648,35 +2276,40 @@ function selectWebGLFormat(caps, hint, options = {}) {
|
|
|
1648
2276
|
};
|
|
1649
2277
|
}
|
|
1650
2278
|
if (caps.astc) return astc(false);
|
|
1651
|
-
if (hint === "color") {
|
|
1652
|
-
|
|
1653
|
-
return { format: TextureFormat.BC1_SRGB, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
1654
|
-
}
|
|
1655
|
-
if (!srgb && caps.s3tc) {
|
|
1656
|
-
return { format: TextureFormat.BC1, encoderClass: BC1WebGLEncoder, astcNormalRemap: false };
|
|
1657
|
-
}
|
|
2279
|
+
if (hint === "color" && (srgb ? caps.s3tcSrgb : caps.s3tc)) {
|
|
2280
|
+
return bc1();
|
|
1658
2281
|
}
|
|
1659
2282
|
return NONE;
|
|
1660
2283
|
}
|
|
1661
2284
|
|
|
1662
2285
|
// src/selectFormat.ts
|
|
1663
2286
|
function selectFormat(adapter, hint, options = {}) {
|
|
1664
|
-
const { colorSpace = "srgb", preferredFormat } = options;
|
|
2287
|
+
const { colorSpace = "srgb", preferredFormat, quality = "high" } = options;
|
|
1665
2288
|
const srgb = colorSpace === "srgb";
|
|
1666
2289
|
const caps = detectCapabilities(adapter);
|
|
2290
|
+
const bc1 = {
|
|
2291
|
+
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
2292
|
+
encoderClass: BC1Encoder,
|
|
2293
|
+
astcNormalRemap: false
|
|
2294
|
+
};
|
|
2295
|
+
const etc2 = {
|
|
2296
|
+
format: srgb ? TextureFormat.ETC2_RGB8_SRGB : TextureFormat.ETC2_RGB8,
|
|
2297
|
+
encoderClass: ETC2Encoder,
|
|
2298
|
+
astcNormalRemap: false
|
|
2299
|
+
};
|
|
1667
2300
|
if (preferredFormat === "bc1") {
|
|
1668
2301
|
if (hint !== "color") {
|
|
1669
2302
|
console.warn(
|
|
1670
2303
|
`[gputex] preferredFormat 'bc1' ignored for hint '${hint}' \u2014 BC1 has no real alpha channel and is unsuitable for normal maps.`
|
|
1671
2304
|
);
|
|
1672
2305
|
} else if (caps.bc) {
|
|
1673
|
-
return
|
|
1674
|
-
format: srgb ? TextureFormat.BC1_SRGB : TextureFormat.BC1,
|
|
1675
|
-
encoderClass: BC1Encoder,
|
|
1676
|
-
astcNormalRemap: false
|
|
1677
|
-
};
|
|
2306
|
+
return bc1;
|
|
1678
2307
|
}
|
|
1679
2308
|
}
|
|
2309
|
+
if (quality === "low" && hint === "color") {
|
|
2310
|
+
if (caps.bc) return bc1;
|
|
2311
|
+
if (caps.etc2) return etc2;
|
|
2312
|
+
}
|
|
1680
2313
|
if (caps.bc) {
|
|
1681
2314
|
if (hint === "normal") {
|
|
1682
2315
|
return { format: TextureFormat.BC5, encoderClass: BC5Encoder, astcNormalRemap: false };
|
|
@@ -1695,6 +2328,9 @@ function selectFormat(adapter, hint, options = {}) {
|
|
|
1695
2328
|
astcNormalRemap: hint === "normal"
|
|
1696
2329
|
};
|
|
1697
2330
|
}
|
|
2331
|
+
if (caps.etc2 && hint === "color") {
|
|
2332
|
+
return etc2;
|
|
2333
|
+
}
|
|
1698
2334
|
return { format: null, encoderClass: null, astcNormalRemap: false };
|
|
1699
2335
|
}
|
|
1700
2336
|
|
|
@@ -1720,24 +2356,28 @@ function downsample2x(src) {
|
|
|
1720
2356
|
const dstW = Math.max(1, src.width >> 1);
|
|
1721
2357
|
const dstH = Math.max(1, src.height >> 1);
|
|
1722
2358
|
const dst = new Uint8ClampedArray(dstW * dstH * 4);
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
2359
|
+
const s = src.data;
|
|
2360
|
+
const rowBytes = src.width * 4;
|
|
2361
|
+
const lastColByte = (src.width - 1) * 4;
|
|
1725
2362
|
const sMaxY = src.height - 1;
|
|
2363
|
+
let o = 0;
|
|
1726
2364
|
for (let y = 0; y < dstH; y++) {
|
|
1727
|
-
const sy0 = y
|
|
1728
|
-
const sy1 =
|
|
2365
|
+
const sy0 = y << 1;
|
|
2366
|
+
const sy1 = sy0 < sMaxY ? sy0 + 1 : sMaxY;
|
|
2367
|
+
const r0 = sy0 * rowBytes;
|
|
2368
|
+
const r1 = sy1 * rowBytes;
|
|
1729
2369
|
for (let x = 0; x < dstW; x++) {
|
|
1730
|
-
const sx0 = x
|
|
1731
|
-
const sx1 =
|
|
1732
|
-
const i00 =
|
|
1733
|
-
const i10 =
|
|
1734
|
-
const i01 =
|
|
1735
|
-
const i11 =
|
|
1736
|
-
|
|
1737
|
-
dst[o] =
|
|
1738
|
-
dst[o +
|
|
1739
|
-
dst[o +
|
|
1740
|
-
|
|
2370
|
+
const sx0 = x << 3;
|
|
2371
|
+
const sx1 = sx0 + 4 <= lastColByte ? sx0 + 4 : lastColByte;
|
|
2372
|
+
const i00 = r0 + sx0;
|
|
2373
|
+
const i10 = r0 + sx1;
|
|
2374
|
+
const i01 = r1 + sx0;
|
|
2375
|
+
const i11 = r1 + sx1;
|
|
2376
|
+
dst[o] = s[i00] + s[i10] + s[i01] + s[i11] + 2 >> 2;
|
|
2377
|
+
dst[o + 1] = s[i00 + 1] + s[i10 + 1] + s[i01 + 1] + s[i11 + 1] + 2 >> 2;
|
|
2378
|
+
dst[o + 2] = s[i00 + 2] + s[i10 + 2] + s[i01 + 2] + s[i11 + 2] + 2 >> 2;
|
|
2379
|
+
dst[o + 3] = s[i00 + 3] + s[i10 + 3] + s[i01 + 3] + s[i11 + 3] + 2 >> 2;
|
|
2380
|
+
o += 4;
|
|
1741
2381
|
}
|
|
1742
2382
|
}
|
|
1743
2383
|
return { data: dst, width: dstW, height: dstH };
|
|
@@ -1764,6 +2404,97 @@ function padToBlockMultiple(level) {
|
|
|
1764
2404
|
return { data: out, width: pw, height: ph };
|
|
1765
2405
|
}
|
|
1766
2406
|
|
|
2407
|
+
// src/gpuMipgen.ts
|
|
2408
|
+
function gpuMipLevelCount(width, height) {
|
|
2409
|
+
return 32 - Math.clz32(Math.max(width, height));
|
|
2410
|
+
}
|
|
2411
|
+
var DOWNSAMPLE_WGSL = (
|
|
2412
|
+
/* wgsl */
|
|
2413
|
+
`
|
|
2414
|
+
@group(0) @binding(0) var src : texture_2d<f32>;
|
|
2415
|
+
@group(0) @binding(1) var dst : texture_storage_2d<rgba8unorm, write>;
|
|
2416
|
+
|
|
2417
|
+
// 2x2 box filter, clamp-to-edge fold on odd source dimensions, integer
|
|
2418
|
+
// round-to-nearest \u2014 bit-exact with mipgen.ts downsample2x.
|
|
2419
|
+
@compute @workgroup_size(8, 8, 1)
|
|
2420
|
+
fn downsample(@builtin(global_invocation_id) gid : vec3<u32>) {
|
|
2421
|
+
let dstSize = textureDimensions(dst);
|
|
2422
|
+
if (gid.x >= dstSize.x || gid.y >= dstSize.y) { return; }
|
|
2423
|
+
let srcMax = textureDimensions(src) - vec2<u32>(1u, 1u);
|
|
2424
|
+
let x0 = min(gid.x * 2u, srcMax.x);
|
|
2425
|
+
let x1 = min(gid.x * 2u + 1u, srcMax.x);
|
|
2426
|
+
let y0 = min(gid.y * 2u, srcMax.y);
|
|
2427
|
+
let y1 = min(gid.y * 2u + 1u, srcMax.y);
|
|
2428
|
+
// round() recovers the exact byte values (unorm->f32 conversion is only
|
|
2429
|
+
// correctly-rounded, so a*255 is n +/- ~1e-5 \u2014 floor() would misround
|
|
2430
|
+
// sums divisible by 4 without it).
|
|
2431
|
+
let s = round(textureLoad(src, vec2<u32>(x0, y0), 0) * 255.0) +
|
|
2432
|
+
round(textureLoad(src, vec2<u32>(x1, y0), 0) * 255.0) +
|
|
2433
|
+
round(textureLoad(src, vec2<u32>(x0, y1), 0) * 255.0) +
|
|
2434
|
+
round(textureLoad(src, vec2<u32>(x1, y1), 0) * 255.0);
|
|
2435
|
+
textureStore(dst, gid.xy, floor((s + 2.0) * 0.25) / 255.0);
|
|
2436
|
+
}
|
|
2437
|
+
`
|
|
2438
|
+
);
|
|
2439
|
+
var pipelineCache = /* @__PURE__ */ new WeakMap();
|
|
2440
|
+
function getDownsamplePipeline(device) {
|
|
2441
|
+
let pipeline = pipelineCache.get(device);
|
|
2442
|
+
if (!pipeline) {
|
|
2443
|
+
pipeline = device.createComputePipelineAsync({
|
|
2444
|
+
label: "gputex-mipgen-pipeline",
|
|
2445
|
+
layout: "auto",
|
|
2446
|
+
compute: {
|
|
2447
|
+
module: device.createShaderModule({ label: "gputex-mipgen", code: DOWNSAMPLE_WGSL }),
|
|
2448
|
+
entryPoint: "downsample"
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
pipelineCache.set(device, pipeline);
|
|
2452
|
+
pipeline.catch(() => pipelineCache.delete(device));
|
|
2453
|
+
}
|
|
2454
|
+
return pipeline;
|
|
2455
|
+
}
|
|
2456
|
+
async function generateGpuMipChain(device, source, { flipY = false } = {}) {
|
|
2457
|
+
const width = source.width;
|
|
2458
|
+
const height = source.height;
|
|
2459
|
+
if (!width || !height) {
|
|
2460
|
+
throw new Error("generateGpuMipChain: source has no dimensions");
|
|
2461
|
+
}
|
|
2462
|
+
const mipLevelCount = gpuMipLevelCount(width, height);
|
|
2463
|
+
const texture = device.createTexture({
|
|
2464
|
+
label: "gputex-mip-chain",
|
|
2465
|
+
size: [width, height, 1],
|
|
2466
|
+
format: "rgba8unorm",
|
|
2467
|
+
mipLevelCount,
|
|
2468
|
+
// COPY_DST + RENDER_ATTACHMENT for copyExternalImageToTexture (a blit
|
|
2469
|
+
// internally), TEXTURE_BINDING for downsample/encoder reads,
|
|
2470
|
+
// STORAGE_BINDING for downsample writes (rgba8unorm write-only storage
|
|
2471
|
+
// is core WebGPU).
|
|
2472
|
+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING
|
|
2473
|
+
});
|
|
2474
|
+
device.queue.copyExternalImageToTexture({ source, flipY }, { texture }, [width, height, 1]);
|
|
2475
|
+
const pipeline = await getDownsamplePipeline(device);
|
|
2476
|
+
const enc = device.createCommandEncoder({ label: "gputex-mipgen" });
|
|
2477
|
+
const pass = enc.beginComputePass();
|
|
2478
|
+
pass.setPipeline(pipeline);
|
|
2479
|
+
for (let level = 0; level + 1 < mipLevelCount; level++) {
|
|
2480
|
+
const bindGroup = device.createBindGroup({
|
|
2481
|
+
label: `gputex-mipgen-bg-${level}`,
|
|
2482
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
2483
|
+
entries: [
|
|
2484
|
+
{ binding: 0, resource: texture.createView({ baseMipLevel: level, mipLevelCount: 1 }) },
|
|
2485
|
+
{ binding: 1, resource: texture.createView({ baseMipLevel: level + 1, mipLevelCount: 1 }) }
|
|
2486
|
+
]
|
|
2487
|
+
});
|
|
2488
|
+
pass.setBindGroup(0, bindGroup);
|
|
2489
|
+
const dstW = Math.max(1, width >> level + 1);
|
|
2490
|
+
const dstH = Math.max(1, height >> level + 1);
|
|
2491
|
+
pass.dispatchWorkgroups(Math.ceil(dstW / 8), Math.ceil(dstH / 8), 1);
|
|
2492
|
+
}
|
|
2493
|
+
pass.end();
|
|
2494
|
+
device.queue.submit([enc.finish()]);
|
|
2495
|
+
return texture;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
1767
2498
|
// src/svg.ts
|
|
1768
2499
|
function isSvgMarkup(source) {
|
|
1769
2500
|
return source.trimStart().startsWith("<");
|
|
@@ -1902,7 +2633,13 @@ async function rasterizeSvg(source, options = {}) {
|
|
|
1902
2633
|
import { ClampToEdgeWrapping as ClampToEdgeWrapping2, LinearFilter as LinearFilter2, LinearSRGBColorSpace as LinearSRGBColorSpace2, SRGBColorSpace as SRGBColorSpace2, Texture } from "three";
|
|
1903
2634
|
|
|
1904
2635
|
// src/three/buildTexture.ts
|
|
1905
|
-
import {
|
|
2636
|
+
import {
|
|
2637
|
+
RED_GREEN_RGTC2_Format,
|
|
2638
|
+
RGB_ETC2_Format,
|
|
2639
|
+
RGBA_ASTC_4x4_Format,
|
|
2640
|
+
RGBA_BPTC_Format,
|
|
2641
|
+
RGBA_S3TC_DXT1_Format
|
|
2642
|
+
} from "three";
|
|
1906
2643
|
|
|
1907
2644
|
// src/three/textureAssembly.ts
|
|
1908
2645
|
import {
|
|
@@ -1944,12 +2681,15 @@ var THREE_FORMAT = {
|
|
|
1944
2681
|
[TextureFormat.BC7]: RGBA_BPTC_Format,
|
|
1945
2682
|
[TextureFormat.BC7_SRGB]: RGBA_BPTC_Format,
|
|
1946
2683
|
[TextureFormat.ASTC_4x4]: RGBA_ASTC_4x4_Format,
|
|
1947
|
-
[TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format
|
|
2684
|
+
[TextureFormat.ASTC_4x4_SRGB]: RGBA_ASTC_4x4_Format,
|
|
2685
|
+
[TextureFormat.ETC2_RGB8]: RGB_ETC2_Format,
|
|
2686
|
+
[TextureFormat.ETC2_RGB8_SRGB]: RGB_ETC2_Format
|
|
1948
2687
|
};
|
|
1949
2688
|
var SRGB_FORMATS = /* @__PURE__ */ new Set([
|
|
1950
2689
|
TextureFormat.BC1_SRGB,
|
|
1951
2690
|
TextureFormat.BC7_SRGB,
|
|
1952
|
-
TextureFormat.ASTC_4x4_SRGB
|
|
2691
|
+
TextureFormat.ASTC_4x4_SRGB,
|
|
2692
|
+
TextureFormat.ETC2_RGB8_SRGB
|
|
1953
2693
|
]);
|
|
1954
2694
|
function isSrgbFormat(format) {
|
|
1955
2695
|
return SRGB_FORMATS.has(format);
|
|
@@ -1960,16 +2700,132 @@ function threeFormatFor(format) {
|
|
|
1960
2700
|
function buildCompressedTexture(levels, format) {
|
|
1961
2701
|
return assembleCompressedTexture(levels, threeFormatFor(format), isSrgbFormat(format));
|
|
1962
2702
|
}
|
|
1963
|
-
async function encodeToTexture(encoder, source, { colorSpace = "srgb",
|
|
2703
|
+
async function encodeToTexture(encoder, source, { colorSpace = "srgb", flipY = false } = {}) {
|
|
1964
2704
|
const formats = encoder.constructor.textureFormats;
|
|
1965
2705
|
const wantSrgb = colorSpace === "srgb" && encoder.supportsSrgb;
|
|
1966
2706
|
const format = formats.find((f) => isSrgbFormat(f) === wantSrgb) ?? formats[0];
|
|
1967
|
-
const bytes = await encoder.encodeToBytes(source, { flipY
|
|
2707
|
+
const bytes = await encoder.encodeToBytes(source, { flipY });
|
|
1968
2708
|
const texture = buildCompressedTexture([bytes], format);
|
|
1969
2709
|
return { ...bytes, texture };
|
|
1970
2710
|
}
|
|
1971
2711
|
|
|
2712
|
+
// src/three/transcodeCache.ts
|
|
2713
|
+
var DEFAULT_LIMIT = 256 * 1024 * 1024;
|
|
2714
|
+
var maxBytes = DEFAULT_LIMIT;
|
|
2715
|
+
var totalBytes = 0;
|
|
2716
|
+
var entries = /* @__PURE__ */ new Map();
|
|
2717
|
+
async function sha256Hex(data) {
|
|
2718
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
2719
|
+
let hex = "";
|
|
2720
|
+
for (const b of new Uint8Array(digest)) hex += b.toString(16).padStart(2, "0");
|
|
2721
|
+
return hex;
|
|
2722
|
+
}
|
|
2723
|
+
async function sourceIdentity(source, cacheKey) {
|
|
2724
|
+
if (cacheKey) return `k:${cacheKey}`;
|
|
2725
|
+
const canHash = typeof crypto !== "undefined" && !!crypto.subtle;
|
|
2726
|
+
if (typeof source === "string") {
|
|
2727
|
+
if (source.length > 1024 || /^data:/i.test(source) || /^\s*</.test(source)) {
|
|
2728
|
+
return canHash ? `s:${await sha256Hex(new TextEncoder().encode(source))}` : null;
|
|
2729
|
+
}
|
|
2730
|
+
return `u:${source}`;
|
|
2731
|
+
}
|
|
2732
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
2733
|
+
return canHash ? `b:${await sha256Hex(await source.arrayBuffer())}` : null;
|
|
2734
|
+
}
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
async function buildTranscodeKey(source, cacheKey, fp) {
|
|
2738
|
+
if (maxBytes <= 0) return null;
|
|
2739
|
+
const id = await sourceIdentity(source, cacheKey);
|
|
2740
|
+
if (!id) return null;
|
|
2741
|
+
const fingerprint = [
|
|
2742
|
+
fp.format,
|
|
2743
|
+
fp.colorSpace,
|
|
2744
|
+
fp.flipY ? "flip" : "noflip",
|
|
2745
|
+
fp.mipmaps ? "mips" : "nomips",
|
|
2746
|
+
fp.svgSize === void 0 ? "" : JSON.stringify(fp.svgSize)
|
|
2747
|
+
].join("|");
|
|
2748
|
+
return `${fingerprint}\0${id}`;
|
|
2749
|
+
}
|
|
2750
|
+
function readTranscodeCache(key) {
|
|
2751
|
+
const hit = entries.get(key);
|
|
2752
|
+
if (!hit) return null;
|
|
2753
|
+
entries.delete(key);
|
|
2754
|
+
entries.set(key, hit);
|
|
2755
|
+
return hit.entry;
|
|
2756
|
+
}
|
|
2757
|
+
function writeTranscodeCache(key, entry) {
|
|
2758
|
+
const bytes = entry.levels.reduce((sum, l) => sum + l.data.byteLength, 0);
|
|
2759
|
+
if (bytes > maxBytes) return;
|
|
2760
|
+
const prev = entries.get(key);
|
|
2761
|
+
if (prev) {
|
|
2762
|
+
totalBytes -= prev.bytes;
|
|
2763
|
+
entries.delete(key);
|
|
2764
|
+
}
|
|
2765
|
+
entries.set(key, { entry, bytes });
|
|
2766
|
+
totalBytes += bytes;
|
|
2767
|
+
evictToLimit();
|
|
2768
|
+
}
|
|
2769
|
+
function evictToLimit() {
|
|
2770
|
+
for (const [key, value] of entries) {
|
|
2771
|
+
if (totalBytes <= maxBytes) break;
|
|
2772
|
+
entries.delete(key);
|
|
2773
|
+
totalBytes -= value.bytes;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
function setTranscodeCacheLimit(bytes) {
|
|
2777
|
+
maxBytes = Math.max(0, bytes);
|
|
2778
|
+
evictToLimit();
|
|
2779
|
+
}
|
|
2780
|
+
function clearTranscodeCache() {
|
|
2781
|
+
entries.clear();
|
|
2782
|
+
totalBytes = 0;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
1972
2785
|
// src/three/compressTexture.ts
|
|
2786
|
+
var sharedGpuPromise = null;
|
|
2787
|
+
var SHARED_DEVICE_FEATURES = [
|
|
2788
|
+
"texture-compression-bc",
|
|
2789
|
+
"texture-compression-astc",
|
|
2790
|
+
"texture-compression-etc2",
|
|
2791
|
+
"shader-f16",
|
|
2792
|
+
"timestamp-query"
|
|
2793
|
+
];
|
|
2794
|
+
async function createSharedGpu() {
|
|
2795
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
2796
|
+
if (!adapter) return null;
|
|
2797
|
+
const requiredFeatures = SHARED_DEVICE_FEATURES.filter((f) => adapter.features.has(f));
|
|
2798
|
+
const device = await adapter.requestDevice({ requiredFeatures });
|
|
2799
|
+
return { adapter, device, encoders: /* @__PURE__ */ new Map() };
|
|
2800
|
+
}
|
|
2801
|
+
function getSharedGpu() {
|
|
2802
|
+
if (!sharedGpuPromise) {
|
|
2803
|
+
const p = createSharedGpu();
|
|
2804
|
+
sharedGpuPromise = p;
|
|
2805
|
+
p.then((shared) => {
|
|
2806
|
+
if (!shared) return;
|
|
2807
|
+
void shared.device.lost.then(() => {
|
|
2808
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2809
|
+
shared.encoders.clear();
|
|
2810
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2811
|
+
});
|
|
2812
|
+
}).catch(() => {
|
|
2813
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
return sharedGpuPromise;
|
|
2817
|
+
}
|
|
2818
|
+
function releaseSharedGpuResources() {
|
|
2819
|
+
const p = sharedGpuPromise;
|
|
2820
|
+
sharedGpuPromise = null;
|
|
2821
|
+
void p?.then((shared) => {
|
|
2822
|
+
if (!shared) return;
|
|
2823
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2824
|
+
shared.encoders.clear();
|
|
2825
|
+
shared.device.destroy();
|
|
2826
|
+
}).catch(() => {
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
1973
2829
|
async function sourceToBitmap(source, svgSize) {
|
|
1974
2830
|
const opts = {
|
|
1975
2831
|
colorSpaceConversion: "none",
|
|
@@ -1979,6 +2835,13 @@ async function sourceToBitmap(source, svgSize) {
|
|
|
1979
2835
|
if (isSvgMarkup(source)) {
|
|
1980
2836
|
return rasterizeSvg(source, { size: svgSize });
|
|
1981
2837
|
}
|
|
2838
|
+
if (/^data:/i.test(source)) {
|
|
2839
|
+
const blob2 = dataUrlToBlob(source);
|
|
2840
|
+
if (isSvgBlob(blob2)) {
|
|
2841
|
+
return rasterizeSvg(blob2, { size: svgSize });
|
|
2842
|
+
}
|
|
2843
|
+
return createImageBitmap(blob2, opts);
|
|
2844
|
+
}
|
|
1982
2845
|
const resp = await fetch(source);
|
|
1983
2846
|
if (!resp.ok) {
|
|
1984
2847
|
throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
|
|
@@ -2013,6 +2876,27 @@ async function sourceToBitmap(source, svgSize) {
|
|
|
2013
2876
|
function isImageMimeType(type) {
|
|
2014
2877
|
return /^image\//i.test(type) && !/svg/i.test(type);
|
|
2015
2878
|
}
|
|
2879
|
+
function dataUrlToBlob(url) {
|
|
2880
|
+
const comma = url.indexOf(",");
|
|
2881
|
+
if (comma < 0) {
|
|
2882
|
+
throw new Error("compressTexture: malformed data: URL (no comma)");
|
|
2883
|
+
}
|
|
2884
|
+
const header = url.slice(5, comma);
|
|
2885
|
+
const isBase64 = /;base64$/i.test(header);
|
|
2886
|
+
const type = header.replace(/;base64$/i, "");
|
|
2887
|
+
if (!isBase64) {
|
|
2888
|
+
return new Blob([decodeURIComponent(url.slice(comma + 1))], { type });
|
|
2889
|
+
}
|
|
2890
|
+
const payload = url.slice(comma + 1);
|
|
2891
|
+
const fromBase64 = Uint8Array.fromBase64;
|
|
2892
|
+
if (fromBase64) {
|
|
2893
|
+
return new Blob([fromBase64(payload)], { type });
|
|
2894
|
+
}
|
|
2895
|
+
const bin = atob(payload);
|
|
2896
|
+
const bytes = new Uint8Array(bin.length);
|
|
2897
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2898
|
+
return new Blob([bytes], { type });
|
|
2899
|
+
}
|
|
2016
2900
|
function bitmapToMipLevel(bitmap, flipY) {
|
|
2017
2901
|
const w = bitmap.width, h = bitmap.height;
|
|
2018
2902
|
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
|
|
@@ -2046,19 +2930,59 @@ async function compressTexture(source, options = {}) {
|
|
|
2046
2930
|
const {
|
|
2047
2931
|
hint = "color",
|
|
2048
2932
|
preferredFormat,
|
|
2933
|
+
quality = "high",
|
|
2049
2934
|
colorSpace = "srgb",
|
|
2050
2935
|
svgSize,
|
|
2051
2936
|
flipY = true,
|
|
2052
2937
|
mipmaps = false,
|
|
2053
|
-
|
|
2938
|
+
cache = false,
|
|
2939
|
+
cacheKey,
|
|
2054
2940
|
device: providedDevice,
|
|
2055
2941
|
adapter: providedAdapter
|
|
2056
2942
|
} = options;
|
|
2057
2943
|
const srgb = colorSpace === "srgb";
|
|
2944
|
+
const t0 = performance.now();
|
|
2945
|
+
const gpu = await resolveWebGPU();
|
|
2946
|
+
const gl = gpu ? null : resolveWebGL();
|
|
2947
|
+
const activeFormat = gpu?.selection.format ?? gl?.selection.format ?? null;
|
|
2948
|
+
let transcodeKey = null;
|
|
2949
|
+
if (cache && activeFormat) {
|
|
2950
|
+
transcodeKey = await buildTranscodeKey(source, cacheKey, {
|
|
2951
|
+
format: activeFormat,
|
|
2952
|
+
colorSpace,
|
|
2953
|
+
flipY,
|
|
2954
|
+
mipmaps,
|
|
2955
|
+
svgSize
|
|
2956
|
+
});
|
|
2957
|
+
if (transcodeKey) {
|
|
2958
|
+
const hit = readTranscodeCache(transcodeKey);
|
|
2959
|
+
if (hit) {
|
|
2960
|
+
const tex2 = buildCompressedTexture(hit.levels, hit.format);
|
|
2961
|
+
return {
|
|
2962
|
+
texture: tex2,
|
|
2963
|
+
format: hit.format,
|
|
2964
|
+
fallbackUncompressed: false,
|
|
2965
|
+
backend: gpu ? "webgpu" : "webgl",
|
|
2966
|
+
astcNormalRemap: (gpu ?? gl).selection.astcNormalRemap,
|
|
2967
|
+
width: hit.width,
|
|
2968
|
+
height: hit.height,
|
|
2969
|
+
mipLevels: hit.levels.length,
|
|
2970
|
+
encodeMs: 0,
|
|
2971
|
+
decodeMs: 0,
|
|
2972
|
+
totalMs: performance.now() - t0,
|
|
2973
|
+
cacheHit: true,
|
|
2974
|
+
destroy: () => {
|
|
2975
|
+
tex2.dispose();
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
const tDecode = performance.now();
|
|
2058
2982
|
const bitmap = await sourceToBitmap(source, svgSize);
|
|
2059
|
-
const
|
|
2060
|
-
if (
|
|
2061
|
-
const viaWebGL = encodeViaWebGL();
|
|
2983
|
+
const decodeMs = performance.now() - tDecode;
|
|
2984
|
+
if (gpu) return encodeViaWebGPU(gpu);
|
|
2985
|
+
const viaWebGL = gl ? encodeViaWebGL(gl) : null;
|
|
2062
2986
|
if (viaWebGL) return viaWebGL;
|
|
2063
2987
|
console.warn(
|
|
2064
2988
|
"[compressTexture] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
@@ -2074,35 +2998,73 @@ async function compressTexture(source, options = {}) {
|
|
|
2074
2998
|
height: bitmap.height,
|
|
2075
2999
|
mipLevels: 1,
|
|
2076
3000
|
encodeMs: 0,
|
|
3001
|
+
decodeMs,
|
|
3002
|
+
totalMs: performance.now() - t0,
|
|
3003
|
+
cacheHit: false,
|
|
2077
3004
|
destroy: () => {
|
|
2078
3005
|
tex.dispose();
|
|
2079
3006
|
}
|
|
2080
3007
|
};
|
|
2081
|
-
async function
|
|
3008
|
+
async function resolveWebGPU() {
|
|
2082
3009
|
if (!("gpu" in navigator)) return null;
|
|
2083
|
-
|
|
3010
|
+
let shared = null;
|
|
3011
|
+
let adapter;
|
|
3012
|
+
if (providedAdapter) {
|
|
3013
|
+
adapter = providedAdapter;
|
|
3014
|
+
} else if (providedDevice) {
|
|
3015
|
+
adapter = await navigator.gpu.requestAdapter();
|
|
3016
|
+
} else {
|
|
3017
|
+
shared = await getSharedGpu();
|
|
3018
|
+
adapter = shared?.adapter ?? null;
|
|
3019
|
+
}
|
|
2084
3020
|
if (!adapter) return null;
|
|
2085
|
-
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat });
|
|
3021
|
+
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat, quality });
|
|
2086
3022
|
if (!selection.format || !selection.encoderClass) return null;
|
|
3023
|
+
return {
|
|
3024
|
+
adapter,
|
|
3025
|
+
shared,
|
|
3026
|
+
selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass }
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
async function encodeViaWebGPU({ adapter, shared, selection }) {
|
|
3030
|
+
const EncoderCtor = selection.encoderClass;
|
|
2087
3031
|
let encoder;
|
|
3032
|
+
let sharedEncoder = false;
|
|
2088
3033
|
if (providedDevice) {
|
|
2089
|
-
const EncoderCtor = selection.encoderClass;
|
|
2090
3034
|
encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
|
|
3035
|
+
} else if (shared) {
|
|
3036
|
+
sharedEncoder = true;
|
|
3037
|
+
let cached = shared.encoders.get(EncoderCtor);
|
|
3038
|
+
if (!cached) {
|
|
3039
|
+
cached = new EncoderCtor({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
|
|
3040
|
+
shared.encoders.set(EncoderCtor, cached);
|
|
3041
|
+
}
|
|
3042
|
+
encoder = cached;
|
|
2091
3043
|
} else {
|
|
2092
|
-
encoder = await
|
|
3044
|
+
encoder = await EncoderCtor.create();
|
|
2093
3045
|
}
|
|
3046
|
+
const destroyEncoder = sharedEncoder ? () => {
|
|
3047
|
+
} : () => encoder.destroy();
|
|
2094
3048
|
try {
|
|
2095
3049
|
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
2096
3050
|
if (!mipmaps) {
|
|
2097
3051
|
let bytes;
|
|
2098
3052
|
if (needsWriteTexture) {
|
|
2099
|
-
const
|
|
2100
|
-
const imageData = mipLevelToImageData(
|
|
2101
|
-
bytes = await encoder.encodeToBytes(imageData
|
|
3053
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
3054
|
+
const imageData = mipLevelToImageData(level0);
|
|
3055
|
+
bytes = await encoder.encodeToBytes(imageData);
|
|
2102
3056
|
} else {
|
|
2103
|
-
bytes = await encoder.encodeToBytes(bitmap, { flipY
|
|
3057
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY });
|
|
2104
3058
|
}
|
|
2105
3059
|
const tex3 = buildCompressedTexture([bytes], selection.format);
|
|
3060
|
+
if (transcodeKey) {
|
|
3061
|
+
writeTranscodeCache(transcodeKey, {
|
|
3062
|
+
format: selection.format,
|
|
3063
|
+
width: bytes.width,
|
|
3064
|
+
height: bytes.height,
|
|
3065
|
+
levels: [bytes]
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
2106
3068
|
return {
|
|
2107
3069
|
texture: tex3,
|
|
2108
3070
|
format: selection.format,
|
|
@@ -2113,55 +3075,82 @@ async function compressTexture(source, options = {}) {
|
|
|
2113
3075
|
height: bytes.height,
|
|
2114
3076
|
mipLevels: 1,
|
|
2115
3077
|
encodeMs: bytes.encodeMs,
|
|
3078
|
+
decodeMs,
|
|
3079
|
+
totalMs: performance.now() - t0,
|
|
3080
|
+
cacheHit: false,
|
|
2116
3081
|
destroy: () => {
|
|
2117
3082
|
tex3.dispose();
|
|
2118
|
-
|
|
3083
|
+
destroyEncoder();
|
|
2119
3084
|
}
|
|
2120
3085
|
};
|
|
2121
3086
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
3087
|
+
let chainResult;
|
|
3088
|
+
if (needsWriteTexture) {
|
|
3089
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
3090
|
+
chainResult = await encoder.encodeMipChainToBytes(generateMipChain(level0).map(padToBlockMultiple));
|
|
3091
|
+
} else {
|
|
3092
|
+
const chainTex = await generateGpuMipChain(encoder.device, bitmap, { flipY });
|
|
3093
|
+
try {
|
|
3094
|
+
chainResult = await encoder.encodeMipChainFromTexture(chainTex);
|
|
3095
|
+
} finally {
|
|
3096
|
+
chainTex.destroy();
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const { levels, encodeMs } = chainResult;
|
|
3100
|
+
const tex2 = buildCompressedTexture(levels, selection.format);
|
|
3101
|
+
if (transcodeKey) {
|
|
3102
|
+
writeTranscodeCache(transcodeKey, {
|
|
3103
|
+
format: selection.format,
|
|
3104
|
+
width: bitmap.width,
|
|
3105
|
+
height: bitmap.height,
|
|
3106
|
+
levels
|
|
3107
|
+
});
|
|
2132
3108
|
}
|
|
2133
|
-
const tex2 = buildCompressedTexture(encodedLevels, selection.format);
|
|
2134
3109
|
return {
|
|
2135
3110
|
texture: tex2,
|
|
2136
3111
|
format: selection.format,
|
|
2137
3112
|
fallbackUncompressed: false,
|
|
2138
3113
|
backend: "webgpu",
|
|
2139
3114
|
astcNormalRemap: selection.astcNormalRemap,
|
|
2140
|
-
width:
|
|
2141
|
-
height:
|
|
2142
|
-
mipLevels:
|
|
2143
|
-
encodeMs
|
|
3115
|
+
width: bitmap.width,
|
|
3116
|
+
height: bitmap.height,
|
|
3117
|
+
mipLevels: levels.length,
|
|
3118
|
+
encodeMs,
|
|
3119
|
+
decodeMs,
|
|
3120
|
+
totalMs: performance.now() - t0,
|
|
3121
|
+
cacheHit: false,
|
|
2144
3122
|
destroy: () => {
|
|
2145
3123
|
tex2.dispose();
|
|
2146
|
-
|
|
3124
|
+
destroyEncoder();
|
|
2147
3125
|
}
|
|
2148
3126
|
};
|
|
2149
3127
|
} catch (e) {
|
|
2150
|
-
|
|
3128
|
+
destroyEncoder();
|
|
2151
3129
|
throw e;
|
|
2152
3130
|
}
|
|
2153
3131
|
}
|
|
2154
|
-
function
|
|
2155
|
-
const
|
|
2156
|
-
if (!
|
|
2157
|
-
const caps = detectWebGLCapabilities(
|
|
2158
|
-
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat });
|
|
3132
|
+
function resolveWebGL() {
|
|
3133
|
+
const gl2 = getSharedWebGLContext();
|
|
3134
|
+
if (!gl2) return null;
|
|
3135
|
+
const caps = detectWebGLCapabilities(gl2);
|
|
3136
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat, quality });
|
|
2159
3137
|
if (!selection.format || !selection.encoderClass) return null;
|
|
2160
|
-
|
|
3138
|
+
return { gl: gl2, selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass } };
|
|
3139
|
+
}
|
|
3140
|
+
function encodeViaWebGL({ gl: gl2, selection }) {
|
|
3141
|
+
const encoder = selection.encoderClass.create(gl2);
|
|
2161
3142
|
try {
|
|
2162
3143
|
if (!mipmaps) {
|
|
2163
3144
|
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
2164
3145
|
const tex3 = buildCompressedTexture([bytes], selection.format);
|
|
3146
|
+
if (transcodeKey) {
|
|
3147
|
+
writeTranscodeCache(transcodeKey, {
|
|
3148
|
+
format: selection.format,
|
|
3149
|
+
width: bytes.width,
|
|
3150
|
+
height: bytes.height,
|
|
3151
|
+
levels: [bytes]
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
2165
3154
|
return {
|
|
2166
3155
|
texture: tex3,
|
|
2167
3156
|
format: selection.format,
|
|
@@ -2172,6 +3161,9 @@ async function compressTexture(source, options = {}) {
|
|
|
2172
3161
|
height: bytes.height,
|
|
2173
3162
|
mipLevels: 1,
|
|
2174
3163
|
encodeMs: bytes.encodeMs,
|
|
3164
|
+
decodeMs,
|
|
3165
|
+
totalMs: performance.now() - t0,
|
|
3166
|
+
cacheHit: false,
|
|
2175
3167
|
destroy: () => {
|
|
2176
3168
|
tex3.dispose();
|
|
2177
3169
|
encoder.destroy();
|
|
@@ -2189,6 +3181,14 @@ async function compressTexture(source, options = {}) {
|
|
|
2189
3181
|
totalEncodeMs += bytes.encodeMs;
|
|
2190
3182
|
}
|
|
2191
3183
|
const tex2 = buildCompressedTexture(encodedLevels, selection.format);
|
|
3184
|
+
if (transcodeKey) {
|
|
3185
|
+
writeTranscodeCache(transcodeKey, {
|
|
3186
|
+
format: selection.format,
|
|
3187
|
+
width: level0.width,
|
|
3188
|
+
height: level0.height,
|
|
3189
|
+
levels: encodedLevels
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
2192
3192
|
return {
|
|
2193
3193
|
texture: tex2,
|
|
2194
3194
|
format: selection.format,
|
|
@@ -2199,6 +3199,9 @@ async function compressTexture(source, options = {}) {
|
|
|
2199
3199
|
height: level0.height,
|
|
2200
3200
|
mipLevels: encodedLevels.length,
|
|
2201
3201
|
encodeMs: totalEncodeMs,
|
|
3202
|
+
decodeMs,
|
|
3203
|
+
totalMs: performance.now() - t0,
|
|
3204
|
+
cacheHit: false,
|
|
2202
3205
|
destroy: () => {
|
|
2203
3206
|
tex2.dispose();
|
|
2204
3207
|
encoder.destroy();
|
|
@@ -2223,6 +3226,12 @@ var GputexLoader = class extends Loader {
|
|
|
2223
3226
|
* `CompressOptions.preferredFormat`.
|
|
2224
3227
|
*/
|
|
2225
3228
|
preferredFormat;
|
|
3229
|
+
/**
|
|
3230
|
+
* Memory/fidelity trade-off: 'high' (default, BC7 / ASTC) or 'low'
|
|
3231
|
+
* (BC1 / ETC2 RGB8 at half the memory, opaque colour only). See
|
|
3232
|
+
* `CompressOptions.quality`.
|
|
3233
|
+
*/
|
|
3234
|
+
quality = "high";
|
|
2226
3235
|
/** Pick the sRGB or linear variant of the chosen format. Default 'srgb'. */
|
|
2227
3236
|
colorSpace = "srgb";
|
|
2228
3237
|
/**
|
|
@@ -2235,8 +3244,10 @@ var GputexLoader = class extends Loader {
|
|
|
2235
3244
|
flipY = true;
|
|
2236
3245
|
/** Generate + encode a full mip chain. Default false. */
|
|
2237
3246
|
mipmaps = false;
|
|
2238
|
-
/**
|
|
2239
|
-
|
|
3247
|
+
/** Reuse compressed bytes from the session's in-memory transcode cache,
|
|
3248
|
+
* skipping decode + encode on repeat loads. See `CompressOptions.cache`.
|
|
3249
|
+
* Default false. */
|
|
3250
|
+
cache = false;
|
|
2240
3251
|
/**
|
|
2241
3252
|
* Optional pre-existing WebGPU device. Reusing the renderer's device
|
|
2242
3253
|
* avoids spinning up a second WebGPU context for encoding.
|
|
@@ -2261,11 +3272,12 @@ var GputexLoader = class extends Loader {
|
|
|
2261
3272
|
compressTexture(url, {
|
|
2262
3273
|
hint: this.hint,
|
|
2263
3274
|
preferredFormat: this.preferredFormat,
|
|
3275
|
+
quality: this.quality,
|
|
2264
3276
|
colorSpace: this.colorSpace,
|
|
2265
3277
|
svgSize: this.svgSize,
|
|
2266
3278
|
flipY: this.flipY,
|
|
2267
3279
|
mipmaps: this.mipmaps,
|
|
2268
|
-
|
|
3280
|
+
cache: this.cache,
|
|
2269
3281
|
device: this.device,
|
|
2270
3282
|
adapter: this.adapter
|
|
2271
3283
|
}).then(
|
|
@@ -2281,6 +3293,9 @@ var GputexLoader = class extends Loader {
|
|
|
2281
3293
|
height: result.height,
|
|
2282
3294
|
mipLevels: result.mipLevels,
|
|
2283
3295
|
encodeMs: result.encodeMs,
|
|
3296
|
+
decodeMs: result.decodeMs,
|
|
3297
|
+
totalMs: result.totalMs,
|
|
3298
|
+
cacheHit: result.cacheHit,
|
|
2284
3299
|
compressedBytes: result.fallbackUncompressed ? result.width * result.height * 4 : mip0?.data.byteLength ?? 0
|
|
2285
3300
|
};
|
|
2286
3301
|
onLoad?.(result.texture);
|
|
@@ -2303,23 +3318,29 @@ export {
|
|
|
2303
3318
|
BC5WebGLEncoder,
|
|
2304
3319
|
BC7Encoder,
|
|
2305
3320
|
BC7WebGLEncoder,
|
|
3321
|
+
ETC2Encoder,
|
|
2306
3322
|
Encoder,
|
|
2307
3323
|
GputexLoader,
|
|
2308
3324
|
TextureFormat,
|
|
2309
3325
|
WebGLBlockEncoder,
|
|
2310
3326
|
WebGPUFeature,
|
|
2311
3327
|
buildCompressedTexture,
|
|
3328
|
+
clearTranscodeCache,
|
|
2312
3329
|
compressTexture,
|
|
2313
3330
|
createWebGLContext,
|
|
2314
3331
|
detectCapabilities,
|
|
2315
3332
|
detectWebGLCapabilities,
|
|
2316
3333
|
encodeToTexture,
|
|
3334
|
+
generateGpuMipChain,
|
|
2317
3335
|
generateMipChain,
|
|
2318
3336
|
getSharedWebGLContext,
|
|
3337
|
+
gpuMipLevelCount,
|
|
2319
3338
|
isWebGLAvailable,
|
|
2320
3339
|
padToBlockMultiple,
|
|
2321
3340
|
rasterizeSvg,
|
|
3341
|
+
releaseSharedGpuResources,
|
|
2322
3342
|
selectFormat,
|
|
2323
3343
|
selectWebGLFormat,
|
|
3344
|
+
setTranscodeCacheLimit,
|
|
2324
3345
|
threeFormatFor
|
|
2325
3346
|
};
|