gputex 0.5.0 → 0.7.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 +63 -37
- package/dist/index.d.ts +153 -8
- package/dist/index.js +1038 -596
- package/dist/testing.d.ts +8 -5
- package/dist/testing.js +190 -34
- package/dist/three.d.ts +9 -130
- package/dist/three.js +659 -724
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,10 @@ function detectCapabilities(adapter) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/workarounds.ts
|
|
47
|
+
function needsWriteTextureWorkaround(adapter) {
|
|
48
|
+
const { vendor, architecture } = adapter.info ?? {};
|
|
49
|
+
return vendor === "img-tec" && architecture === "d-series";
|
|
50
|
+
}
|
|
47
51
|
function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
|
|
48
52
|
if (source instanceof ImageData && !flipY) {
|
|
49
53
|
if (srcTex.format === "rg8unorm") {
|
|
@@ -68,6 +72,22 @@ function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
|
|
|
68
72
|
|
|
69
73
|
// src/Encoder.ts
|
|
70
74
|
var CHAIN_ALIGN = 256;
|
|
75
|
+
var PARAMS_SIZE = 32;
|
|
76
|
+
var PARAMS_ALIGN = 256;
|
|
77
|
+
var MAX_BANDS = 8;
|
|
78
|
+
var BAND_BYTES = 2 << 20;
|
|
79
|
+
function bandRows(blocksY, outByteLen, wgY) {
|
|
80
|
+
const n = Math.min(MAX_BANDS, Math.max(1, Math.round(outByteLen / BAND_BYTES)));
|
|
81
|
+
const per = Math.ceil(blocksY / n / wgY) * wgY;
|
|
82
|
+
const bands = [];
|
|
83
|
+
for (let y0 = 0; y0 < blocksY; y0 += per) bands.push({ y0, rows: Math.min(per, blocksY - y0) });
|
|
84
|
+
return bands;
|
|
85
|
+
}
|
|
86
|
+
function allocPrefaulted(byteLength) {
|
|
87
|
+
const out = new Uint8Array(byteLength);
|
|
88
|
+
for (let i = 0; i < byteLength; i += 4096) out[i] = 0;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
71
91
|
var Encoder = class {
|
|
72
92
|
/**
|
|
73
93
|
* Subclasses set this to the WebGPU feature string the output texture
|
|
@@ -140,10 +160,11 @@ var Encoder = class {
|
|
|
140
160
|
_cachedSrcSource = null;
|
|
141
161
|
_cachedSrcFlipY = false;
|
|
142
162
|
_cachedDst = null;
|
|
143
|
-
|
|
163
|
+
// One MAP_READ staging buffer and bind group per row band (see bandRows).
|
|
164
|
+
_cachedStagings = [];
|
|
144
165
|
_cachedParams = null;
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
_lastParamsKey = null;
|
|
167
|
+
_cachedBindGroups = [];
|
|
147
168
|
_cachedPrepPlanes = null;
|
|
148
169
|
_cachedPrepBindGroup = null;
|
|
149
170
|
_resourcesBusy = false;
|
|
@@ -163,8 +184,11 @@ var Encoder = class {
|
|
|
163
184
|
_chainPrepBindGroups = [];
|
|
164
185
|
_chainParams = null;
|
|
165
186
|
_chainBindGroups = [];
|
|
187
|
+
// Level-0 bind groups of the base-level row bands (see bandRows).
|
|
188
|
+
_chainBandBindGroups = [];
|
|
166
189
|
_chainDst = null;
|
|
167
|
-
|
|
190
|
+
// One MAP_READ staging buffer per chain submission (base-level band).
|
|
191
|
+
_chainStagings = [];
|
|
168
192
|
_chainBusy = false;
|
|
169
193
|
constructor({ device, adapter, ownsDevice = false, disableF16 = false }) {
|
|
170
194
|
this.device = device;
|
|
@@ -208,14 +232,14 @@ var Encoder = class {
|
|
|
208
232
|
this._cachedPrepPlanes = null;
|
|
209
233
|
this._cachedPrepBindGroup = null;
|
|
210
234
|
this._cachedDst?.destroy();
|
|
211
|
-
this.
|
|
235
|
+
for (const st of this._cachedStagings) st.destroy();
|
|
212
236
|
this._cachedParams?.destroy();
|
|
213
237
|
this._cachedSrcTex = null;
|
|
214
238
|
this._cachedSrcSource = null;
|
|
215
239
|
this._cachedDst = null;
|
|
216
|
-
this.
|
|
240
|
+
this._cachedStagings = [];
|
|
217
241
|
this._cachedParams = null;
|
|
218
|
-
this.
|
|
242
|
+
this._cachedBindGroups = [];
|
|
219
243
|
for (const tex of this._chainTextures) tex.destroy();
|
|
220
244
|
this._chainTextures = [];
|
|
221
245
|
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
@@ -223,11 +247,12 @@ var Encoder = class {
|
|
|
223
247
|
this._chainPrepBindGroups = [];
|
|
224
248
|
this._chainParams?.destroy();
|
|
225
249
|
this._chainDst?.destroy();
|
|
226
|
-
this.
|
|
250
|
+
for (const st of this._chainStagings) st.destroy();
|
|
227
251
|
this._chainParams = null;
|
|
228
252
|
this._chainDst = null;
|
|
229
|
-
this.
|
|
253
|
+
this._chainStagings = [];
|
|
230
254
|
this._chainBindGroups = [];
|
|
255
|
+
this._chainBandBindGroups = [];
|
|
231
256
|
this._chainSig = null;
|
|
232
257
|
if (this.ownsDevice) this.device.destroy();
|
|
233
258
|
}
|
|
@@ -314,7 +339,7 @@ var Encoder = class {
|
|
|
314
339
|
entries: [
|
|
315
340
|
{ binding: 0, resource: srcView },
|
|
316
341
|
{ binding: 1, resource: planes[0].createView() },
|
|
317
|
-
{ binding: 2, resource: { buffer: params, offset: paramsOffset, size:
|
|
342
|
+
{ binding: 2, resource: { buffer: params, offset: paramsOffset, size: PARAMS_SIZE } },
|
|
318
343
|
{ binding: 3, resource: planes[1].createView() }
|
|
319
344
|
]
|
|
320
345
|
});
|
|
@@ -358,7 +383,8 @@ var Encoder = class {
|
|
|
358
383
|
let srcTex;
|
|
359
384
|
let dstBuffer;
|
|
360
385
|
let paramsBuffer;
|
|
361
|
-
|
|
386
|
+
const stagings = [];
|
|
387
|
+
const transientStagings = [];
|
|
362
388
|
let transientPrepPlanes = null;
|
|
363
389
|
try {
|
|
364
390
|
let srcTexIsNew = true;
|
|
@@ -421,66 +447,82 @@ var Encoder = class {
|
|
|
421
447
|
this._cachedDst = dstBuffer;
|
|
422
448
|
}
|
|
423
449
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
450
|
+
const [wgX, wgY] = this.workgroupSize;
|
|
451
|
+
const bands = bandRows(blocksY, outByteLen, wgY);
|
|
452
|
+
const rowBytes = blocksX * this.bytesPerBlock;
|
|
453
|
+
for (let b = 0; b < bands.length; b++) {
|
|
454
|
+
const len = bands[b].rows * rowBytes;
|
|
455
|
+
let st = useCache ? this._cachedStagings[b] : void 0;
|
|
456
|
+
if (!st || st.size < len) {
|
|
457
|
+
st = device.createBuffer({
|
|
458
|
+
label: `${this.label}-staging-${b}`,
|
|
459
|
+
size: len,
|
|
460
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
461
|
+
});
|
|
462
|
+
if (useCache) {
|
|
463
|
+
this._cachedStagings[b]?.destroy();
|
|
464
|
+
this._cachedStagings[b] = st;
|
|
465
|
+
} else {
|
|
466
|
+
transientStagings.push(st);
|
|
467
|
+
}
|
|
435
468
|
}
|
|
469
|
+
stagings.push(st);
|
|
436
470
|
}
|
|
471
|
+
const paramsData = new Uint32Array(MAX_BANDS * PARAMS_ALIGN / 4);
|
|
472
|
+
bands.forEach((band, b) => {
|
|
473
|
+
paramsData.set([blocksX, blocksY, width, height, band.y0], b * PARAMS_ALIGN / 4);
|
|
474
|
+
});
|
|
475
|
+
const paramsKey = `${blocksX},${blocksY},${width},${height},${bands.length}`;
|
|
437
476
|
if (useCache) {
|
|
438
477
|
if (!this._cachedParams) {
|
|
439
478
|
this._cachedParams = device.createBuffer({
|
|
440
479
|
label: `${this.label}-params`,
|
|
441
|
-
size:
|
|
480
|
+
size: MAX_BANDS * PARAMS_ALIGN,
|
|
442
481
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
443
482
|
});
|
|
444
|
-
this.
|
|
483
|
+
this._lastParamsKey = null;
|
|
445
484
|
}
|
|
446
485
|
paramsBuffer = this._cachedParams;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this._lastParams = [blocksX, blocksY, width, height];
|
|
486
|
+
if (this._lastParamsKey !== paramsKey) {
|
|
487
|
+
device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
488
|
+
this._lastParamsKey = paramsKey;
|
|
451
489
|
}
|
|
452
490
|
} else {
|
|
453
491
|
paramsBuffer = device.createBuffer({
|
|
454
492
|
label: `${this.label}-params`,
|
|
455
|
-
size:
|
|
493
|
+
size: MAX_BANDS * PARAMS_ALIGN,
|
|
456
494
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
457
495
|
});
|
|
458
|
-
device.queue.writeBuffer(paramsBuffer, 0,
|
|
496
|
+
device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
459
497
|
}
|
|
460
|
-
if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
498
|
+
if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this._cachedBindGroups = [];
|
|
499
|
+
const bindGroups = [];
|
|
500
|
+
for (let b = 0; b < bands.length; b++) {
|
|
501
|
+
let bg = useCache ? this._cachedBindGroups[b] : void 0;
|
|
502
|
+
if (!bg) {
|
|
503
|
+
const entries2 = [
|
|
504
|
+
{ binding: 0, resource: prepPlanes ? prepPlanes[0].createView() : srcTex.createView() },
|
|
505
|
+
{ binding: 1, resource: { buffer: dstBuffer } },
|
|
506
|
+
{ binding: 2, resource: { buffer: paramsBuffer, offset: b * PARAMS_ALIGN, size: PARAMS_SIZE } }
|
|
507
|
+
];
|
|
508
|
+
if (prepPlanes) {
|
|
509
|
+
entries2.push({ binding: 3, resource: prepPlanes[1].createView() });
|
|
510
|
+
} else if (this._usesSampler) {
|
|
511
|
+
this._sampler ??= device.createSampler({
|
|
512
|
+
label: `${this.label}-clamp-sampler`,
|
|
513
|
+
addressModeU: "clamp-to-edge",
|
|
514
|
+
addressModeV: "clamp-to-edge"
|
|
515
|
+
});
|
|
516
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
517
|
+
}
|
|
518
|
+
bg = device.createBindGroup({
|
|
519
|
+
label: `${this.label}-bg-${b}`,
|
|
520
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
521
|
+
entries: entries2
|
|
475
522
|
});
|
|
476
|
-
|
|
523
|
+
if (useCache) this._cachedBindGroups[b] = bg;
|
|
477
524
|
}
|
|
478
|
-
|
|
479
|
-
label: `${this.label}-bg`,
|
|
480
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
481
|
-
entries
|
|
482
|
-
});
|
|
483
|
-
if (useCache) this._cachedBindGroup = bindGroup;
|
|
525
|
+
bindGroups.push(bg);
|
|
484
526
|
}
|
|
485
527
|
let prepBindGroup = useCache && !prepPlanesNew && !srcTexIsNew ? this._cachedPrepBindGroup : null;
|
|
486
528
|
if (prepPipeline && prepPlanes && !prepBindGroup) {
|
|
@@ -488,41 +530,48 @@ var Encoder = class {
|
|
|
488
530
|
if (useCache) this._cachedPrepBindGroup = prepBindGroup;
|
|
489
531
|
}
|
|
490
532
|
const timing = withGpuTime ? this._createTiming() : null;
|
|
491
|
-
const [wgX, wgY] = this.workgroupSize;
|
|
492
533
|
const t0 = performance.now();
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
534
|
+
const maps = [];
|
|
535
|
+
for (let b = 0; b < bands.length; b++) {
|
|
536
|
+
const { y0, rows } = bands[b];
|
|
537
|
+
const first = b === 0;
|
|
538
|
+
const last = b === bands.length - 1;
|
|
539
|
+
const enc = device.createCommandEncoder({ label: `${this.label}-encode-${b}` });
|
|
540
|
+
if (first && prepPipeline && prepBindGroup) {
|
|
541
|
+
const prepPass = enc.beginComputePass(
|
|
542
|
+
timing ? { timestampWrites: { querySet: timing.querySet, beginningOfPassWriteIndex: 0 } } : void 0
|
|
543
|
+
);
|
|
544
|
+
prepPass.setPipeline(prepPipeline);
|
|
545
|
+
prepPass.setBindGroup(0, prepBindGroup);
|
|
546
|
+
const [px, py] = this.prepDispatch(blocksX, blocksY);
|
|
547
|
+
prepPass.dispatchWorkgroups(Math.ceil(px / wgX), Math.ceil(py / wgY), 1);
|
|
548
|
+
prepPass.end();
|
|
549
|
+
}
|
|
550
|
+
const tsw = timing && (first || last) ? {
|
|
551
|
+
querySet: timing.querySet,
|
|
552
|
+
...first && !prepPipeline ? { beginningOfPassWriteIndex: 0 } : {},
|
|
553
|
+
...last ? { endOfPassWriteIndex: 1 } : {}
|
|
554
|
+
} : void 0;
|
|
555
|
+
const pass = enc.beginComputePass(tsw ? { timestampWrites: tsw } : void 0);
|
|
556
|
+
pass.setPipeline(pipeline);
|
|
557
|
+
pass.setBindGroup(0, bindGroups[b]);
|
|
558
|
+
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(rows / wgY), 1);
|
|
559
|
+
pass.end();
|
|
560
|
+
enc.copyBufferToBuffer(dstBuffer, y0 * rowBytes, stagings[b], 0, rows * rowBytes);
|
|
561
|
+
if (timing && last) {
|
|
562
|
+
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
563
|
+
enc.copyBufferToBuffer(timing.resolve, 0, timing.staging, 0, 16);
|
|
564
|
+
}
|
|
565
|
+
device.queue.submit([enc.finish()]);
|
|
566
|
+
maps.push(stagings[b].mapAsync(GPUMapMode.READ, 0, rows * rowBytes));
|
|
503
567
|
}
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
} : void 0
|
|
512
|
-
);
|
|
513
|
-
pass.setPipeline(pipeline);
|
|
514
|
-
pass.setBindGroup(0, bindGroup);
|
|
515
|
-
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
516
|
-
pass.end();
|
|
517
|
-
enc.copyBufferToBuffer(dstBuffer, 0, staging, 0, outByteLen);
|
|
518
|
-
if (timing) {
|
|
519
|
-
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
520
|
-
enc.copyBufferToBuffer(timing.resolve, 0, timing.staging, 0, 16);
|
|
568
|
+
const data = allocPrefaulted(outByteLen);
|
|
569
|
+
for (let b = 0; b < bands.length; b++) {
|
|
570
|
+
const { y0, rows } = bands[b];
|
|
571
|
+
await maps[b];
|
|
572
|
+
data.set(new Uint8Array(stagings[b].getMappedRange(0, rows * rowBytes)), y0 * rowBytes);
|
|
573
|
+
stagings[b].unmap();
|
|
521
574
|
}
|
|
522
|
-
device.queue.submit([enc.finish()]);
|
|
523
|
-
await staging.mapAsync(GPUMapMode.READ, 0, outByteLen);
|
|
524
|
-
const data = new Uint8Array(staging.getMappedRange(0, outByteLen).slice(0));
|
|
525
|
-
staging.unmap();
|
|
526
575
|
const encodeMs = performance.now() - t0;
|
|
527
576
|
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
528
577
|
return { width, height, paddedWidth, paddedHeight, data, encodeMs, gpuMs };
|
|
@@ -532,7 +581,7 @@ var Encoder = class {
|
|
|
532
581
|
} else {
|
|
533
582
|
srcTex?.destroy();
|
|
534
583
|
dstBuffer?.destroy();
|
|
535
|
-
|
|
584
|
+
for (const st of transientStagings) st.destroy();
|
|
536
585
|
paramsBuffer?.destroy();
|
|
537
586
|
if (transientPrepPlanes) for (const t of transientPrepPlanes) t.destroy();
|
|
538
587
|
}
|
|
@@ -575,7 +624,7 @@ var Encoder = class {
|
|
|
575
624
|
);
|
|
576
625
|
}
|
|
577
626
|
});
|
|
578
|
-
const { geoms, byteSpan } = this._chainGeometry(levels);
|
|
627
|
+
const { geoms, byteSpan, bands } = this._chainGeometry(levels);
|
|
579
628
|
const sig = geoms.map((g) => `${g.width}x${g.height}`).join();
|
|
580
629
|
const useCache = !this._chainBusy;
|
|
581
630
|
if (useCache) this._chainBusy = true;
|
|
@@ -584,8 +633,9 @@ var Encoder = class {
|
|
|
584
633
|
let prepBindGroups;
|
|
585
634
|
let params;
|
|
586
635
|
let bindGroups;
|
|
636
|
+
let bandBindGroups;
|
|
587
637
|
let dst;
|
|
588
|
-
|
|
638
|
+
const transientStagings = [];
|
|
589
639
|
let transientLevelSet = false;
|
|
590
640
|
try {
|
|
591
641
|
let dstIsNew = true;
|
|
@@ -607,6 +657,7 @@ var Encoder = class {
|
|
|
607
657
|
textures = this._chainTextures;
|
|
608
658
|
params = this._chainParams;
|
|
609
659
|
bindGroups = this._chainBindGroups;
|
|
660
|
+
bandBindGroups = this._chainBandBindGroups;
|
|
610
661
|
prepPlaneSets = this._chainPrepPlanes;
|
|
611
662
|
prepBindGroups = this._chainPrepBindGroups;
|
|
612
663
|
} else {
|
|
@@ -621,16 +672,7 @@ var Encoder = class {
|
|
|
621
672
|
})
|
|
622
673
|
);
|
|
623
674
|
textures = texs;
|
|
624
|
-
params =
|
|
625
|
-
label: `${this.label}-chain-params`,
|
|
626
|
-
size: geoms.length * CHAIN_ALIGN,
|
|
627
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
628
|
-
});
|
|
629
|
-
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
630
|
-
geoms.forEach((g, i) => {
|
|
631
|
-
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
632
|
-
});
|
|
633
|
-
device.queue.writeBuffer(params, 0, paramsData);
|
|
675
|
+
params = this._createChainParams(geoms, bands);
|
|
634
676
|
if (this._usesSampler) {
|
|
635
677
|
this._sampler ??= device.createSampler({
|
|
636
678
|
label: `${this.label}-clamp-sampler`,
|
|
@@ -645,26 +687,29 @@ var Encoder = class {
|
|
|
645
687
|
prepBindGroups = prepPipeline ? geoms.map(
|
|
646
688
|
(g, i) => this._createPrepBindGroup(prepPipeline, texs[i].createView(), planeSets[i], paramsBuf, i * CHAIN_ALIGN)
|
|
647
689
|
) : [];
|
|
648
|
-
|
|
649
|
-
const
|
|
690
|
+
const makeBg = (i, slot) => {
|
|
691
|
+
const g = geoms[i];
|
|
692
|
+
const entries2 = [
|
|
650
693
|
{
|
|
651
694
|
binding: 0,
|
|
652
695
|
resource: prepPipeline ? planeSets[i][0].createView() : texs[i].createView()
|
|
653
696
|
},
|
|
654
697
|
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
655
|
-
{ binding: 2, resource: { buffer: paramsBuf, offset:
|
|
698
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: slot * CHAIN_ALIGN, size: PARAMS_SIZE } }
|
|
656
699
|
];
|
|
657
700
|
if (prepPipeline) {
|
|
658
|
-
|
|
701
|
+
entries2.push({ binding: 3, resource: planeSets[i][1].createView() });
|
|
659
702
|
} else if (this._usesSampler) {
|
|
660
|
-
|
|
703
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
661
704
|
}
|
|
662
705
|
return device.createBindGroup({
|
|
663
706
|
label: `${this.label}-chain-bg-${i}`,
|
|
664
707
|
layout: pipeline.getBindGroupLayout(0),
|
|
665
|
-
entries
|
|
708
|
+
entries: entries2
|
|
666
709
|
});
|
|
667
|
-
}
|
|
710
|
+
};
|
|
711
|
+
bindGroups = geoms.map((_, i) => makeBg(i, i));
|
|
712
|
+
bandBindGroups = bands.map((_, b) => makeBg(0, geoms.length + b));
|
|
668
713
|
if (useCache) {
|
|
669
714
|
for (const tex of this._chainTextures) tex.destroy();
|
|
670
715
|
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
@@ -674,24 +719,12 @@ var Encoder = class {
|
|
|
674
719
|
this._chainPrepBindGroups = prepBindGroups;
|
|
675
720
|
this._chainParams = params;
|
|
676
721
|
this._chainBindGroups = bindGroups;
|
|
722
|
+
this._chainBandBindGroups = bandBindGroups;
|
|
677
723
|
this._chainSig = sig;
|
|
678
724
|
} else {
|
|
679
725
|
transientLevelSet = true;
|
|
680
726
|
}
|
|
681
727
|
}
|
|
682
|
-
if (useCache && this._chainStaging && this._chainStaging.size >= byteSpan) {
|
|
683
|
-
staging = this._chainStaging;
|
|
684
|
-
} else {
|
|
685
|
-
staging = device.createBuffer({
|
|
686
|
-
label: `${this.label}-chain-staging`,
|
|
687
|
-
size: byteSpan,
|
|
688
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
689
|
-
});
|
|
690
|
-
if (useCache) {
|
|
691
|
-
this._chainStaging?.destroy();
|
|
692
|
-
this._chainStaging = staging;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
728
|
for (let i = 0; i < levels.length; i++) {
|
|
696
729
|
const level = levels[i];
|
|
697
730
|
device.queue.writeTexture({ texture: textures[i] }, level.data, { bytesPerRow: level.width * 4 }, [
|
|
@@ -704,9 +737,12 @@ var Encoder = class {
|
|
|
704
737
|
pipeline,
|
|
705
738
|
geoms,
|
|
706
739
|
byteSpan,
|
|
740
|
+
bands,
|
|
707
741
|
bindGroups,
|
|
742
|
+
bandBindGroups,
|
|
708
743
|
dst,
|
|
709
|
-
|
|
744
|
+
useCache,
|
|
745
|
+
transientStagings,
|
|
710
746
|
withGpuTime,
|
|
711
747
|
t0,
|
|
712
748
|
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
@@ -716,8 +752,8 @@ var Encoder = class {
|
|
|
716
752
|
this._chainBusy = false;
|
|
717
753
|
} else {
|
|
718
754
|
dst?.destroy();
|
|
719
|
-
staging?.destroy();
|
|
720
755
|
}
|
|
756
|
+
for (const st of transientStagings) st.destroy();
|
|
721
757
|
if (transientLevelSet) {
|
|
722
758
|
if (textures) for (const tex of textures) tex.destroy();
|
|
723
759
|
if (prepPlaneSets) for (const planes of prepPlaneSets) for (const t of planes) t.destroy();
|
|
@@ -747,11 +783,11 @@ var Encoder = class {
|
|
|
747
783
|
for (let i = 0; i < srcTex.mipLevelCount; i++) {
|
|
748
784
|
dims.push({ width: Math.max(1, srcTex.width >> i), height: Math.max(1, srcTex.height >> i) });
|
|
749
785
|
}
|
|
750
|
-
const { geoms, byteSpan } = this._chainGeometry(dims);
|
|
786
|
+
const { geoms, byteSpan, bands } = this._chainGeometry(dims);
|
|
751
787
|
const useCache = !this._chainBusy;
|
|
752
788
|
if (useCache) this._chainBusy = true;
|
|
753
789
|
let dst;
|
|
754
|
-
|
|
790
|
+
const transientStagings = [];
|
|
755
791
|
let params;
|
|
756
792
|
let planeSets = null;
|
|
757
793
|
try {
|
|
@@ -769,29 +805,7 @@ var Encoder = class {
|
|
|
769
805
|
this._chainSig = null;
|
|
770
806
|
}
|
|
771
807
|
}
|
|
772
|
-
|
|
773
|
-
staging = this._chainStaging;
|
|
774
|
-
} else {
|
|
775
|
-
staging = device.createBuffer({
|
|
776
|
-
label: `${this.label}-chain-staging`,
|
|
777
|
-
size: byteSpan,
|
|
778
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
779
|
-
});
|
|
780
|
-
if (useCache) {
|
|
781
|
-
this._chainStaging?.destroy();
|
|
782
|
-
this._chainStaging = staging;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
params = device.createBuffer({
|
|
786
|
-
label: `${this.label}-chain-params`,
|
|
787
|
-
size: geoms.length * CHAIN_ALIGN,
|
|
788
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
789
|
-
});
|
|
790
|
-
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
791
|
-
geoms.forEach((g, i) => {
|
|
792
|
-
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
793
|
-
});
|
|
794
|
-
device.queue.writeBuffer(params, 0, paramsData);
|
|
808
|
+
params = this._createChainParams(geoms, bands);
|
|
795
809
|
if (this._usesSampler) {
|
|
796
810
|
this._sampler ??= device.createSampler({
|
|
797
811
|
label: `${this.label}-clamp-sampler`,
|
|
@@ -812,33 +826,39 @@ var Encoder = class {
|
|
|
812
826
|
i * CHAIN_ALIGN
|
|
813
827
|
)
|
|
814
828
|
) : null;
|
|
815
|
-
const
|
|
816
|
-
const
|
|
829
|
+
const makeBg = (i, slot) => {
|
|
830
|
+
const g = geoms[i];
|
|
831
|
+
const entries2 = [
|
|
817
832
|
{
|
|
818
833
|
binding: 0,
|
|
819
834
|
resource: planes ? planes[i][0].createView() : srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 })
|
|
820
835
|
},
|
|
821
836
|
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
822
|
-
{ binding: 2, resource: { buffer: paramsBuf, offset:
|
|
837
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: slot * CHAIN_ALIGN, size: PARAMS_SIZE } }
|
|
823
838
|
];
|
|
824
839
|
if (planes) {
|
|
825
|
-
|
|
840
|
+
entries2.push({ binding: 3, resource: planes[i][1].createView() });
|
|
826
841
|
} else if (this._usesSampler) {
|
|
827
|
-
|
|
842
|
+
entries2.push({ binding: 3, resource: this._sampler });
|
|
828
843
|
}
|
|
829
844
|
return device.createBindGroup({
|
|
830
845
|
label: `${this.label}-chain-bg-${i}`,
|
|
831
846
|
layout: pipeline.getBindGroupLayout(0),
|
|
832
|
-
entries
|
|
847
|
+
entries: entries2
|
|
833
848
|
});
|
|
834
|
-
}
|
|
849
|
+
};
|
|
850
|
+
const bindGroups = geoms.map((_, i) => makeBg(i, i));
|
|
851
|
+
const bandBindGroups = bands.map((_, b) => makeBg(0, geoms.length + b));
|
|
835
852
|
return await this._submitChainAndRead(
|
|
836
853
|
pipeline,
|
|
837
854
|
geoms,
|
|
838
855
|
byteSpan,
|
|
856
|
+
bands,
|
|
839
857
|
bindGroups,
|
|
858
|
+
bandBindGroups,
|
|
840
859
|
dst,
|
|
841
|
-
|
|
860
|
+
useCache,
|
|
861
|
+
transientStagings,
|
|
842
862
|
withGpuTime,
|
|
843
863
|
t0,
|
|
844
864
|
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
@@ -848,8 +868,8 @@ var Encoder = class {
|
|
|
848
868
|
this._chainBusy = false;
|
|
849
869
|
} else {
|
|
850
870
|
dst?.destroy();
|
|
851
|
-
staging?.destroy();
|
|
852
871
|
}
|
|
872
|
+
for (const st of transientStagings) st.destroy();
|
|
853
873
|
params?.destroy();
|
|
854
874
|
if (planeSets) for (const planes of planeSets) for (const t of planes) t.destroy();
|
|
855
875
|
}
|
|
@@ -857,7 +877,7 @@ var Encoder = class {
|
|
|
857
877
|
/** Block-grid geometry + packed output offsets for a chain of levels.
|
|
858
878
|
* `byteSpan` is both the dst buffer size and the readback copy size (a
|
|
859
879
|
* multiple of 4: byteLen is a multiple of bytesPerBlock ≥ 8, offsets are
|
|
860
|
-
* CHAIN_ALIGN-ed). */
|
|
880
|
+
* CHAIN_ALIGN-ed). `bands` splits the base level into row bands. */
|
|
861
881
|
_chainGeometry(dims) {
|
|
862
882
|
let dstCursor = 0;
|
|
863
883
|
const geoms = dims.map(({ width, height }) => {
|
|
@@ -871,60 +891,131 @@ var Encoder = class {
|
|
|
871
891
|
return { width, height, paddedWidth, paddedHeight, blocksX, blocksY, byteLen, dstOffset };
|
|
872
892
|
});
|
|
873
893
|
const last = geoms[geoms.length - 1];
|
|
874
|
-
|
|
894
|
+
const base = geoms[0];
|
|
895
|
+
return {
|
|
896
|
+
geoms,
|
|
897
|
+
byteSpan: last.dstOffset + last.byteLen,
|
|
898
|
+
bands: bandRows(base.blocksY, base.byteLen, this.workgroupSize[1])
|
|
899
|
+
};
|
|
875
900
|
}
|
|
876
|
-
/**
|
|
877
|
-
*
|
|
878
|
-
|
|
901
|
+
/** One uniform buffer, one write: level i's { blocksX, blocksY, width,
|
|
902
|
+
* height, y0 = 0 } at slot i, then base-level band b at slot
|
|
903
|
+
* levels + b (y0 = the band's first block row). Slots are CHAIN_ALIGN
|
|
904
|
+
* apart. */
|
|
905
|
+
_createChainParams(geoms, bands) {
|
|
906
|
+
const slots = geoms.length + bands.length;
|
|
907
|
+
const params = this.device.createBuffer({
|
|
908
|
+
label: `${this.label}-chain-params`,
|
|
909
|
+
size: slots * CHAIN_ALIGN,
|
|
910
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
911
|
+
});
|
|
912
|
+
const data = new Uint32Array(slots * CHAIN_ALIGN / 4);
|
|
913
|
+
geoms.forEach((g, i) => data.set([g.blocksX, g.blocksY, g.width, g.height, 0], i * CHAIN_ALIGN / 4));
|
|
914
|
+
const g0 = geoms[0];
|
|
915
|
+
bands.forEach(
|
|
916
|
+
(band, b) => data.set([g0.blocksX, g0.blocksY, g0.width, g0.height, band.y0], (geoms.length + b) * CHAIN_ALIGN / 4)
|
|
917
|
+
);
|
|
918
|
+
this.device.queue.writeBuffer(params, 0, data);
|
|
919
|
+
return params;
|
|
920
|
+
}
|
|
921
|
+
/** Shared chain-encode tail: one submission per base-level row band —
|
|
922
|
+
* band b dispatches its rows of level 0, the last band also every tail
|
|
923
|
+
* level — each read back through its own staging buffer as it lands,
|
|
924
|
+
* then sliced into per-level byte arrays. */
|
|
925
|
+
async _submitChainAndRead(pipeline, geoms, byteSpan, bands, bindGroups, bandBindGroups, dst, useCache, transientStagings, withGpuTime, t0, prep = null) {
|
|
879
926
|
const device = this.device;
|
|
880
927
|
const timing = withGpuTime ? this._createTiming() : null;
|
|
881
928
|
const [wgX, wgY] = this.workgroupSize;
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
929
|
+
const base = geoms[0];
|
|
930
|
+
const rowBytes = base.blocksX * this.bytesPerBlock;
|
|
931
|
+
const ranges = bands.map((band, b) => {
|
|
932
|
+
const start = base.dstOffset + band.y0 * rowBytes;
|
|
933
|
+
const end = b === bands.length - 1 ? byteSpan : start + band.rows * rowBytes;
|
|
934
|
+
return { start, len: end - start };
|
|
935
|
+
});
|
|
936
|
+
const stagings = ranges.map(({ len }, b) => {
|
|
937
|
+
let st = useCache ? this._chainStagings[b] : void 0;
|
|
938
|
+
if (!st || st.size < len) {
|
|
939
|
+
st = device.createBuffer({
|
|
940
|
+
label: `${this.label}-chain-staging-${b}`,
|
|
941
|
+
size: len,
|
|
942
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
943
|
+
});
|
|
944
|
+
if (useCache) {
|
|
945
|
+
this._chainStagings[b]?.destroy();
|
|
946
|
+
this._chainStagings[b] = st;
|
|
947
|
+
} else {
|
|
948
|
+
transientStagings.push(st);
|
|
949
|
+
}
|
|
893
950
|
}
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
951
|
+
return st;
|
|
952
|
+
});
|
|
953
|
+
const maps = [];
|
|
954
|
+
for (let b = 0; b < bands.length; b++) {
|
|
955
|
+
const first = b === 0;
|
|
956
|
+
const last = b === bands.length - 1;
|
|
957
|
+
const enc = device.createCommandEncoder({ label: `${this.label}-encode-chain-${b}` });
|
|
958
|
+
if (first && prep) {
|
|
959
|
+
const prepPass = enc.beginComputePass(
|
|
960
|
+
timing ? { timestampWrites: { querySet: timing.querySet, beginningOfPassWriteIndex: 0 } } : void 0
|
|
961
|
+
);
|
|
962
|
+
prepPass.setPipeline(prep.pipeline);
|
|
963
|
+
for (let i = 0; i < geoms.length; i++) {
|
|
964
|
+
const g = geoms[i];
|
|
965
|
+
prepPass.setBindGroup(0, prep.bindGroups[i]);
|
|
966
|
+
const [px, py] = this.prepDispatch(g.blocksX, g.blocksY);
|
|
967
|
+
prepPass.dispatchWorkgroups(Math.ceil(px / wgX), Math.ceil(py / wgY), 1);
|
|
902
968
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
969
|
+
prepPass.end();
|
|
970
|
+
}
|
|
971
|
+
const tsw = timing && (first || last) ? {
|
|
972
|
+
querySet: timing.querySet,
|
|
973
|
+
...first && !prep ? { beginningOfPassWriteIndex: 0 } : {},
|
|
974
|
+
...last ? { endOfPassWriteIndex: 1 } : {}
|
|
975
|
+
} : void 0;
|
|
976
|
+
const pass = enc.beginComputePass(tsw ? { timestampWrites: tsw } : void 0);
|
|
977
|
+
pass.setPipeline(pipeline);
|
|
978
|
+
pass.setBindGroup(0, bandBindGroups[b]);
|
|
979
|
+
pass.dispatchWorkgroups(Math.ceil(base.blocksX / wgX), Math.ceil(bands[b].rows / wgY), 1);
|
|
980
|
+
if (last) {
|
|
981
|
+
for (let i = 1; i < geoms.length; i++) {
|
|
982
|
+
const g = geoms[i];
|
|
983
|
+
pass.setBindGroup(0, bindGroups[i]);
|
|
984
|
+
pass.dispatchWorkgroups(Math.ceil(g.blocksX / wgX), Math.ceil(g.blocksY / wgY), 1);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
pass.end();
|
|
988
|
+
enc.copyBufferToBuffer(dst, ranges[b].start, stagings[b], 0, ranges[b].len);
|
|
989
|
+
if (timing && last) {
|
|
990
|
+
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
991
|
+
enc.copyBufferToBuffer(timing.resolve, 0, timing.staging, 0, 16);
|
|
992
|
+
}
|
|
993
|
+
device.queue.submit([enc.finish()]);
|
|
994
|
+
maps.push(stagings[b].mapAsync(GPUMapMode.READ, 0, ranges[b].len));
|
|
916
995
|
}
|
|
917
|
-
device.queue.submit([enc.finish()]);
|
|
918
|
-
await staging.mapAsync(GPUMapMode.READ, 0, byteSpan);
|
|
919
|
-
const mapped = staging.getMappedRange(0, byteSpan);
|
|
920
996
|
const out = geoms.map((g) => ({
|
|
921
997
|
width: g.width,
|
|
922
998
|
height: g.height,
|
|
923
999
|
paddedWidth: g.paddedWidth,
|
|
924
1000
|
paddedHeight: g.paddedHeight,
|
|
925
|
-
data:
|
|
1001
|
+
data: allocPrefaulted(g.byteLen)
|
|
926
1002
|
}));
|
|
927
|
-
|
|
1003
|
+
for (let b = 0; b < bands.length; b++) {
|
|
1004
|
+
const { start, len } = ranges[b];
|
|
1005
|
+
await maps[b];
|
|
1006
|
+
const mapped = stagings[b].getMappedRange(0, len);
|
|
1007
|
+
if (b < bands.length - 1) {
|
|
1008
|
+
out[0].data.set(new Uint8Array(mapped), start - base.dstOffset);
|
|
1009
|
+
} else {
|
|
1010
|
+
const lvl0Rest = base.dstOffset + base.byteLen - start;
|
|
1011
|
+
out[0].data.set(new Uint8Array(mapped, 0, lvl0Rest), start - base.dstOffset);
|
|
1012
|
+
for (let i = 1; i < geoms.length; i++) {
|
|
1013
|
+
const g = geoms[i];
|
|
1014
|
+
out[i].data.set(new Uint8Array(mapped, g.dstOffset - start, g.byteLen));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
stagings[b].unmap();
|
|
1018
|
+
}
|
|
928
1019
|
const encodeMs = performance.now() - t0;
|
|
929
1020
|
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
930
1021
|
return { levels: out, encodeMs, gpuMs };
|
|
@@ -968,250 +1059,10 @@ var Encoder = class {
|
|
|
968
1059
|
};
|
|
969
1060
|
|
|
970
1061
|
// src/bc1.wgsl
|
|
971
|
-
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";
|
|
1062
|
+
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 (same as bc1_fast_f16.wgsl, which documents the measurements):\n// near-flat blocks take a solid colour \u2014 per channel the endpoint pair whose\n// \u2154/\u2153 interpolant lands nearest the block mean; other blocks get a\n// principal-axis endpoint seed (covariance power-iteration; inset bbox on\n// degenerate blocks), inset by ~half a 565 cell along the axis. A\n// projection pass then assigns every pixel the rounded projection onto the\n// decoded-endpoint line (the 4 palette entries are colinear and evenly\n// spaced, so that is the nearest entry) while accumulating the block error\n// and projection moments, followed by up to TWO least-squares refit rounds\n// solved from those moments (re-quantise, reproject, accept only on lower\n// block error).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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// Force 4-colour mode: c0 > c1 strictly.\nfn order565(a: u32, b: u32) -> vec2<u32> {\n var c0 = a; var c1 = b;\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 return vec2<u32>(c0, c1);\n}\n\n// One projection pass against the decoded endpoints of (c0,c1): levels\n// L = 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014 colinear, evenly\n// spaced, so rounding the projection IS the nearest-entry search), the\n// packed indices, the block's squared error, and the projection MOMENTS a\n// refit needs: \u03A3L, \u03A3L\xB2, \u03A3u, \u03A3L\xB7u (u = v \u2212 p0). Level \u2192 BC1 index: 0\u21920\n// (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a packed LUT: (0x78 >> 2L) & 3.\nstruct Moments { sL: f32, sLL: f32, sU: vec3<f32>, sLu: vec3<f32>, indices: u32, err: f32 };\nfn moments(pix: ptr<function, array<vec3<f32>, 16>>, c0: u32, c1: u32) -> Moments {\n let p0 = from565(c0);\n let dir = from565(c1) - p0;\n let inv = 3.0 / dot(dir, dir);\n var out: Moments;\n out.sL = 0.0;\n out.sLL = 0.0;\n out.sU = vec3<f32>(0.0);\n out.sLu = vec3<f32>(0.0);\n out.indices = 0u;\n out.err = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let u = (*pix)[k] - p0;\n let L = clamp(floor(dot(u, dir) * inv + 0.5), 0.0, 3.0);\n out.sL = out.sL + L;\n out.sLL = out.sLL + L * L;\n out.sU = out.sU + u;\n out.sLu = out.sLu + L * u;\n out.indices = out.indices | (((0x78u >> (u32(L) * 2u)) & 3u) << (k * 2u));\n let e = u - L * (1.0 / 3.0) * dir;\n out.err = out.err + dot(e, e);\n }\n return out;\n}\n\n// One least-squares refit from moments (b = L/3, a = 1 \u2212 b):\n// sBB = \u03A3L\xB2/9 sAB = \u03A3L/3 \u2212 \u03A3L\xB2/9 sAA = 16 \u2212 2\u03A3L/3 + \u03A3L\xB2/9\n// \u03A3b\xB7u = \u03A3L\xB7u/3 \u03A3a\xB7u = \u03A3u \u2212 \u03A3b\xB7u\n// clamped to [lim_lo, lim_hi], re-quantised and ordered. Returns (c0, c1)\n// unchanged when every pixel sits on ONE level (16\xB7\u03A3L\xB2 == (\u03A3L)\xB2, singular).\nfn solve(m: Moments, c0: u32, c1: u32, lim_lo: vec3<f32>, lim_hi: vec3<f32>) -> vec2<u32> {\n if (16.0 * m.sLL == m.sL * m.sL) { return vec2<u32>(c0, c1); }\n let sBB = m.sLL * (1.0 / 9.0);\n let sAB = m.sL * (1.0 / 3.0) - sBB;\n let sAA = 16.0 - m.sL * (2.0 / 3.0) + sBB;\n let det = sAA * sBB - sAB * sAB;\n let p0 = from565(c0);\n let sBu = m.sLu * (1.0 / 3.0);\n let sAu = m.sU - sBu;\n let e0 = clamp(p0 + (sBB * sAu - sAB * sBu) / det, lim_lo, lim_hi);\n let e1 = clamp(p0 + (sAA * sBu - sAB * sAu) / det, lim_lo, lim_hi);\n return order565(to565(e0), to565(e1));\n}\n\n// Solid-colour channel code: the pair (a, b) of `bits`-bit codes whose \u2154/\u2153\n// interpolant (2\xB7dec(a) + dec(b))/3 \u2014 palette index 2 \u2014 lands nearest v\n// (8-bit units). See bc1_fast_f16.wgsl.\nfn solid_pair(v: f32, bits: u32) -> vec2<u32> {\n let maxc = (1u << bits) - 1u;\n let q = min(u32(v * f32(maxc) / 255.0), maxc - 1u);\n var x: f32; var y: f32;\n if (bits == 5u) {\n x = f32((q * 527u + 23u) >> 6u);\n y = f32(((q + 1u) * 527u + 23u) >> 6u);\n } else {\n x = f32((q * 259u + 33u) >> 6u);\n y = f32(((q + 1u) * 259u + 33u) >> 6u);\n }\n var best = vec2<u32>(q, q);\n var be = abs(x - v);\n let c1 = (2.0 * x + y) / 3.0;\n if (abs(c1 - v) < be) { be = abs(c1 - v); best = vec2<u32>(q, q + 1u); }\n let c2 = (x + 2.0 * y) / 3.0;\n if (abs(c2 - v) < be) { be = abs(c2 - v); best = vec2<u32>(q + 1u, q); }\n if (abs(y - v) < be) { best = vec2<u32>(q + 1u, q + 1u); }\n return best;\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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 // Near-flat blocks (every channel within 3 levels): solid colour at the\n // block mean, per channel the endpoint pair whose \u2154/\u2153 interpolant lands\n // nearest; they share the index pass below and skip the seed + refits.\n let span = bb_max - bb_min;\n let flat = max(max(span.x, span.y), span.z) <= 3.0 / 255.0;\n var c0: u32;\n var c1: u32;\n if (flat) {\n let m8 = mean * 255.0;\n let pr = solid_pair(m8.x, 5u);\n let pg = solid_pair(m8.y, 6u);\n let pb = solid_pair(m8.z, 5u);\n let s0 = (pr.x << 11u) | (pg.x << 5u) | pb.x;\n let s1 = (pr.y << 11u) | (pg.y << 5u) | pb.y;\n c0 = max(s0, s1);\n c1 = min(s0, s1);\n } else {\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\n // (stb_dxt heuristic). Degenerate 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 let seed = order565(to565(seed_hi), to565(seed_lo));\n c0 = seed.x;\n c1 = seed.y;\n }\n\n // Projection pass on the seed, then up to two least-squares refit rounds\n // (solve() off the previous pass's moments), each re-projected and\n // accepted only if the block error drops \u2014 the refit minimises a\n // continuous objective and can lose after 565 quantisation. Equal flat\n // codes encode the colour itself: index 0 (opaque in either mode).\n var indices = 0u;\n if (c0 != c1) {\n var cur = moments(&pixels, c0, c1);\n for (var it: u32 = 0u; it < select(2u, 0u, flat); it = it + 1u) {\n let cand = solve(cur, c0, c1, lim_lo, lim_hi);\n if (cand.x == c0 && cand.y == c1) { break; }\n let nxt = moments(&pixels, cand.x, cand.y);\n if (nxt.err >= cur.err) { break; }\n c0 = cand.x;\n c1 = cand.y;\n cur = nxt;\n }\n indices = cur.indices;\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = indices;\n}\n";
|
|
972
1063
|
|
|
973
1064
|
// src/bc1_fast_f16.wgsl
|
|
974
|
-
var bc1_fast_f16_default = `// bc1 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
975
|
-
//
|
|
976
|
-
// BC1 quantises endpoints to RGB565 anyway, so the fast path needs nothing
|
|
977
|
-
// f32 can do that f16 can't \u2014 all projection / least-squares math runs in
|
|
978
|
-
// f16 ([0,1] domain). The algorithm is the same family as the BC7/ASTC fast
|
|
979
|
-
// paths rather than a port of bc1.wgsl's fast branch:
|
|
980
|
-
//
|
|
981
|
-
// 1. principal-axis endpoint seed (covariance power-iteration; inset bbox
|
|
982
|
-
// on degenerate blocks), inset by ~half a 565 cell (stb_dxt heuristic)
|
|
983
|
-
// 2. quantise to 565, force 4-colour mode (c0 > c1)
|
|
984
|
-
// 3. ONE fused pass: project every pixel onto the decoded-endpoint line
|
|
985
|
-
// (the 4 palette entries are colinear and evenly spaced, so the nearest
|
|
986
|
-
// entry is the rounded projection \u2014 no 4-entry search) while
|
|
987
|
-
// accumulating the least-squares refit sums, the packed indices and the
|
|
988
|
-
// squared error
|
|
989
|
-
// 4. up to TWO refit rounds (mirroring the high path's iterated refits):
|
|
990
|
-
// re-quantise the refit endpoints, reproject (indices packed on the
|
|
991
|
-
// fly, sums re-accumulated to seed the next round), and accept each
|
|
992
|
-
// round only if the block error decreases \u2014 flat/single-level blocks
|
|
993
|
-
// skip these passes entirely
|
|
994
|
-
//
|
|
995
|
-
// vs the pre-projection fast branch (build palette + full 4-entry search \xD7 3
|
|
996
|
-
// passes + refit sums pass) this does roughly half the ALU per block. The
|
|
997
|
-
// 565 decode uses exact integer math, so the palette base points are exact.
|
|
998
|
-
//
|
|
999
|
-
// The host selects this module only when the device reports shader-f16,
|
|
1000
|
-
// falling back to bc1.wgsl otherwise. "high" never uses this.
|
|
1001
|
-
enable f16;
|
|
1002
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
1003
|
-
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1004
|
-
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1005
|
-
@group(0) @binding(2) var<uniform> params: Params;
|
|
1006
|
-
alias h = f16;
|
|
1007
|
-
alias h3 = vec3<f16>;
|
|
1008
|
-
|
|
1009
|
-
fn to565(c: h3) -> u32 {
|
|
1010
|
-
let r = u32(clamp(floor(c.r * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
1011
|
-
let g = u32(clamp(floor(c.g * h(63.0) + h(0.5)), h(0.0), h(63.0)));
|
|
1012
|
-
let b = u32(clamp(floor(c.b * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
1013
|
-
return (r << 11u) | (g << 5u) | b;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Decode a 565 endpoint to [0,1]: (x*527+23)>>6 (6-bit: 259/33) \u2014
|
|
1017
|
-
// round-to-nearest scaling, matching bc1_ref.ts / bc1.wgsl and typical
|
|
1018
|
-
// hardware decoders. Exact in u32 integer math (f16 could not evaluate the
|
|
1019
|
-
// products exactly).
|
|
1020
|
-
fn from565(c: u32) -> h3 {
|
|
1021
|
-
let r = (c >> 11u) & 31u;
|
|
1022
|
-
let g = (c >> 5u) & 63u;
|
|
1023
|
-
let b = c & 31u;
|
|
1024
|
-
let r8 = (r * 527u + 23u) >> 6u;
|
|
1025
|
-
let g8 = (g * 259u + 33u) >> 6u;
|
|
1026
|
-
let b8 = (b * 527u + 23u) >> 6u;
|
|
1027
|
-
return h3(vec3<f32>(vec3<u32>(r8, g8, b8))) * h(1.0 / 255.0);
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Force 4-colour mode: c0 > c1 strictly.
|
|
1031
|
-
fn order565(a: u32, b: u32) -> vec2<u32> {
|
|
1032
|
-
var c0 = a; var c1 = b;
|
|
1033
|
-
if (c0 == c1) {
|
|
1034
|
-
if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }
|
|
1035
|
-
} else if (c0 < c1) {
|
|
1036
|
-
let t = c0; c0 = c1; c1 = t;
|
|
1037
|
-
}
|
|
1038
|
-
return vec2<u32>(c0, c1);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// One projection pass against the decoded endpoints of (c0,c1): the packed
|
|
1042
|
-
// 2-bit indices, the block's squared error, and the LSQ normal-equation sums
|
|
1043
|
-
// of the resulting assignment \u2014 so an accepted refit can seed the next
|
|
1044
|
-
// round. Levels s run 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014
|
|
1045
|
-
// colinear, evenly spaced, so rounding the projection IS the nearest-entry
|
|
1046
|
-
// search). Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a
|
|
1047
|
-
// packed LUT: (0x78 >> 2L) & 3.
|
|
1048
|
-
struct Proj {
|
|
1049
|
-
indices: u32,
|
|
1050
|
-
err: h,
|
|
1051
|
-
sAA: h, sBB: h, sAB: h,
|
|
1052
|
-
sAV: h3, sBV: h3,
|
|
1053
|
-
s_min: h, s_max: h,
|
|
1054
|
-
};
|
|
1055
|
-
fn project_stats(pix: ptr<function, array<h3, 16>>, c0: u32, c1: u32) -> Proj {
|
|
1056
|
-
var out: Proj;
|
|
1057
|
-
out.indices = 0u;
|
|
1058
|
-
out.err = h(0.0);
|
|
1059
|
-
out.sAA = h(0.0); out.sBB = h(0.0); out.sAB = h(0.0);
|
|
1060
|
-
out.sAV = h3(0.0); out.sBV = h3(0.0);
|
|
1061
|
-
out.s_min = h(3.0); out.s_max = h(0.0);
|
|
1062
|
-
let p0 = from565(c0);
|
|
1063
|
-
let p1 = from565(c1);
|
|
1064
|
-
let dir = p1 - p0;
|
|
1065
|
-
let dd = dot(dir, dir);
|
|
1066
|
-
if (dd == h(0.0)) {
|
|
1067
|
-
// Unreachable for distinct 565 codes (the decode is injective); kept so
|
|
1068
|
-
// a degenerate call still returns a consistent error.
|
|
1069
|
-
out.s_min = h(0.0);
|
|
1070
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1071
|
-
let e = (*pix)[k] - p0;
|
|
1072
|
-
out.err = out.err + dot(e, e);
|
|
1073
|
-
}
|
|
1074
|
-
return out;
|
|
1075
|
-
}
|
|
1076
|
-
let inv = h(3.0) / dd;
|
|
1077
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1078
|
-
let v = (*pix)[k];
|
|
1079
|
-
let s = clamp(floor(dot(v - p0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
1080
|
-
out.s_min = min(out.s_min, s); out.s_max = max(out.s_max, s);
|
|
1081
|
-
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
1082
|
-
out.sAA = out.sAA + a * a; out.sBB = out.sBB + b * b; out.sAB = out.sAB + a * b;
|
|
1083
|
-
out.sAV = out.sAV + a * v; out.sBV = out.sBV + b * v;
|
|
1084
|
-
let e = v - (p0 + b * dir);
|
|
1085
|
-
out.err = out.err + dot(e, e);
|
|
1086
|
-
out.indices = out.indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
1087
|
-
}
|
|
1088
|
-
return out;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
@compute @workgroup_size(8, 8, 1)
|
|
1092
|
-
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1093
|
-
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1094
|
-
let bi = gid.y * params.blocks_x + gid.x;
|
|
1095
|
-
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
1096
|
-
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
1097
|
-
|
|
1098
|
-
var pix: array<h3, 16>;
|
|
1099
|
-
var mn = h3(1.0);
|
|
1100
|
-
var mxv = h3(0.0);
|
|
1101
|
-
var mean = h3(0.0);
|
|
1102
|
-
var gd = h(0.0);
|
|
1103
|
-
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
1104
|
-
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
1105
|
-
let px = h3(textureLoad(src_tex, p, 0).rgb);
|
|
1106
|
-
pix[i] = px; mn = min(mn, px); mxv = max(mxv, px);
|
|
1107
|
-
mean = mean + px;
|
|
1108
|
-
gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));
|
|
1109
|
-
}
|
|
1110
|
-
mean = mean * h(1.0 / 16.0);
|
|
1111
|
-
// Exactly-gray blocks free the refit from the bbox clamp below: a gray
|
|
1112
|
-
// block has no hue to bend (the clamp's whole purpose), and on smooth
|
|
1113
|
-
// gradients the LSQ optimum often lies OUTSIDE the data range \u2014 endpoints
|
|
1114
|
-
// spread wider than the block so the 1/3-2/3 interpolants land on the
|
|
1115
|
-
// values. Same rationale as the BC5 scalar channels (+0.32 dB there).
|
|
1116
|
-
let gray = gd == h(0.0);
|
|
1117
|
-
let lim_lo = select(mn, h3(0.0), gray);
|
|
1118
|
-
let lim_hi = select(mxv, h3(1.0), gray);
|
|
1119
|
-
|
|
1120
|
-
// Seed endpoints from the block's principal colour axis (covariance
|
|
1121
|
-
// power-iteration, seeded with the bbox diagonal \u2014 same family as the
|
|
1122
|
-
// 'high' path). The bbox diagonal is sign-blind: on anti-correlated
|
|
1123
|
-
// channels (normal maps, hue edges) it points across the data instead of
|
|
1124
|
-
// along it, the projection indices come out garbage, and the LSQ refit \u2014
|
|
1125
|
-
// which fits endpoints GIVEN those indices \u2014 can't recover. Deviations are
|
|
1126
|
-
// pre-scaled \xD716 so covariance entries for shallow blocks stay in f16's
|
|
1127
|
-
// normal range (span ~1/255 \u2192 d\xB2 \u2248 1e-3) while full-range sums stay \u22644096;
|
|
1128
|
-
// the iteration renormalises by the max component (a plain length() of the
|
|
1129
|
-
// matvec output could overflow f16), so only the direction survives \u2014 the
|
|
1130
|
-
// \xD7256 covariance scale is irrelevant.
|
|
1131
|
-
var seed_hi: h3;
|
|
1132
|
-
var seed_lo: h3;
|
|
1133
|
-
var c0v = h3(0.0);
|
|
1134
|
-
var c1v = h3(0.0);
|
|
1135
|
-
var c2v = h3(0.0);
|
|
1136
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1137
|
-
let d = (pix[k] - mean) * h(16.0);
|
|
1138
|
-
c0v = c0v + d.x * d;
|
|
1139
|
-
c1v = c1v + d.y * d;
|
|
1140
|
-
c2v = c2v + d.z * d;
|
|
1141
|
-
}
|
|
1142
|
-
var axis = mxv - mn;
|
|
1143
|
-
var axis_ok = true;
|
|
1144
|
-
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1145
|
-
let nv = h3(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis));
|
|
1146
|
-
let m = max(max(abs(nv.x), abs(nv.y)), abs(nv.z));
|
|
1147
|
-
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1148
|
-
axis = nv / m;
|
|
1149
|
-
}
|
|
1150
|
-
if (axis_ok) {
|
|
1151
|
-
axis = axis / length(axis);
|
|
1152
|
-
var t_min = h(4.0);
|
|
1153
|
-
var t_max = h(-4.0);
|
|
1154
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1155
|
-
let t = dot(pix[k] - mean, axis);
|
|
1156
|
-
t_min = min(t_min, t);
|
|
1157
|
-
t_max = max(t_max, t);
|
|
1158
|
-
}
|
|
1159
|
-
// Inset along the axis by ~half a 565 cell (stb_dxt heuristic, matching
|
|
1160
|
-
// the degenerate-case bbox inset below).
|
|
1161
|
-
let pad = (t_max - t_min) * h(1.0 / 16.0);
|
|
1162
|
-
seed_hi = clamp(mean + (t_max - pad) * axis, h3(0.0), h3(1.0));
|
|
1163
|
-
seed_lo = clamp(mean + (t_min + pad) * axis, h3(0.0), h3(1.0));
|
|
1164
|
-
} else {
|
|
1165
|
-
// Degenerate (near-flat) block: inset bbox seed, as before.
|
|
1166
|
-
let inset = (mxv - mn) * h(1.0 / 16.0);
|
|
1167
|
-
seed_hi = clamp(mxv - inset, h3(0.0), h3(1.0));
|
|
1168
|
-
seed_lo = clamp(mn + inset, h3(0.0), h3(1.0));
|
|
1169
|
-
}
|
|
1170
|
-
let seed = order565(to565(seed_hi), to565(seed_lo));
|
|
1171
|
-
var c0 = seed.x;
|
|
1172
|
-
var c1 = seed.y;
|
|
1173
|
-
|
|
1174
|
-
// Fused seed pass, then up to TWO least-squares refit rounds (mirroring
|
|
1175
|
-
// the high path's iterated refits, at projection cost), each accepted only
|
|
1176
|
-
// if the block's squared error actually decreases \u2014 the refit minimises a
|
|
1177
|
-
// continuous objective and can lose after 565 quantisation. Every pass
|
|
1178
|
-
// re-accumulates the normal-equation sums, so an accepted round seeds the
|
|
1179
|
-
// next.
|
|
1180
|
-
var cur = project_stats(&pix, c0, c1);
|
|
1181
|
-
for (var it: u32 = 0u; it < 2u; it = it + 1u) {
|
|
1182
|
-
// Refit only on a well-conditioned system. When every pixel lands on ONE
|
|
1183
|
-
// level (flat / near-flat blocks \u2014 note the 4-colour-mode nudge forces
|
|
1184
|
-
// c0 \u2260 c1 even for perfectly flat blocks) the system is rank-1: det is 0
|
|
1185
|
-
// in exact math and the f16-accumulated det/numerators are pure rounding
|
|
1186
|
-
// noise, so the solve returns garbage endpoints. With \u22652 distinct levels
|
|
1187
|
-
// 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
|
|
1188
|
-
// noise floor \u2014 0.5 separates the two regimes cleanly.
|
|
1189
|
-
if (cur.s_min >= cur.s_max) { break; }
|
|
1190
|
-
let det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;
|
|
1191
|
-
if (abs(det) <= h(0.5)) { break; }
|
|
1192
|
-
// Clamp the refit to the block bbox (not [0,1]) \u2014 except for exactly
|
|
1193
|
-
// gray blocks, see the load pass: on multi-cluster blocks the
|
|
1194
|
-
// unconstrained solve extrapolates far outside the block's colours
|
|
1195
|
-
// and the per-channel clamp then bends the hue \u2014 fringe pixels decode to
|
|
1196
|
-
// colours that exist nowhere in the block. Constraining to the bbox also
|
|
1197
|
-
// measures better in plain SSE (+1.6 dB on the colour test card), so the
|
|
1198
|
-
// accept-if-better guard below keeps more refits.
|
|
1199
|
-
let e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, lim_lo, lim_hi);
|
|
1200
|
-
let e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, lim_lo, lim_hi);
|
|
1201
|
-
let rq = order565(to565(e0), to565(e1));
|
|
1202
|
-
if (rq.x == c0 && rq.y == c1) { break; }
|
|
1203
|
-
let nxt = project_stats(&pix, rq.x, rq.y);
|
|
1204
|
-
if (nxt.err >= cur.err) { break; }
|
|
1205
|
-
c0 = rq.x;
|
|
1206
|
-
c1 = rq.y;
|
|
1207
|
-
cur = nxt;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
let o = bi * 2u;
|
|
1211
|
-
dst[o] = c0 | (c1 << 16u);
|
|
1212
|
-
dst[o + 1u] = cur.indices;
|
|
1213
|
-
}
|
|
1214
|
-
`;
|
|
1065
|
+
var bc1_fast_f16_default = "// bc1 encoder \u2014 f16 variant (requires the shader-f16 feature).\n//\n// BC1 quantises endpoints to RGB565 anyway, so nothing here needs f32\n// precision except the refit sums (see moments()). Same algorithm as the\n// f32 fallback in bc1.wgsl:\n//\n// 1. NEAR-FLAT blocks (every channel within 3 levels) take a solid colour\n// at the block mean: per channel the endpoint pair whose \u2154/\u2153\n// interpolant lands nearest (solid_pair \u2014 the stb_dxt single-colour\n// idea). Direct 565 quantisation is up to 4 levels off on R/B there,\n// and a line fit has nothing to fit. +0.05..3.9 dB on content with\n// flat regions (displacement/AO maps, UI, the normal card).\n// 2. Otherwise a principal-axis endpoint seed (covariance power\n// iteration; inset bbox on degenerate blocks), inset by ~half a 565\n// cell (stb_dxt heuristic), quantised to 565 in 4-colour mode (c0 > c1).\n// 3. Projection passes: every pixel's level is the rounded projection\n// onto the decoded-endpoint line (the 4 palette entries are colinear\n// and evenly spaced, so that IS the nearest entry), packed as indices\n// on the fly, with the block error and projection MOMENTS accumulated.\n// 4. Up to TWO least-squares refit rounds solved from those moments\n// (solve()), each re-projected and accepted only if the block error\n// drops. The moments replace the full normal-equation sums a refit\n// used to accumulate per pixel (the refit rounds were ~60% of the\n// kernel): \u221210..15% GPU on non-flat content at equal quality.\n//\n// The host selects this module only when the device reports shader-f16,\n// falling back to bc1.wgsl otherwise.\nenable f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: 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 h3 = vec3<f16>;\n\nfn to565(c: h3) -> u32 {\n let r = u32(clamp(floor(c.r * h(31.0) + h(0.5)), h(0.0), h(31.0)));\n let g = u32(clamp(floor(c.g * h(63.0) + h(0.5)), h(0.0), h(63.0)));\n let b = u32(clamp(floor(c.b * h(31.0) + h(0.5)), h(0.0), h(31.0)));\n return (r << 11u) | (g << 5u) | b;\n}\n\n// Decode a 565 endpoint to [0,1]: (x*527+23)>>6 (6-bit: 259/33) \u2014\n// round-to-nearest scaling, matching bc1_ref.ts / bc1.wgsl and typical\n// hardware decoders. Exact in u32 integer math (f16 could not evaluate the\n// products exactly).\nfn from565(c: u32) -> h3 {\n let r = (c >> 11u) & 31u;\n let g = (c >> 5u) & 63u;\n let b = c & 31u;\n let r8 = (r * 527u + 23u) >> 6u;\n let g8 = (g * 259u + 33u) >> 6u;\n let b8 = (b * 527u + 23u) >> 6u;\n return h3(vec3<f32>(vec3<u32>(r8, g8, b8))) * h(1.0 / 255.0);\n}\n\n// Force 4-colour mode: c0 > c1 strictly.\nfn order565(a: u32, b: u32) -> vec2<u32> {\n var c0 = a; var c1 = b;\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 return vec2<u32>(c0, c1);\n}\n\n// Projection MOMENTS against the decoded endpoints of (c0, c1): levels\n// L = 0..3 along p0\u2192p1 (the rounded projection \u2014 the palette is colinear\n// and evenly spaced, so that IS the nearest entry), their BC1 indices, and\n// \u03A3L, \u03A3L\xB2 (exact small integers), \u03A3u, \u03A3L\xB7u (u = v \u2212 p0, f32: the f16 block\n// mean is off by up to ~\xBD level \u2014 its running sum reaches ~8, where f16's\n// ulp is a whole level \u2014 which skews the closed-form solve by several\n// levels) \u2014 plus the block's exact squared error against this palette.\n// Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922, 2\u21923, 3\u21921 (c1); packed LUT\n// (0x78 >> 2L) & 3.\nstruct Moments { sL: f32, sLL: f32, sU: vec3<f32>, sLu: vec3<f32>, indices: u32, err: f32 };\nfn moments(pix: ptr<function, array<h3, 16>>, c0: u32, c1: u32) -> Moments {\n let p0 = from565(c0);\n let dir = from565(c1) - p0;\n let inv = h(3.0) / dot(dir, dir);\n var sL = h(0.0);\n var sLL = h(0.0);\n var out: Moments;\n out.sU = vec3<f32>(0.0);\n out.sLu = vec3<f32>(0.0);\n out.indices = 0u;\n var err = h(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let u = (*pix)[k] - p0;\n let L = clamp(floor(dot(u, dir) * inv + h(0.5)), h(0.0), h(3.0));\n sL = sL + L;\n sLL = sLL + L * L;\n let uf = vec3<f32>(u);\n out.sU = out.sU + uf;\n out.sLu = out.sLu + f32(L) * uf;\n out.indices = out.indices | (((0x78u >> (u32(L) * 2u)) & 3u) << (k * 2u));\n let e = u - L * h(1.0 / 3.0) * dir;\n err = err + dot(e, e);\n }\n out.err = f32(err);\n out.sL = f32(sL);\n out.sLL = f32(sLL);\n return out;\n}\n\n// One least-squares refit from moments: every normal-equation sum is an\n// O(1) function of them (b = L/3, a = 1 \u2212 b)\n// sBB = \u03A3L\xB2/9 sAB = \u03A3L/3 \u2212 \u03A3L\xB2/9 sAA = 16 \u2212 2\u03A3L/3 + \u03A3L\xB2/9\n// \u03A3b\xB7u = \u03A3L\xB7u/3 \u03A3a\xB7u = \u03A3u \u2212 \u03A3b\xB7u\n// solved in f32 for endpoints relative to p0, clamped to [lim_lo, lim_hi]\n// (the block bbox, except exactly-gray blocks \u2014 see the load pass: on\n// multi-cluster blocks the unconstrained solve extrapolates outside the\n// block's colours and the clamp would bend the hue), re-quantised and\n// ordered for 4-colour mode. Returns (c0, c1) unchanged when every pixel\n// sits on ONE level \u2014 then 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2 exactly and the system is\n// singular.\nfn solve(m: Moments, c0: u32, c1: u32, lim_lo: h3, lim_hi: h3) -> vec2<u32> {\n if (16.0 * m.sLL == m.sL * m.sL) { return vec2<u32>(c0, c1); }\n let sBB = m.sLL * (1.0 / 9.0);\n let sAB = m.sL * (1.0 / 3.0) - sBB;\n let sAA = 16.0 - m.sL * (2.0 / 3.0) + sBB;\n let det = sAA * sBB - sAB * sAB;\n let p0 = vec3<f32>(from565(c0));\n let sBu = m.sLu * (1.0 / 3.0);\n let sAu = m.sU - sBu;\n let e0 = clamp(p0 + (sBB * sAu - sAB * sBu) / det, vec3<f32>(lim_lo), vec3<f32>(lim_hi));\n let e1 = clamp(p0 + (sAA * sBu - sAB * sAu) / det, vec3<f32>(lim_lo), vec3<f32>(lim_hi));\n return order565(to565(h3(e0)), to565(h3(e1)));\n}\n\n// Solid-colour channel code: the pair (a, b) of `bits`-bit codes whose \u2154/\u2153\n// interpolant (2\xB7dec(a) + dec(b))/3 \u2014 palette index 2 \u2014 lands nearest v\n// (8-bit units). With a == b it's a plain endpoint; straddling pairs reach\n// the ~2.7-level sub-steps between codes that direct quantisation (steps of\n// ~8 levels at 5 bits) cannot.\nfn solid_pair(v: f32, bits: u32) -> vec2<u32> {\n let maxc = (1u << bits) - 1u;\n let q = min(u32(v * f32(maxc) / 255.0), maxc - 1u);\n var x: f32; var y: f32;\n if (bits == 5u) {\n x = f32((q * 527u + 23u) >> 6u);\n y = f32(((q + 1u) * 527u + 23u) >> 6u);\n } else {\n x = f32((q * 259u + 33u) >> 6u);\n y = f32(((q + 1u) * 259u + 33u) >> 6u);\n }\n var best = vec2<u32>(q, q);\n var be = abs(x - v);\n let c1 = (2.0 * x + y) / 3.0;\n if (abs(c1 - v) < be) { be = abs(c1 - v); best = vec2<u32>(q, q + 1u); }\n let c2 = (x + 2.0 * y) / 3.0;\n if (abs(c2 - v) < be) { be = abs(c2 - v); best = vec2<u32>(q + 1u, q); }\n if (abs(y - v) < be) { best = vec2<u32>(q + 1u, q + 1u); }\n return best;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 var pix: array<h3, 16>;\n var mn = h3(1.0);\n var mxv = h3(0.0);\n var mean = h3(0.0);\n var gd = h(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 = h3(textureLoad(src_tex, p, 0).rgb);\n pix[i] = px; mn = min(mn, px); mxv = max(mxv, px);\n mean = mean + px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n mean = mean * h(1.0 / 16.0);\n // Exactly-gray blocks free the refit from the bbox clamp below: a gray\n // block has no hue to bend (the clamp's whole purpose), and on smooth\n // gradients the LSQ optimum often lies OUTSIDE the data range \u2014 endpoints\n // spread wider than the block so the 1/3-2/3 interpolants land on the\n // values. Same rationale as the BC5 scalar channels (+0.32 dB there).\n let gray = gd == h(0.0);\n let lim_lo = select(mn, h3(0.0), gray);\n let lim_hi = select(mxv, h3(1.0), gray);\n\n // NEAR-FLAT blocks (every channel within 3 levels) take a solid colour\n // at the block mean: per channel the endpoint pair whose \u2154/\u2153 interpolant\n // lands nearest (see solid_pair). A line fit has nothing to fit there,\n // and direct 565 quantisation is up to 4 levels off on R/B. They share\n // the index pass below (pixels \xB11 level off the mean may sit closer to\n // an endpoint than to the interpolant) and skip the PCA seed and refits.\n let span = mxv - mn;\n let flat = max(max(span.x, span.y), span.z) <= h(3.0 / 255.0);\n var c0: u32;\n var c1: u32;\n if (flat) {\n let m8 = vec3<f32>(mean) * 255.0;\n let pr = solid_pair(m8.x, 5u);\n let pg = solid_pair(m8.y, 6u);\n let pb = solid_pair(m8.z, 5u);\n let s0 = (pr.x << 11u) | (pg.x << 5u) | pb.x;\n let s1 = (pr.y << 11u) | (pg.y << 5u) | pb.y;\n // c0 > c1 keeps 4-colour mode (index 2 = \u2154\xB7c0 + \u2153\xB7c1; swapped, the\n // same colour is index 3). Equal codes encode the colour itself.\n c0 = max(s0, s1);\n c1 = min(s0, s1);\n } else {\n // Seed endpoints from the block's principal colour axis (covariance\n // power-iteration, seeded with the bbox diagonal). The bbox diagonal is\n // sign-blind: on anti-correlated channels (normal maps, hue edges) it\n // points across the data instead of along it, the projection indices\n // come out garbage, and the LSQ refit \u2014 which fits endpoints GIVEN\n // those indices \u2014 can't recover. Deviations are pre-scaled \xD716 so\n // covariance entries for shallow blocks stay in f16's normal range\n // (span ~1/255 \u2192 d\xB2 \u2248 1e-3) while full-range sums stay \u22644096; the\n // iteration renormalises by the max component (a plain length() of the\n // matvec output could overflow f16), so only the direction survives.\n var seed_hi: h3;\n var seed_lo: h3;\n var c0v = h3(0.0);\n var c1v = h3(0.0);\n var c2v = h3(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (pix[k] - mean) * h(16.0);\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var axis = mxv - mn;\n var axis_ok = true;\n for (var it: u32 = 0u; it < 4u; it = it + 1u) {\n let nv = h3(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis));\n let m = max(max(abs(nv.x), abs(nv.y)), abs(nv.z));\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 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, 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 degenerate-case bbox inset below).\n let pad = (t_max - t_min) * h(1.0 / 16.0);\n seed_hi = clamp(mean + (t_max - pad) * axis, h3(0.0), h3(1.0));\n seed_lo = clamp(mean + (t_min + pad) * axis, h3(0.0), h3(1.0));\n } else {\n // Degenerate block: inset bbox seed.\n let inset = (mxv - mn) * h(1.0 / 16.0);\n seed_hi = clamp(mxv - inset, h3(0.0), h3(1.0));\n seed_lo = clamp(mn + inset, h3(0.0), h3(1.0));\n }\n let seed = order565(to565(seed_hi), to565(seed_lo));\n c0 = seed.x;\n c1 = seed.y;\n }\n\n // Projection pass on the seed, then up to two refit rounds (solve() off\n // the previous pass's moments), each re-projected and accepted only if\n // the block error drops \u2014 the refit minimises a continuous objective and\n // can lose after 565 quantisation. Flat blocks keep their solid pair\n // (equal codes: the colour itself, index 0 \u2014 opaque in either mode).\n var indices = 0u;\n if (c0 != c1) {\n var cur = moments(&pix, c0, c1);\n for (var it: u32 = 0u; it < select(2u, 0u, flat); it = it + 1u) {\n let cand = solve(cur, c0, c1, lim_lo, lim_hi);\n if (cand.x == c0 && cand.y == c1) { break; }\n let nxt = moments(&pix, cand.x, cand.y);\n if (nxt.err >= cur.err) { break; }\n c0 = cand.x;\n c1 = cand.y;\n cur = nxt;\n }\n indices = cur.indices;\n }\n\n let o = bi * 2u;\n dst[o] = c0 | (c1 << 16u);\n dst[o + 1u] = indices;\n}\n";
|
|
1215
1066
|
|
|
1216
1067
|
// src/BC1Encoder.ts
|
|
1217
1068
|
var BC1Encoder = class extends Encoder {
|
|
@@ -1238,7 +1089,7 @@ var BC1Encoder = class extends Encoder {
|
|
|
1238
1089
|
};
|
|
1239
1090
|
|
|
1240
1091
|
// src/bc5.wgsl
|
|
1241
|
-
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)
|
|
1092
|
+
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 y0: u32, // first block row of this dispatch (row-band encodes)\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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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";
|
|
1242
1093
|
|
|
1243
1094
|
// src/bc5_fast_f16.wgsl
|
|
1244
1095
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -1314,7 +1165,7 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1314
1165
|
enable f16;
|
|
1315
1166
|
alias h = f16;
|
|
1316
1167
|
alias h2 = vec2<f16>;
|
|
1317
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
1168
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: u32, };
|
|
1318
1169
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1319
1170
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1320
1171
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
@@ -1356,7 +1207,9 @@ fn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, sp
|
|
|
1356
1207
|
}
|
|
1357
1208
|
|
|
1358
1209
|
@compute @workgroup_size(8, 8, 1)
|
|
1359
|
-
fn encode(@builtin(global_invocation_id)
|
|
1210
|
+
fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
1211
|
+
// Row-band encodes dispatch a slice of the block grid starting at row y0.
|
|
1212
|
+
let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);
|
|
1360
1213
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1361
1214
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
1362
1215
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
@@ -1500,10 +1353,10 @@ var BC5Encoder = class extends Encoder {
|
|
|
1500
1353
|
};
|
|
1501
1354
|
|
|
1502
1355
|
// src/bc7.wgsl
|
|
1503
|
-
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)
|
|
1356
|
+
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 y0: u32, // first block row of this dispatch (row-band encodes)\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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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";
|
|
1504
1357
|
|
|
1505
1358
|
// src/bc7_fast_f16.wgsl
|
|
1506
|
-
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";
|
|
1359
|
+
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, y0: 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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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";
|
|
1507
1360
|
|
|
1508
1361
|
// src/BC7Encoder.ts
|
|
1509
1362
|
var BC7Encoder = class extends Encoder {
|
|
@@ -1540,80 +1393,93 @@ var BC7Encoder = class extends Encoder {
|
|
|
1540
1393
|
};
|
|
1541
1394
|
|
|
1542
1395
|
// src/astc4x4.wgsl
|
|
1543
|
-
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";
|
|
1396
|
+
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 documents the design\n// measurements) and is preferred when the 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-channel PCA extents, two bit budgets:\n// span > 12 \u2192 QUANT_192 endpoints (trit ISE) + 4-bit\n// weights, mode 0x242; span \u2264 12 \u2192 8-bit endpoints +\n// 3-bit weights, mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, block mode 0x042 \u2014 PCA\n// seed \u2192 fused projection + least-squares refit, the fit\n// pass's weights shipped\n// The palette entries of a single-line fit are colinear, so the nearest is\n// the rounded projection \u2014 no per-entry search. The endpoint ordering rule\n// is applied before the weight pass, so no reflection is needed.\n//\n// RESTRICTED SUBSET + BLOCK LAYOUT: see astc4x4_ref.ts (single partition,\n// no dual-plane, CEM 0/8/12, plain-bit weights; block mode derivations,\n// the QUANT_192 trit ISE and the weight-stream bit order are documented\n// 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) (unquantised values); the encoder swaps\n// endpoints up front (weights are assigned after the swap, so no\n// reflection pass). CEM 0 has no rule (L0 \u2264 L1 by construction).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n y0: u32, // first block row of this dispatch (row-band encodes)\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// Principal RGB axis for opaque blocks (alpha is constant there, so the\n// 4th covariance lane is dead weight). Same iteration as principal_axis4.\nfn principal_axis3(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\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 = vec3<f32>((*pixels)[k].xyz) - 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// ISE trit-block encoder: 5 trits \u2192 the 8-bit T field (the inverse of the\n// spec's trit-block decode; see astc4x4_ref.ts).\nfn trit_enc(t0: u32, t1: u32, t2: u32, t3: u32, t4: u32) -> u32 {\n let c = select(select((t2 << 4u) | (t1 << 2u) | t0, (t1 << 4u) | (t0 << 2u) | 3u, t2 == 2u), 12u | t0, t2 == 2u && t1 == 2u);\n return select(select((t4 << 7u) | (t3 << 5u) | c, (t3 << 7u) | 96u | c, t4 == 2u), ((c >> 2u) << 5u) | 28u | (c & 3u), t3 == 2u && t4 == 2u);\n}\n\n// Nearest QUANT_192 endpoint to x \u2208 [0,255]; returns (ISE value =\n// trit\xB764 + bits, unquantised level). See astc4x4_fast_f16.wgsl.\nfn q192(x: f32) -> vec2<u32> {\n let v = u32(clamp(floor(x + 0.5), 0.0, 255.0));\n let up = v > 127u;\n var u = select(v, 255u - v, up);\n if ((u & 3u) == 3u) {\n let xu = select(x, 255.0 - x, up);\n u = select(u - 1u, u + 1u, xu > f32(u) && u < 127u);\n }\n return vec2<u32>(((u & 3u) << 6u) | ((u >> 2u) << 1u) | u32(up), select(u, 255u - u, up));\n}\n\n// ------------------------------- Entry ---------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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 if (opaque) {\n // ------------- Opaque colour: CEM 8, two bit budgets -------------------\n // span > 12 \u2192 QUANT_192 endpoints + 4-bit weights (mode 0x242), else\n // exact 8-bit endpoints + 3-bit weights (mode 0x053); endpoints are the\n // bbox-clamped PCA extents. See astc4x4_fast_f16.wgsl for the design\n // and measurements.\n let mean3 = vec3<f32>(isum.xyz) * (1.0 / 16.0);\n let lo3 = vec3<f32>(lo.xyz);\n let hi3 = vec3<f32>(hi.xyz);\n var x0 = lo3;\n var x1 = hi3;\n let axis = principal_axis3(&pixels, mean3, hi3 - lo3);\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(vec3<f32>(pixels[k].xyz) - mean3, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n x0 = clamp(mean3 + t_min * axis, lo3, hi3);\n x1 = clamp(mean3 + t_max * axis, lo3, hi3);\n }\n let span3 = hi.xyz - lo.xyz;\n let small = max(max(span3.x, span3.y), span3.z) <= 12;\n var r0: vec2<u32>; var g0: vec2<u32>; var b0: vec2<u32>;\n var r1: vec2<u32>; var g1: vec2<u32>; var b1: vec2<u32>;\n if (small) {\n let q0 = vec3<u32>(clamp(floor(x0 + 0.5), vec3<f32>(0.0), vec3<f32>(255.0)));\n let q1 = vec3<u32>(clamp(floor(x1 + 0.5), vec3<f32>(0.0), vec3<f32>(255.0)));\n r0 = vec2<u32>(q0.x); g0 = vec2<u32>(q0.y); b0 = vec2<u32>(q0.z);\n r1 = vec2<u32>(q1.x); g1 = vec2<u32>(q1.y); b1 = vec2<u32>(q1.z);\n } else {\n r0 = q192(x0.x); g0 = q192(x0.y); b0 = q192(x0.z);\n r1 = q192(x1.x); g1 = q192(x1.y); b1 = q192(x1.z);\n }\n // Blue-contraction ordering on the unquantised levels.\n if (r0.y + g0.y + b0.y > r1.y + g1.y + b1.y) {\n let tr = r0; r0 = r1; r1 = tr;\n let tg = g0; g0 = g1; g1 = tg;\n let tb = b0; b0 = b1; b1 = tb;\n }\n let d0 = vec3<f32>(vec3<u32>(r0.y, g0.y, b0.y));\n let d1 = vec3<f32>(vec3<u32>(r1.y, g1.y, b1.y));\n // One weight loop for both budgets; weights as 4-bit nibbles.\n let lmax = select(15.0, 7.0, small);\n let dir = d1 - d0;\n let dd = dot(dir, dir);\n var s0 = 0u; var s1 = 0u;\n if (dd > 0.0) {\n let inv = lmax / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec3<f32>(pixels[k].xyz) - d0, dir) * inv + 0.5), 0.0, lmax));\n s0 = s0 | (w << (4u * k));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec3<f32>(pixels[k].xyz) - d0, dir) * inv + 0.5), 0.0, lmax));\n s1 = s1 | (w << (4u * (k - 8u)));\n }\n }\n if (small) {\n // Mode 0x053: plain 8-bit endpoints; the 3-bit weight stream is the\n // nibbles compacted (8 nibbles \u2192 24 bits).\n var c0 = (s0 & 0x07070707u) | ((s0 & 0x70707070u) >> 1u);\n c0 = (c0 & 0x003F003Fu) | ((c0 & 0x3F003F00u) >> 2u);\n c0 = (c0 & 0x00000FFFu) | ((c0 & 0x0FFF0000u) >> 4u);\n var c1 = (s1 & 0x07070707u) | ((s1 & 0x70707070u) >> 1u);\n c1 = (c1 & 0x003F003Fu) | ((c1 & 0x3F003F00u) >> 2u);\n c1 = (c1 & 0x00000FFFu) | ((c1 & 0x0FFF0000u) >> 4u);\n w0 = 0x053u | (8u << 13u) | (r0.x << 17u) | (r1.x << 25u);\n w1 = (r1.x >> 7u) | (g0.x << 1u) | (g1.x << 9u) | (b0.x << 17u) | (b1.x << 25u);\n w2 = (b1.x >> 7u) | reverseBits(c1 >> 8u);\n w3 = reverseBits(c0 | (c1 << 24u));\n } else {\n // Mode 0x242: trit-ISE QUANT_192 endpoints (v0..v5 = R0 R1 G0 G1 B0\n // B1, 46 bits from bit 17: group 1 = v0..v4 with trit field T, group\n // 2 = v5 with its lone trit as 2 bits), 4-bit weights.\n let tg = trit_enc(r0.x >> 6u, r1.x >> 6u, g0.x >> 6u, g1.x >> 6u, b0.x >> 6u);\n w0 = 0x242u | (8u << 13u) | ((r0.x & 63u) << 17u) | ((tg & 3u) << 23u) | ((r1.x & 63u) << 25u) | (((tg >> 2u) & 1u) << 31u);\n w1 = ((tg >> 3u) & 1u) | ((g0.x & 63u) << 1u) | (((tg >> 4u) & 1u) << 7u) | ((g1.x & 63u) << 8u)\n | (((tg >> 5u) & 3u) << 14u) | ((b0.x & 63u) << 16u) | ((tg >> 7u) << 22u) | ((b1.x & 63u) << 23u)\n | ((b1.x >> 6u) << 29u);\n w2 = reverseBits(s1);\n w3 = reverseBits(s0);\n }\n } else {\n // ------------- Translucent: CEM 12, 2-bit weights, PCA seed + refit ----\n let mean = vec4<f32>(isum) * (1.0 / 16.0);\n\n // Fused LSQ fit seeded from the block's principal RGBA axis (4 power\n // iterations \u2014 the refit absorbs residual axis error) at the exact\n // projection extents. The refit is clamped to the block bbox: on\n // multi-cluster blocks the unconstrained solve extrapolates outside the\n // block's colours and the per-channel clamp would bend the hue.\n var seed0 = lo;\n var seed1 = hi;\n let axis = principal_axis4(&pixels, mean, vec4<f32>(hi - lo), 4u);\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 var e0 = lo;\n var e1 = hi;\n var fitStream = 0u;\n var haveFitWeights = false;\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 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 // Valid fits ship the fit-pass weights; the blue-contraction swap is a\n // full reflection w \u2192 3\u2212w = bitwise NOT of the packed stream.\n var s0 = 0u;\n if (haveFitWeights) {\n s0 = select(fitStream, ~fitStream, swapped);\n } else {\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n let e0f = vec4<f32>(e0);\n 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 }\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 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";
|
|
1544
1397
|
|
|
1545
1398
|
// src/astc4x4_fast_f16.wgsl
|
|
1546
|
-
var astc4x4_fast_f16_default = `// astc4x4
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
// a fused least-squares refit \u2192 reproject), tuned for throughput:
|
|
1399
|
+
var astc4x4_fast_f16_default = `// astc4x4 encoder \u2014 f16 variant (requires the shader-f16 feature). Same
|
|
1400
|
+
// algorithm as the f32 fallback in astc4x4.wgsl, with the projection / fit
|
|
1401
|
+
// math in f16:
|
|
1550
1402
|
//
|
|
1551
1403
|
// \u2022 THREE block classes, picked per block from the loaded pixels (the
|
|
1552
1404
|
// ASTC bit budget trades endpoint bits against weight bits, so a block
|
|
1553
1405
|
// only pays for the channels it uses):
|
|
1554
1406
|
// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253
|
|
1555
|
-
// opaque \u2192 CEM 8 (RGB),
|
|
1556
|
-
//
|
|
1407
|
+
// opaque \u2192 CEM 8 (RGB), two budgets:
|
|
1408
|
+
// span > 12 \u2192 QUANT_192 endpoints + 4-bit
|
|
1409
|
+
// weights (QUANT_16), mode 0x242
|
|
1410
|
+
// span \u2264 12 \u2192 QUANT_256 endpoints + 3-bit
|
|
1411
|
+
// weights (QUANT_8), mode 0x053
|
|
1412
|
+
// translucent \u2192 CEM 12 (RGBA), 2-bit weights, mode 0x042
|
|
1557
1413
|
// "gray" = every texel R == G == B exactly (f16 equality is exact for
|
|
1558
|
-
// 8-bit sources), "opaque" = every texel A == 1.
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
// (
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1414
|
+
// 8-bit sources), "opaque" = every texel A == 1. Every class fills the
|
|
1415
|
+
// 128-bit block; the old opaque layout (8-bit endpoints + 3-bit weights
|
|
1416
|
+
// for every block) left 15 bits unused.
|
|
1417
|
+
// \u2022 OPAQUE BUDGETS. 16 weight levels need QUANT_192 endpoints (46 bits)
|
|
1418
|
+
// to fit \u2014 trit-ISE coded, 1/4 of the 8-bit values fall on excluded
|
|
1419
|
+
// slots and round to a neighbour. On wide blocks the finer weights win
|
|
1420
|
+
// by far (+0.6..1.2 dB on photographic colour); when the block's colour
|
|
1421
|
+
// span is small the endpoint error dominates instead (the weight error
|
|
1422
|
+
// scales with the span, the endpoint error doesn't), and exact 8-bit
|
|
1423
|
+
// endpoints with 8 levels win (the 12-level threshold was swept:
|
|
1424
|
+
// 8/12/16/24 on the /eval corpus). Both budgets share ONE weight loop
|
|
1425
|
+
// (weights accumulate as 4-bit nibbles; the 3-bit stream is compacted
|
|
1426
|
+
// afterwards) \u2014 two loops measured ~8% slower on mixed warps.
|
|
1427
|
+
// \u2022 The opaque path runs in 3-channel math (alpha is constant): the dead
|
|
1428
|
+
// 4th lane in the covariance, power iteration, extents and weight
|
|
1429
|
+
// loops cost ~10% of the kernel. Opaque endpoints are the PCA extents
|
|
1430
|
+
// (8 power iterations; 6 measured \u22120.1 dB on normal maps), bbox-clamped
|
|
1431
|
+
// \u2014 no LSQ refit: with 8\u201316 weight levels a converged axis carries the
|
|
1432
|
+
// quality.
|
|
1433
|
+
// \u2022 QUANT_16 weight levels are not uniform (0 4 8 12 17 21 25 29 35 \u2026);
|
|
1434
|
+
// the weights are the uniformly rounded projection anyway \u2014 the exact
|
|
1435
|
+
// nearest-level mapping measured +0.03..0.06 dB for +15% GPU.
|
|
1436
|
+
// \u2022 The GRAY path is scalar \u2014 no covariance, no power iteration \u2014 in
|
|
1437
|
+
// [0,255]-integer f16 math (exact endpoints, and 64/span \u2264 64 never
|
|
1438
|
+
// overflows f16, unlike a [0,1]-domain 1/dd).
|
|
1439
|
+
// \u2022 TRANSLUCENT blocks: PCA seed (4 power iterations) \u2192 fused projection
|
|
1440
|
+
// + LSQ refit (the 4-level grid is coarse enough to need it, and the fit
|
|
1441
|
+
// pulls the line through the dominant cluster of multi-cluster blocks)
|
|
1442
|
+
// \u2192 the fit-pass weights are shipped without a reprojection. A
|
|
1443
|
+
// 3-bit-weight + QUANT_192 translucent budget measured +1.5 dB on
|
|
1444
|
+
// translucent content but +10..20% GPU there \u2014 not taken.
|
|
1445
|
+
// \u2022 f16 range: projection directions are pre-scaled by 32 \u2014 a shallow
|
|
1446
|
+
// block (endpoints ~1/255 apart) has dd \u2248 1.5e-5, where L/dd overflows
|
|
1447
|
+
// f16 (max 65504) and the projection dots go subnormal, turning weights
|
|
1448
|
+
// and the refit to garbage (banding on smooth gradients). Scaled, every
|
|
1449
|
+
// intermediate stays in f16's normal range (worst case, 16 levels:
|
|
1450
|
+
// inv = 480/0.0157 \u2248 3.1e4).
|
|
1575
1451
|
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
1576
|
-
// exceed sum(e1.rgb)
|
|
1577
|
-
// weight-reflection pass is needed.
|
|
1452
|
+
// exceed sum(e1.rgb), compared on the UNQUANTISED values) is applied
|
|
1453
|
+
// BEFORE the weight pass, so no weight-reflection pass is needed.
|
|
1578
1454
|
// \u2022 Weight streams are accumulated LSB-first into u32 words and placed
|
|
1579
1455
|
// into the block's reversed-bit-order field with reverseBits() \u2014
|
|
1580
|
-
// stream bit q lives at block bit 127 \u2212 q
|
|
1581
|
-
// maps onto a block word with a single bit reversal.
|
|
1582
|
-
// \u2022 The covariance moments stay in their OWN pass, deliberately: fusing
|
|
1583
|
-
// them into the load loop bc7-style (pixel-0 residuals, with or without
|
|
1584
|
-
// hoisting pixel 0 out of the loop) measured +5% GPU time on 4096\xB2
|
|
1585
|
-
// (/ab A/B, 2026-07, M3) \u2014 the separate loop overlaps the 16 texture
|
|
1586
|
-
// loads' latency better than a longer in-loop dependency chain does.
|
|
1587
|
-
// Per-pass cost on the same rig, for future tuning: covariance+power-
|
|
1588
|
-
// iteration \u2248 18%, LSQ fit pass \u2248 24%, extents pass \u2248 6% of the kernel;
|
|
1589
|
-
// @workgroup_size 16\xD78 measured exactly at par with 8\xD78.
|
|
1456
|
+
// stream bit q lives at block bit 127 \u2212 q.
|
|
1590
1457
|
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
// 0x253/0x053/0x042).
|
|
1458
|
+
// BLOCK LAYOUTS + ISE: see astc4x4_ref.ts (single partition, CEM 0/8/12,
|
|
1459
|
+
// trit-ISE QUANT_192 endpoints, plain-bit weights, block modes
|
|
1460
|
+
// 0x253/0x242/0x053/0x042).
|
|
1594
1461
|
//
|
|
1595
1462
|
// The host selects this module only when the device reports shader-f16,
|
|
1596
1463
|
// falling back to astc4x4.wgsl otherwise.
|
|
1597
1464
|
enable f16;
|
|
1598
1465
|
alias h = f16;
|
|
1466
|
+
alias h3 = vec3<f16>;
|
|
1599
1467
|
alias h4 = vec4<f16>;
|
|
1600
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
1468
|
+
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, y0: u32, };
|
|
1601
1469
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1602
1470
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1603
1471
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
1604
1472
|
|
|
1605
1473
|
// Fused projection + least-squares refit against the 4-level QUANT_4
|
|
1606
|
-
// palette
|
|
1607
|
-
//
|
|
1608
|
-
// more from spending the same time elsewhere (see header).
|
|
1474
|
+
// palette (translucent blocks). Returns the refit endpoints and the
|
|
1475
|
+
// projection's packed weight stream.
|
|
1609
1476
|
struct Fit { e0: h4, e1: h4, valid: bool, wstream: u32 };
|
|
1610
1477
|
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
1611
1478
|
var out: Fit;
|
|
1612
1479
|
out.valid = false;
|
|
1613
1480
|
out.wstream = 0u;
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
// possible only for non-8-bit sources) are treated as flat.
|
|
1481
|
+
// Spans below ~0.7 of an 8-bit step (dd\u2083\u2082 < 0.008, possible only for
|
|
1482
|
+
// non-8-bit sources) are treated as flat.
|
|
1617
1483
|
let dir = (e1 - e0) * h(32.0);
|
|
1618
1484
|
let dd = dot(dir, dir);
|
|
1619
1485
|
if (dd < h(0.008)) { return out; }
|
|
@@ -1621,7 +1487,7 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1621
1487
|
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
1622
1488
|
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
1623
1489
|
var s_min = h(3.0); var s_max = h(0.0);
|
|
1624
|
-
// Value sums accumulate v \u2212 e0 (basis is affine, a + b = 1, so the fit
|
|
1490
|
+
// Value sums accumulate v \u2212 e0 (the basis is affine, a + b = 1, so the fit
|
|
1625
1491
|
// commutes with the shift): accumulators scale with the block span, keeping
|
|
1626
1492
|
// f16 rounding a fraction of the span instead of \xB11 level at high absolute
|
|
1627
1493
|
// values.
|
|
@@ -1636,9 +1502,9 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1636
1502
|
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
1637
1503
|
}
|
|
1638
1504
|
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
1639
|
-
// singular \u2014 det/numerators are pure f16 rounding noise
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1505
|
+
// singular \u2014 det/numerators are pure f16 rounding noise. With \u22652 distinct
|
|
1506
|
+
// levels det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15\xB7(1/3)\xB2 \u2248 1.67 \u2014 0.5 separates
|
|
1507
|
+
// cleanly.
|
|
1642
1508
|
if (s_min == s_max) { return out; }
|
|
1643
1509
|
let det = sAA * sBB - sAB * sAB;
|
|
1644
1510
|
if (abs(det) < h(0.5)) { return out; }
|
|
@@ -1648,12 +1514,39 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1648
1514
|
return out;
|
|
1649
1515
|
}
|
|
1650
1516
|
|
|
1517
|
+
// ISE trit-block encoder: 5 trits \u2192 the 8-bit T field (the inverse of the
|
|
1518
|
+
// spec's trit-block decode; all 243 tuples round-trip \u2014 see the ref tests).
|
|
1519
|
+
fn trit_enc(t0: u32, t1: u32, t2: u32, t3: u32, t4: u32) -> u32 {
|
|
1520
|
+
let c = select(select((t2 << 4u) | (t1 << 2u) | t0, (t1 << 4u) | (t0 << 2u) | 3u, t2 == 2u), 12u | t0, t2 == 2u && t1 == 2u);
|
|
1521
|
+
return select(select((t4 << 7u) | (t3 << 5u) | c, (t3 << 7u) | 96u | c, t4 == 2u), ((c >> 2u) << 5u) | 28u | (c & 3u), t3 == 2u && t4 == 2u);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Nearest QUANT_192 endpoint (one trit + 6 bits) to x \u2208 [0,255]. The 192
|
|
1525
|
+
// unquantised levels are every value \u2264 127 that is not \u2261 3 (mod 4), plus
|
|
1526
|
+
// their mirror images 255 \u2212 u above: lower-half value u = 4q + t (t < 3)
|
|
1527
|
+
// is ISE value trit t, bits q << 1; the upper half sets bit 0 (the spec's
|
|
1528
|
+
// A-mask inversion). A value that falls on an excluded slot moves to the
|
|
1529
|
+
// nearer representable neighbour (a fixed direction instead costs up to
|
|
1530
|
+
// 0.4 dB on smooth content). Returns (ISE value = trit\xB764 + bits, level).
|
|
1531
|
+
fn q192(x: f32) -> vec2<u32> {
|
|
1532
|
+
let v = u32(clamp(floor(x + 0.5), 0.0, 255.0));
|
|
1533
|
+
let up = v > 127u;
|
|
1534
|
+
var u = select(v, 255u - v, up);
|
|
1535
|
+
if ((u & 3u) == 3u) {
|
|
1536
|
+
let xu = select(x, 255.0 - x, up);
|
|
1537
|
+
u = select(u - 1u, u + 1u, xu > f32(u) && u < 127u);
|
|
1538
|
+
}
|
|
1539
|
+
return vec2<u32>(((u & 3u) << 6u) | ((u >> 2u) << 1u) | u32(up), select(u, 255u - u, up));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1651
1542
|
fn q8(e: h4) -> vec4<u32> {
|
|
1652
1543
|
return vec4<u32>(clamp(floor(e * h(255.0) + h(0.5)), h4(0.0), h4(255.0)));
|
|
1653
1544
|
}
|
|
1654
1545
|
|
|
1655
1546
|
@compute @workgroup_size(8, 8, 1)
|
|
1656
|
-
fn encode(@builtin(global_invocation_id)
|
|
1547
|
+
fn encode(@builtin(global_invocation_id) gid_raw: vec3<u32>) {
|
|
1548
|
+
// Row-band encodes dispatch a slice of the block grid starting at row y0.
|
|
1549
|
+
let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);
|
|
1657
1550
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1658
1551
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
1659
1552
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
@@ -1716,20 +1609,136 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1716
1609
|
w1 = (L1 >> 7u) | reverseBits(s2);
|
|
1717
1610
|
w2 = reverseBits(s1);
|
|
1718
1611
|
w3 = reverseBits(s0);
|
|
1719
|
-
} else {
|
|
1720
|
-
// -------------
|
|
1612
|
+
} else if (opaque) {
|
|
1613
|
+
// ------------- Opaque colour: CEM 8, two bit budgets (header) -------------
|
|
1614
|
+
// 3-channel math throughout (alpha is constant 1 here, so a 4-lane
|
|
1615
|
+
// pass would carry a dead lane through the covariance, iteration,
|
|
1616
|
+
// extents and weight loops).
|
|
1617
|
+
//
|
|
1721
1618
|
// Seed endpoints from the block's principal colour axis (covariance
|
|
1722
1619
|
// power-iteration, seeded with the bbox diagonal). The bbox diagonal is
|
|
1723
1620
|
// sign-blind: on anti-correlated channels (normal maps, hue edges) it
|
|
1724
|
-
// points across the data instead of along it
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1621
|
+
// points across the data instead of along it. Deviations are
|
|
1622
|
+
// pre-scaled \xD716 so covariance entries for shallow blocks stay in f16's
|
|
1623
|
+
// normal range (span ~1/255 \u2192 d\xB2 \u2248 1e-3) while full-range sums stay
|
|
1624
|
+
// \u22644096; the iteration renormalises by the max component (a plain
|
|
1625
|
+
// length() of the matvec output could overflow f16), so only the
|
|
1626
|
+
// direction survives.
|
|
1627
|
+
let lo3 = lo.xyz;
|
|
1628
|
+
let hi3 = hi.xyz;
|
|
1629
|
+
let mean3 = mean.xyz;
|
|
1630
|
+
var c0v = h3(0.0);
|
|
1631
|
+
var c1v = h3(0.0);
|
|
1632
|
+
var c2v = h3(0.0);
|
|
1633
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1634
|
+
let d = (pix[k].xyz - mean3) * h(16.0);
|
|
1635
|
+
c0v = c0v + d.x * d;
|
|
1636
|
+
c1v = c1v + d.y * d;
|
|
1637
|
+
c2v = c2v + d.z * d;
|
|
1638
|
+
}
|
|
1639
|
+
var seed_lo = lo3;
|
|
1640
|
+
var seed_hi = hi3;
|
|
1641
|
+
var axis = hi3 - lo3;
|
|
1642
|
+
var axis_ok = true;
|
|
1643
|
+
// 8 iterations: the axis IS the endpoint quality here (no refit).
|
|
1644
|
+
for (var it: u32 = 0u; it < 8u; it = it + 1u) {
|
|
1645
|
+
let nv = h3(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis));
|
|
1646
|
+
let m = max(max(abs(nv.x), abs(nv.y)), abs(nv.z));
|
|
1647
|
+
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1648
|
+
axis = nv / m;
|
|
1649
|
+
}
|
|
1650
|
+
if (axis_ok) {
|
|
1651
|
+
axis = axis / length(axis);
|
|
1652
|
+
var t_min = h(4.0);
|
|
1653
|
+
var t_max = h(-4.0);
|
|
1654
|
+
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1655
|
+
let t = dot(pix[k].xyz - mean3, axis);
|
|
1656
|
+
t_min = min(t_min, t);
|
|
1657
|
+
t_max = max(t_max, t);
|
|
1658
|
+
}
|
|
1659
|
+
seed_lo = mean3 + t_min * axis;
|
|
1660
|
+
seed_hi = mean3 + t_max * axis;
|
|
1661
|
+
}
|
|
1662
|
+
// Endpoints: the PCA extents, bbox-clamped (on multi-cluster blocks the
|
|
1663
|
+
// axis extents overshoot the data per channel and would decode to
|
|
1664
|
+
// colours that exist nowhere in the block).
|
|
1665
|
+
let x0 = vec3<f32>(clamp(seed_lo, lo3, hi3)) * 255.0;
|
|
1666
|
+
let x1 = vec3<f32>(clamp(seed_hi, lo3, hi3)) * 255.0;
|
|
1667
|
+
// Bit budget: span \u2264 12 \u2192 exact QUANT_256 endpoints + 8 weight levels
|
|
1668
|
+
// (mode 0x053), wider \u2192 QUANT_192 endpoints + 16 levels (mode 0x242).
|
|
1669
|
+
let span3 = hi3 - lo3;
|
|
1670
|
+
let small = max(max(span3.x, span3.y), span3.z) <= h(12.0 / 255.0);
|
|
1671
|
+
var r0: vec2<u32>; var g0: vec2<u32>; var b0: vec2<u32>;
|
|
1672
|
+
var r1: vec2<u32>; var g1: vec2<u32>; var b1: vec2<u32>;
|
|
1673
|
+
if (small) {
|
|
1674
|
+
let e0 = vec3<u32>(clamp(floor(x0 + 0.5), vec3<f32>(0.0), vec3<f32>(255.0)));
|
|
1675
|
+
let e1 = vec3<u32>(clamp(floor(x1 + 0.5), vec3<f32>(0.0), vec3<f32>(255.0)));
|
|
1676
|
+
r0 = vec2<u32>(e0.x); g0 = vec2<u32>(e0.y); b0 = vec2<u32>(e0.z);
|
|
1677
|
+
r1 = vec2<u32>(e1.x); g1 = vec2<u32>(e1.y); b1 = vec2<u32>(e1.z);
|
|
1678
|
+
} else {
|
|
1679
|
+
r0 = q192(x0.x); g0 = q192(x0.y); b0 = q192(x0.z);
|
|
1680
|
+
r1 = q192(x1.x); g1 = q192(x1.y); b1 = q192(x1.z);
|
|
1681
|
+
}
|
|
1682
|
+
// Blue-contraction ordering on the UNQUANTISED levels, before the
|
|
1683
|
+
// weight pass so the weights come out oriented (no reflection).
|
|
1684
|
+
if (r0.y + g0.y + b0.y > r1.y + g1.y + b1.y) {
|
|
1685
|
+
let tr = r0; r0 = r1; r1 = tr;
|
|
1686
|
+
let tg = g0; g0 = g1; g1 = tg;
|
|
1687
|
+
let tb = b0; b0 = b1; b1 = tb;
|
|
1688
|
+
}
|
|
1689
|
+
let d0 = h3(vec3<f32>(vec3<u32>(r0.y, g0.y, b0.y))) * h(1.0 / 255.0);
|
|
1690
|
+
let d1 = h3(vec3<f32>(vec3<u32>(r1.y, g1.y, b1.y))) * h(1.0 / 255.0);
|
|
1691
|
+
// Weight pass \u2014 ONE loop for both budgets (mixed warps would otherwise
|
|
1692
|
+
// run two): weights land as 4-bit nibbles, levels = 16 or 8. \xD732
|
|
1693
|
+
// pre-scale as in the other formats (distinct levels are \u22651/255 apart,
|
|
1694
|
+
// so dd\u2083\u2082 \u2265 0.0157 and the flat threshold only catches identical ones).
|
|
1695
|
+
let lmax = select(h(15.0), h(7.0), small);
|
|
1696
|
+
let dir = (d1 - d0) * h(32.0);
|
|
1697
|
+
let dd = dot(dir, dir);
|
|
1698
|
+
var s0 = 0u; var s1 = 0u;
|
|
1699
|
+
if (dd >= h(0.008)) {
|
|
1700
|
+
let inv = lmax * h(32.0) / dd;
|
|
1701
|
+
for (var k: u32 = 0u; k < 8u; k = k + 1u) {
|
|
1702
|
+
let w = u32(clamp(floor(dot(pix[k].xyz - d0, dir) * inv + h(0.5)), h(0.0), lmax));
|
|
1703
|
+
s0 = s0 | (w << (4u * k));
|
|
1704
|
+
}
|
|
1705
|
+
for (var k: u32 = 8u; k < 16u; k = k + 1u) {
|
|
1706
|
+
let w = u32(clamp(floor(dot(pix[k].xyz - d0, dir) * inv + h(0.5)), h(0.0), lmax));
|
|
1707
|
+
s1 = s1 | (w << (4u * (k - 8u)));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (small) {
|
|
1711
|
+
// Mode 0x053: endpoints as plain bytes from bit 17; the 3-bit weight
|
|
1712
|
+
// stream (48 bits) is the nibbles compacted: 8 nibbles \u2192 24 bits.
|
|
1713
|
+
var c0 = (s0 & 0x07070707u) | ((s0 & 0x70707070u) >> 1u);
|
|
1714
|
+
c0 = (c0 & 0x003F003Fu) | ((c0 & 0x3F003F00u) >> 2u);
|
|
1715
|
+
c0 = (c0 & 0x00000FFFu) | ((c0 & 0x0FFF0000u) >> 4u);
|
|
1716
|
+
var c1 = (s1 & 0x07070707u) | ((s1 & 0x70707070u) >> 1u);
|
|
1717
|
+
c1 = (c1 & 0x003F003Fu) | ((c1 & 0x3F003F00u) >> 2u);
|
|
1718
|
+
c1 = (c1 & 0x00000FFFu) | ((c1 & 0x0FFF0000u) >> 4u);
|
|
1719
|
+
w0 = 0x053u | (8u << 13u) | (r0.x << 17u) | (r1.x << 25u);
|
|
1720
|
+
w1 = (r1.x >> 7u) | (g0.x << 1u) | (g1.x << 9u) | (b0.x << 17u) | (b1.x << 25u);
|
|
1721
|
+
w2 = (b1.x >> 7u) | reverseBits(c1 >> 8u);
|
|
1722
|
+
w3 = reverseBits(c0 | (c1 << 24u));
|
|
1723
|
+
} else {
|
|
1724
|
+
// Mode 0x242, CEM 8 @13. Endpoint ISE (v0..v5 = R0 R1 G0 G1 B0 B1,
|
|
1725
|
+
// 46 bits from bit 17): group 1 = v0..v4 with trit field T, group 2 =
|
|
1726
|
+
// v5 with its lone trit as 2 bits (T of (t5,0,0,0,0) is t5 itself).
|
|
1727
|
+
// Weight stream bit q = 4k + j lives at block bit 127 \u2212 q.
|
|
1728
|
+
let tg = trit_enc(r0.x >> 6u, r1.x >> 6u, g0.x >> 6u, g1.x >> 6u, b0.x >> 6u);
|
|
1729
|
+
w0 = 0x242u | (8u << 13u) | ((r0.x & 63u) << 17u) | ((tg & 3u) << 23u) | ((r1.x & 63u) << 25u) | (((tg >> 2u) & 1u) << 31u);
|
|
1730
|
+
w1 = ((tg >> 3u) & 1u) | ((g0.x & 63u) << 1u) | (((tg >> 4u) & 1u) << 7u) | ((g1.x & 63u) << 8u)
|
|
1731
|
+
| (((tg >> 5u) & 3u) << 14u) | ((b0.x & 63u) << 16u) | ((tg >> 7u) << 22u) | ((b1.x & 63u) << 23u)
|
|
1732
|
+
| ((b1.x >> 6u) << 29u);
|
|
1733
|
+
w2 = reverseBits(s1);
|
|
1734
|
+
w3 = reverseBits(s0);
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
// ------ Translucent: CEM 12, 2-bit weights, PCA seed + LSQ refit ------
|
|
1738
|
+
// Seed from the principal RGBA axis (covariance power iteration, bbox
|
|
1739
|
+
// diagonal start \u2014 sign-blind on anti-correlated channels, hence the
|
|
1740
|
+
// iteration). Deviations pre-scaled \xD716 (f16 normal range for shallow
|
|
1741
|
+
// blocks, full-range sums \u22644096); renormalised by the max component.
|
|
1733
1742
|
var c0v = h4(0.0);
|
|
1734
1743
|
var c1v = h4(0.0);
|
|
1735
1744
|
var c2v = h4(0.0);
|
|
@@ -1741,29 +1750,18 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1741
1750
|
c2v = c2v + d.z * d;
|
|
1742
1751
|
c3v = c3v + d.w * d;
|
|
1743
1752
|
}
|
|
1753
|
+
var seed_lo = lo;
|
|
1754
|
+
var seed_hi = hi;
|
|
1744
1755
|
var axis = hi - lo;
|
|
1745
1756
|
var axis_ok = true;
|
|
1746
|
-
// 4
|
|
1747
|
-
//
|
|
1748
|
-
// unroll. Opaque blocks need the converged axis (it IS the endpoint
|
|
1749
|
-
// quality there; 4 was under-converged on noisy 4-D content), while
|
|
1750
|
-
// translucent blocks' LSQ refit absorbs residual axis error \u2014 their
|
|
1751
|
-
// extra 4 steps measured exactly 0.000 dB on the alpha card for
|
|
1752
|
-
// ~5% GPU.
|
|
1757
|
+
// 4 iterations: the refit absorbs residual axis error (4 more measured
|
|
1758
|
+
// 0.000 dB on the alpha card).
|
|
1753
1759
|
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1754
1760
|
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1755
1761
|
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1756
1762
|
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1757
1763
|
axis = nv / m;
|
|
1758
1764
|
}
|
|
1759
|
-
if (axis_ok && opaque) {
|
|
1760
|
-
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1761
|
-
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1762
|
-
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1763
|
-
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1764
|
-
axis = nv / m;
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
1765
|
if (axis_ok) {
|
|
1768
1766
|
axis = axis / length(axis);
|
|
1769
1767
|
var t_min = h(4.0);
|
|
@@ -1776,100 +1774,52 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1776
1774
|
seed_lo = clamp(mean + t_min * axis, h4(0.0), h4(1.0));
|
|
1777
1775
|
seed_hi = clamp(mean + t_max * axis, h4(0.0), h4(1.0));
|
|
1778
1776
|
}
|
|
1779
|
-
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
// +26% GPU for \u22120.06..+0.31 dB against the converged 8-step axis
|
|
1783
|
-
// (/ab + PSNR A/B, 2026-07): at 8 weight levels the projection is fine
|
|
1784
|
-
// enough that a good axis, not a refit, carries the quality.
|
|
1785
|
-
// TRANSLUCENT blocks (CEM 12) keep the fit: 4 levels are coarse enough
|
|
1786
|
-
// that dropping it costs 0.5+ dB. Its result is clamped to the block
|
|
1787
|
-
// bbox: on multi-cluster blocks the unconstrained solve extrapolates
|
|
1788
|
-
// far outside the block's colours and the per-channel [0,1] clamp then
|
|
1789
|
-
// bends the hue \u2014 fringe pixels decode to colours that exist nowhere
|
|
1790
|
-
// in the block (and the bbox constraint also measures better in plain
|
|
1791
|
-
// SSE, +1.8 dB on the colour test card).
|
|
1777
|
+
// Refit result clamped to the block bbox: on multi-cluster blocks the
|
|
1778
|
+
// unconstrained solve extrapolates outside the block's colours and the
|
|
1779
|
+
// per-channel [0,1] clamp would bend the hue.
|
|
1792
1780
|
var e0 = lo;
|
|
1793
1781
|
var e1 = hi;
|
|
1794
1782
|
var fitStream = 0u;
|
|
1795
1783
|
var haveFitWeights = false;
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
e1 = clamp(seed_hi, lo, hi);
|
|
1803
|
-
} else {
|
|
1804
|
-
let r = proj_fit(&pix, seed_lo, seed_hi);
|
|
1805
|
-
if (r.valid) {
|
|
1806
|
-
e0 = clamp(r.e0, lo, hi);
|
|
1807
|
-
e1 = clamp(r.e1, lo, hi);
|
|
1808
|
-
fitStream = r.wstream;
|
|
1809
|
-
haveFitWeights = true;
|
|
1810
|
-
}
|
|
1784
|
+
let r = proj_fit(&pix, seed_lo, seed_hi);
|
|
1785
|
+
if (r.valid) {
|
|
1786
|
+
e0 = clamp(r.e0, lo, hi);
|
|
1787
|
+
e1 = clamp(r.e1, lo, hi);
|
|
1788
|
+
fitStream = r.wstream;
|
|
1789
|
+
haveFitWeights = true;
|
|
1811
1790
|
}
|
|
1812
1791
|
var E0 = q8(e0);
|
|
1813
1792
|
var E1 = q8(e1);
|
|
1814
|
-
|
|
1815
|
-
// Blue-contraction ordering, applied before the weight pass so weights
|
|
1816
|
-
// are already oriented (no reflection needed).
|
|
1817
1793
|
var swapped = false;
|
|
1818
1794
|
if (E0.x + E0.y + E0.z > E1.x + E1.y + E1.z) {
|
|
1819
1795
|
let t = E0; E0 = E1; E1 = t;
|
|
1820
1796
|
swapped = true;
|
|
1821
1797
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
let dd = dot(dir, dir);
|
|
1830
|
-
if (opaque) {
|
|
1831
|
-
// CEM 8: 3-bit weights, stream bit q = 3k.
|
|
1832
|
-
var s0 = 0u; var s1 = 0u;
|
|
1833
|
-
if (dd >= h(0.008)) {
|
|
1834
|
-
let inv = h(224.0) / dd;
|
|
1835
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1836
|
-
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(7.0)));
|
|
1837
|
-
let off = 3u * k;
|
|
1838
|
-
if (off < 30u) { s0 = s0 | (w << off); }
|
|
1839
|
-
else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }
|
|
1840
|
-
else { s1 = s1 | (w << (off - 32u)); }
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
// Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.
|
|
1844
|
-
w0 = 0x053u | (8u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1845
|
-
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1846
|
-
w2 = (E1.z >> 7u) | reverseBits(s1);
|
|
1847
|
-
w3 = reverseBits(s0);
|
|
1798
|
+
// Valid fits ship the FIT-PASS weights instead of reprojecting (a whole
|
|
1799
|
+
// 16-pixel pass for \u22120.09 dB on the alpha card); the swap is a full
|
|
1800
|
+
// reflection w \u2192 3 \u2212 w, i.e. bitwise NOT of the stream. Degenerate fits
|
|
1801
|
+
// reproject against the bbox endpoints.
|
|
1802
|
+
var s0 = 0u;
|
|
1803
|
+
if (haveFitWeights) {
|
|
1804
|
+
s0 = select(fitStream, ~fitStream, swapped);
|
|
1848
1805
|
} else {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
// pay it). The blue-contraction swap is a full reflection w \u2192 3\u2212w,
|
|
1855
|
-
// i.e. bitwise NOT of the packed stream. Invalid fits (rank-1 /
|
|
1856
|
-
// degenerate) fall back to reprojection against the bbox endpoints.
|
|
1857
|
-
var s0 = 0u;
|
|
1858
|
-
if (haveFitWeights) {
|
|
1859
|
-
s0 = select(fitStream, ~fitStream, swapped);
|
|
1860
|
-
} else if (dd >= h(0.008)) {
|
|
1806
|
+
let d0 = h4(vec4<f32>(E0)) * h(1.0 / 255.0);
|
|
1807
|
+
let d1 = h4(vec4<f32>(E1)) * h(1.0 / 255.0);
|
|
1808
|
+
let dir = (d1 - d0) * h(32.0);
|
|
1809
|
+
let dd = dot(dir, dir);
|
|
1810
|
+
if (dd >= h(0.008)) {
|
|
1861
1811
|
let inv = h(96.0) / dd;
|
|
1862
1812
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1863
1813
|
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
1864
1814
|
s0 = s0 | (w << (2u * k));
|
|
1865
1815
|
}
|
|
1866
1816
|
}
|
|
1867
|
-
// Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.
|
|
1868
|
-
w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1869
|
-
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1870
|
-
w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);
|
|
1871
|
-
w3 = reverseBits(s0);
|
|
1872
1817
|
}
|
|
1818
|
+
// Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.
|
|
1819
|
+
w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1820
|
+
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1821
|
+
w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);
|
|
1822
|
+
w3 = reverseBits(s0);
|
|
1873
1823
|
}
|
|
1874
1824
|
|
|
1875
1825
|
let o = bi * 4u;
|
|
@@ -1902,10 +1852,10 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1902
1852
|
};
|
|
1903
1853
|
|
|
1904
1854
|
// src/etc2.wgsl
|
|
1905
|
-
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";
|
|
1855
|
+
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 y0: u32, // first block row of this dispatch (row-band encodes)\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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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";
|
|
1906
1856
|
|
|
1907
1857
|
// src/etc2_fast_f16.wgsl
|
|
1908
|
-
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";
|
|
1858
|
+
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 y0: u32, // first block row of this dispatch (row-band encodes)\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_raw: vec3<u32>) {\n // Row-band encodes dispatch a slice of the block grid starting at row y0.\n let gid = vec3<u32>(gid_raw.x, gid_raw.y + params.y0, gid_raw.z);\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";
|
|
1909
1859
|
|
|
1910
1860
|
// src/ETC2Encoder.ts
|
|
1911
1861
|
var ETC2Encoder = class extends Encoder {
|
|
@@ -2492,6 +2442,18 @@ async function generateGpuMipChain(device, source, { flipY = false } = {}) {
|
|
|
2492
2442
|
}
|
|
2493
2443
|
|
|
2494
2444
|
// src/svg.ts
|
|
2445
|
+
function isSvgMarkup(source) {
|
|
2446
|
+
return source.trimStart().startsWith("<");
|
|
2447
|
+
}
|
|
2448
|
+
function hasSvgExtension(url) {
|
|
2449
|
+
return /\.svg$/i.test(url.split(/[?#]/, 1)[0]);
|
|
2450
|
+
}
|
|
2451
|
+
function isSvgBlob(blob) {
|
|
2452
|
+
if (blob.type) {
|
|
2453
|
+
return blob.type.split(";", 1)[0].trim().toLowerCase() === "image/svg+xml";
|
|
2454
|
+
}
|
|
2455
|
+
return typeof File !== "undefined" && blob instanceof File && hasSvgExtension(blob.name);
|
|
2456
|
+
}
|
|
2495
2457
|
var ROOT_TAG_RE = /<svg(?=[\s/>])[^>]*>/;
|
|
2496
2458
|
function getAttr(tag, name) {
|
|
2497
2459
|
const m = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(tag);
|
|
@@ -2612,6 +2574,482 @@ async function rasterizeSvg(source, options = {}) {
|
|
|
2612
2574
|
URL.revokeObjectURL(url);
|
|
2613
2575
|
}
|
|
2614
2576
|
}
|
|
2577
|
+
|
|
2578
|
+
// src/transcodeCache.ts
|
|
2579
|
+
var DEFAULT_LIMIT = 256 * 1024 * 1024;
|
|
2580
|
+
var maxBytes = DEFAULT_LIMIT;
|
|
2581
|
+
var totalBytes = 0;
|
|
2582
|
+
var entries = /* @__PURE__ */ new Map();
|
|
2583
|
+
async function sha256Hex(data) {
|
|
2584
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
2585
|
+
let hex = "";
|
|
2586
|
+
for (const b of new Uint8Array(digest)) hex += b.toString(16).padStart(2, "0");
|
|
2587
|
+
return hex;
|
|
2588
|
+
}
|
|
2589
|
+
async function sourceIdentity(source, cacheKey) {
|
|
2590
|
+
if (cacheKey) return `k:${cacheKey}`;
|
|
2591
|
+
const canHash = typeof crypto !== "undefined" && !!crypto.subtle;
|
|
2592
|
+
if (typeof source === "string") {
|
|
2593
|
+
if (source.length > 1024 || /^data:/i.test(source) || /^\s*</.test(source)) {
|
|
2594
|
+
return canHash ? `s:${await sha256Hex(new TextEncoder().encode(source))}` : null;
|
|
2595
|
+
}
|
|
2596
|
+
return `u:${source}`;
|
|
2597
|
+
}
|
|
2598
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
2599
|
+
return canHash ? `b:${await sha256Hex(await source.arrayBuffer())}` : null;
|
|
2600
|
+
}
|
|
2601
|
+
return null;
|
|
2602
|
+
}
|
|
2603
|
+
async function buildTranscodeKey(source, cacheKey, fp) {
|
|
2604
|
+
if (maxBytes <= 0) return null;
|
|
2605
|
+
const id = await sourceIdentity(source, cacheKey);
|
|
2606
|
+
if (!id) return null;
|
|
2607
|
+
const fingerprint = [
|
|
2608
|
+
fp.format,
|
|
2609
|
+
fp.colorSpace,
|
|
2610
|
+
fp.flipY ? "flip" : "noflip",
|
|
2611
|
+
fp.mipmaps ? "mips" : "nomips",
|
|
2612
|
+
fp.svgSize === void 0 ? "" : JSON.stringify(fp.svgSize)
|
|
2613
|
+
].join("|");
|
|
2614
|
+
return `${fingerprint}\0${id}`;
|
|
2615
|
+
}
|
|
2616
|
+
function readTranscodeCache(key) {
|
|
2617
|
+
const hit = entries.get(key);
|
|
2618
|
+
if (!hit) return null;
|
|
2619
|
+
entries.delete(key);
|
|
2620
|
+
entries.set(key, hit);
|
|
2621
|
+
return hit.entry;
|
|
2622
|
+
}
|
|
2623
|
+
function writeTranscodeCache(key, entry) {
|
|
2624
|
+
const bytes = entry.levels.reduce((sum, l) => sum + l.data.byteLength, 0);
|
|
2625
|
+
if (bytes > maxBytes) return;
|
|
2626
|
+
const prev = entries.get(key);
|
|
2627
|
+
if (prev) {
|
|
2628
|
+
totalBytes -= prev.bytes;
|
|
2629
|
+
entries.delete(key);
|
|
2630
|
+
}
|
|
2631
|
+
entries.set(key, { entry, bytes });
|
|
2632
|
+
totalBytes += bytes;
|
|
2633
|
+
evictToLimit();
|
|
2634
|
+
}
|
|
2635
|
+
function evictToLimit() {
|
|
2636
|
+
for (const [key, value] of entries) {
|
|
2637
|
+
if (totalBytes <= maxBytes) break;
|
|
2638
|
+
entries.delete(key);
|
|
2639
|
+
totalBytes -= value.bytes;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
function setTranscodeCacheLimit(bytes) {
|
|
2643
|
+
maxBytes = Math.max(0, bytes);
|
|
2644
|
+
evictToLimit();
|
|
2645
|
+
}
|
|
2646
|
+
function clearTranscodeCache() {
|
|
2647
|
+
entries.clear();
|
|
2648
|
+
totalBytes = 0;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/compressTexture.ts
|
|
2652
|
+
var sharedGpuPromise = null;
|
|
2653
|
+
var SHARED_DEVICE_FEATURES = [
|
|
2654
|
+
"texture-compression-bc",
|
|
2655
|
+
"texture-compression-astc",
|
|
2656
|
+
"texture-compression-etc2",
|
|
2657
|
+
"shader-f16",
|
|
2658
|
+
"timestamp-query"
|
|
2659
|
+
];
|
|
2660
|
+
async function createSharedGpu() {
|
|
2661
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
2662
|
+
if (!adapter) return null;
|
|
2663
|
+
const requiredFeatures = SHARED_DEVICE_FEATURES.filter((f) => adapter.features.has(f));
|
|
2664
|
+
const device = await adapter.requestDevice({ requiredFeatures });
|
|
2665
|
+
return { adapter, device, encoders: /* @__PURE__ */ new Map() };
|
|
2666
|
+
}
|
|
2667
|
+
function getSharedGpu() {
|
|
2668
|
+
if (!sharedGpuPromise) {
|
|
2669
|
+
const p = createSharedGpu();
|
|
2670
|
+
sharedGpuPromise = p;
|
|
2671
|
+
p.then((shared) => {
|
|
2672
|
+
if (!shared) return;
|
|
2673
|
+
void shared.device.lost.then(() => {
|
|
2674
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2675
|
+
shared.encoders.clear();
|
|
2676
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2677
|
+
});
|
|
2678
|
+
}).catch(() => {
|
|
2679
|
+
if (sharedGpuPromise === p) sharedGpuPromise = null;
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
return sharedGpuPromise;
|
|
2683
|
+
}
|
|
2684
|
+
function releaseSharedGpuResources() {
|
|
2685
|
+
const p = sharedGpuPromise;
|
|
2686
|
+
sharedGpuPromise = null;
|
|
2687
|
+
void p?.then((shared) => {
|
|
2688
|
+
if (!shared) return;
|
|
2689
|
+
shared.encoders.forEach((encoder) => encoder.destroy());
|
|
2690
|
+
shared.encoders.clear();
|
|
2691
|
+
shared.device.destroy();
|
|
2692
|
+
}).catch(() => {
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
async function sourceToBitmap(source, svgSize) {
|
|
2696
|
+
const opts = {
|
|
2697
|
+
colorSpaceConversion: "none",
|
|
2698
|
+
premultiplyAlpha: "none"
|
|
2699
|
+
};
|
|
2700
|
+
if (typeof source === "string") {
|
|
2701
|
+
if (isSvgMarkup(source)) {
|
|
2702
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
2703
|
+
}
|
|
2704
|
+
if (/^data:/i.test(source)) {
|
|
2705
|
+
const blob2 = dataUrlToBlob(source);
|
|
2706
|
+
if (isSvgBlob(blob2)) {
|
|
2707
|
+
return rasterizeSvg(blob2, { size: svgSize });
|
|
2708
|
+
}
|
|
2709
|
+
return createImageBitmap(blob2, opts);
|
|
2710
|
+
}
|
|
2711
|
+
const resp = await fetch(source);
|
|
2712
|
+
if (!resp.ok) {
|
|
2713
|
+
throw new Error(`compressTexture: fetch ${source} failed (${resp.status})`);
|
|
2714
|
+
}
|
|
2715
|
+
const blob = await resp.blob();
|
|
2716
|
+
if (isSvgBlob(blob) || !isImageMimeType(blob.type) && hasSvgExtension(source)) {
|
|
2717
|
+
return rasterizeSvg(blob, { size: svgSize });
|
|
2718
|
+
}
|
|
2719
|
+
return createImageBitmap(blob, opts);
|
|
2720
|
+
}
|
|
2721
|
+
if (source instanceof Blob) {
|
|
2722
|
+
if (isSvgBlob(source)) {
|
|
2723
|
+
return rasterizeSvg(source, { size: svgSize });
|
|
2724
|
+
}
|
|
2725
|
+
return createImageBitmap(source, opts);
|
|
2726
|
+
}
|
|
2727
|
+
if (source instanceof ImageBitmap) {
|
|
2728
|
+
return source;
|
|
2729
|
+
}
|
|
2730
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
2731
|
+
const src = source.currentSrc || source.src;
|
|
2732
|
+
if (src && (hasSvgExtension(src) || /^data:image\/svg\+xml/i.test(src))) {
|
|
2733
|
+
const resp = await fetch(src);
|
|
2734
|
+
if (!resp.ok) {
|
|
2735
|
+
throw new Error(`compressTexture: fetch ${src} failed (${resp.status})`);
|
|
2736
|
+
}
|
|
2737
|
+
return rasterizeSvg(await resp.blob(), { size: svgSize });
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return createImageBitmap(source, opts);
|
|
2741
|
+
}
|
|
2742
|
+
function isImageMimeType(type) {
|
|
2743
|
+
return /^image\//i.test(type) && !/svg/i.test(type);
|
|
2744
|
+
}
|
|
2745
|
+
function dataUrlToBlob(url) {
|
|
2746
|
+
const comma = url.indexOf(",");
|
|
2747
|
+
if (comma < 0) {
|
|
2748
|
+
throw new Error("compressTexture: malformed data: URL (no comma)");
|
|
2749
|
+
}
|
|
2750
|
+
const header = url.slice(5, comma);
|
|
2751
|
+
const isBase64 = /;base64$/i.test(header);
|
|
2752
|
+
const type = header.replace(/;base64$/i, "");
|
|
2753
|
+
if (!isBase64) {
|
|
2754
|
+
return new Blob([decodeURIComponent(url.slice(comma + 1))], { type });
|
|
2755
|
+
}
|
|
2756
|
+
const payload = url.slice(comma + 1);
|
|
2757
|
+
const fromBase64 = Uint8Array.fromBase64;
|
|
2758
|
+
if (fromBase64) {
|
|
2759
|
+
return new Blob([fromBase64(payload)], { type });
|
|
2760
|
+
}
|
|
2761
|
+
const bin = atob(payload);
|
|
2762
|
+
const bytes = new Uint8Array(bin.length);
|
|
2763
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2764
|
+
return new Blob([bytes], { type });
|
|
2765
|
+
}
|
|
2766
|
+
function bitmapToMipLevel(bitmap, flipY) {
|
|
2767
|
+
const w = bitmap.width, h = bitmap.height;
|
|
2768
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
|
|
2769
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
2770
|
+
if (!ctx) {
|
|
2771
|
+
throw new Error("compressTexture: no 2D context available for mip generation");
|
|
2772
|
+
}
|
|
2773
|
+
if (flipY) {
|
|
2774
|
+
ctx.translate(0, h);
|
|
2775
|
+
ctx.scale(1, -1);
|
|
2776
|
+
}
|
|
2777
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
2778
|
+
const imageData = ctx.getImageData(0, 0, w, h);
|
|
2779
|
+
return { data: imageData.data, width: w, height: h };
|
|
2780
|
+
}
|
|
2781
|
+
function mipLevelToImageData(level) {
|
|
2782
|
+
return new ImageData(level.data, level.width, level.height);
|
|
2783
|
+
}
|
|
2784
|
+
async function compressTextureToBytes(source, options = {}) {
|
|
2785
|
+
const {
|
|
2786
|
+
hint = "color",
|
|
2787
|
+
preferredFormat,
|
|
2788
|
+
quality = "high",
|
|
2789
|
+
colorSpace = "srgb",
|
|
2790
|
+
svgSize,
|
|
2791
|
+
flipY = true,
|
|
2792
|
+
mipmaps = false,
|
|
2793
|
+
cache = false,
|
|
2794
|
+
cacheKey,
|
|
2795
|
+
device: providedDevice,
|
|
2796
|
+
adapter: providedAdapter
|
|
2797
|
+
} = options;
|
|
2798
|
+
const t0 = performance.now();
|
|
2799
|
+
const gpu = await resolveWebGPU();
|
|
2800
|
+
const gl = gpu ? null : resolveWebGL();
|
|
2801
|
+
const activeFormat = gpu?.selection.format ?? gl?.selection.format ?? null;
|
|
2802
|
+
let transcodeKey = null;
|
|
2803
|
+
if (cache && activeFormat) {
|
|
2804
|
+
transcodeKey = await buildTranscodeKey(source, cacheKey, {
|
|
2805
|
+
format: activeFormat,
|
|
2806
|
+
colorSpace,
|
|
2807
|
+
flipY,
|
|
2808
|
+
mipmaps,
|
|
2809
|
+
svgSize
|
|
2810
|
+
});
|
|
2811
|
+
if (transcodeKey) {
|
|
2812
|
+
const hit = readTranscodeCache(transcodeKey);
|
|
2813
|
+
if (hit) {
|
|
2814
|
+
return {
|
|
2815
|
+
levels: hit.levels,
|
|
2816
|
+
fallbackBitmap: null,
|
|
2817
|
+
format: hit.format,
|
|
2818
|
+
fallbackUncompressed: false,
|
|
2819
|
+
backend: gpu ? "webgpu" : "webgl",
|
|
2820
|
+
astcNormalRemap: (gpu ?? gl).selection.astcNormalRemap,
|
|
2821
|
+
width: hit.width,
|
|
2822
|
+
height: hit.height,
|
|
2823
|
+
mipLevels: hit.levels.length,
|
|
2824
|
+
encodeMs: 0,
|
|
2825
|
+
decodeMs: 0,
|
|
2826
|
+
totalMs: performance.now() - t0,
|
|
2827
|
+
cacheHit: true
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
const tDecode = performance.now();
|
|
2833
|
+
const bitmap = await sourceToBitmap(source, svgSize);
|
|
2834
|
+
const decodeMs = performance.now() - tDecode;
|
|
2835
|
+
if (gpu) return encodeViaWebGPU(gpu);
|
|
2836
|
+
const viaWebGL = gl ? encodeViaWebGL(gl) : null;
|
|
2837
|
+
if (viaWebGL) return viaWebGL;
|
|
2838
|
+
console.warn(
|
|
2839
|
+
"[compressTextureToBytes] No compressed path available (WebGPU and WebGL2 both lack a usable compressed-texture format); returning uncompressed RGBA8."
|
|
2840
|
+
);
|
|
2841
|
+
return {
|
|
2842
|
+
levels: null,
|
|
2843
|
+
fallbackBitmap: bitmap,
|
|
2844
|
+
format: null,
|
|
2845
|
+
fallbackUncompressed: true,
|
|
2846
|
+
backend: "none",
|
|
2847
|
+
astcNormalRemap: false,
|
|
2848
|
+
width: bitmap.width,
|
|
2849
|
+
height: bitmap.height,
|
|
2850
|
+
mipLevels: 1,
|
|
2851
|
+
encodeMs: 0,
|
|
2852
|
+
decodeMs,
|
|
2853
|
+
totalMs: performance.now() - t0,
|
|
2854
|
+
cacheHit: false
|
|
2855
|
+
};
|
|
2856
|
+
async function resolveWebGPU() {
|
|
2857
|
+
if (!("gpu" in navigator)) return null;
|
|
2858
|
+
let shared = null;
|
|
2859
|
+
let adapter;
|
|
2860
|
+
if (providedAdapter) {
|
|
2861
|
+
adapter = providedAdapter;
|
|
2862
|
+
} else if (providedDevice) {
|
|
2863
|
+
adapter = await navigator.gpu.requestAdapter();
|
|
2864
|
+
} else {
|
|
2865
|
+
shared = await getSharedGpu();
|
|
2866
|
+
adapter = shared?.adapter ?? null;
|
|
2867
|
+
}
|
|
2868
|
+
if (!adapter) return null;
|
|
2869
|
+
const selection = selectFormat(adapter, hint, { colorSpace, preferredFormat, quality });
|
|
2870
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
2871
|
+
return {
|
|
2872
|
+
adapter,
|
|
2873
|
+
shared,
|
|
2874
|
+
selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass }
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
async function encodeViaWebGPU({ adapter, shared, selection }) {
|
|
2878
|
+
const EncoderCtor = selection.encoderClass;
|
|
2879
|
+
let encoder;
|
|
2880
|
+
let sharedEncoder = false;
|
|
2881
|
+
if (providedDevice) {
|
|
2882
|
+
encoder = new EncoderCtor({ device: providedDevice, adapter, ownsDevice: false });
|
|
2883
|
+
} else if (shared) {
|
|
2884
|
+
sharedEncoder = true;
|
|
2885
|
+
let cached = shared.encoders.get(EncoderCtor);
|
|
2886
|
+
if (!cached) {
|
|
2887
|
+
cached = new EncoderCtor({ device: shared.device, adapter: shared.adapter, ownsDevice: false });
|
|
2888
|
+
shared.encoders.set(EncoderCtor, cached);
|
|
2889
|
+
}
|
|
2890
|
+
encoder = cached;
|
|
2891
|
+
} else {
|
|
2892
|
+
encoder = await EncoderCtor.create();
|
|
2893
|
+
}
|
|
2894
|
+
const destroyEncoder = sharedEncoder ? () => {
|
|
2895
|
+
} : () => encoder.destroy();
|
|
2896
|
+
try {
|
|
2897
|
+
const needsWriteTexture = needsWriteTextureWorkaround(adapter);
|
|
2898
|
+
if (!mipmaps) {
|
|
2899
|
+
let bytes;
|
|
2900
|
+
if (needsWriteTexture) {
|
|
2901
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
2902
|
+
const imageData = mipLevelToImageData(level0);
|
|
2903
|
+
bytes = await encoder.encodeToBytes(imageData);
|
|
2904
|
+
} else {
|
|
2905
|
+
bytes = await encoder.encodeToBytes(bitmap, { flipY });
|
|
2906
|
+
}
|
|
2907
|
+
if (transcodeKey) {
|
|
2908
|
+
writeTranscodeCache(transcodeKey, {
|
|
2909
|
+
format: selection.format,
|
|
2910
|
+
width: bytes.width,
|
|
2911
|
+
height: bytes.height,
|
|
2912
|
+
levels: [bytes]
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
destroyEncoder();
|
|
2916
|
+
return {
|
|
2917
|
+
levels: [bytes],
|
|
2918
|
+
fallbackBitmap: null,
|
|
2919
|
+
format: selection.format,
|
|
2920
|
+
fallbackUncompressed: false,
|
|
2921
|
+
backend: "webgpu",
|
|
2922
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
2923
|
+
width: bytes.width,
|
|
2924
|
+
height: bytes.height,
|
|
2925
|
+
mipLevels: 1,
|
|
2926
|
+
encodeMs: bytes.encodeMs,
|
|
2927
|
+
decodeMs,
|
|
2928
|
+
totalMs: performance.now() - t0,
|
|
2929
|
+
cacheHit: false
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
let chainResult;
|
|
2933
|
+
if (needsWriteTexture) {
|
|
2934
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
2935
|
+
chainResult = await encoder.encodeMipChainToBytes(generateMipChain(level0).map(padToBlockMultiple));
|
|
2936
|
+
} else {
|
|
2937
|
+
const chainTex = await generateGpuMipChain(encoder.device, bitmap, { flipY });
|
|
2938
|
+
try {
|
|
2939
|
+
chainResult = await encoder.encodeMipChainFromTexture(chainTex);
|
|
2940
|
+
} finally {
|
|
2941
|
+
chainTex.destroy();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const { levels, encodeMs } = chainResult;
|
|
2945
|
+
if (transcodeKey) {
|
|
2946
|
+
writeTranscodeCache(transcodeKey, {
|
|
2947
|
+
format: selection.format,
|
|
2948
|
+
width: bitmap.width,
|
|
2949
|
+
height: bitmap.height,
|
|
2950
|
+
levels
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
destroyEncoder();
|
|
2954
|
+
return {
|
|
2955
|
+
levels,
|
|
2956
|
+
fallbackBitmap: null,
|
|
2957
|
+
format: selection.format,
|
|
2958
|
+
fallbackUncompressed: false,
|
|
2959
|
+
backend: "webgpu",
|
|
2960
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
2961
|
+
width: bitmap.width,
|
|
2962
|
+
height: bitmap.height,
|
|
2963
|
+
mipLevels: levels.length,
|
|
2964
|
+
encodeMs,
|
|
2965
|
+
decodeMs,
|
|
2966
|
+
totalMs: performance.now() - t0,
|
|
2967
|
+
cacheHit: false
|
|
2968
|
+
};
|
|
2969
|
+
} catch (e) {
|
|
2970
|
+
destroyEncoder();
|
|
2971
|
+
throw e;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function resolveWebGL() {
|
|
2975
|
+
const gl2 = getSharedWebGLContext();
|
|
2976
|
+
if (!gl2) return null;
|
|
2977
|
+
const caps = detectWebGLCapabilities(gl2);
|
|
2978
|
+
const selection = selectWebGLFormat(caps, hint, { colorSpace, preferredFormat, quality });
|
|
2979
|
+
if (!selection.format || !selection.encoderClass) return null;
|
|
2980
|
+
return { gl: gl2, selection: { ...selection, format: selection.format, encoderClass: selection.encoderClass } };
|
|
2981
|
+
}
|
|
2982
|
+
function encodeViaWebGL({ gl: gl2, selection }) {
|
|
2983
|
+
const encoder = selection.encoderClass.create(gl2);
|
|
2984
|
+
try {
|
|
2985
|
+
if (!mipmaps) {
|
|
2986
|
+
const bytes = encoder.encodeToBytes(bitmap, { flipY });
|
|
2987
|
+
if (transcodeKey) {
|
|
2988
|
+
writeTranscodeCache(transcodeKey, {
|
|
2989
|
+
format: selection.format,
|
|
2990
|
+
width: bytes.width,
|
|
2991
|
+
height: bytes.height,
|
|
2992
|
+
levels: [bytes]
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
encoder.destroy();
|
|
2996
|
+
return {
|
|
2997
|
+
levels: [bytes],
|
|
2998
|
+
fallbackBitmap: null,
|
|
2999
|
+
format: selection.format,
|
|
3000
|
+
fallbackUncompressed: false,
|
|
3001
|
+
backend: "webgl",
|
|
3002
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
3003
|
+
width: bytes.width,
|
|
3004
|
+
height: bytes.height,
|
|
3005
|
+
mipLevels: 1,
|
|
3006
|
+
encodeMs: bytes.encodeMs,
|
|
3007
|
+
decodeMs,
|
|
3008
|
+
totalMs: performance.now() - t0,
|
|
3009
|
+
cacheHit: false
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
const level0 = bitmapToMipLevel(bitmap, flipY);
|
|
3013
|
+
const chain = generateMipChain(level0);
|
|
3014
|
+
const encodedLevels = [];
|
|
3015
|
+
let totalEncodeMs = 0;
|
|
3016
|
+
for (const level of chain) {
|
|
3017
|
+
const padded = padToBlockMultiple(level);
|
|
3018
|
+
const bytes = encoder.encodeToBytes(padded);
|
|
3019
|
+
encodedLevels.push(bytes);
|
|
3020
|
+
totalEncodeMs += bytes.encodeMs;
|
|
3021
|
+
}
|
|
3022
|
+
if (transcodeKey) {
|
|
3023
|
+
writeTranscodeCache(transcodeKey, {
|
|
3024
|
+
format: selection.format,
|
|
3025
|
+
width: level0.width,
|
|
3026
|
+
height: level0.height,
|
|
3027
|
+
levels: encodedLevels
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
encoder.destroy();
|
|
3031
|
+
return {
|
|
3032
|
+
levels: encodedLevels,
|
|
3033
|
+
fallbackBitmap: null,
|
|
3034
|
+
format: selection.format,
|
|
3035
|
+
fallbackUncompressed: false,
|
|
3036
|
+
backend: "webgl",
|
|
3037
|
+
astcNormalRemap: selection.astcNormalRemap,
|
|
3038
|
+
width: level0.width,
|
|
3039
|
+
height: level0.height,
|
|
3040
|
+
mipLevels: encodedLevels.length,
|
|
3041
|
+
encodeMs: totalEncodeMs,
|
|
3042
|
+
decodeMs,
|
|
3043
|
+
totalMs: performance.now() - t0,
|
|
3044
|
+
cacheHit: false
|
|
3045
|
+
};
|
|
3046
|
+
} catch (e) {
|
|
3047
|
+
encoder.destroy();
|
|
3048
|
+
console.warn("[compressTextureToBytes] WebGL fallback encode failed; returning uncompressed RGBA8.", e);
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
2615
3053
|
export {
|
|
2616
3054
|
ASTC4x4Encoder,
|
|
2617
3055
|
ASTC4x4WebGLEncoder,
|
|
@@ -2626,6 +3064,8 @@ export {
|
|
|
2626
3064
|
TextureFormat,
|
|
2627
3065
|
WebGLBlockEncoder,
|
|
2628
3066
|
WebGPUFeature,
|
|
3067
|
+
clearTranscodeCache,
|
|
3068
|
+
compressTextureToBytes,
|
|
2629
3069
|
createWebGLContext,
|
|
2630
3070
|
detectCapabilities,
|
|
2631
3071
|
detectWebGLCapabilities,
|
|
@@ -2636,6 +3076,8 @@ export {
|
|
|
2636
3076
|
isWebGLAvailable,
|
|
2637
3077
|
padToBlockMultiple,
|
|
2638
3078
|
rasterizeSvg,
|
|
3079
|
+
releaseSharedGpuResources,
|
|
2639
3080
|
selectFormat,
|
|
2640
|
-
selectWebGLFormat
|
|
3081
|
+
selectWebGLFormat,
|
|
3082
|
+
setTranscodeCacheLimit
|
|
2641
3083
|
};
|