gputex 0.6.0 → 0.8.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 +86 -55
- package/dist/index.d.ts +15 -7
- package/dist/index.js +533 -587
- package/dist/testing.d.ts +8 -5
- package/dist/testing.js +194 -36
- package/dist/three.js +533 -587
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -72,6 +72,22 @@ function uploadSourceTexture(device, srcTex, source, width, height, flipY) {
|
|
|
72
72
|
|
|
73
73
|
// src/Encoder.ts
|
|
74
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
|
+
}
|
|
75
91
|
var Encoder = class {
|
|
76
92
|
/**
|
|
77
93
|
* Subclasses set this to the WebGPU feature string the output texture
|
|
@@ -144,10 +160,11 @@ var Encoder = class {
|
|
|
144
160
|
_cachedSrcSource = null;
|
|
145
161
|
_cachedSrcFlipY = false;
|
|
146
162
|
_cachedDst = null;
|
|
147
|
-
|
|
163
|
+
// One MAP_READ staging buffer and bind group per row band (see bandRows).
|
|
164
|
+
_cachedStagings = [];
|
|
148
165
|
_cachedParams = null;
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
_lastParamsKey = null;
|
|
167
|
+
_cachedBindGroups = [];
|
|
151
168
|
_cachedPrepPlanes = null;
|
|
152
169
|
_cachedPrepBindGroup = null;
|
|
153
170
|
_resourcesBusy = false;
|
|
@@ -167,8 +184,11 @@ var Encoder = class {
|
|
|
167
184
|
_chainPrepBindGroups = [];
|
|
168
185
|
_chainParams = null;
|
|
169
186
|
_chainBindGroups = [];
|
|
187
|
+
// Level-0 bind groups of the base-level row bands (see bandRows).
|
|
188
|
+
_chainBandBindGroups = [];
|
|
170
189
|
_chainDst = null;
|
|
171
|
-
|
|
190
|
+
// One MAP_READ staging buffer per chain submission (base-level band).
|
|
191
|
+
_chainStagings = [];
|
|
172
192
|
_chainBusy = false;
|
|
173
193
|
constructor({ device, adapter, ownsDevice = false, disableF16 = false }) {
|
|
174
194
|
this.device = device;
|
|
@@ -212,14 +232,14 @@ var Encoder = class {
|
|
|
212
232
|
this._cachedPrepPlanes = null;
|
|
213
233
|
this._cachedPrepBindGroup = null;
|
|
214
234
|
this._cachedDst?.destroy();
|
|
215
|
-
this.
|
|
235
|
+
for (const st of this._cachedStagings) st.destroy();
|
|
216
236
|
this._cachedParams?.destroy();
|
|
217
237
|
this._cachedSrcTex = null;
|
|
218
238
|
this._cachedSrcSource = null;
|
|
219
239
|
this._cachedDst = null;
|
|
220
|
-
this.
|
|
240
|
+
this._cachedStagings = [];
|
|
221
241
|
this._cachedParams = null;
|
|
222
|
-
this.
|
|
242
|
+
this._cachedBindGroups = [];
|
|
223
243
|
for (const tex of this._chainTextures) tex.destroy();
|
|
224
244
|
this._chainTextures = [];
|
|
225
245
|
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
@@ -227,11 +247,12 @@ var Encoder = class {
|
|
|
227
247
|
this._chainPrepBindGroups = [];
|
|
228
248
|
this._chainParams?.destroy();
|
|
229
249
|
this._chainDst?.destroy();
|
|
230
|
-
this.
|
|
250
|
+
for (const st of this._chainStagings) st.destroy();
|
|
231
251
|
this._chainParams = null;
|
|
232
252
|
this._chainDst = null;
|
|
233
|
-
this.
|
|
253
|
+
this._chainStagings = [];
|
|
234
254
|
this._chainBindGroups = [];
|
|
255
|
+
this._chainBandBindGroups = [];
|
|
235
256
|
this._chainSig = null;
|
|
236
257
|
if (this.ownsDevice) this.device.destroy();
|
|
237
258
|
}
|
|
@@ -318,7 +339,7 @@ var Encoder = class {
|
|
|
318
339
|
entries: [
|
|
319
340
|
{ binding: 0, resource: srcView },
|
|
320
341
|
{ binding: 1, resource: planes[0].createView() },
|
|
321
|
-
{ binding: 2, resource: { buffer: params, offset: paramsOffset, size:
|
|
342
|
+
{ binding: 2, resource: { buffer: params, offset: paramsOffset, size: PARAMS_SIZE } },
|
|
322
343
|
{ binding: 3, resource: planes[1].createView() }
|
|
323
344
|
]
|
|
324
345
|
});
|
|
@@ -362,7 +383,8 @@ var Encoder = class {
|
|
|
362
383
|
let srcTex;
|
|
363
384
|
let dstBuffer;
|
|
364
385
|
let paramsBuffer;
|
|
365
|
-
|
|
386
|
+
const stagings = [];
|
|
387
|
+
const transientStagings = [];
|
|
366
388
|
let transientPrepPlanes = null;
|
|
367
389
|
try {
|
|
368
390
|
let srcTexIsNew = true;
|
|
@@ -425,66 +447,82 @@ var Encoder = class {
|
|
|
425
447
|
this._cachedDst = dstBuffer;
|
|
426
448
|
}
|
|
427
449
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
}
|
|
439
468
|
}
|
|
469
|
+
stagings.push(st);
|
|
440
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}`;
|
|
441
476
|
if (useCache) {
|
|
442
477
|
if (!this._cachedParams) {
|
|
443
478
|
this._cachedParams = device.createBuffer({
|
|
444
479
|
label: `${this.label}-params`,
|
|
445
|
-
size:
|
|
480
|
+
size: MAX_BANDS * PARAMS_ALIGN,
|
|
446
481
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
447
482
|
});
|
|
448
|
-
this.
|
|
483
|
+
this._lastParamsKey = null;
|
|
449
484
|
}
|
|
450
485
|
paramsBuffer = this._cachedParams;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this._lastParams = [blocksX, blocksY, width, height];
|
|
486
|
+
if (this._lastParamsKey !== paramsKey) {
|
|
487
|
+
device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
488
|
+
this._lastParamsKey = paramsKey;
|
|
455
489
|
}
|
|
456
490
|
} else {
|
|
457
491
|
paramsBuffer = device.createBuffer({
|
|
458
492
|
label: `${this.label}-params`,
|
|
459
|
-
size:
|
|
493
|
+
size: MAX_BANDS * PARAMS_ALIGN,
|
|
460
494
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
461
495
|
});
|
|
462
|
-
device.queue.writeBuffer(paramsBuffer, 0,
|
|
496
|
+
device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
463
497
|
}
|
|
464
|
-
if (useCache && (srcTexIsNew || dstIsNew || prepPlanesNew)) this.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
|
479
522
|
});
|
|
480
|
-
|
|
523
|
+
if (useCache) this._cachedBindGroups[b] = bg;
|
|
481
524
|
}
|
|
482
|
-
|
|
483
|
-
label: `${this.label}-bg`,
|
|
484
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
485
|
-
entries: entries2
|
|
486
|
-
});
|
|
487
|
-
if (useCache) this._cachedBindGroup = bindGroup;
|
|
525
|
+
bindGroups.push(bg);
|
|
488
526
|
}
|
|
489
527
|
let prepBindGroup = useCache && !prepPlanesNew && !srcTexIsNew ? this._cachedPrepBindGroup : null;
|
|
490
528
|
if (prepPipeline && prepPlanes && !prepBindGroup) {
|
|
@@ -492,41 +530,48 @@ var Encoder = class {
|
|
|
492
530
|
if (useCache) this._cachedPrepBindGroup = prepBindGroup;
|
|
493
531
|
}
|
|
494
532
|
const timing = withGpuTime ? this._createTiming() : null;
|
|
495
|
-
const [wgX, wgY] = this.workgroupSize;
|
|
496
533
|
const t0 = performance.now();
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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));
|
|
507
567
|
}
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
} : void 0
|
|
516
|
-
);
|
|
517
|
-
pass.setPipeline(pipeline);
|
|
518
|
-
pass.setBindGroup(0, bindGroup);
|
|
519
|
-
pass.dispatchWorkgroups(Math.ceil(blocksX / wgX), Math.ceil(blocksY / wgY), 1);
|
|
520
|
-
pass.end();
|
|
521
|
-
enc.copyBufferToBuffer(dstBuffer, 0, staging, 0, outByteLen);
|
|
522
|
-
if (timing) {
|
|
523
|
-
enc.resolveQuerySet(timing.querySet, 0, 2, timing.resolve, 0);
|
|
524
|
-
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();
|
|
525
574
|
}
|
|
526
|
-
device.queue.submit([enc.finish()]);
|
|
527
|
-
await staging.mapAsync(GPUMapMode.READ, 0, outByteLen);
|
|
528
|
-
const data = new Uint8Array(staging.getMappedRange(0, outByteLen).slice(0));
|
|
529
|
-
staging.unmap();
|
|
530
575
|
const encodeMs = performance.now() - t0;
|
|
531
576
|
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
532
577
|
return { width, height, paddedWidth, paddedHeight, data, encodeMs, gpuMs };
|
|
@@ -536,7 +581,7 @@ var Encoder = class {
|
|
|
536
581
|
} else {
|
|
537
582
|
srcTex?.destroy();
|
|
538
583
|
dstBuffer?.destroy();
|
|
539
|
-
|
|
584
|
+
for (const st of transientStagings) st.destroy();
|
|
540
585
|
paramsBuffer?.destroy();
|
|
541
586
|
if (transientPrepPlanes) for (const t of transientPrepPlanes) t.destroy();
|
|
542
587
|
}
|
|
@@ -579,7 +624,7 @@ var Encoder = class {
|
|
|
579
624
|
);
|
|
580
625
|
}
|
|
581
626
|
});
|
|
582
|
-
const { geoms, byteSpan } = this._chainGeometry(levels);
|
|
627
|
+
const { geoms, byteSpan, bands } = this._chainGeometry(levels);
|
|
583
628
|
const sig = geoms.map((g) => `${g.width}x${g.height}`).join();
|
|
584
629
|
const useCache = !this._chainBusy;
|
|
585
630
|
if (useCache) this._chainBusy = true;
|
|
@@ -588,8 +633,9 @@ var Encoder = class {
|
|
|
588
633
|
let prepBindGroups;
|
|
589
634
|
let params;
|
|
590
635
|
let bindGroups;
|
|
636
|
+
let bandBindGroups;
|
|
591
637
|
let dst;
|
|
592
|
-
|
|
638
|
+
const transientStagings = [];
|
|
593
639
|
let transientLevelSet = false;
|
|
594
640
|
try {
|
|
595
641
|
let dstIsNew = true;
|
|
@@ -611,6 +657,7 @@ var Encoder = class {
|
|
|
611
657
|
textures = this._chainTextures;
|
|
612
658
|
params = this._chainParams;
|
|
613
659
|
bindGroups = this._chainBindGroups;
|
|
660
|
+
bandBindGroups = this._chainBandBindGroups;
|
|
614
661
|
prepPlaneSets = this._chainPrepPlanes;
|
|
615
662
|
prepBindGroups = this._chainPrepBindGroups;
|
|
616
663
|
} else {
|
|
@@ -625,16 +672,7 @@ var Encoder = class {
|
|
|
625
672
|
})
|
|
626
673
|
);
|
|
627
674
|
textures = texs;
|
|
628
|
-
params =
|
|
629
|
-
label: `${this.label}-chain-params`,
|
|
630
|
-
size: geoms.length * CHAIN_ALIGN,
|
|
631
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
632
|
-
});
|
|
633
|
-
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
634
|
-
geoms.forEach((g, i) => {
|
|
635
|
-
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
636
|
-
});
|
|
637
|
-
device.queue.writeBuffer(params, 0, paramsData);
|
|
675
|
+
params = this._createChainParams(geoms, bands);
|
|
638
676
|
if (this._usesSampler) {
|
|
639
677
|
this._sampler ??= device.createSampler({
|
|
640
678
|
label: `${this.label}-clamp-sampler`,
|
|
@@ -649,14 +687,15 @@ var Encoder = class {
|
|
|
649
687
|
prepBindGroups = prepPipeline ? geoms.map(
|
|
650
688
|
(g, i) => this._createPrepBindGroup(prepPipeline, texs[i].createView(), planeSets[i], paramsBuf, i * CHAIN_ALIGN)
|
|
651
689
|
) : [];
|
|
652
|
-
|
|
690
|
+
const makeBg = (i, slot) => {
|
|
691
|
+
const g = geoms[i];
|
|
653
692
|
const entries2 = [
|
|
654
693
|
{
|
|
655
694
|
binding: 0,
|
|
656
695
|
resource: prepPipeline ? planeSets[i][0].createView() : texs[i].createView()
|
|
657
696
|
},
|
|
658
697
|
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
659
|
-
{ binding: 2, resource: { buffer: paramsBuf, offset:
|
|
698
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: slot * CHAIN_ALIGN, size: PARAMS_SIZE } }
|
|
660
699
|
];
|
|
661
700
|
if (prepPipeline) {
|
|
662
701
|
entries2.push({ binding: 3, resource: planeSets[i][1].createView() });
|
|
@@ -668,7 +707,9 @@ var Encoder = class {
|
|
|
668
707
|
layout: pipeline.getBindGroupLayout(0),
|
|
669
708
|
entries: entries2
|
|
670
709
|
});
|
|
671
|
-
}
|
|
710
|
+
};
|
|
711
|
+
bindGroups = geoms.map((_, i) => makeBg(i, i));
|
|
712
|
+
bandBindGroups = bands.map((_, b) => makeBg(0, geoms.length + b));
|
|
672
713
|
if (useCache) {
|
|
673
714
|
for (const tex of this._chainTextures) tex.destroy();
|
|
674
715
|
for (const planes of this._chainPrepPlanes) for (const t of planes) t.destroy();
|
|
@@ -678,24 +719,12 @@ var Encoder = class {
|
|
|
678
719
|
this._chainPrepBindGroups = prepBindGroups;
|
|
679
720
|
this._chainParams = params;
|
|
680
721
|
this._chainBindGroups = bindGroups;
|
|
722
|
+
this._chainBandBindGroups = bandBindGroups;
|
|
681
723
|
this._chainSig = sig;
|
|
682
724
|
} else {
|
|
683
725
|
transientLevelSet = true;
|
|
684
726
|
}
|
|
685
727
|
}
|
|
686
|
-
if (useCache && this._chainStaging && this._chainStaging.size >= byteSpan) {
|
|
687
|
-
staging = this._chainStaging;
|
|
688
|
-
} else {
|
|
689
|
-
staging = device.createBuffer({
|
|
690
|
-
label: `${this.label}-chain-staging`,
|
|
691
|
-
size: byteSpan,
|
|
692
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
693
|
-
});
|
|
694
|
-
if (useCache) {
|
|
695
|
-
this._chainStaging?.destroy();
|
|
696
|
-
this._chainStaging = staging;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
728
|
for (let i = 0; i < levels.length; i++) {
|
|
700
729
|
const level = levels[i];
|
|
701
730
|
device.queue.writeTexture({ texture: textures[i] }, level.data, { bytesPerRow: level.width * 4 }, [
|
|
@@ -708,9 +737,12 @@ var Encoder = class {
|
|
|
708
737
|
pipeline,
|
|
709
738
|
geoms,
|
|
710
739
|
byteSpan,
|
|
740
|
+
bands,
|
|
711
741
|
bindGroups,
|
|
742
|
+
bandBindGroups,
|
|
712
743
|
dst,
|
|
713
|
-
|
|
744
|
+
useCache,
|
|
745
|
+
transientStagings,
|
|
714
746
|
withGpuTime,
|
|
715
747
|
t0,
|
|
716
748
|
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
@@ -720,8 +752,8 @@ var Encoder = class {
|
|
|
720
752
|
this._chainBusy = false;
|
|
721
753
|
} else {
|
|
722
754
|
dst?.destroy();
|
|
723
|
-
staging?.destroy();
|
|
724
755
|
}
|
|
756
|
+
for (const st of transientStagings) st.destroy();
|
|
725
757
|
if (transientLevelSet) {
|
|
726
758
|
if (textures) for (const tex of textures) tex.destroy();
|
|
727
759
|
if (prepPlaneSets) for (const planes of prepPlaneSets) for (const t of planes) t.destroy();
|
|
@@ -751,11 +783,11 @@ var Encoder = class {
|
|
|
751
783
|
for (let i = 0; i < srcTex.mipLevelCount; i++) {
|
|
752
784
|
dims.push({ width: Math.max(1, srcTex.width >> i), height: Math.max(1, srcTex.height >> i) });
|
|
753
785
|
}
|
|
754
|
-
const { geoms, byteSpan } = this._chainGeometry(dims);
|
|
786
|
+
const { geoms, byteSpan, bands } = this._chainGeometry(dims);
|
|
755
787
|
const useCache = !this._chainBusy;
|
|
756
788
|
if (useCache) this._chainBusy = true;
|
|
757
789
|
let dst;
|
|
758
|
-
|
|
790
|
+
const transientStagings = [];
|
|
759
791
|
let params;
|
|
760
792
|
let planeSets = null;
|
|
761
793
|
try {
|
|
@@ -773,29 +805,7 @@ var Encoder = class {
|
|
|
773
805
|
this._chainSig = null;
|
|
774
806
|
}
|
|
775
807
|
}
|
|
776
|
-
|
|
777
|
-
staging = this._chainStaging;
|
|
778
|
-
} else {
|
|
779
|
-
staging = device.createBuffer({
|
|
780
|
-
label: `${this.label}-chain-staging`,
|
|
781
|
-
size: byteSpan,
|
|
782
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
783
|
-
});
|
|
784
|
-
if (useCache) {
|
|
785
|
-
this._chainStaging?.destroy();
|
|
786
|
-
this._chainStaging = staging;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
params = device.createBuffer({
|
|
790
|
-
label: `${this.label}-chain-params`,
|
|
791
|
-
size: geoms.length * CHAIN_ALIGN,
|
|
792
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
793
|
-
});
|
|
794
|
-
const paramsData = new Uint32Array(geoms.length * CHAIN_ALIGN / 4);
|
|
795
|
-
geoms.forEach((g, i) => {
|
|
796
|
-
paramsData.set([g.blocksX, g.blocksY, g.width, g.height], i * CHAIN_ALIGN / 4);
|
|
797
|
-
});
|
|
798
|
-
device.queue.writeBuffer(params, 0, paramsData);
|
|
808
|
+
params = this._createChainParams(geoms, bands);
|
|
799
809
|
if (this._usesSampler) {
|
|
800
810
|
this._sampler ??= device.createSampler({
|
|
801
811
|
label: `${this.label}-clamp-sampler`,
|
|
@@ -816,14 +826,15 @@ var Encoder = class {
|
|
|
816
826
|
i * CHAIN_ALIGN
|
|
817
827
|
)
|
|
818
828
|
) : null;
|
|
819
|
-
const
|
|
829
|
+
const makeBg = (i, slot) => {
|
|
830
|
+
const g = geoms[i];
|
|
820
831
|
const entries2 = [
|
|
821
832
|
{
|
|
822
833
|
binding: 0,
|
|
823
834
|
resource: planes ? planes[i][0].createView() : srcTex.createView({ baseMipLevel: i, mipLevelCount: 1 })
|
|
824
835
|
},
|
|
825
836
|
{ binding: 1, resource: { buffer: dstBuf, offset: g.dstOffset, size: g.byteLen } },
|
|
826
|
-
{ binding: 2, resource: { buffer: paramsBuf, offset:
|
|
837
|
+
{ binding: 2, resource: { buffer: paramsBuf, offset: slot * CHAIN_ALIGN, size: PARAMS_SIZE } }
|
|
827
838
|
];
|
|
828
839
|
if (planes) {
|
|
829
840
|
entries2.push({ binding: 3, resource: planes[i][1].createView() });
|
|
@@ -835,14 +846,19 @@ var Encoder = class {
|
|
|
835
846
|
layout: pipeline.getBindGroupLayout(0),
|
|
836
847
|
entries: entries2
|
|
837
848
|
});
|
|
838
|
-
}
|
|
849
|
+
};
|
|
850
|
+
const bindGroups = geoms.map((_, i) => makeBg(i, i));
|
|
851
|
+
const bandBindGroups = bands.map((_, b) => makeBg(0, geoms.length + b));
|
|
839
852
|
return await this._submitChainAndRead(
|
|
840
853
|
pipeline,
|
|
841
854
|
geoms,
|
|
842
855
|
byteSpan,
|
|
856
|
+
bands,
|
|
843
857
|
bindGroups,
|
|
858
|
+
bandBindGroups,
|
|
844
859
|
dst,
|
|
845
|
-
|
|
860
|
+
useCache,
|
|
861
|
+
transientStagings,
|
|
846
862
|
withGpuTime,
|
|
847
863
|
t0,
|
|
848
864
|
prepPipeline && prepBindGroups ? { pipeline: prepPipeline, bindGroups: prepBindGroups } : null
|
|
@@ -852,8 +868,8 @@ var Encoder = class {
|
|
|
852
868
|
this._chainBusy = false;
|
|
853
869
|
} else {
|
|
854
870
|
dst?.destroy();
|
|
855
|
-
staging?.destroy();
|
|
856
871
|
}
|
|
872
|
+
for (const st of transientStagings) st.destroy();
|
|
857
873
|
params?.destroy();
|
|
858
874
|
if (planeSets) for (const planes of planeSets) for (const t of planes) t.destroy();
|
|
859
875
|
}
|
|
@@ -861,7 +877,7 @@ var Encoder = class {
|
|
|
861
877
|
/** Block-grid geometry + packed output offsets for a chain of levels.
|
|
862
878
|
* `byteSpan` is both the dst buffer size and the readback copy size (a
|
|
863
879
|
* multiple of 4: byteLen is a multiple of bytesPerBlock ≥ 8, offsets are
|
|
864
|
-
* CHAIN_ALIGN-ed). */
|
|
880
|
+
* CHAIN_ALIGN-ed). `bands` splits the base level into row bands. */
|
|
865
881
|
_chainGeometry(dims) {
|
|
866
882
|
let dstCursor = 0;
|
|
867
883
|
const geoms = dims.map(({ width, height }) => {
|
|
@@ -875,60 +891,131 @@ var Encoder = class {
|
|
|
875
891
|
return { width, height, paddedWidth, paddedHeight, blocksX, blocksY, byteLen, dstOffset };
|
|
876
892
|
});
|
|
877
893
|
const last = geoms[geoms.length - 1];
|
|
878
|
-
|
|
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
|
+
};
|
|
879
900
|
}
|
|
880
|
-
/**
|
|
881
|
-
*
|
|
882
|
-
|
|
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) {
|
|
883
926
|
const device = this.device;
|
|
884
927
|
const timing = withGpuTime ? this._createTiming() : null;
|
|
885
928
|
const [wgX, wgY] = this.workgroupSize;
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
+
}
|
|
897
950
|
}
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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);
|
|
906
968
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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));
|
|
920
995
|
}
|
|
921
|
-
device.queue.submit([enc.finish()]);
|
|
922
|
-
await staging.mapAsync(GPUMapMode.READ, 0, byteSpan);
|
|
923
|
-
const mapped = staging.getMappedRange(0, byteSpan);
|
|
924
996
|
const out = geoms.map((g) => ({
|
|
925
997
|
width: g.width,
|
|
926
998
|
height: g.height,
|
|
927
999
|
paddedWidth: g.paddedWidth,
|
|
928
1000
|
paddedHeight: g.paddedHeight,
|
|
929
|
-
data:
|
|
1001
|
+
data: allocPrefaulted(g.byteLen)
|
|
930
1002
|
}));
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1019
|
const encodeMs = performance.now() - t0;
|
|
933
1020
|
const gpuMs = timing ? await this._readTimingMs(timing) : void 0;
|
|
934
1021
|
return { levels: out, encodeMs, gpuMs };
|
|
@@ -972,250 +1059,10 @@ var Encoder = class {
|
|
|
972
1059
|
};
|
|
973
1060
|
|
|
974
1061
|
// src/bc1.wgsl
|
|
975
|
-
var bc1_default = "// BC1 (DXT1) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte BC1 block\n// written as 2 x u32 into the destination storage buffer. This is the f32\n// fallback; bc1_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\n//\n// BC1 block layout (little-endian):\n// u32[0]: color0 (low 16) | color1 (high 16) both in RGB565\n// u32[1]: 16 x 2-bit indices, pixel 0 = bits 0..1, pixel 15 = bits 30..31\n//\n// We always force the 4-color mode (color0 > color1, numeric 16-bit):\n// idx 0 -> color0\n// idx 1 -> color1\n// idx 2 -> (2*color0 + color1) / 3\n// idx 3 -> ( color0 + 2*color1) / 3\n//\n// ALGORITHM: principal-axis endpoint seed (covariance power-iteration; inset\n// bbox on degenerate blocks), inset by ~half a 565 cell along the axis, then\n// a fused pass that projects every pixel onto the decoded-endpoint line (the\n// 4 palette entries are colinear and evenly spaced, so the nearest entry is\n// the rounded projection \u2014 no 4-entry search) while accumulating the\n// least-squares refit sums, followed by up to TWO refit rounds (re-quantise,\n// reproject with indices packed on the fly, accept only on lower block\n// error).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to565(c: vec3<f32>) -> u32 {\n // Round-to-nearest quantization into 5-6-5.\n let r = u32(clamp(floor(c.r * 31.0 + 0.5), 0.0, 31.0));\n let g = u32(clamp(floor(c.g * 63.0 + 0.5), 0.0, 63.0));\n let b = u32(clamp(floor(c.b * 31.0 + 0.5), 0.0, 31.0));\n return (r << 11u) | (g << 5u) | b;\n}\n\nfn from565(c: u32) -> vec3<f32> {\n let r = (c >> 11u) & 31u;\n let g = (c >> 5u) & 63u;\n let b = c & 31u;\n // 5/6-bit -> 8-bit: (x*527+23)>>6 (6-bit: 259/33) \u2014 round-to-nearest\n // scaling, matching bc1_ref.ts and typical hardware decoders (white ->\n // 255). Integer u32 math is exact. NOTE: this is NOT plain bit-replication\n // ((x<<3)|(x>>2)) \u2014 they differ for some codes (e.g. 5-bit 3 -> 25 vs 24).\n // Selecting indices against this palette is what makes the encoder agree\n // with what the GPU will actually sample.\n let r8 = (r * 527u + 23u) >> 6u;\n let g8 = (g * 259u + 33u) >> 6u;\n let b8 = (b * 527u + 23u) >> 6u;\n return vec3<f32>(vec3<u32>(r8, g8, b8)) / 255.0;\n}\n\n// One projection pass against the decoded endpoints of (c0,c1): the packed\n// 2-bit indices, the block's squared error, and the LSQ normal-equation sums\n// of the resulting assignment \u2014 so an accepted refit can seed the next\n// round. Levels s run 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014\n// colinear, evenly spaced, so rounding the projection IS the nearest-entry\n// search). Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a\n// packed LUT: (0x78 >> 2L) & 3.\nstruct ProjStats {\n indices: u32,\n err: f32,\n sAA: f32, sBB: f32, sAB: f32,\n sAV: vec3<f32>, sBV: vec3<f32>,\n s_min: f32, s_max: f32,\n};\nfn project_stats(pix: ptr<function, array<vec3<f32>, 16>>, c0: u32, c1: u32) -> ProjStats {\n var out: ProjStats;\n out.indices = 0u;\n out.err = 0.0;\n out.sAA = 0.0; out.sBB = 0.0; out.sAB = 0.0;\n out.sAV = vec3<f32>(0.0); out.sBV = vec3<f32>(0.0);\n out.s_min = 3.0; out.s_max = 0.0;\n let p0 = from565(c0);\n let p1 = from565(c1);\n let dir = p1 - p0;\n let dd = dot(dir, dir);\n if (dd == 0.0) {\n // Unreachable for distinct 565 codes (the decode is injective); kept so\n // a degenerate call still returns a consistent error.\n out.s_min = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let e = (*pix)[k] - p0;\n out.err = out.err + dot(e, e);\n }\n return out;\n }\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = (*pix)[k];\n let s = clamp(floor(dot(v - p0, dir) * inv + 0.5), 0.0, 3.0);\n out.s_min = min(out.s_min, s); out.s_max = max(out.s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n out.sAA = out.sAA + a * a; out.sBB = out.sBB + b * b; out.sAB = out.sAB + a * b;\n out.sAV = out.sAV + a * v; out.sBV = out.sBV + b * v;\n let e = v - (p0 + b * dir);\n out.err = out.err + dot(e, e);\n out.indices = out.indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));\n }\n return out;\n}\n\n// Principal colour axis via covariance power-iteration, seeded with the bbox\n// diagonal. Returns a unit axis, or vec3(0) for a degenerate (constant)\n// block. The bbox diagonal alone is sign-blind and points across\n// anti-correlated data (normal maps, hue edges) instead of along it.\nfn principal_axis(\n pixels: ptr<function, array<vec3<f32>, 16>>,\n mean: vec3<f32>,\n seed: vec3<f32>,\n) -> vec3<f32> {\n // Symmetric 3x3 covariance, stored as its three rows.\n var c0v = vec3<f32>(0.0);\n var c1v = vec3<f32>(0.0);\n var c2v = vec3<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = (*pixels)[k] - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec3<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec3<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v));\n len = length(nv);\n if (len < 1e-12) { return vec3<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec3<f32>, 16>;\n var bb_min = vec3<f32>(1.0, 1.0, 1.0);\n var bb_max = vec3<f32>(0.0, 0.0, 0.0);\n var mean = vec3<f32>(0.0);\n var gd = 0.0;\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n // Clamp to edge for non-multiple-of-4 textures.\n let p = clamp(base + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = textureLoad(src_tex, p, 0).rgb;\n pixels[i] = c;\n bb_min = min(bb_min, c);\n bb_max = max(bb_max, c);\n mean = mean + c;\n gd = max(gd, max(abs(c.x - c.y), abs(c.x - c.z)));\n }\n mean = mean * (1.0 / 16.0);\n // Exactly-gray blocks free the refit from the bbox clamp (no hue to\n // protect; smooth gradients want endpoints outside the data range) \u2014\n // see bc1_fast_f16.wgsl.\n let gray = gd == 0.0;\n let lim_lo = select(bb_min, vec3<f32>(0.0), gray);\n let lim_hi = select(bb_max, vec3<f32>(1.0), gray);\n\n // Seed endpoints from the block's principal colour axis at the exact\n // projection extents, inset by ~half a 565 cell along the axis (stb_dxt\n // heuristic). Degenerate (near-flat) blocks keep the inset-bbox seed.\n var seed_hi: vec3<f32>;\n var seed_lo: vec3<f32>;\n let axis = principal_axis(&pixels, mean, bb_max - bb_min);\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pixels[k] - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n let pad = (t_max - t_min) / 16.0;\n seed_hi = clamp(mean + (t_max - pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n seed_lo = clamp(mean + (t_min + pad) * axis, vec3<f32>(0.0), vec3<f32>(1.0));\n } else {\n let inset = (bb_max - bb_min) / 16.0;\n seed_hi = clamp(bb_max - inset, vec3<f32>(0.0), vec3<f32>(1.0));\n seed_lo = clamp(bb_min + inset, vec3<f32>(0.0), vec3<f32>(1.0));\n }\n var c0 = to565(seed_hi);\n var c1 = to565(seed_lo);\n if (c0 == c1) {\n if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }\n } else if (c0 < c1) {\n let t = c0; c0 = c1; c1 = t;\n }\n\n // Fused seed pass, then up to TWO least-squares refit rounds, each\n // accepted only if the block's squared error actually decreases \u2014 the\n // refit minimises a continuous objective and can lose after 565\n // quantisation. Every pass re-accumulates the normal-equation sums, so an\n // accepted round seeds the next.\n var cur = project_stats(&pixels, c0, c1);\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n // Refit only on a well-conditioned system: when every pixel lands on\n // ONE level (flat blocks \u2014 the 4-colour nudge forces c0 \u2260 c1 even\n // then) the system is rank-1 and det/numerators are pure float noise;\n // the solve would return garbage endpoints. With \u22652 levels\n // det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 ~1.67, so 1e-3 is a safe guard.\n if (cur.s_min >= cur.s_max) { break; }\n let det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;\n if (abs(det) <= 1e-3) { break; }\n // Clamp the refit to the block bbox (not [0,1]): on multi-cluster\n // blocks the unconstrained solve extrapolates far outside the block's\n // colours and the per-channel clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to\n // the bbox also measures better in plain SSE (+1.6 dB on the colour\n // test card), so the accept-if-better guard below keeps more refits.\n let e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, lim_lo, lim_hi);\n let e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, lim_lo, lim_hi);\n var nc0 = to565(e0);\n var nc1 = to565(e1);\n if (nc0 == nc1) {\n if (nc1 > 0u) { nc1 = nc1 - 1u; } else { nc0 = nc0 + 1u; }\n } else if (nc0 < nc1) {\n let t = nc0; nc0 = nc1; nc1 = t;\n }\n if (nc0 == c0 && nc1 == c1) { break; }\n let nxt = project_stats(&pixels, nc0, nc1);\n if (nxt.err >= cur.err) { break; }\n c0 = nc0;\n c1 = nc1;\n cur = nxt;\n }\n\n let out = block_index * 2u;\n dst[out] = c0 | (c1 << 16u);\n dst[out + 1u] = cur.indices;\n}\n";
|
|
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";
|
|
976
1063
|
|
|
977
1064
|
// src/bc1_fast_f16.wgsl
|
|
978
|
-
var bc1_fast_f16_default = `// bc1 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
979
|
-
//
|
|
980
|
-
// BC1 quantises endpoints to RGB565 anyway, so the fast path needs nothing
|
|
981
|
-
// f32 can do that f16 can't \u2014 all projection / least-squares math runs in
|
|
982
|
-
// f16 ([0,1] domain). The algorithm is the same family as the BC7/ASTC fast
|
|
983
|
-
// paths rather than a port of bc1.wgsl's fast branch:
|
|
984
|
-
//
|
|
985
|
-
// 1. principal-axis endpoint seed (covariance power-iteration; inset bbox
|
|
986
|
-
// on degenerate blocks), inset by ~half a 565 cell (stb_dxt heuristic)
|
|
987
|
-
// 2. quantise to 565, force 4-colour mode (c0 > c1)
|
|
988
|
-
// 3. ONE fused pass: project every pixel onto the decoded-endpoint line
|
|
989
|
-
// (the 4 palette entries are colinear and evenly spaced, so the nearest
|
|
990
|
-
// entry is the rounded projection \u2014 no 4-entry search) while
|
|
991
|
-
// accumulating the least-squares refit sums, the packed indices and the
|
|
992
|
-
// squared error
|
|
993
|
-
// 4. up to TWO refit rounds (mirroring the high path's iterated refits):
|
|
994
|
-
// re-quantise the refit endpoints, reproject (indices packed on the
|
|
995
|
-
// fly, sums re-accumulated to seed the next round), and accept each
|
|
996
|
-
// round only if the block error decreases \u2014 flat/single-level blocks
|
|
997
|
-
// skip these passes entirely
|
|
998
|
-
//
|
|
999
|
-
// vs the pre-projection fast branch (build palette + full 4-entry search \xD7 3
|
|
1000
|
-
// passes + refit sums pass) this does roughly half the ALU per block. The
|
|
1001
|
-
// 565 decode uses exact integer math, so the palette base points are exact.
|
|
1002
|
-
//
|
|
1003
|
-
// The host selects this module only when the device reports shader-f16,
|
|
1004
|
-
// falling back to bc1.wgsl otherwise. "high" never uses this.
|
|
1005
|
-
enable f16;
|
|
1006
|
-
struct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };
|
|
1007
|
-
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1008
|
-
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1009
|
-
@group(0) @binding(2) var<uniform> params: Params;
|
|
1010
|
-
alias h = f16;
|
|
1011
|
-
alias h3 = vec3<f16>;
|
|
1012
|
-
|
|
1013
|
-
fn to565(c: h3) -> u32 {
|
|
1014
|
-
let r = u32(clamp(floor(c.r * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
1015
|
-
let g = u32(clamp(floor(c.g * h(63.0) + h(0.5)), h(0.0), h(63.0)));
|
|
1016
|
-
let b = u32(clamp(floor(c.b * h(31.0) + h(0.5)), h(0.0), h(31.0)));
|
|
1017
|
-
return (r << 11u) | (g << 5u) | b;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Decode a 565 endpoint to [0,1]: (x*527+23)>>6 (6-bit: 259/33) \u2014
|
|
1021
|
-
// round-to-nearest scaling, matching bc1_ref.ts / bc1.wgsl and typical
|
|
1022
|
-
// hardware decoders. Exact in u32 integer math (f16 could not evaluate the
|
|
1023
|
-
// products exactly).
|
|
1024
|
-
fn from565(c: u32) -> h3 {
|
|
1025
|
-
let r = (c >> 11u) & 31u;
|
|
1026
|
-
let g = (c >> 5u) & 63u;
|
|
1027
|
-
let b = c & 31u;
|
|
1028
|
-
let r8 = (r * 527u + 23u) >> 6u;
|
|
1029
|
-
let g8 = (g * 259u + 33u) >> 6u;
|
|
1030
|
-
let b8 = (b * 527u + 23u) >> 6u;
|
|
1031
|
-
return h3(vec3<f32>(vec3<u32>(r8, g8, b8))) * h(1.0 / 255.0);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Force 4-colour mode: c0 > c1 strictly.
|
|
1035
|
-
fn order565(a: u32, b: u32) -> vec2<u32> {
|
|
1036
|
-
var c0 = a; var c1 = b;
|
|
1037
|
-
if (c0 == c1) {
|
|
1038
|
-
if (c1 > 0u) { c1 = c1 - 1u; } else { c0 = c0 + 1u; }
|
|
1039
|
-
} else if (c0 < c1) {
|
|
1040
|
-
let t = c0; c0 = c1; c1 = t;
|
|
1041
|
-
}
|
|
1042
|
-
return vec2<u32>(c0, c1);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// One projection pass against the decoded endpoints of (c0,c1): the packed
|
|
1046
|
-
// 2-bit indices, the block's squared error, and the LSQ normal-equation sums
|
|
1047
|
-
// of the resulting assignment \u2014 so an accepted refit can seed the next
|
|
1048
|
-
// round. Levels s run 0..3 along p0\u2192p1 (palette = p0, p0+\u2153d, p0+\u2154d, p1 \u2014
|
|
1049
|
-
// colinear, evenly spaced, so rounding the projection IS the nearest-entry
|
|
1050
|
-
// search). Level \u2192 BC1 index: 0\u21920 (c0), 1\u21922 (\u2154c0+\u2153c1), 2\u21923, 3\u21921 (c1); as a
|
|
1051
|
-
// packed LUT: (0x78 >> 2L) & 3.
|
|
1052
|
-
struct Proj {
|
|
1053
|
-
indices: u32,
|
|
1054
|
-
err: h,
|
|
1055
|
-
sAA: h, sBB: h, sAB: h,
|
|
1056
|
-
sAV: h3, sBV: h3,
|
|
1057
|
-
s_min: h, s_max: h,
|
|
1058
|
-
};
|
|
1059
|
-
fn project_stats(pix: ptr<function, array<h3, 16>>, c0: u32, c1: u32) -> Proj {
|
|
1060
|
-
var out: Proj;
|
|
1061
|
-
out.indices = 0u;
|
|
1062
|
-
out.err = h(0.0);
|
|
1063
|
-
out.sAA = h(0.0); out.sBB = h(0.0); out.sAB = h(0.0);
|
|
1064
|
-
out.sAV = h3(0.0); out.sBV = h3(0.0);
|
|
1065
|
-
out.s_min = h(3.0); out.s_max = h(0.0);
|
|
1066
|
-
let p0 = from565(c0);
|
|
1067
|
-
let p1 = from565(c1);
|
|
1068
|
-
let dir = p1 - p0;
|
|
1069
|
-
let dd = dot(dir, dir);
|
|
1070
|
-
if (dd == h(0.0)) {
|
|
1071
|
-
// Unreachable for distinct 565 codes (the decode is injective); kept so
|
|
1072
|
-
// a degenerate call still returns a consistent error.
|
|
1073
|
-
out.s_min = h(0.0);
|
|
1074
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1075
|
-
let e = (*pix)[k] - p0;
|
|
1076
|
-
out.err = out.err + dot(e, e);
|
|
1077
|
-
}
|
|
1078
|
-
return out;
|
|
1079
|
-
}
|
|
1080
|
-
let inv = h(3.0) / dd;
|
|
1081
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1082
|
-
let v = (*pix)[k];
|
|
1083
|
-
let s = clamp(floor(dot(v - p0, dir) * inv + h(0.5)), h(0.0), h(3.0));
|
|
1084
|
-
out.s_min = min(out.s_min, s); out.s_max = max(out.s_max, s);
|
|
1085
|
-
let b = s * h(1.0 / 3.0); let a = h(1.0) - b;
|
|
1086
|
-
out.sAA = out.sAA + a * a; out.sBB = out.sBB + b * b; out.sAB = out.sAB + a * b;
|
|
1087
|
-
out.sAV = out.sAV + a * v; out.sBV = out.sBV + b * v;
|
|
1088
|
-
let e = v - (p0 + b * dir);
|
|
1089
|
-
out.err = out.err + dot(e, e);
|
|
1090
|
-
out.indices = out.indices | (((0x78u >> (u32(s) * 2u)) & 3u) << (k * 2u));
|
|
1091
|
-
}
|
|
1092
|
-
return out;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
@compute @workgroup_size(8, 8, 1)
|
|
1096
|
-
fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1097
|
-
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1098
|
-
let bi = gid.y * params.blocks_x + gid.x;
|
|
1099
|
-
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
1100
|
-
let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);
|
|
1101
|
-
|
|
1102
|
-
var pix: array<h3, 16>;
|
|
1103
|
-
var mn = h3(1.0);
|
|
1104
|
-
var mxv = h3(0.0);
|
|
1105
|
-
var mean = h3(0.0);
|
|
1106
|
-
var gd = h(0.0);
|
|
1107
|
-
for (var i: u32 = 0u; i < 16u; i = i + 1u) {
|
|
1108
|
-
let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);
|
|
1109
|
-
let px = h3(textureLoad(src_tex, p, 0).rgb);
|
|
1110
|
-
pix[i] = px; mn = min(mn, px); mxv = max(mxv, px);
|
|
1111
|
-
mean = mean + px;
|
|
1112
|
-
gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));
|
|
1113
|
-
}
|
|
1114
|
-
mean = mean * h(1.0 / 16.0);
|
|
1115
|
-
// Exactly-gray blocks free the refit from the bbox clamp below: a gray
|
|
1116
|
-
// block has no hue to bend (the clamp's whole purpose), and on smooth
|
|
1117
|
-
// gradients the LSQ optimum often lies OUTSIDE the data range \u2014 endpoints
|
|
1118
|
-
// spread wider than the block so the 1/3-2/3 interpolants land on the
|
|
1119
|
-
// values. Same rationale as the BC5 scalar channels (+0.32 dB there).
|
|
1120
|
-
let gray = gd == h(0.0);
|
|
1121
|
-
let lim_lo = select(mn, h3(0.0), gray);
|
|
1122
|
-
let lim_hi = select(mxv, h3(1.0), gray);
|
|
1123
|
-
|
|
1124
|
-
// Seed endpoints from the block's principal colour axis (covariance
|
|
1125
|
-
// power-iteration, seeded with the bbox diagonal \u2014 same family as the
|
|
1126
|
-
// 'high' path). The bbox diagonal is sign-blind: on anti-correlated
|
|
1127
|
-
// channels (normal maps, hue edges) it points across the data instead of
|
|
1128
|
-
// along it, the projection indices come out garbage, and the LSQ refit \u2014
|
|
1129
|
-
// which fits endpoints GIVEN those indices \u2014 can't recover. Deviations are
|
|
1130
|
-
// pre-scaled \xD716 so covariance entries for shallow blocks stay in f16's
|
|
1131
|
-
// normal range (span ~1/255 \u2192 d\xB2 \u2248 1e-3) while full-range sums stay \u22644096;
|
|
1132
|
-
// the iteration renormalises by the max component (a plain length() of the
|
|
1133
|
-
// matvec output could overflow f16), so only the direction survives \u2014 the
|
|
1134
|
-
// \xD7256 covariance scale is irrelevant.
|
|
1135
|
-
var seed_hi: h3;
|
|
1136
|
-
var seed_lo: h3;
|
|
1137
|
-
var c0v = h3(0.0);
|
|
1138
|
-
var c1v = h3(0.0);
|
|
1139
|
-
var c2v = h3(0.0);
|
|
1140
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1141
|
-
let d = (pix[k] - mean) * h(16.0);
|
|
1142
|
-
c0v = c0v + d.x * d;
|
|
1143
|
-
c1v = c1v + d.y * d;
|
|
1144
|
-
c2v = c2v + d.z * d;
|
|
1145
|
-
}
|
|
1146
|
-
var axis = mxv - mn;
|
|
1147
|
-
var axis_ok = true;
|
|
1148
|
-
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1149
|
-
let nv = h3(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis));
|
|
1150
|
-
let m = max(max(abs(nv.x), abs(nv.y)), abs(nv.z));
|
|
1151
|
-
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1152
|
-
axis = nv / m;
|
|
1153
|
-
}
|
|
1154
|
-
if (axis_ok) {
|
|
1155
|
-
axis = axis / length(axis);
|
|
1156
|
-
var t_min = h(4.0);
|
|
1157
|
-
var t_max = h(-4.0);
|
|
1158
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1159
|
-
let t = dot(pix[k] - mean, axis);
|
|
1160
|
-
t_min = min(t_min, t);
|
|
1161
|
-
t_max = max(t_max, t);
|
|
1162
|
-
}
|
|
1163
|
-
// Inset along the axis by ~half a 565 cell (stb_dxt heuristic, matching
|
|
1164
|
-
// the degenerate-case bbox inset below).
|
|
1165
|
-
let pad = (t_max - t_min) * h(1.0 / 16.0);
|
|
1166
|
-
seed_hi = clamp(mean + (t_max - pad) * axis, h3(0.0), h3(1.0));
|
|
1167
|
-
seed_lo = clamp(mean + (t_min + pad) * axis, h3(0.0), h3(1.0));
|
|
1168
|
-
} else {
|
|
1169
|
-
// Degenerate (near-flat) block: inset bbox seed, as before.
|
|
1170
|
-
let inset = (mxv - mn) * h(1.0 / 16.0);
|
|
1171
|
-
seed_hi = clamp(mxv - inset, h3(0.0), h3(1.0));
|
|
1172
|
-
seed_lo = clamp(mn + inset, h3(0.0), h3(1.0));
|
|
1173
|
-
}
|
|
1174
|
-
let seed = order565(to565(seed_hi), to565(seed_lo));
|
|
1175
|
-
var c0 = seed.x;
|
|
1176
|
-
var c1 = seed.y;
|
|
1177
|
-
|
|
1178
|
-
// Fused seed pass, then up to TWO least-squares refit rounds (mirroring
|
|
1179
|
-
// the high path's iterated refits, at projection cost), each accepted only
|
|
1180
|
-
// if the block's squared error actually decreases \u2014 the refit minimises a
|
|
1181
|
-
// continuous objective and can lose after 565 quantisation. Every pass
|
|
1182
|
-
// re-accumulates the normal-equation sums, so an accepted round seeds the
|
|
1183
|
-
// next.
|
|
1184
|
-
var cur = project_stats(&pix, c0, c1);
|
|
1185
|
-
for (var it: u32 = 0u; it < 2u; it = it + 1u) {
|
|
1186
|
-
// Refit only on a well-conditioned system. When every pixel lands on ONE
|
|
1187
|
-
// level (flat / near-flat blocks \u2014 note the 4-colour-mode nudge forces
|
|
1188
|
-
// c0 \u2260 c1 even for perfectly flat blocks) the system is rank-1: det is 0
|
|
1189
|
-
// in exact math and the f16-accumulated det/numerators are pure rounding
|
|
1190
|
-
// noise, so the solve returns garbage endpoints. With \u22652 distinct levels
|
|
1191
|
-
// det = \u03A3_i<j (b_j \u2212 b_i)\xB2 \u2265 15\xB7(1/3)\xB2 \u2248 1.67, far above the ~0.05 f16
|
|
1192
|
-
// noise floor \u2014 0.5 separates the two regimes cleanly.
|
|
1193
|
-
if (cur.s_min >= cur.s_max) { break; }
|
|
1194
|
-
let det = cur.sAA * cur.sBB - cur.sAB * cur.sAB;
|
|
1195
|
-
if (abs(det) <= h(0.5)) { break; }
|
|
1196
|
-
// Clamp the refit to the block bbox (not [0,1]) \u2014 except for exactly
|
|
1197
|
-
// gray blocks, see the load pass: on multi-cluster blocks the
|
|
1198
|
-
// unconstrained solve extrapolates far outside the block's colours
|
|
1199
|
-
// and the per-channel clamp then bends the hue \u2014 fringe pixels decode to
|
|
1200
|
-
// colours that exist nowhere in the block. Constraining to the bbox also
|
|
1201
|
-
// measures better in plain SSE (+1.6 dB on the colour test card), so the
|
|
1202
|
-
// accept-if-better guard below keeps more refits.
|
|
1203
|
-
let e0 = clamp((cur.sBB * cur.sAV - cur.sAB * cur.sBV) / det, lim_lo, lim_hi);
|
|
1204
|
-
let e1 = clamp((cur.sAA * cur.sBV - cur.sAB * cur.sAV) / det, lim_lo, lim_hi);
|
|
1205
|
-
let rq = order565(to565(e0), to565(e1));
|
|
1206
|
-
if (rq.x == c0 && rq.y == c1) { break; }
|
|
1207
|
-
let nxt = project_stats(&pix, rq.x, rq.y);
|
|
1208
|
-
if (nxt.err >= cur.err) { break; }
|
|
1209
|
-
c0 = rq.x;
|
|
1210
|
-
c1 = rq.y;
|
|
1211
|
-
cur = nxt;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
let o = bi * 2u;
|
|
1215
|
-
dst[o] = c0 | (c1 << 16u);
|
|
1216
|
-
dst[o + 1u] = cur.indices;
|
|
1217
|
-
}
|
|
1218
|
-
`;
|
|
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";
|
|
1219
1066
|
|
|
1220
1067
|
// src/BC1Encoder.ts
|
|
1221
1068
|
var BC1Encoder = class extends Encoder {
|
|
@@ -1242,7 +1089,7 @@ var BC1Encoder = class extends Encoder {
|
|
|
1242
1089
|
};
|
|
1243
1090
|
|
|
1244
1091
|
// src/bc5.wgsl
|
|
1245
|
-
var bc5_default = "// BC5 (RGTC2) compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into a 16-byte BC5 block\n// written as 4 x u32 into the destination storage buffer. This is the f32\n// fallback; bc5_fast_f16.wgsl is the same algorithm and is preferred when\n// the device reports shader-f16.\n//\n// BC5 = two BC4 blocks concatenated:\n// block bytes 0..7 : BC4 of R channel (normal.x for tangent-space normals)\n// block bytes 8..15 : BC4 of G channel (normal.y)\n//\n// Each BC4 half-block (8 bytes):\n// byte 0 : red0 (8-bit endpoint)\n// byte 1 : red1 (8-bit endpoint)\n// bytes 2..7 : 16 \xD7 3-bit indices, LSB-first, pixel 0 at bit 0\n//\n// We always produce the 6-interpolation mode (red0 > red1). See `bc4_ref.ts`\n// for the reference this encoder is validated against.\n//\n// Same algorithm as bc5_fast_f16.wgsl (see that file for the full notes):\n// \u2022 both channels processed as vec2 lanes of the fused passes;\n// \u2022 pass 1 accumulates MOMENTS (\u03A3L, \u03A3L\xB2, \u03A3\u03C1, \u03A3L\u03C1) from which every LSQ\n// normal-equation sum is an O(1) per-block expression; the rank guard\n// is the exact 16\xB7\u03A3L\xB2 == (\u03A3L)\xB2 test;\n// \u2022 the closed-form refit prices the nearest rounding of the solve\n// through E(\u03B4) = err \u2212 2(\u03B40\xB7sAR + \u03B41\xB7sBR) + \u03B40\xB2sAA + 2\u03B40\u03B41\xB7sAB\n// + \u03B41\xB2sBB, accept-if-better;\n// \u2022 pass 2 packs the indices ONCE, against the FINAL endpoints \u2014 full\n// reprojection quality at parity cost;\n// \u2022 the 16 texel reads are 8 textureGather fetches (4 quads \xD7 R,G)\n// through a clamp-to-edge sampler, byte-identical to per-texel loads;\n// \u2022 3-bit indices accumulate branch-free into two 24-bit words (pixels\n// 0..7 and 8..15) recombined with constant shifts \u2014 no per-pixel\n// straddle branches.\n// Values are kept in the [0,255] f32 domain throughout.\n//\n// Level \u2192 BC4 index LUT (0,2,3,4,5,6,7,1) packed as 3-bit entries in\n// 0x3F58D0.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n@group(0) @binding(3) var smp: sampler;\n\nconst IDX_LUT: u32 = 0x3F58D0u;\n\n// Closed-form accept-if-better endpoint refinement for one channel \u2014 same\n// as the f16 module's `refine` (both run this per-block step in f32).\n// Endpoints clamp to [0,255], NOT the block's value range: for a scalar\n// channel, endpoints beyond the data range are often genuinely optimal and\n// there is no colour axis to bend.\nfn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, spread: bool) -> vec2<u32> {\n var out = vec2<u32>(b0, b1);\n let det = sAA * sBB - sAB * sAB;\n if (!spread || abs(det) <= 1e-3) { return out; }\n let b0f = f32(b0);\n let b1f = f32(b1);\n let e0 = clamp(b0f + (sBB * sAR - sAB * sBR) / det, 0.0, 255.0);\n let e1 = clamp(b1f + (sAA * sBR - sAB * sAR) / det, 0.0, 255.0);\n let q0f = floor(e0 + 0.5);\n let q1f = floor(e1 + 0.5);\n let q0 = u32(q0f);\n let q1 = u32(q1f);\n if (q0 > q1 && !(q0 == b0 && q1 == b1)) {\n let dd0 = q0f - b0f;\n let dd1 = q1f - b1f;\n let eNew = -2.0 * (dd0 * sAR + dd1 * sBR)\n + dd0 * dd0 * sAA + 2.0 * dd0 * dd1 * sAB + dd1 * dd1 * sBB;\n if (eNew < 0.0) {\n out = vec2<u32>(q0, q1);\n }\n }\n return out;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id)
|
|
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";
|
|
1246
1093
|
|
|
1247
1094
|
// src/bc5_fast_f16.wgsl
|
|
1248
1095
|
var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires the shader-f16 feature).
|
|
@@ -1318,7 +1165,7 @@ var bc5_fast_f16_default = `// bc5 "fast" encoder \u2014 f16 variant (requires t
|
|
|
1318
1165
|
enable f16;
|
|
1319
1166
|
alias h = f16;
|
|
1320
1167
|
alias h2 = vec2<f16>;
|
|
1321
|
-
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, };
|
|
1322
1169
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1323
1170
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1324
1171
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
@@ -1360,7 +1207,9 @@ fn refine(sAA: f32, sBB: f32, sAB: f32, sAR: f32, sBR: f32, b0: u32, b1: u32, sp
|
|
|
1360
1207
|
}
|
|
1361
1208
|
|
|
1362
1209
|
@compute @workgroup_size(8, 8, 1)
|
|
1363
|
-
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);
|
|
1364
1213
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1365
1214
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
1366
1215
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
@@ -1504,10 +1353,10 @@ var BC5Encoder = class extends Encoder {
|
|
|
1504
1353
|
};
|
|
1505
1354
|
|
|
1506
1355
|
// src/bc7.wgsl
|
|
1507
|
-
var bc7_default = "// BC7 (BPTC) mode 6 compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`. This is the f32 fallback;\n// bc7_fast_f16.wgsl is the same algorithm and is preferred when the device\n// reports shader-f16.\n//\n// ALGORITHM: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents, quantised directly \u2014\n// no LSQ refit; with the seed on the principal axis, mode 6's 16-level\n// palette leaves the refit under 0.15 dB, unlike the 4-level BC1/ASTC\n// encoders which keep theirs \u2014 then one pass that projects each pixel onto\n// the endpoint line (the 16 palette entries are colinear, so the nearest\n// index is the rounded projection \u2014 no palette build, no 16-entry search),\n// packed on the fly into two nibble words.\n//\n// A MODE 1 (2-subset) candidate was built and evaluated (2026-07) and\n// dropped: ~+1.3 dB on multi-modal content but up to ~3\xD7 the pass cost on\n// exactly that content \u2014 see bc7_fast_f16.wgsl. The CPU reference decoder\n// keeps mode 1 support (bc7_ref.ts).\n//\n// MODE 6 LAYOUT (LSB-first, bit 0 = byte 0's bit 0)\n// bits 0..6 mode field (0b0000001 \u2014 only bit 6 is 1)\n// bits 7..13 R0 (7-bit) bits 14..20 R1 bits 21..27 G0 bits 28..34 G1\n// bits 35..41 B0 bits 42..48 B1 bits 49..55 A0 bits 56..62 A1\n// bit 63 P0 bit 64 P1\n// bits 65..67 pixel 0 index (3 bits; anchor, MSB implicit 0)\n// bits 68..71 pixel 1 index (4 bits) ... bits 124..127 pixel 15 index\n//\n// Effective 8-bit endpoint channel = (7_bit_value << 1) | p_bit.\n// Palette[i] = ((64 \u2212 W4[i]) \xD7 e0_8 + W4[i] \xD7 e1_8 + 32) >> 6, integer.\n//\n// The block is assembled with straight-line constant shifts (see the layout\n// summary in bc7_fast_f16.wgsl) \u2014 a generic write_bits() helper's dynamic\n// word indexing keeps the output array out of registers.\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\nfn dist2(a: vec4<i32>, b: vec4<i32>) -> i32 {\n let d = a - b;\n let e = d * d;\n return e.x + e.y + e.z + e.w;\n}\n\n// Quantize an 8-bit ideal endpoint to (7-bit value, reconstructed 8-bit)\n// under a fixed p-bit, all four channels at once. q7 = round((ideal8 \u2212 p)/2).\nstruct QuantPair { seven: vec4<i32>, eight: vec4<i32> };\nfn quantize_endpoint(ideal8: vec4<i32>, p: u32) -> QuantPair {\n let q = vec4<i32>(clamp(\n floor((vec4<f32>(ideal8) - f32(p)) / 2.0 + 0.5),\n vec4<f32>(0.0), vec4<f32>(127.0),\n ));\n let eff = (q << vec4<u32>(1u)) | vec4<i32>(i32(p));\n return QuantPair(q, eff);\n}\n\n// Endpoint with its chosen p-bit, picked by minimum quantisation error.\nstruct Ep { seven: vec4<i32>, eight: vec4<i32>, p: u32 };\nfn pick_ep(ideal: vec4<i32>) -> Ep {\n let a = quantize_endpoint(ideal, 0u);\n let b = quantize_endpoint(ideal, 1u);\n if (dist2(b.eight, ideal) < dist2(a.eight, ideal)) { return Ep(b.seven, b.eight, 1u); }\n return Ep(a.seven, a.eight, 0u);\n}\n\n// Principal colour axis via power-iteration over precomputed, mean-corrected\n// covariance rows (the moments are accumulated for free in the pixel-load\n// loop), seeded with the bbox diagonal. Returns a unit axis, or vec4(0) for\n// a degenerate (constant) block. Same family as bc1.wgsl's principal_axis \u2014\n// the bbox diagonal alone is sign-blind and points across anti-correlated\n// data (normal maps, hue edges) instead of along it.\nfn principal_axis4(\n c0v: vec4<f32>,\n c1v: vec4<f32>,\n c2v: vec4<f32>,\n c3v: vec4<f32>,\n seed: vec4<f32>,\n) -> vec4<f32> {\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < 8u; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n\n// ------------------------------- Entry --------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id)
|
|
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";
|
|
1508
1357
|
|
|
1509
1358
|
// src/bc7_fast_f16.wgsl
|
|
1510
|
-
var bc7_fast_f16_default = "// bc7 \"fast\" encoder \u2014 f16 variant (requires the shader-f16 feature).\n// Same algorithm family as the f32 fast path in bc7.wgsl (principal-axis\n// seed at the exact projection extents \u2192 quantise \u2192 one projection-based\n// index-assignment pass), tuned for throughput:\n//\n// \u2022 All projection math in f16 ([0,1] domain). ~2\xD7 ALU throughput on\n// f16-capable GPUs. The projection direction is pre-scaled by 32:\n// a shallow block (endpoints ~1/255 apart) has dd = dot(dir,dir) \u2248 1.5e-5,\n// where 15/dd \u2248 10\u2076 overflows f16 (max 65504) to +inf and the products\n// inside the projection dot are subnormal \u2014 the indices turn to garbage\n// (visible as banding on smooth gradients). Scaling dir by 32 multiplies\n// the dots by 32 and dd by 1024; s = dot\xB7(32\xB7L/dd\u2083\u2082) is the same\n// quantity with every intermediate in f16's normal range (worst case\n// inv = 480/0.0157 \u2248 3.0e4 < 65504).\n// \u2022 TWO MODES (mode 4 OPT-IN via the enable_mode4 override constant,\n// default off and dead-coded at pipeline creation \u2014 see the constant's\n// comment for the measured cost/benefit), decided per block BEFORE\n// encoding \u2014 never encoded both:\n// mode 6 (single RGBA line, 4-bit indices) by default, mode 4 (rotation:\n// one channel split into its own scalar plane with 3-bit indices, the\n// remaining three on a 2-bit line) when the principal axis leaves a\n// large share of the block's variance unexplained \u2014 decorrelated data\n// (normal maps, channel-packed atlases) where any single 4-D line fails.\n// The decision reads the covariance already in registers (\u03BB = axis\u1D40Ca,\n// residual = trace \u2212 \u03BB) and costs no extra pass. An encode-both-and-\n// compare trial was priced at ~2\xD7 on exactly this content (see the mode\n// 1 postmortem below) \u2014 deciding first keeps it at ~1.2\xD7.\n// \u2022 The two modes SHARE the per-pixel passes (axis matvecs, projection\n// extents, the index/weight pass runs once with per-thread level count,\n// index width and packing split) so warps holding a mix of mode-4 and\n// mode-6 blocks do not execute two disjoint kernels back to back \u2014 a\n// first cut with separate per-mode passes measured 1.77\xD7 on normal maps\n// from exactly that divergence; the only mode-4-extra 16-pixel work is\n// the cheap scalar-plane pass.\n// \u2022 GRAY + opaque blocks (every texel R == G == B, A == 1) have their\n// principal axis analytically: (1,1,1,0)/\u221A3, with projection extents at\n// the luma min/max. They skip the power iteration AND the extents pass\n// (\u221234% GPU on roughness/AO/displacement content) and always take\n// mode 6 \u2014 a gray single line fits gray data exactly.\n// \u2022 NO least-squares refit, unlike the BC1/BC5/ASTC fast paths: with the\n// seed already on the principal axis at the exact projection extents,\n// mode 6's fine 16-level palette leaves the refit \u22640.05 dB on the colour\n// card, \u22640.15 dB on the normal card and +0.03 dB on the channel-packed\n// packed-materials atlas \u2014 not worth its two extra 16-pixel passes. The\n// coarse 4-level formats DO need it (dropping it there costs 0.5\u20131.3 dB).\n// \u2022 A MODE 1 (2-subset) candidate was built and evaluated (2026-07): it\n// buys ~+1.3 dB on multi-modal content but its candidate evaluation\n// costs up to ~3\xD7 the mode-6 pass on exactly that content \u2014 dropped in\n// favour of the decided (not compared) mode 4 above, which covers the\n// decorrelated-channel share of that content at a fraction of the cost.\n// The CPU reference decoder keeps mode 1 support (bc7_ref.ts).\n// \u2022 Indices are packed into two u32 words ON THE FLY during the\n// projection pass \u2014 no array<u32,16> private array. The BC7 anchor\n// reflection is then just a bitwise NOT of the packed words.\n// \u2022 The 128-bit block is assembled with straight-line constant shifts\n// instead of a generic write_bits() helper (whose dynamic word indexing\n// defeats register promotion of the output array).\n//\n// The host selects this module only when the device reports shader-f16,\n// falling back to bc7.wgsl otherwise.\n//\n// MODE 6 BIT LAYOUT (LSB-first): see bc7.wgsl. Summary:\n// w0: mode(7 bits, 0x40) R0 R1 G0 G1[3:0]\n// w1: G1[6:4] B0 B1 A0 A1 P0\n// w2: P1, pixel0 index (3 bits), pixels 1..7 (4 bits each)\n// w3: pixels 8..15 (4 bits each)\n// MODE 4 BIT LAYOUT (LSB-first): mode 0b00001, rotation @5 (channel swapped\n// with alpha), idxMode @7 (0 = colour \u2192 2-bit set, scalar \u2192 3-bit set),\n// colour endpoints 6\xD75 bits @8, alpha endpoints 2\xD76 @38, 31-bit 2-bit index\n// field @50 (pixel 0 anchored to 1 bit), 47-bit 3-bit index field @81\n// (pixel 0 anchored to 2 bits). Validated bit-exact against hardware\n// bc7-rgba-unorm sampling; decode reference in bc7_ref.ts.\nenable f16;\nstruct Params { blocks_x: u32, blocks_y: u32, width: u32, height: u32, };\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\nalias h = f16;\nalias h4 = vec4<f16>;\n\n// Mode-4 gate: encode mode 4 when the principal axis leaves more than\n// MODE4_THETA of the (\xD7256-scaled) total variance unexplained and the block\n// isn't near-flat. Tuned against per-content mode histograms and PSNR.\nconst MODE4_THETA: f16 = 0.2;\nconst MODE4_FLOOR: f16 = 1.0;\nconst MODE4_CONC: f16 = 0.5;\n\n// OPT-IN adaptive mode 4, folded at pipeline creation (WebGPU override\n// constant; default OFF dead-codes the whole path \u2014 measured at exact par\n// with the mode-6-only kernel). Rationale: the quality is real (+2.5\u20132.9 dB\n// on normal maps, +1.9\u20132.4 on channel-packed atlases) but any warp holding\n// one mode-4 block executes both modes' passes, and content that benefits\n// runs 1.4\u20131.5\xD7; a \u03B8 sweep showed quality and warp-poisoning scale together\n// (no per-block middle ground without subgroup ballots). So the trade is\n// the CALLER's: BC7Encoder({ adaptiveMode4: true }).\noverride enable_mode4: bool = false;\n\n// Quantise an ideal endpoint (h4 in [0,1]) to 7-bit + p-bit, choosing the\n// p-bit with the lower quantisation error. `eight` is the decoded value the\n// hardware will interpolate with, back in [0,1].\nstruct Ep { seven: vec4<u32>, eight: h4, p: u32 };\nfn pick_ep(ideal01: h4) -> Ep {\n let ideal = ideal01 * h(255.0);\n let q0 = clamp(floor(ideal * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=0\n let e0 = q0 * h(2.0);\n let q1 = clamp(floor((ideal - h(1.0)) * h(0.5) + h(0.5)), h4(0.0), h4(127.0)); // p=1\n let e1 = q1 * h(2.0) + h(1.0);\n let d0 = e0 - ideal; let d1 = e1 - ideal;\n if (dot(d1, d1) < dot(d0, d0)) { return Ep(vec4<u32>(q1), e1 * h(1.0 / 255.0), 1u); }\n return Ep(vec4<u32>(q0), e0 * h(1.0 / 255.0), 0u);\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }\n let bi = gid.y * params.blocks_x + gid.x;\n let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let mx = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Load pass, with the covariance moments FUSED in (no separate 16-pixel\n // pass): d = (px \u2212 pixel0)\xB716, relative to the block's first pixel so the\n // accumulators scale with the block's span \u2014 raw \u03A3v\xB7v\u1D40 moments would\n // cancel catastrophically in f16 \u2014 and pre-scaled \xD716 so shallow blocks\n // (span ~1/255 \u2192 d\xB2 \u2248 1e-3) clear the subnormal floor while full-range\n // sums stay \u22644096. C = \u03A3dd\u1D40 \u2212 (\u03A3d)(\u03A3d)\u1D40/16 is the \xD7256-scaled covariance.\n var pix: array<h4, 16>;\n var lo = h4(1.0);\n var hi = h4(0.0);\n var gd = h(0.0);\n var p0v = h4(0.0);\n var sd = h4(0.0);\n var c0v = h4(0.0);\n var c1v = h4(0.0);\n var c2v = h4(0.0);\n var c3v = h4(0.0);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0), mx);\n let px = h4(textureLoad(src_tex, p, 0));\n pix[i] = px; lo = min(lo, px); hi = max(hi, px);\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n if (i == 0u) { p0v = px; }\n let d = (px - p0v) * h(16.0);\n sd = sd + d;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n let mean = p0v + sd * h(1.0 / 256.0);\n // Mean-correction via sd4\xB7sd4\u1D40 with sd4 = \u03A3d/4: (\u03A3d)(\u03A3d)\u1D40/16 with every\n // product \u22644096 (a direct \u03A3d\xB7\u03A3d\u1D40 could hit 65536 and overflow f16).\n let sd4 = sd * h(0.25);\n c0v = c0v - sd4.x * sd4;\n c1v = c1v - sd4.y * sd4;\n c2v = c2v - sd4.z * sd4;\n c3v = c3v - sd4.w * sd4;\n\n // Seed endpoints + per-block mode decision (see header).\n var seed_lo = lo;\n var seed_hi = hi;\n var use4 = false;\n var cmask = h4(1.0);\n var ch = 0u;\n if (lo.w == h(1.0) && gd == h(0.0)) {\n // GRAY + opaque: analytic axis (1,1,1,0)/\u221A3, extents at luma min/max,\n // always mode 6 \u2014 and a fully specialised tail: gray textures are\n // warp-uniform, and routing them through the parametric shared loop\n // below (runtime index width/split) measured +28% on displacement\n // content purely from the lost constant-shift codegen.\n var ep0g = pick_ep(h4(lo.x, lo.x, lo.x, h(1.0)));\n var ep1g = pick_ep(h4(hi.x, hi.x, hi.x, h(1.0)));\n var ilo = 0u;\n var ihi = 0u;\n let dirg = (ep1g.eight - ep0g.eight) * h(32.0);\n let ddg = dot(dirg, dirg);\n if (ddg >= h(0.008)) {\n let invg = h(480.0) / ddg;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ilo = ilo | (u32(sg) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let sg = clamp(floor(dot(pix[k] - ep0g.eight, dirg) * invg + h(0.5)), h(0.0), h(15.0));\n ihi = ihi | (u32(sg) << ((k - 8u) * 4u));\n }\n }\n if ((ilo & 0x8u) != 0u) {\n let t = ep0g; ep0g = ep1g; ep1g = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0g = ep0g.seven;\n let e1g = ep1g.seven;\n let og = bi * 4u;\n dst[og] = 0x40u | (e0g.x << 7u) | (e1g.x << 14u) | (e0g.y << 21u) | (e1g.y << 28u);\n dst[og + 1u] = (e1g.y >> 4u) | (e0g.z << 3u) | (e1g.z << 10u) | (e0g.w << 17u) | (e1g.w << 24u) | (ep0g.p << 31u);\n dst[og + 2u] = ep1g.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[og + 3u] = ihi;\n return;\n }\n {\n var axis = hi - lo;\n var axis_ok = true;\n // 8 iterations: 4 was under-converged on noisy 4-D blocks (heavily\n // downscaled photographic/channel-packed content) \u2014 going to 8 measured\n // +0.75 dB on the normal card, +0.12 colour, +0.08 packed-materials, and\n // matches the f32 fallback's iteration count. Four extra 4-dot matvecs\n // per block are noise next to the index pass.\n for (var it: u32 = 0u; it < 8u; it = it + 1u) {\n let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { axis_ok = false; break; }\n axis = nv / m;\n }\n if (axis_ok) {\n axis = axis / length(axis);\n var axisF = axis;\n\n // Mode decision from the covariance already in registers: \u03BB is the\n // variance the mode-6 line explains, trace \u2212 \u03BB what it cannot.\n let Ca = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));\n let lam = dot(Ca, axis);\n let diag = h4(c0v.x, c1v.y, c2v.z, c3v.w);\n let trace = diag.x + diag.y + diag.z + diag.w;\n let resid = trace - lam;\n let rc = diag - lam * axis * axis;\n var rbest = rc.x;\n if (rc.y > rbest) { ch = 1u; rbest = rc.y; }\n if (rc.z > rbest) { ch = 2u; rbest = rc.z; }\n if (rc.w > rbest) { ch = 3u; rbest = rc.w; }\n use4 = enable_mode4 && resid > MODE4_THETA * trace && trace > MODE4_FLOOR && rbest > MODE4_CONC * resid;\n if (use4) {\n // The colour plane is the remaining three channels, handled as\n // masked 4-vectors so every vec4 pass below applies unchanged.\n // Branchless mask build \u2014 a dynamic component store spills the\n // vector to scratch on some compilers.\n cmask = h4(1.0) - h4(h(f32(u32(ch == 0u))), h(f32(u32(ch == 1u))), h(f32(u32(ch == 2u))), h(f32(u32(ch == 3u))));\n var a3 = (hi - lo) * cmask;\n var ok3 = true;\n for (var it: u32 = 0u; it < 2u; it = it + 1u) {\n let nv = h4(dot(c0v, a3), dot(c1v, a3), dot(c2v, a3), dot(c3v, a3)) * cmask;\n let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));\n if (m < h(1e-4)) { ok3 = false; break; }\n a3 = nv / m;\n }\n if (ok3) {\n axisF = a3 / length(a3);\n } else {\n use4 = false;\n cmask = h4(1.0);\n }\n }\n\n // Exact projection extents along the fit axis \u2014 ONE shared pass for\n // both modes (for mode 4 axisF[ch] = 0, so the scalar plane is\n // invisible to it). (A Rayleigh-quotient span estimate was tried in\n // place of this pass \u2014 it saves 16 dots but costs 0.1\u20130.8 dB and\n // 4\u201310\xD7 on the worst-easy-block gate.)\n var t_min = h(4.0);\n var t_max = h(-4.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(pix[k] - mean, axisF);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed_lo = clamp(mean + t_min * axisF, h4(0.0), h4(1.0));\n seed_hi = clamp(mean + t_max * axisF, h4(0.0), h4(1.0));\n }\n }\n\n // Endpoints, per mode. d0/d1 are the DECODED values the weight pass\n // projects against.\n var ep0: Ep;\n var ep1: Ep;\n var q0c = vec4<u32>(0u);\n var q1c = vec4<u32>(0u);\n var A0 = 0u;\n var A1 = 0u;\n var iA = 0u;\n var iB = 0u;\n var d0: h4;\n var d1: h4;\n var d0a = h(0.0);\n var sca = h(0.0);\n let chs = h4(1.0) - cmask;\n ep0 = pick_ep(seed_lo);\n ep1 = pick_ep(seed_hi);\n d0 = ep0.eight;\n d1 = ep1.eight;\n if (use4) {\n // Scalar plane (3-bit index set): 6-bit endpoints at the channel's\n // exact extremes. Its projection is FUSED into the shared weight pass\n // below \u2014 a separate 16-pixel pass here measured +46% on normal maps\n // (mixed warps paid it wholesale); fused, the marginal cost is one dot\n // per pixel under a warp-uniform predicate.\n let a0q = u32(floor(dot(lo, chs) * h(63.0) + h(0.5)));\n let a1q = u32(floor(dot(hi, chs) * h(63.0) + h(0.5)));\n A0 = a0q;\n A1 = a1q;\n let d0av = h(f32((a0q << 2u) | (a0q >> 4u))) * h(1.0 / 255.0);\n let d1av = h(f32((a1q << 2u) | (a1q >> 4u))) * h(1.0 / 255.0);\n let aspan = d1av - d0av;\n if (aspan > h(0.001)) {\n d0a = d0av;\n sca = h(7.0) / aspan;\n }\n // Colour plane: 5-bit endpoints from the masked extents seed.\n q0c = vec4<u32>(clamp(floor(seed_lo * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n q1c = vec4<u32>(clamp(floor(seed_hi * h(31.0) + h(0.5)), h4(0.0), h4(31.0)));\n d0 = h4(vec4<f32>((q0c << vec4<u32>(3u)) | (q0c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n d1 = h4(vec4<f32>((q1c << vec4<u32>(3u)) | (q1c >> vec4<u32>(2u)))) * h(1.0 / 255.0) * cmask;\n }\n\n // Index/weight pass: per-mode SPECIALISED loops (constant level counts\n // and shifts, so each unrolls cleanly \u2014 a single parametric loop with\n // runtime width/split measured +22% on pure mode-6 photo content).\n // Mixed warps execute both loops; the mode-4 one carries the fused\n // scalar-plane projection. For mode 4 pix[ch]\xB7dir[ch] = 0, so the\n // scalar plane never perturbs the colour projection.\n var a_lo = 0u;\n var a_hi = 0u;\n // Same \xD732 pre-scale as the extents math; distinct quantised endpoints\n // are \u22651/255 apart (dd\u2083\u2082 \u2265 0.0157), so the flat-block threshold only\n // catches truly identical ones.\n let dir = (d1 - d0) * h(32.0);\n let dd = dot(dir, dir);\n let live = dd >= h(0.008);\n if (use4) {\n if (live) {\n let inv = h(96.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iA = iA | (u32(sv) << (k * 3u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0));\n let sv = clamp(floor((dot(pix[k], chs) - d0a) * sca + h(0.5)), h(0.0), h(7.0));\n a_lo = a_lo | (u32(s) << (k * 2u));\n iB = iB | (u32(sv) << ((k - 8u) * 3u));\n }\n }\n } else if (live) {\n let inv = h(480.0) / dd;\n for (var k: u32 = 0u; k < 8u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_lo = a_lo | (u32(s) << (k * 4u));\n }\n for (var k: u32 = 8u; k < 16u; k = k + 1u) {\n let s = clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(15.0));\n a_hi = a_hi | (u32(s) << ((k - 8u) * 4u));\n }\n }\n\n // Anchors + packing. Mode 6 packs unconditionally (one-sided branches\n // compile better than two-sided divergence); mode-4 threads overwrite.\n let o = bi * 4u;\n {\n var ilo = a_lo;\n var ihi = a_hi;\n if ((ilo & 0x8u) != 0u) {\n let t = ep0; ep0 = ep1; ep1 = t;\n ilo = ~ilo; ihi = ~ihi;\n }\n let e0 = ep0.seven;\n let e1 = ep1.seven;\n dst[o] = 0x40u | (e0.x << 7u) | (e1.x << 14u) | (e0.y << 21u) | (e1.y << 28u);\n dst[o + 1u] = (e1.y >> 4u) | (e0.z << 3u) | (e1.z << 10u) | (e0.w << 17u) | (e1.w << 24u) | (ep0.p << 31u);\n dst[o + 2u] = ep1.p | ((ilo & 0x7u) << 1u) | (ilo & 0xFFFFFFF0u);\n dst[o + 3u] = ihi;\n }\n if (use4) {\n // 3-bit anchor: pixel 0's MSB must be 0; reflect = bitwise NOT.\n if ((iA & 4u) != 0u) {\n let tA = A0;\n A0 = A1;\n A1 = tA;\n iA = ~iA & 0xFFFFFFu;\n iB = ~iB & 0xFFFFFFu;\n }\n var c2 = a_lo;\n // 2-bit anchor: pixel 0's MSB must be 0.\n if ((c2 & 2u) != 0u) {\n let tq = q0c;\n q0c = q1c;\n q1c = tq;\n c2 = ~c2;\n }\n // Rotated-space RGB: position ch carries the original alpha.\n let R0 = select(q0c.x, q0c.w, ch == 0u);\n let G0 = select(q0c.y, q0c.w, ch == 1u);\n let B0 = select(q0c.z, q0c.w, ch == 2u);\n let R1 = select(q1c.x, q1c.w, ch == 0u);\n let G1 = select(q1c.y, q1c.w, ch == 1u);\n let B1 = select(q1c.z, q1c.w, ch == 2u);\n let rot = (ch + 1u) & 3u;\n // Index fields drop the anchors' MSBs: 31 bits (2-bit set) and 47 bits\n // (3-bit set).\n let field2 = (c2 & 1u) | ((c2 >> 2u) << 1u);\n let f_lo = (iA & 3u) | ((iA >> 3u) << 2u) | (iB << 23u);\n let f_hi = iB >> 9u;\n dst[o] = 0x10u | (rot << 5u) | (R0 << 8u) | (R1 << 13u) | (G0 << 18u) | (G1 << 23u) | (B0 << 28u);\n dst[o + 1u] = (B0 >> 4u) | (B1 << 1u) | (A0 << 6u) | (A1 << 12u) | ((field2 & 0x3FFFu) << 18u);\n dst[o + 2u] = (field2 >> 14u) | (f_lo << 17u);\n dst[o + 3u] = (f_lo >> 15u) | (f_hi << 17u);\n }\n}\n";
|
|
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";
|
|
1511
1360
|
|
|
1512
1361
|
// src/BC7Encoder.ts
|
|
1513
1362
|
var BC7Encoder = class extends Encoder {
|
|
@@ -1544,80 +1393,93 @@ var BC7Encoder = class extends Encoder {
|
|
|
1544
1393
|
};
|
|
1545
1394
|
|
|
1546
1395
|
// src/astc4x4.wgsl
|
|
1547
|
-
var astc4x4_default = "// ASTC 4\xD74 LDR compute shader encoder.\n//\n// One invocation per 4\xD74 block. Emits 16 bytes = 4 u32s into the storage\n// buffer at `dst[block_index * 4 .. + 3]`. This is the f32 fallback;\n// astc4x4_fast_f16.wgsl is the same algorithm and is preferred when the\n// device reports shader-f16.\n//\n// ALGORITHM: per-block class selection, then a single-line fit:\n// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, block mode 0x253 \u2014\n// scalar path: exact min/max endpoints, 32-level weight\n// assignment, no covariance / iteration / refit needed\n// opaque \u2192 CEM 8 (RGB), 3-bit weights, block mode 0x053\n// translucent \u2192 CEM 12 (RGBA), 2-bit weights, block mode 0x042\n// Colour paths: principal-axis seed (covariance power-iteration; bbox on\n// degenerate blocks) at the exact projection extents \u2192 one fused pass that\n// projects each pixel onto the endpoint line (the palette entries are\n// colinear, so the nearest is the rounded projection \u2014 no per-entry search)\n// while accumulating the least-squares refit sums, then a reprojection\n// against the quantised refit endpoints with the weights packed on the fly.\n// The endpoint ordering rule is applied before the weight pass, so no\n// reflection is needed.\n//\n// RESTRICTED SUBSET + BLOCK LAYOUT: see astc4x4_ref.ts (single partition,\n// no dual-plane, CEM 0/8/12, 8-bit endpoints, plain-bit weight ISE; block\n// mode derivations and the weight-stream bit order are documented there).\n//\n// WEIGHT PLACEMENT: stream bit q (bit j of weight k, q = nBits\xB7k + j) lives\n// at block bit 127 \u2212 q, so a stream word assembled LSB-first maps onto a\n// block word with a single reverseBits().\n//\n// ENDPOINT ORDERING: CEM 8/12 decoders branch into blue contraction when\n// sum(e0.rgb) > sum(e1.rgb); the encoder swaps endpoints up front (weights\n// are assigned after the swap, so no reflection pass). CEM 0 has no rule\n// (L0 \u2264 L1 by construction from min/max).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nfn to8(v: vec4<f32>) -> vec4<i32> {\n return vec4<i32>(clamp(floor(v * 255.0 + 0.5), vec4<f32>(0.0), vec4<f32>(255.0)));\n}\n\n// One pass over the block: project every pixel onto the e0\u2192e1 line\n// (lmax + 1 colinear levels, so the nearest entry is the rounded\n// projection) and accumulate the least-squares normal-equation sums;\n// solve for the refit endpoints. Weights are not produced here \u2014 the\n// caller reprojects against the quantised refit endpoints anyway.\nstruct Fit { e0: vec4<i32>, e1: vec4<i32>, valid: bool, wstream: u32 };\nfn proj_fit(pixels: ptr<function, array<vec4<i32>, 16>>, e0: vec4<i32>, e1: vec4<i32>) -> Fit {\n var out: Fit;\n out.valid = false;\n out.wstream = 0u;\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n if (dd == 0.0) { return out; }\n let e0f = vec4<f32>(e0);\n let inv = 3.0 / dd;\n var sAA: f32 = 0.0; var sBB: f32 = 0.0; var sAB: f32 = 0.0;\n var sAV: vec4<f32> = vec4<f32>(0.0); var sBV: vec4<f32> = vec4<f32>(0.0);\n var s_min = 3.0; var s_max = 0.0;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let v = vec4<f32>((*pixels)[k]);\n let s = clamp(floor(dot(v - e0f, dir) * inv + 0.5), 0.0, 3.0);\n out.wstream = out.wstream | (u32(s) << (2u * k));\n s_min = min(s_min, s); s_max = max(s_max, s);\n let b = s * (1.0 / 3.0); let a = 1.0 - b;\n sAA = sAA + a * a; sBB = sBB + b * b; sAB = sAB + a * b;\n sAV = sAV + a * v; sBV = sBV + b * v;\n }\n // Rank-1 guard: if every pixel projects to ONE level the system is\n // singular \u2014 det and the numerators are pure float rounding noise and the\n // solve returns garbage endpoints. With \u22652 levels det \u2265 15\xB7(1/3)\xB2 \u2248 1.67.\n if (s_min == s_max) { return out; }\n let det = sAA * sBB - sAB * sAB;\n if (abs(det) < 1e-3) { return out; }\n out.e0 = vec4<i32>(clamp(round((sBB * sAV - sAB * sBV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.e1 = vec4<i32>(clamp(round((sAA * sBV - sAB * sAV) / det), vec4<f32>(0.0), vec4<f32>(255.0)));\n out.valid = true;\n return out;\n}\n\n// Principal colour axis via covariance power-iteration (RGBA, 8-bit integer\n// pixel domain), seeded with the bbox diagonal. Returns a unit axis, or\n// vec4(0) for a degenerate (constant) block. Used to seed the LSQ fit \u2014 the\n// bbox diagonal is sign-blind and points across anti-correlated data (normal\n// maps, hue edges) instead of along it.\nfn principal_axis4(\n pixels: ptr<function, array<vec4<i32>, 16>>,\n mean: vec4<f32>,\n seed: vec4<f32>,\n iters: u32,\n) -> vec4<f32> {\n var c0v = vec4<f32>(0.0);\n var c1v = vec4<f32>(0.0);\n var c2v = vec4<f32>(0.0);\n var c3v = vec4<f32>(0.0);\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let d = vec4<f32>((*pixels)[k]) - mean;\n c0v = c0v + d.x * d;\n c1v = c1v + d.y * d;\n c2v = c2v + d.z * d;\n c3v = c3v + d.w * d;\n }\n var v = seed;\n var len = length(v);\n if (len < 1e-9) { return vec4<f32>(0.0); }\n v = v / len;\n for (var iter: u32 = 0u; iter < iters; iter = iter + 1u) {\n let nv = vec4<f32>(dot(c0v, v), dot(c1v, v), dot(c2v, v), dot(c3v, v));\n len = length(nv);\n if (len < 1e-12) { return vec4<f32>(0.0); }\n v = nv / len;\n }\n return v;\n}\n\n// ------------------------------- Entry ---------------------------------- //\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let bx = gid.x;\n let by = gid.y;\n let block_index = by * params.blocks_x + bx;\n\n let base = vec2<i32>(i32(bx) * 4, i32(by) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var pixels: array<vec4<i32>, 16>;\n var lo = vec4<i32>(255);\n var hi = vec4<i32>(0);\n var isum = vec4<i32>(0);\n var gd = 0; // max |R\u2212G|, |R\u2212B| over the block; 0 \u21D4 exactly grayscale\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let p = clamp(base + vec2<i32>(i32(i & 3u), i32(i >> 2u)), vec2<i32>(0, 0), max_xy);\n let px = to8(textureLoad(src_tex, p, 0));\n pixels[i] = px;\n lo = min(lo, px);\n hi = max(hi, px);\n isum = isum + px;\n gd = max(gd, max(abs(px.x - px.y), abs(px.x - px.z)));\n }\n let opaque = lo.w == 255;\n\n var w0: u32; var w1: u32; var w2: u32; var w3: u32;\n\n if (opaque && gd == 0) {\n // ---------------- Luminance path: CEM 0, 5-bit weights ----------------\n // Endpoints at the exact extremes; 32 palette levels make an LSQ refit\n // unnecessary.\n let L0 = u32(lo.x);\n let L1 = u32(hi.x);\n var s0 = 0u; var s1 = 0u; var s2 = 0u;\n if (L1 > L0) {\n let sc = 64.0 / f32(hi.x - lo.x);\n // Exact nearest entry of the QUANT_32 grid: unq = 2w for w \u2264 15,\n // 2w + 2 for w \u2265 16 (4-wide gap at the middle, so uniform rounding\n // is wrong there). Best candidate of each half, keep the closer.\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let u = clamp(f32(pixels[k].x - lo.x) * sc, 0.0, 64.0);\n let wlo = clamp(floor(u * 0.5 + 0.5), 0.0, 15.0);\n let whi = clamp(floor((u - 2.0) * 0.5 + 0.5), 16.0, 31.0);\n let pick = abs(u - wlo * 2.0) <= abs(u - (whi * 2.0 + 2.0));\n let w = u32(select(whi, wlo, pick));\n // Stream bit q = 5k + j; straddles handled with constant shifts.\n let off = 5u * k;\n if (off < 28u) { s0 = s0 | (w << off); }\n else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }\n else if (off < 60u) { s1 = s1 | (w << (off - 32u)); }\n else if (off == 60u) { s1 = s1 | (w << 28u); s2 = s2 | (w >> 4u); }\n else { s2 = s2 | (w << (off - 64u)); }\n }\n }\n // Mode 0x253, partitions\u22121 = 0, CEM 0, L0 @17, L1 @25 (top bit spills\n // into w1 bit 0); stream words map onto block words via reverseBits.\n w0 = 0x253u | (L0 << 17u) | (L1 << 25u);\n w1 = (L1 >> 7u) | reverseBits(s2);\n w2 = reverseBits(s1);\n w3 = reverseBits(s0);\n } else {\n // ------------- Colour paths: shared PCA seed ---------------------------\n let mean = vec4<f32>(isum) * (1.0 / 16.0);\n\n // Fused LSQ fit seeded from the block's principal colour axis at the\n // exact projection extents, quantised refit endpoints, ordering applied\n // BEFORE the weight pass so no reflection is needed.\n // The refit is clamped to the block bbox: on multi-cluster blocks the\n // unconstrained solve extrapolates far outside the block's colours and\n // the per-channel [0,255] clamp then bends the hue \u2014 fringe pixels\n // decode to colours that exist nowhere in the block. Constraining to\n // the bbox also measures better in plain SSE (+1.8 dB on the colour\n // test card).\n var seed0 = lo;\n var seed1 = hi;\n // 8 iterations for opaque blocks (the axis is the endpoint quality\n // there), 4 for translucent ones whose LSQ refit absorbs residual\n // axis error (see astc4x4_fast_f16.wgsl).\n let axis = principal_axis4(&pixels, mean, vec4<f32>(hi - lo), select(4u, 8u, opaque));\n if (dot(axis, axis) > 0.0) {\n var t_min: f32 = 1e30;\n var t_max: f32 = -1e30;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let t = dot(vec4<f32>(pixels[k]) - mean, axis);\n t_min = min(t_min, t);\n t_max = max(t_max, t);\n }\n seed0 = vec4<i32>(clamp(round(mean + t_min * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n seed1 = vec4<i32>(clamp(round(mean + t_max * axis), vec4<f32>(0.0), vec4<f32>(255.0)));\n }\n // Opaque blocks (CEM 8, 8-level weights) ship the quantised PCA\n // extents directly; only the translucent CEM 12 path refits its coarse\n // 4-level grid (see astc4x4_fast_f16.wgsl for the measured trade).\n var e0 = lo;\n var e1 = hi;\n var fitStream = 0u;\n var haveFitWeights = false;\n if (opaque) {\n // Bbox-clamped like the fit output (see astc4x4_fast_f16.wgsl).\n e0 = clamp(seed0, lo, hi);\n e1 = clamp(seed1, lo, hi);\n } else {\n let r = proj_fit(&pixels, seed0, seed1);\n if (r.valid) {\n e0 = clamp(r.e0, lo, hi);\n e1 = clamp(r.e1, lo, hi);\n fitStream = r.wstream;\n haveFitWeights = true;\n }\n }\n var swapped = false;\n if (e0.x + e0.y + e0.z > e1.x + e1.y + e1.z) {\n let tmp = e0; e0 = e1; e1 = tmp;\n swapped = true;\n }\n let E0 = vec4<u32>(e0);\n let E1 = vec4<u32>(e1);\n\n // Weight pass against the final endpoints, packing on the fly.\n let dir = vec4<f32>(e1 - e0);\n let dd = dot(dir, dir);\n let e0f = vec4<f32>(e0);\n if (opaque) {\n // CEM 8: 3-bit weights, stream bit q = 3k.\n var s0 = 0u; var s1 = 0u;\n if (dd > 0.0) {\n let inv = 7.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 7.0));\n let off = 3u * k;\n if (off < 30u) { s0 = s0 | (w << off); }\n else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }\n else { s1 = s1 | (w << (off - 32u)); }\n }\n }\n // Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.\n w0 = 0x053u | (8u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n w2 = (E1.z >> 7u) | reverseBits(s1);\n w3 = reverseBits(s0);\n } else {\n // CEM 12: 2-bit weights, stream bit q = 2k (single stream word).\n // Valid fits ship the fit-pass weights; the blue-contraction swap is\n // a full reflection w \u2192 3\u2212w = bitwise NOT of the packed stream (see\n // astc4x4_fast_f16.wgsl for the measured trade).\n var s0 = 0u;\n if (haveFitWeights) {\n s0 = select(fitStream, ~fitStream, swapped);\n } else if (dd > 0.0) {\n let inv = 3.0 / dd;\n for (var k: u32 = 0u; k < 16u; k = k + 1u) {\n let w = u32(clamp(floor(dot(vec4<f32>(pixels[k]) - e0f, dir) * inv + 0.5), 0.0, 3.0));\n s0 = s0 | (w << (2u * k));\n }\n }\n // Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.\n w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);\n w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);\n w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);\n w3 = reverseBits(s0);\n }\n }\n\n let out = block_index * 4u;\n dst[out + 0u] = w0;\n dst[out + 1u] = w1;\n dst[out + 2u] = w2;\n dst[out + 3u] = w3;\n}\n";
|
|
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";
|
|
1548
1397
|
|
|
1549
1398
|
// src/astc4x4_fast_f16.wgsl
|
|
1550
|
-
var astc4x4_fast_f16_default = `// astc4x4
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
// 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:
|
|
1554
1402
|
//
|
|
1555
1403
|
// \u2022 THREE block classes, picked per block from the loaded pixels (the
|
|
1556
1404
|
// ASTC bit budget trades endpoint bits against weight bits, so a block
|
|
1557
1405
|
// only pays for the channels it uses):
|
|
1558
1406
|
// gray + opaque \u2192 CEM 0 (luminance), 5-bit weights, mode 0x253
|
|
1559
|
-
// opaque \u2192 CEM 8 (RGB),
|
|
1560
|
-
//
|
|
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
|
|
1561
1413
|
// "gray" = every texel R == G == B exactly (f16 equality is exact for
|
|
1562
|
-
// 8-bit sources), "opaque" = every texel A == 1.
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
// (
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
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).
|
|
1579
1451
|
// \u2022 Endpoint ordering (the blue-contraction rule: sum(e0.rgb) must not
|
|
1580
|
-
// exceed sum(e1.rgb)
|
|
1581
|
-
// 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.
|
|
1582
1454
|
// \u2022 Weight streams are accumulated LSB-first into u32 words and placed
|
|
1583
1455
|
// into the block's reversed-bit-order field with reverseBits() \u2014
|
|
1584
|
-
// stream bit q lives at block bit 127 \u2212 q
|
|
1585
|
-
// maps onto a block word with a single bit reversal.
|
|
1586
|
-
// \u2022 The covariance moments stay in their OWN pass, deliberately: fusing
|
|
1587
|
-
// them into the load loop bc7-style (pixel-0 residuals, with or without
|
|
1588
|
-
// hoisting pixel 0 out of the loop) measured +5% GPU time on 4096\xB2
|
|
1589
|
-
// (/ab A/B, 2026-07, M3) \u2014 the separate loop overlaps the 16 texture
|
|
1590
|
-
// loads' latency better than a longer in-loop dependency chain does.
|
|
1591
|
-
// Per-pass cost on the same rig, for future tuning: covariance+power-
|
|
1592
|
-
// iteration \u2248 18%, LSQ fit pass \u2248 24%, extents pass \u2248 6% of the kernel;
|
|
1593
|
-
// @workgroup_size 16\xD78 measured exactly at par with 8\xD78.
|
|
1456
|
+
// stream bit q lives at block bit 127 \u2212 q.
|
|
1594
1457
|
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
// 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).
|
|
1598
1461
|
//
|
|
1599
1462
|
// The host selects this module only when the device reports shader-f16,
|
|
1600
1463
|
// falling back to astc4x4.wgsl otherwise.
|
|
1601
1464
|
enable f16;
|
|
1602
1465
|
alias h = f16;
|
|
1466
|
+
alias h3 = vec3<f16>;
|
|
1603
1467
|
alias h4 = vec4<f16>;
|
|
1604
|
-
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, };
|
|
1605
1469
|
@group(0) @binding(0) var src_tex: texture_2d<f32>;
|
|
1606
1470
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
1607
1471
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
1608
1472
|
|
|
1609
1473
|
// Fused projection + least-squares refit against the 4-level QUANT_4
|
|
1610
|
-
// palette
|
|
1611
|
-
//
|
|
1612
|
-
// 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.
|
|
1613
1476
|
struct Fit { e0: h4, e1: h4, valid: bool, wstream: u32 };
|
|
1614
1477
|
fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
1615
1478
|
var out: Fit;
|
|
1616
1479
|
out.valid = false;
|
|
1617
1480
|
out.wstream = 0u;
|
|
1618
|
-
//
|
|
1619
|
-
//
|
|
1620
|
-
// 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.
|
|
1621
1483
|
let dir = (e1 - e0) * h(32.0);
|
|
1622
1484
|
let dd = dot(dir, dir);
|
|
1623
1485
|
if (dd < h(0.008)) { return out; }
|
|
@@ -1625,7 +1487,7 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1625
1487
|
var sAA = h(0.0); var sBB = h(0.0); var sAB = h(0.0);
|
|
1626
1488
|
var sAV = h4(0.0); var sBV = h4(0.0);
|
|
1627
1489
|
var s_min = h(3.0); var s_max = h(0.0);
|
|
1628
|
-
// 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
|
|
1629
1491
|
// commutes with the shift): accumulators scale with the block span, keeping
|
|
1630
1492
|
// f16 rounding a fraction of the span instead of \xB11 level at high absolute
|
|
1631
1493
|
// values.
|
|
@@ -1640,9 +1502,9 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1640
1502
|
sAV = sAV + a * vr; sBV = sBV + b * vr;
|
|
1641
1503
|
}
|
|
1642
1504
|
// Rank-1 guard: if every pixel projects to ONE level the system is
|
|
1643
|
-
// singular \u2014 det/numerators are pure f16 rounding noise
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
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.
|
|
1646
1508
|
if (s_min == s_max) { return out; }
|
|
1647
1509
|
let det = sAA * sBB - sAB * sAB;
|
|
1648
1510
|
if (abs(det) < h(0.5)) { return out; }
|
|
@@ -1652,12 +1514,39 @@ fn proj_fit(pix: ptr<function, array<h4, 16>>, e0: h4, e1: h4) -> Fit {
|
|
|
1652
1514
|
return out;
|
|
1653
1515
|
}
|
|
1654
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
|
+
|
|
1655
1542
|
fn q8(e: h4) -> vec4<u32> {
|
|
1656
1543
|
return vec4<u32>(clamp(floor(e * h(255.0) + h(0.5)), h4(0.0), h4(255.0)));
|
|
1657
1544
|
}
|
|
1658
1545
|
|
|
1659
1546
|
@compute @workgroup_size(8, 8, 1)
|
|
1660
|
-
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);
|
|
1661
1550
|
if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) { return; }
|
|
1662
1551
|
let bi = gid.y * params.blocks_x + gid.x;
|
|
1663
1552
|
let base = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);
|
|
@@ -1720,20 +1609,136 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1720
1609
|
w1 = (L1 >> 7u) | reverseBits(s2);
|
|
1721
1610
|
w2 = reverseBits(s1);
|
|
1722
1611
|
w3 = reverseBits(s0);
|
|
1723
|
-
} else {
|
|
1724
|
-
// -------------
|
|
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
|
+
//
|
|
1725
1618
|
// Seed endpoints from the block's principal colour axis (covariance
|
|
1726
1619
|
// power-iteration, seeded with the bbox diagonal). The bbox diagonal is
|
|
1727
1620
|
// sign-blind: on anti-correlated channels (normal maps, hue edges) it
|
|
1728
|
-
// points across the data instead of along it
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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.
|
|
1737
1742
|
var c0v = h4(0.0);
|
|
1738
1743
|
var c1v = h4(0.0);
|
|
1739
1744
|
var c2v = h4(0.0);
|
|
@@ -1745,29 +1750,18 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1745
1750
|
c2v = c2v + d.z * d;
|
|
1746
1751
|
c3v = c3v + d.w * d;
|
|
1747
1752
|
}
|
|
1753
|
+
var seed_lo = lo;
|
|
1754
|
+
var seed_hi = hi;
|
|
1748
1755
|
var axis = hi - lo;
|
|
1749
1756
|
var axis_ok = true;
|
|
1750
|
-
// 4
|
|
1751
|
-
//
|
|
1752
|
-
// unroll. Opaque blocks need the converged axis (it IS the endpoint
|
|
1753
|
-
// quality there; 4 was under-converged on noisy 4-D content), while
|
|
1754
|
-
// translucent blocks' LSQ refit absorbs residual axis error \u2014 their
|
|
1755
|
-
// extra 4 steps measured exactly 0.000 dB on the alpha card for
|
|
1756
|
-
// ~5% GPU.
|
|
1757
|
+
// 4 iterations: the refit absorbs residual axis error (4 more measured
|
|
1758
|
+
// 0.000 dB on the alpha card).
|
|
1757
1759
|
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1758
1760
|
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1759
1761
|
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1760
1762
|
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1761
1763
|
axis = nv / m;
|
|
1762
1764
|
}
|
|
1763
|
-
if (axis_ok && opaque) {
|
|
1764
|
-
for (var it: u32 = 0u; it < 4u; it = it + 1u) {
|
|
1765
|
-
let nv = h4(dot(c0v, axis), dot(c1v, axis), dot(c2v, axis), dot(c3v, axis));
|
|
1766
|
-
let m = max(max(abs(nv.x), abs(nv.y)), max(abs(nv.z), abs(nv.w)));
|
|
1767
|
-
if (m < h(1e-4)) { axis_ok = false; break; }
|
|
1768
|
-
axis = nv / m;
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
1765
|
if (axis_ok) {
|
|
1772
1766
|
axis = axis / length(axis);
|
|
1773
1767
|
var t_min = h(4.0);
|
|
@@ -1780,100 +1774,52 @@ fn encode(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
1780
1774
|
seed_lo = clamp(mean + t_min * axis, h4(0.0), h4(1.0));
|
|
1781
1775
|
seed_hi = clamp(mean + t_max * axis, h4(0.0), h4(1.0));
|
|
1782
1776
|
}
|
|
1783
|
-
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
// +26% GPU for \u22120.06..+0.31 dB against the converged 8-step axis
|
|
1787
|
-
// (/ab + PSNR A/B, 2026-07): at 8 weight levels the projection is fine
|
|
1788
|
-
// enough that a good axis, not a refit, carries the quality.
|
|
1789
|
-
// TRANSLUCENT blocks (CEM 12) keep the fit: 4 levels are coarse enough
|
|
1790
|
-
// that dropping it costs 0.5+ dB. Its result is clamped to the block
|
|
1791
|
-
// bbox: on multi-cluster blocks the unconstrained solve extrapolates
|
|
1792
|
-
// far outside the block's colours and the per-channel [0,1] clamp then
|
|
1793
|
-
// bends the hue \u2014 fringe pixels decode to colours that exist nowhere
|
|
1794
|
-
// in the block (and the bbox constraint also measures better in plain
|
|
1795
|
-
// SSE, +1.8 dB on the colour test card).
|
|
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.
|
|
1796
1780
|
var e0 = lo;
|
|
1797
1781
|
var e1 = hi;
|
|
1798
1782
|
var fitStream = 0u;
|
|
1799
1783
|
var haveFitWeights = false;
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
e1 = clamp(seed_hi, lo, hi);
|
|
1807
|
-
} else {
|
|
1808
|
-
let r = proj_fit(&pix, seed_lo, seed_hi);
|
|
1809
|
-
if (r.valid) {
|
|
1810
|
-
e0 = clamp(r.e0, lo, hi);
|
|
1811
|
-
e1 = clamp(r.e1, lo, hi);
|
|
1812
|
-
fitStream = r.wstream;
|
|
1813
|
-
haveFitWeights = true;
|
|
1814
|
-
}
|
|
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;
|
|
1815
1790
|
}
|
|
1816
1791
|
var E0 = q8(e0);
|
|
1817
1792
|
var E1 = q8(e1);
|
|
1818
|
-
|
|
1819
|
-
// Blue-contraction ordering, applied before the weight pass so weights
|
|
1820
|
-
// are already oriented (no reflection needed).
|
|
1821
1793
|
var swapped = false;
|
|
1822
1794
|
if (E0.x + E0.y + E0.z > E1.x + E1.y + E1.z) {
|
|
1823
1795
|
let t = E0; E0 = E1; E1 = t;
|
|
1824
1796
|
swapped = true;
|
|
1825
1797
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
//
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
let dd = dot(dir, dir);
|
|
1834
|
-
if (opaque) {
|
|
1835
|
-
// CEM 8: 3-bit weights, stream bit q = 3k.
|
|
1836
|
-
var s0 = 0u; var s1 = 0u;
|
|
1837
|
-
if (dd >= h(0.008)) {
|
|
1838
|
-
let inv = h(224.0) / dd;
|
|
1839
|
-
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1840
|
-
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(7.0)));
|
|
1841
|
-
let off = 3u * k;
|
|
1842
|
-
if (off < 30u) { s0 = s0 | (w << off); }
|
|
1843
|
-
else if (off == 30u) { s0 = s0 | (w << 30u); s1 = s1 | (w >> 2u); }
|
|
1844
|
-
else { s1 = s1 | (w << (off - 32u)); }
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
// Mode 0x053, CEM 8 @13, endpoints R0 R1 G0 G1 B0 B1 from bit 17.
|
|
1848
|
-
w0 = 0x053u | (8u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1849
|
-
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1850
|
-
w2 = (E1.z >> 7u) | reverseBits(s1);
|
|
1851
|
-
w3 = reverseBits(s0);
|
|
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);
|
|
1852
1805
|
} else {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
// pay it). The blue-contraction swap is a full reflection w \u2192 3\u2212w,
|
|
1859
|
-
// i.e. bitwise NOT of the packed stream. Invalid fits (rank-1 /
|
|
1860
|
-
// degenerate) fall back to reprojection against the bbox endpoints.
|
|
1861
|
-
var s0 = 0u;
|
|
1862
|
-
if (haveFitWeights) {
|
|
1863
|
-
s0 = select(fitStream, ~fitStream, swapped);
|
|
1864
|
-
} else if (dd >= h(0.008)) {
|
|
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)) {
|
|
1865
1811
|
let inv = h(96.0) / dd;
|
|
1866
1812
|
for (var k: u32 = 0u; k < 16u; k = k + 1u) {
|
|
1867
1813
|
let w = u32(clamp(floor(dot(pix[k] - d0, dir) * inv + h(0.5)), h(0.0), h(3.0)));
|
|
1868
1814
|
s0 = s0 | (w << (2u * k));
|
|
1869
1815
|
}
|
|
1870
1816
|
}
|
|
1871
|
-
// Mode 0x042, CEM 12 @13, endpoints R0 R1 G0 G1 B0 B1 A0 A1 from 17.
|
|
1872
|
-
w0 = 0x042u | (12u << 13u) | (E0.x << 17u) | (E1.x << 25u);
|
|
1873
|
-
w1 = (E1.x >> 7u) | (E0.y << 1u) | (E1.y << 9u) | (E0.z << 17u) | (E1.z << 25u);
|
|
1874
|
-
w2 = (E1.z >> 7u) | (E0.w << 1u) | (E1.w << 9u);
|
|
1875
|
-
w3 = reverseBits(s0);
|
|
1876
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);
|
|
1877
1823
|
}
|
|
1878
1824
|
|
|
1879
1825
|
let o = bi * 4u;
|
|
@@ -1906,10 +1852,10 @@ var ASTC4x4Encoder = class extends Encoder {
|
|
|
1906
1852
|
};
|
|
1907
1853
|
|
|
1908
1854
|
// src/etc2.wgsl
|
|
1909
|
-
var etc2_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. The f16 module (etc2_fast_f16.wgsl) is an\n// EXACT-VALUE port \u2014 byte-identical output; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\nfn sb_table_score(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let thr = THR[t];\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = select(a3, b3, ad > thr);\n acc = acc + m3 * (m3 - 2.0 * ad);\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32) -> SearchOut {\n var mx = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let cover = min(\n u32(mx > 24.0) + u32(mx > 51.0) + u32(mx > 87.0) + u32(mx > 126.0) +\n u32(mx > 180.0) + u32(mx > 240.0) + u32(mx > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f32, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, out.lb0);\n let s1 = sb_search(luma, flip, 1u, out.lb1);\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f32, 16>>, flip: u32, sb: u32, lb: f32, t: u32) -> u32 {\n let thr = THR[t];\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n var luma: array<f32, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = l;\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, sel.lb0, t0);\n let fit1 = sb_indices(&luma, bflip, 1u, sel.lb1, t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = qv.b;\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
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. This is the f32 fallback; the f16 module\n// (etc2_fast_f16.wgsl) is an EXACT-VALUE port of it \u2014 byte-identical where\n// the sampler's unorm\u2192float conversion is exact; see its header.\n//\n// ALGORITHM \u2014 scalar-luma selection:\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, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums equal) have no\n// chroma to steer the preselect, so both flips are scored \u2014 worth\n// ~0.3 dB on roughness/AO content over any O(1) proxy tried (luma\n// variance, luma range, squared range all land at \u22120.30 dB). They use\n// a one-channel copy of the fit (fit_gray) and the second flip is a\n// separate straight-line call: the older single-call-site loop cost\n// 7-9% even on colour content that never ran its second iteration.\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 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 ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\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 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// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\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\nfn max4(v: vec4<f32>) -> f32 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let dk = b3 * b3 - a3 * a3;\n let dm = -2.0 * (b3 - a3);\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return 8.0 * a3 * a3 - 2.0 * a3 * sad + dot(eu + ev, ONE4);\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// One subblock (lumas u, v) against base luma lb.\nfn sb_search(u: vec4<f32>, v: vec4<f32>, lb: f32) -> SearchOut {\n let au = abs(u - lb);\n let av = abs(v - lb);\n let mx = max(max4(au), max4(av));\n let sad = dot(au + av, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > 126.0;\n let s2 = mx > select(51.0, 240.0, s1);\n let s3 = mx > select(select(24.0, 87.0, s2), select(180.0, 318.0, s2), s1);\n let cover = select(0u, 4u, s1) + select(0u, 2u, s2) + select(0u, 1u, s3);\n let t_lo = max(cover, 1u) - 1u;\n let acc_lo = table_score(au, av, sad, t_lo);\n let acc_hi = table_score(au, av, sad, cover);\n let lo_wins = acc_lo <= acc_hi;\n var out: SearchOut;\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 fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f32>,\n s0v: vec4<f32>,\n s1u: vec4<f32>,\n s1v: vec4<f32>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f32>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = r + g + b;\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\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\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f32>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f32>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f32>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f32>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f32>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = c.r + c.g + c.b;\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n // ------------------------------------------------------------ planar --\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[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 = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // ------------------------------------------------ flip + base selection --\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb);\n\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a.r, sum1a.r);\n let alt = fit_gray(\n vec4<f32>(col[0].xy, col[1].xy),\n vec4<f32>(col[2].xy, col[3].xy),\n vec4<f32>(col[0].zw, col[1].zw),\n vec4<f32>(col[2].zw, col[3].zw),\n sum0b.r,\n sum1b.r,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f32>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f32>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f32>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f32>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\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 // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = sel.lb0;\n let lb1 = sel.lb1;\n let th0 = THR[t0];\n let th1 = THR[t1];\n let lb_rows = vec4<f32>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f32>(th0, th0, th1, th1);\n let lb_l = select(vec4<f32>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f32>(lb1), lb_rows, fb);\n let th_l = select(vec4<f32>(th0), th_rows, fb);\n let th_r = select(vec4<f32>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f32>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1910
1856
|
|
|
1911
1857
|
// src/etc2_fast_f16.wgsl
|
|
1912
|
-
var etc2_fast_f16_default = "// ETC2 RGB8 compute shader encoder.\n//\n// Each invocation encodes one 4x4 pixel block into an 8-byte ETC2 RGB8 block\n// written as 2 x u32 into the destination storage buffer. ETC2 blocks are\n// big-endian on the wire (byte 0 = bits 63..56), so both words are byte-\n// swapped on the way out. This is that f16 module.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), every f16 value in this shader is an\n// integer that f16 represents exactly \u2014 lumas and bases (<= 765), D values\n// (|D| <= 765) and thresholds (<= 549) all sit below f16's 2048 integer-\n// exactness limit. Sums of squares, scores and estimates stay f32 (they\n// reach +-5e5..9e6, far past f16's 65504 max). The output is therefore\n// BYTE-IDENTICAL to the f32 module \u2014 verified per-block on the suite\n// textures \u2014 and the two modules share every pin and every test gate.\n//\n// What f16 buys here is register pressure (the luma array halves), not\n// arithmetic rate: on Apple/metal-3 the two modules measure identical\n// (the shader is DRAM-read-bound), but on the mobile GPUs where ETC2 is\n// actually the target format, occupancy from smaller registers is the\n// cheapest speed there is. The COLOUR accumulators deliberately stay f32\n// even though quadrant/pair sums (<= 2040) would be exact in f16: porting\n// them measured 15% SLOWER on Apple (conversion traffic outweighs the\n// register saving). Luma + the table search are the f16 surface.\n//\n// ALGORITHM \u2014 scalar-luma selection (2026-07 rewrite; the original\n// brute-force 8-table \xD7 4-modifier \xD7 vec3-with-clamp search measured\n// 6.0 ms @2048\xB2 on Apple/metal-3, this one ~0.197 ms with the DRAM read\n// floor \u2014 16 loads + store, nothing else \u2014 at ~0.15). This is the SETTLED\n// speed/quality point: the two-candidate scored search below was once\n// swapped for an O(1) hedged pick (\u22124-7% GPU) but cost \u22120.5 dB average \u2014\n// a ~10\xD7 worse dB-per-percent trade than the refit drop \u2014 and was\n// restored. A two-pass prepared-source variant (encode pass 0.115 ms) is\n// in git history: its prep pass is also DRAM-bound and cannot overlap,\n// so the per-texture total regressed. Reading the full RGBA8 source once\n// is this machine's hard floor for any single-pass encoder; the ~0.045\n// above it is the whole algorithm.\n//\n// \u2022 The ETC1 modifier is a SCALAR shift along (1,1,1), so per texel\n// err(m) = ||e||\xB2 \u2212 2mD + 3m\xB2 with D = luma(p) \u2212 luma(base), where\n// luma(x) = x.r+x.g+x.b. Selection therefore needs only |D| threshold\n// tests: the best table entry is the m with 3m nearest D (A3/B3/THR\n// below), and \u03A3||e||\xB2 per subblock is O(1) from the load loop's\n// quadrant sums (\u03A3||p||\xB2 \u2212 2\xB7base\xB7\u03A3p + 8\xB7||base||\xB2). This estimate is\n// EXACT for unclamped decode and an UPPER BOUND on the true clamped\n// error (clamping toward [0,255] can only shrink per-channel error),\n// so every est-based gate is conservative.\n// \u2022 Flip preselect, O(1): per subblock the residual after PERFECT\n// continuous luma modulation is within-variance \u2212 (luma variance)/3;\n// the flip with the smaller summed residual wins and only it is\n// searched (both-flip est search measured +23% GPU for \u22640.15 dB).\n// Exact-grayscale blocks have BOTH residuals identically zero (all\n// variance is along luma), so near-ties fall back to scoring both\n// flips \u2014 without that, roughness/AO-style content loses ~1.25 dB.\n// \u2022 Table search is pruned to two candidates \u2014 the table whose LARGE\n// magnitude covers max|D| and its lower neighbour (outlier hedge).\n// One candidate loses ~1.2-1.6 dB on photos; all eight gain \u22640.05 dB.\n// \u2022 NO base refit. The refit family (base \u2190 subblock mean \u2212 mean chosen\n// modifier) was worth ~0.2 dB on photographic colour (rock-color\n// 33.98 \u2192 33.79 without it) but even its cheapest accepted form cost\n// ~13% GPU and the exact-accept original ~30% \u2014 dropped 2026-07 as a\n// deliberate speed/quality trade; see the suite baselines.\n// \u2022 PLANAR runs unconditionally: with the right-hand sides folded into\n// the load loop the LSQ solve is O(1) (the Gram inverse of the fixed\n// sample positions is a constant, det = 25) and its residual is the\n// closed-form \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED,\n// clamped corners \u2014 exact up to decode's floor-rounding, and crucially\n// clamp-aware (a continuous-corner estimate mis-picks planar on steep\n// gradients). Gating planar cost \u22120.31 dB on smooth content for zero\n// measured speed.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks (the colour card's per-pixel\n// chroma checkers are the visible gap) and needs a clustering pass.\n//\n// Numeric notes: texel loads use round(load\xB7255) (integer-exact unorm trip);\n// every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact; est values are\n// integer sums held exactly in f32 (< 2^24).\n\nenable f16;\n\nstruct Params {\n blocks_x: u32,\n blocks_y: u32,\n width: u32,\n height: u32,\n};\n\n@group(0) @binding(0) var src_tex: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n@group(0) @binding(2) var<uniform> params: Params;\n\nconst A3 = array<f32, 8>(6.0, 15.0, 27.0, 39.0, 54.0, 72.0, 99.0, 141.0);\nconst B3 = array<f32, 8>(24.0, 51.0, 87.0, 126.0, 180.0, 240.0, 318.0, 549.0);\nconst THR = array<f32, 8>(15.0, 33.0, 57.0, 82.5, 117.0, 156.0, 208.5, 345.0);\n\n// Planar's closed-form estimate models the QUANTISED corners exactly; only\n// decode's floor-rounding (\xB1\xBD per sample) is unmodelled. This small bias\n// keeps near-ties on the predictable ETC1 side.\nconst PLANAR_FUDGE = 8.0;\n\nfn texel_of(flip: u32, sb: u32, i: u32) -> u32 {\n if (flip == 0u) {\n return (i >> 1u) * 4u + sb * 2u + (i & 1u);\n }\n return (sb * 2u + (i >> 2u)) * 4u + (i & 3u);\n}\n\nfn quant_codes(v: vec3<f32>, max_code: vec3<f32>) -> vec3<u32> {\n return vec3<u32>(clamp(floor(v * max_code * (1.0 / 255.0) + 0.5), vec3<f32>(0.0), max_code));\n}\n\nfn extend4(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(4u)) | c);\n}\nfn extend5(c: vec3<u32>) -> vec3<f32> {\n return vec3<f32>((c << vec3<u32>(3u)) | (c >> vec3<u32>(2u)));\n}\n\nfn signed3(bits: u32) -> i32 {\n return select(i32(bits), i32(bits) - 8, bits > 3u);\n}\n\nfn bswap(x: u32) -> u32 {\n return ((x & 0xffu) << 24u) | ((x & 0xff00u) << 8u) | ((x >> 8u) & 0xff00u) | (x >> 24u);\n}\n\nstruct BasePair {\n codes0: vec3<u32>,\n codes1: vec3<u32>,\n ok: bool,\n};\nfn quantise_bases(avg0: vec3<f32>, avg1: vec3<f32>, diff: bool, clamp_delta: bool) -> BasePair {\n var out: BasePair;\n out.ok = true;\n if (!diff) {\n out.codes0 = quant_codes(avg0, vec3<f32>(15.0));\n out.codes1 = quant_codes(avg1, vec3<f32>(15.0));\n return out;\n }\n let q0 = vec3<i32>(quant_codes(avg0, vec3<f32>(31.0)));\n let q1 = vec3<i32>(quant_codes(avg1, vec3<f32>(31.0)));\n let d = q1 - q0;\n if (any(d < vec3<i32>(-4)) || any(d > vec3<i32>(3))) {\n if (!clamp_delta) {\n out.ok = false;\n return out;\n }\n }\n out.codes0 = vec3<u32>(q0);\n out.codes1 = vec3<u32>(q0 + clamp(d, vec3<i32>(-4), vec3<i32>(3)));\n return out;\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// D-domain values (|D| <= 765, thresholds <= 549) are exact in f16; the\n// score PRODUCTS reach +-5e5 and must be f32.\nfn sb_table_score(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> f32 {\n let a3 = f16(A3[t]);\n let b3 = f16(B3[t]);\n let thr = f16(THR[t]);\n var acc = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let ad = abs((*luma)[texel_of(flip, sb, i)] - lb);\n let m3 = f32(select(a3, b3, ad > thr));\n acc = acc + m3 * (m3 - 2.0 * f32(ad));\n }\n return acc;\n}\nfn sb_search(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16) -> SearchOut {\n var mx: f16 = 0.0;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n mx = max(mx, abs((*luma)[texel_of(flip, sb, i)] - lb));\n }\n let mxf = f32(mx);\n let cover = min(\n u32(mxf > 24.0) + u32(mxf > 51.0) + u32(mxf > 87.0) + u32(mxf > 126.0) +\n u32(mxf > 180.0) + u32(mxf > 240.0) + u32(mxf > 318.0),\n 7u,\n );\n let t_lo = select(cover - 1u, 0u, cover == 0u);\n let acc_lo = sb_table_score(luma, flip, sb, lb, t_lo);\n let acc_hi = sb_table_score(luma, flip, sb, lb, cover);\n var out: SearchOut;\n let lo_wins = acc_lo <= acc_hi;\n out.table = select(cover, t_lo, lo_wins);\n out.acc = select(acc_hi, acc_lo, lo_wins);\n return out;\n}\n\n// One flip's base quantisation + table search: everything the flip contest\n// and the index derivation need.\nstruct FlipFit {\n est: f32,\n diff: bool,\n bases: BasePair,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn eval_flip(\n luma: ptr<function, array<f16, 16>>,\n flip: u32,\n sum0: vec3<f32>,\n sq0: f32,\n sum1: vec3<f32>,\n sq1: f32,\n) -> FlipFit {\n let avg0 = sum0 * 0.125;\n let avg1 = sum1 * 0.125;\n let try_diff = quantise_bases(avg0, avg1, true, false);\n var out: FlipFit;\n out.diff = try_diff.ok;\n if (out.diff) {\n out.bases = try_diff;\n } else {\n out.bases = quantise_bases(avg0, avg1, false, false);\n }\n var b0: vec3<f32>;\n var b1: vec3<f32>;\n if (out.diff) {\n b0 = extend5(out.bases.codes0);\n b1 = extend5(out.bases.codes1);\n } else {\n b0 = extend4(out.bases.codes0);\n b1 = extend4(out.bases.codes1);\n }\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let s0 = sb_search(luma, flip, 0u, f16(out.lb0));\n let s1 = sb_search(luma, flip, 1u, f16(out.lb1));\n out.t0 = s0.table;\n out.t1 = s1.table;\n out.est = (sq0 - 2.0 * dot(b0, sum0) + 8.0 * dot(b0, b0)) +\n (sq1 - 2.0 * dot(b1, sum1) + 8.0 * dot(b1, b1)) +\n (s0.acc + s1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// Wire indices for a chosen table \u2014 computed ONCE, from the final base.\nfn sb_indices(luma: ptr<function, array<f16, 16>>, flip: u32, sb: u32, lb: f16, t: u32) -> u32 {\n let thr = f16(THR[t]);\n var indices = 0u;\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let d = (*luma)[texel_of(flip, sb, i)] - lb;\n let large = abs(d) > thr;\n let neg = d < 0.0;\n indices = indices | ((select(0u, 1u, large) | select(0u, 2u, neg)) << (i * 2u));\n }\n return indices;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn encode(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.blocks_x || gid.y >= params.blocks_y) {\n return;\n }\n\n let block_index = gid.y * params.blocks_x + gid.x;\n let base_xy = vec2<i32>(i32(gid.x) * 4, i32(gid.y) * 4);\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n\n // Luma lives in f16: every value is an integer <= 765, exact in f16.\n var luma: array<f16, 16>;\n var qsum: array<vec3<f32>, 4>;\n var qsq: array<f32, 4>;\n var qlsq: array<f32, 4>;\n // Planar right-hand sides, folded into the load: rB = \u03A3 (x/4)\xB7p and\n // rC = \u03A3 (y/4)\xB7p accumulate unscaled; rA = \u03A3p \u2212 rB \u2212 rC afterwards.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i32(i & 3u);\n let ly = i32(i >> 2u);\n let p = clamp(base_xy + vec2<i32>(lx, ly), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n let l = c.r + c.g + c.b;\n luma[i] = f16(l);\n let q = u32(lx >= 2) | (u32(ly >= 2) << 1u);\n qsum[q] = qsum[q] + c;\n qsq[q] = qsq[q] + dot(c, c);\n qlsq[q] = qlsq[q] + l * l;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n\n // ----------------------------------------------- flip + base selection --\n // Flip preselect, O(1) from quadrant sums: per subblock the residual after\n // PERFECT continuous luma modulation is (\u03A3||p||\xB2 \u2212 ||\u03A3p||\xB2/8) \u2212\n // (\u03A3\u2113\xB2 \u2212 (\u03A3\u2113)\xB2/8)/3 \u2014 the within-variance minus the (1,1,1)-direction\n // component the modifier tables can absorb. The flip minimising the summed\n // residual wins and only it gets the table search \u2014 EXCEPT when the two\n // residuals are indistinguishable: for exact-grayscale blocks (r=g=b) both\n // are identically zero, so the contest falls back to scoring both flips\n // (this recovered \u22121.25 dB on roughness/AO-style content).\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sq0a = qsq[0] + qsq[2];\n let sq1a = qsq[1] + qsq[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let sq0b = qsq[0] + qsq[1];\n let sq1b = qsq[2] + qsq[3];\n let lsq0a = qlsq[0] + qlsq[2];\n let lsq1a = qlsq[1] + qlsq[3];\n let lsq0b = qlsq[0] + qlsq[1];\n let lsq1b = qlsq[2] + qlsq[3];\n let res_a = (sq0a - dot(sum0a, sum0a) * 0.125) - (lsq0a - dot(sum0a, vec3<f32>(1.0)) * dot(sum0a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1a - dot(sum1a, sum1a) * 0.125) - (lsq1a - dot(sum1a, vec3<f32>(1.0)) * dot(sum1a, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n let res_b = (sq0b - dot(sum0b, sum0b) * 0.125) - (lsq0b - dot(sum0b, vec3<f32>(1.0)) * dot(sum0b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0)\n + (sq1b - dot(sum1b, sum1b) * 0.125) - (lsq1b - dot(sum1b, vec3<f32>(1.0)) * dot(sum1b, vec3<f32>(1.0)) * 0.125) * (1.0 / 3.0);\n\n // Single eval_flip call site (a second inlined copy measured +50% GPU):\n // attempt 0 scores the primary flip, attempt 1 runs only in the dual\n // (indistinguishable-residuals) case and scores the other flip.\n let dual = abs(res_a - res_b) < 1.0;\n let primary = select(select(0u, 1u, res_b < res_a), 0u, dual);\n var bflip = primary;\n var sel: FlipFit;\n for (var attempt = 0u; attempt < 2u; attempt = attempt + 1u) {\n if (attempt == 1u && !dual) {\n break;\n }\n let f = select(primary, 1u, attempt == 1u);\n let cand = eval_flip(\n &luma,\n f,\n select(sum0a, sum0b, f == 1u),\n select(sq0a, sq0b, f == 1u),\n select(sum1a, sum1b, f == 1u),\n select(sq1a, sq1b, f == 1u),\n );\n if (attempt == 0u || cand.est < sel.est) {\n sel = cand;\n bflip = f;\n }\n }\n let bdiff = sel.diff;\n\n let best_est = sel.est;\n let codes0 = sel.bases.codes0;\n let codes1 = sel.bases.codes1;\n let t0 = sel.t0;\n let t1 = sel.t1;\n let fit0 = sb_indices(&luma, bflip, 0u, f16(sel.lb0), t0);\n let fit1 = sb_indices(&luma, bflip, 1u, f16(sel.lb1), t1);\n\n // ------------------------------------------------------------ planar --\n // Always evaluated: with the rhs folded into the load loop this is O(1),\n // and gating it on the ETC1 estimate measured \u22120.31 dB on smooth content\n // for zero speed.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[3];\n let sqtotal = qsq[0] + qsq[1] + qsq[2] + qsq[3];\n let rB = sxp * 0.25;\n let rC = syp * 0.25;\n let rA = total - rB - rC;\n let po = 0.2875 * rA - 0.0125 * rB - 0.0125 * rC;\n let ph = -0.0125 * rA + 0.4875 * rB - 0.3125 * rC;\n let pv = -0.0125 * rA - 0.3125 * rB + 0.4875 * rC;\n let pmax = vec3<f32>(63.0, 127.0, 63.0);\n let qo = quant_codes(po, pmax);\n let qh = quant_codes(ph, pmax);\n let qv = quant_codes(pv, pmax);\n // Residual of the plane the hardware will ACTUALLY decode \u2014 the\n // quantised, clamped corners \u2014 via the normal-equation identity\n // \u03A3||p \u2212 f||\xB2 = \u03A3||p||\xB2 \u2212 2\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 (G is the constant Gram matrix\n // of the fixed sample positions). Estimating with the CONTINUOUS corners\n // instead is blind to corner clamping and mis-picks planar on steep\n // gradients (a 1.4-normalised-SSE easy-block artifact on the colour\n // card). Only decode's floor-rounding stays unmodelled (\u2264 ~12 SSE).\n let shl = vec3<u32>(2u, 1u, 2u);\n let shr = vec3<u32>(4u, 6u, 4u);\n let eo = vec3<f32>((qo << shl) | (qo >> shr));\n let eh = vec3<f32>((qh << shl) | (qh >> shr));\n let ev = vec3<f32>((qv << shl) | (qv >> shr));\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * eh + 0.5 * eo * ev + 4.5 * eh * ev;\n let planar_est = sqtotal - 2.0 * (dot(eo, rA) + dot(eh, rB) + dot(ev, rC)) +\n dot(gram, vec3<f32>(1.0)) + PLANAR_FUDGE;\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (best_est <= planar_est) {\n if (bdiff) {\n let d = vec3<u32>(vec3<i32>(codes1) - vec3<i32>(codes0)) & vec3<u32>(7u);\n hi = (codes0.r << 27u) | (d.r << 24u) | (codes0.g << 19u) | (d.g << 16u) | (codes0.b << 11u) | (d.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | 2u | bflip;\n } else {\n hi = (codes0.r << 28u) | (codes1.r << 24u) | (codes0.g << 20u) | (codes1.g << 16u) | (codes0.b << 12u) | (codes1.b << 8u)\n | (t0 << 5u) | (t1 << 2u) | bflip;\n }\n lo = 0u;\n for (var sb: u32 = 0u; sb < 2u; sb = sb + 1u) {\n let indices = select(fit0, fit1, sb == 1u);\n for (var i: u32 = 0u; i < 8u; i = i + 1u) {\n let k = texel_of(bflip, sb, i);\n let wire = (k & 3u) * 4u + (k >> 2u);\n let idx = (indices >> (i * 2u)) & 3u;\n lo = lo | ((idx & 1u) << wire) | ((idx >> 1u) << (16u + wire));\n }\n }\n } else {\n let ro = qo.r; let go = qo.g; let bo = qo.b;\n let rh = qh.r; let gh = qh.g; let bh = qh.b;\n let rv = qv.r; let gv = qv.g; let bv = qv.b;\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
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 the f16 module; etc2.wgsl is the f32\n// fallback with the same algorithm.\n//\n// EXACT-VALUE f16: unlike the other formats' f16 fast paths (which accept\n// float rounding in a [0,1] domain), the f16 values here are integers (or\n// half-integers) that f16 represents exactly \u2014 per-texel lumas and base\n// lumas (<= 765), luma deviations |D| (<= 765) and the index thresholds\n// (<= 345, halves included) all sit below f16's exactness limits. Sums,\n// scores and estimates stay f32 (they reach ~1e6). Where the sampler's\n// unorm\u2192float conversion is exact (verified on Apple/metal-3), the f16 and\n// f32 modules are BYTE-IDENTICAL; elsewhere they can differ only on exact\n// decision ties. f16 buys register space (the 16 lumas are 4 \xD7 vec4<f16>)\n// and measured 1-3% faster than the f32 module on Apple.\n//\n// ALGORITHM \u2014 scalar-luma selection:\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, and \u03A3||e||\xB2 per subblock is O(1) from the quadrant sums. The\n// block-constant \u03A3||p||\xB2 is dropped from EVERY estimate (ETC1 flips and\n// planar alike): only differences between estimates are ever used.\n// The estimate is exact for unclamped decode and an upper bound on the\n// true clamped error.\n// \u2022 Loads: 4 textureGather quads \xD7 R,G,B for interior blocks (the gather\n// point, normalised by the PHYSICAL texture size, sits exactly between\n// the quad's texel centres; interior quads never touch the zeroed\n// padding strip). Blocks straddling the edge of a non-multiple-of-4\n// image fall back to clamped per-texel loads. Lumas are kept as 4\n// COLUMN vectors \u2014 wire pixel order is x\xB74 + y \u2014 so both flips' half-\n// blocks and the index packing use only constant indexing.\n// \u2022 Flip preselect, O(1): per subblock the residual after continuous luma\n// modulation is within-variance \u2212 \u03BA\xB7(luma variance)/3, \u03BA = 0.9. \u03BA = 1\n// is the exact chroma residual; keeping a tenth of the luma variance\n// prefers the split with less luma spread for the 4-level tables to\n// cover (+0.07-0.10 dB on photo colour vs \u03BA = 1, free). Only the chosen\n// flip is searched.\n// \u2022 Exactly-gray blocks (every quadrant's R, G and B sums equal) have no\n// chroma to steer the preselect, so both flips are scored \u2014 worth\n// ~0.3 dB on roughness/AO content over any O(1) proxy tried (luma\n// variance, luma range, squared range all land at \u22120.30 dB). They use\n// a one-channel copy of the fit (fit_gray) and the second flip is a\n// separate straight-line call: the older single-call-site loop cost\n// 7-9% even on colour content that never ran its second iteration.\n// Widening the second evaluation to chroma near-ties (the previous\n// rule) cost 12-25% on colour textures through warp divergence for\n// \u2264 0.015 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 ~0.7-2.9 dB; all eight gain \u2264 0.05 dB. Scores use\n// the min form: per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) is the\n// threshold rule exactly, and its a3 part sums in closed form.\n// \u2022 NO base refit (worth ~0.2 dB on photo colour for \u2265 13% GPU).\n// \u2022 PLANAR runs unconditionally: the LSQ solve is O(1) from the block sum\n// and the first moments \u03A3x\xB7p, \u03A3y\xB7p (the Gram inverse of the fixed\n// sample positions is a constant; folding it into fewer coefficients\n// saved ~1% but resolved rounding ties unlike the CPU mirror on ~9% of\n// the colour card's blocks), and its residual is the closed form\n// \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8 evaluated with the QUANTISED, clamped corners \u2014\n// clamp-aware, which a continuous-corner estimate is not. Gating the\n// quantised evaluation on the continuous plane's residual (an exact\n// lower bound) is byte-identical but measured 0-1%: ~half the warps\n// still hold a block that needs it.\n// \u2022 T and H modes are decoded by hardware but never emitted \u2014 their win\n// is limited to two-chroma-cluster blocks and needs a clustering pass.\n//\n// Numeric notes: every m3 in A3/B3 is divisible by 3 so m = m3/3 is exact;\n// est values are integer sums held exactly in f32 (< 2^24) apart from the\n// planar solve's decimal weights.\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@group(0) @binding(3) var smp: sampler;\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// Fraction of the luma variance the flip preselect treats as absorbed.\nconst KAPPA = 0.9;\nconst ONE3 = vec3<f32>(1.0);\nconst ONE4 = vec4<f32>(1.0);\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\nfn max4(v: vec4<f16>) -> f16 {\n return max(max(v.x, v.y), max(v.z, v.w));\n}\n\n// Base colours from subblock SUMS (8 texels each): codes (as floats) and\n// their 8-bit expansions. Differential mode when the 5-bit codes are within\n// the 3-bit delta range, else individual 4-bit. Expansions in float:\n// (q<<3)|(q>>2) = floor(8.25\xB7q) for 5 bits, (q<<4)|q = 17\xB7q for 4 bits.\nstruct Bases {\n c0: vec3<f32>,\n c1: vec3<f32>,\n b0: vec3<f32>,\n b1: vec3<f32>,\n diff: bool,\n};\nfn quantise_bases(sum0: vec3<f32>, sum1: vec3<f32>) -> Bases {\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n var o: Bases;\n o.diff = all(d >= vec3<f32>(-4.0)) && all(d <= vec3<f32>(3.0));\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n o.c0 = select(i0, q0, o.diff);\n o.c1 = select(i1, q1, o.diff);\n o.b0 = select(i0 * 17.0, floor(q0 * 8.25), o.diff);\n o.b1 = select(i1 * 17.0, floor(q1 * 8.25), o.diff);\n return o;\n}\n\n// Subblock error (\xD73) of table t under the threshold rule, in min form:\n// per texel min(a3\xB2 \u2212 2\xB7a3\xB7ad, b3\xB2 \u2212 2\xB7b3\xB7ad) = (a3\xB2 \u2212 2\xB7a3\xB7ad) +\n// min(0, (b3\xB2 \u2212 a3\xB2) \u2212 2\xB7(b3 \u2212 a3)\xB7ad); the a3 part sums in closed form\n// from sad = \u03A3 ad.\nfn table_score(au: vec4<f32>, av: vec4<f32>, sad: f32, t: u32) -> f32 {\n let a3 = A3[t];\n let b3 = B3[t];\n let dk = b3 * b3 - a3 * a3;\n let dm = -2.0 * (b3 - a3);\n let eu = min(vec4<f32>(0.0), au * dm + dk);\n let ev = min(vec4<f32>(0.0), av * dm + dk);\n return 8.0 * a3 * a3 - 2.0 * a3 * sad + dot(eu + ev, ONE4);\n}\n\nstruct SearchOut {\n table: u32,\n acc: f32,\n};\n// One subblock (lumas u, v) against base luma lb. |D| is exact in f16; the\n// scores need f32.\nfn sb_search(u: vec4<f16>, v: vec4<f16>, lbf: f32) -> SearchOut {\n let lb = f16(lbf);\n let ah = abs(u - lb);\n let bh = abs(v - lb);\n let mx = max(max4(ah), max4(bh));\n let au = vec4<f32>(ah);\n let av = vec4<f32>(bh);\n let sad = dot(au + av, ONE4);\n // cover = #{B3[k] < mx : k < 7}, the first table whose large modifier\n // reaches mx \u2014 a binary search over the 7 thresholds.\n let s1 = mx > 126.0h;\n let s2 = mx > select(51.0h, 240.0h, s1);\n let s3 = mx > select(select(24.0h, 87.0h, s2), select(180.0h, 318.0h, s2), s1);\n let cover = select(0u, 4u, s1) + select(0u, 2u, s2) + select(0u, 1u, s3);\n let t_lo = max(cover, 1u) - 1u;\n let acc_lo = table_score(au, av, sad, t_lo);\n let acc_hi = table_score(au, av, sad, cover);\n let lo_wins = acc_lo <= acc_hi;\n var out: SearchOut;\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 fit: base quantisation + table search, and its estimate\n// (\u03A3||p||\xB2 omitted).\nstruct FlipFit {\n est: f32,\n bases: Bases,\n lb0: f32,\n lb1: f32,\n t0: u32,\n t1: u32,\n};\nfn fit_flip(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: vec3<f32>,\n sum1: vec3<f32>,\n) -> FlipFit {\n var out: FlipFit;\n out.bases = quantise_bases(sum0, sum1);\n let b0 = out.bases.b0;\n let b1 = out.bases.b1;\n out.lb0 = b0.r + b0.g + b0.b;\n out.lb1 = b1.r + b1.g + b1.b;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = dot(b0, 8.0 * b0 - 2.0 * sum0) + dot(b1, 8.0 * b1 - 2.0 * sum1) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// fit_flip for exactly-gray blocks (r = g = b): the same arithmetic on one\n// channel; sum0/sum1 are one channel's subblock sums.\nfn fit_gray(\n s0u: vec4<f16>,\n s0v: vec4<f16>,\n s1u: vec4<f16>,\n s1v: vec4<f16>,\n sum0: f32,\n sum1: f32,\n) -> FlipFit {\n var out: FlipFit;\n let q0 = floor(sum0 * (31.0 / 2040.0) + 0.5);\n let q1 = floor(sum1 * (31.0 / 2040.0) + 0.5);\n let d = q1 - q0;\n let diff = d >= -4.0 && d <= 3.0;\n let i0 = floor(sum0 * (15.0 / 2040.0) + 0.5);\n let i1 = floor(sum1 * (15.0 / 2040.0) + 0.5);\n out.bases.diff = diff;\n out.bases.c0 = vec3<f32>(select(i0, q0, diff));\n out.bases.c1 = vec3<f32>(select(i1, q1, diff));\n let b0 = select(i0 * 17.0, floor(q0 * 8.25), diff);\n let b1 = select(i1 * 17.0, floor(q1 * 8.25), diff);\n out.lb0 = 3.0 * b0;\n out.lb1 = 3.0 * b1;\n let p0 = sb_search(s0u, s0v, out.lb0);\n let p1 = sb_search(s1u, s1v, out.lb1);\n out.t0 = p0.table;\n out.t1 = p1.table;\n out.est = 3.0 * (b0 * (8.0 * b0 - 2.0 * sum0) + b1 * (8.0 * b1 - 2.0 * sum1)) + (p0.acc + p1.acc) * (1.0 / 3.0);\n return out;\n}\n\n// One gathered 2\xD72 quad: per-texel luma (gather order), channel sums, and\n// the sums of its right column and bottom row (the planar moments' local\n// parts). Gather order: w=(0,0) z=(1,0) x=(0,1) y=(1,1).\nstruct Quad {\n l: vec4<f16>,\n s: vec3<f32>,\n right: vec3<f32>,\n bottom: vec3<f32>,\n};\nfn gather_quad(cc: vec2<f32>) -> Quad {\n let r = textureGather(0, src_tex, smp, cc) * 255.0;\n let g = textureGather(1, src_tex, smp, cc) * 255.0;\n let b = textureGather(2, src_tex, smp, cc) * 255.0;\n var o: Quad;\n o.l = vec4<f16>(r + g + b);\n o.right = vec3<f32>(r.z + r.y, g.z + g.y, b.z + b.y);\n o.s = o.right + vec3<f32>(r.w + r.x, g.w + g.x, b.w + b.x);\n o.bottom = vec3<f32>(r.x + r.y, g.x + g.y, b.x + b.y);\n return o;\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\n // Luma by column: col[x][y]. Quadrant q = (x >= 2) | (y >= 2) << 1.\n var col: array<vec4<f16>, 4>;\n var qsum: array<vec3<f32>, 4>;\n // Planar right-hand sides: \u03A3 x\xB7p and \u03A3 y\xB7p.\n var sxp = vec3<f32>(0.0);\n var syp = vec3<f32>(0.0);\n if (u32(base_xy.x) + 4u <= params.width && u32(base_xy.y) + 4u <= params.height) {\n let inv = vec2<f32>(1.0) / vec2<f32>(textureDimensions(src_tex));\n let c0 = (vec2<f32>(base_xy) + 1.0) * inv;\n let q0 = gather_quad(c0);\n let q1 = gather_quad(c0 + vec2<f32>(2.0, 0.0) * inv);\n let q2 = gather_quad(c0 + vec2<f32>(0.0, 2.0) * inv);\n let q3 = gather_quad(c0 + vec2<f32>(2.0, 2.0) * inv);\n qsum[0] = q0.s;\n qsum[1] = q1.s;\n qsum[2] = q2.s;\n qsum[3] = q3.s;\n sxp = q0.right + q2.right + 2.0 * (q1.s + q3.s) + q1.right + q3.right;\n syp = q0.bottom + q1.bottom + 2.0 * (q2.s + q3.s) + q2.bottom + q3.bottom;\n col[0] = vec4<f16>(q0.l.w, q0.l.x, q2.l.w, q2.l.x);\n col[1] = vec4<f16>(q0.l.z, q0.l.y, q2.l.z, q2.l.y);\n col[2] = vec4<f16>(q1.l.w, q1.l.x, q3.l.w, q3.l.x);\n col[3] = vec4<f16>(q1.l.z, q1.l.y, q3.l.z, q3.l.y);\n } else {\n let max_xy = vec2<i32>(i32(params.width) - 1, i32(params.height) - 1);\n for (var i: u32 = 0u; i < 16u; i = i + 1u) {\n let lx = i & 3u;\n let ly = i >> 2u;\n let p = clamp(base_xy + vec2<i32>(i32(lx), i32(ly)), vec2<i32>(0, 0), max_xy);\n let c = round(textureLoad(src_tex, p, 0).rgb * 255.0);\n col[lx][ly] = f16(c.r + c.g + c.b);\n let q = u32(lx >= 2u) | (u32(ly >= 2u) << 1u);\n qsum[q] = qsum[q] + c;\n sxp = sxp + f32(lx) * c;\n syp = syp + f32(ly) * c;\n }\n }\n\n // ------------------------------------------------------------ planar --\n // LSQ plane in closed form: rhs rA = \u03A3(1 \u2212 x/4 \u2212 y/4)\xB7p, rB = \u03A3(x/4)\xB7p,\n // rC = \u03A3(y/4)\xB7p times the constant inverse Gram matrix (the same\n // coefficient form as the CPU mirror, so rounding ties resolve alike);\n // estimate with the quantised, clamped corners: \u22122\xB7\u03B8\xB7rhs + \u03B8\u1D40G\u03B8.\n let total = qsum[0] + qsum[1] + qsum[2] + qsum[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 = clamp(floor(po * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qh = clamp(floor(ph * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n let qv = clamp(floor(pv * (pmax / 255.0) + 0.5), vec3<f32>(0.0), pmax);\n // 6-bit expand (q<<2)|(q>>4) = floor(4.0625\xB7q); 7-bit (q<<1)|(q>>6) = floor(2.015625\xB7q).\n let xk = vec3<f32>(4.0625, 2.015625, 4.0625);\n let eo = floor(qo * xk);\n let eh = floor(qh * xk);\n let ev = floor(qv * xk);\n let gram = 3.5 * (eo * eo + eh * eh + ev * ev) + 0.5 * eo * (eh + ev) + 4.5 * eh * ev;\n let planar_est = dot(gram - 2.0 * (eo * rA + eh * rB + ev * rC), ONE3) + PLANAR_FUDGE;\n\n // ------------------------------------------------ flip + base selection --\n // Flip 0 splits columns (sum0a = left half), flip 1 splits rows (sum0b =\n // top half). Per flip, the preselect residual minus the flip-independent\n // \u03A3||p||\xB2 and \u03A3\u2113\xB2 terms: \u2212\u03A3||s||\xB2/8 + \u03BA\xB7(\u03A3\u2113)\xB2/24 over its two subblocks.\n let sum0a = qsum[0] + qsum[2];\n let sum1a = qsum[1] + qsum[3];\n let sum0b = qsum[0] + qsum[1];\n let sum1b = qsum[2] + qsum[3];\n let l0a = dot(sum0a, ONE3);\n let l1a = dot(sum1a, ONE3);\n let l0b = dot(sum0b, ONE3);\n let l1b = dot(sum1b, ONE3);\n let res_a = KAPPA / 24.0 * (l0a * l0a + l1a * l1a) - 0.125 * (dot(sum0a, sum0a) + dot(sum1a, sum1a));\n let res_b = KAPPA / 24.0 * (l0b * l0b + l1b * l1b) - 0.125 * (dot(sum0b, sum0b) + dot(sum1b, sum1b));\n let gray = all(qsum[0].rg == qsum[0].gb) && all(qsum[1].rg == qsum[1].gb) &&\n all(qsum[2].rg == qsum[2].gb) && all(qsum[3].rg == qsum[3].gb);\n\n var bflip = 0u;\n var sel: FlipFit;\n if (gray) {\n sel = fit_gray(col[0], col[1], col[2], col[3], sum0a.r, sum1a.r);\n let alt = fit_gray(\n vec4<f16>(col[0].xy, col[1].xy),\n vec4<f16>(col[2].xy, col[3].xy),\n vec4<f16>(col[0].zw, col[1].zw),\n vec4<f16>(col[2].zw, col[3].zw),\n sum0b.r,\n sum1b.r,\n );\n if (alt.est < sel.est) {\n sel = alt;\n bflip = 1u;\n }\n } else {\n let fb = res_b < res_a;\n bflip = select(0u, 1u, fb);\n sel = fit_flip(\n select(col[0], vec4<f16>(col[0].xy, col[1].xy), fb),\n select(col[1], vec4<f16>(col[2].xy, col[3].xy), fb),\n select(col[2], vec4<f16>(col[0].zw, col[1].zw), fb),\n select(col[3], vec4<f16>(col[2].zw, col[3].zw), fb),\n select(sum0a, sum0b, fb),\n select(sum1a, sum1b, fb),\n );\n }\n\n // ------------------------------------------------------------ packing --\n var hi: u32;\n var lo: u32;\n if (sel.est <= planar_est) {\n let codes0 = vec3<u32>(sel.bases.c0);\n let codes1 = vec3<u32>(sel.bases.c1);\n let t0 = sel.t0;\n let t1 = sel.t1;\n if (sel.bases.diff) {\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 // Wire indices, column by column (bit x\xB74 + y): flip 0 gives columns\n // 0,1 subblock 0; flip 1 gives rows 0,1 (lanes x, y) subblock 0.\n // LSB = large modifier, MSB = negative.\n let fb = bflip == 1u;\n let lb0 = f16(sel.lb0);\n let lb1 = f16(sel.lb1);\n let th0 = f16(THR[t0]);\n let th1 = f16(THR[t1]);\n let lb_rows = vec4<f16>(lb0, lb0, lb1, lb1);\n let th_rows = vec4<f16>(th0, th0, th1, th1);\n let lb_l = select(vec4<f16>(lb0), lb_rows, fb);\n let lb_r = select(vec4<f16>(lb1), lb_rows, fb);\n let th_l = select(vec4<f16>(th0), th_rows, fb);\n let th_r = select(vec4<f16>(th1), th_rows, fb);\n let bitv = vec4<u32>(1u, 2u, 4u, 8u);\n var lsb = 0u;\n var msb = 0u;\n for (var c: u32 = 0u; c < 4u; c = c + 1u) {\n let d = col[c] - select(lb_l, lb_r, c >= 2u);\n let large = select(vec4<u32>(0u), bitv, abs(d) > select(th_l, th_r, c >= 2u));\n let neg = select(vec4<u32>(0u), bitv, d < vec4<f16>(0.0));\n lsb = lsb | ((large.x | large.y | large.z | large.w) << (c * 4u));\n msb = msb | ((neg.x | neg.y | neg.z | neg.w) << (c * 4u));\n }\n lo = lsb | (msb << 16u);\n } else {\n let ro = u32(qo.r); let go = u32(qo.g); let bo = u32(qo.b);\n let rh = u32(qh.r); let gh = u32(qh.g); let bh = u32(qh.b);\n let rv = u32(qv.r); let gv = u32(qv.g); let bv = u32(qv.b);\n let r_sum = i32(ro >> 2u) + signed3(((ro & 3u) << 1u) | (go >> 6u));\n let r_fix = select(0u, 1u, r_sum < 0);\n let g_sum = i32((go >> 2u) & 15u) + signed3(((go & 3u) << 1u) | (bo >> 5u));\n let g_fix = select(0u, 1u, g_sum < 0);\n let p = (bo >> 3u) & 3u;\n let q = (bo >> 1u) & 3u;\n let b_fix3 = select(0u, 7u, p + q >= 4u);\n let b_fix1 = select(1u, 0u, p + q >= 4u);\n hi = (r_fix << 31u) | (ro << 25u) | ((go >> 6u) << 24u) | (g_fix << 23u) | ((go & 63u) << 17u)\n | ((bo >> 5u) << 16u) | (b_fix3 << 13u) | (((bo >> 3u) & 3u) << 11u) | (b_fix1 << 10u)\n | ((bo & 7u) << 7u) | ((rh >> 1u) << 2u) | 2u | (rh & 1u);\n lo = (gh << 25u) | (bh << 19u) | (rv << 13u) | (gv << 6u) | bv;\n }\n\n let out = block_index * 2u;\n dst[out] = bswap(hi);\n dst[out + 1u] = bswap(lo);\n}\n";
|
|
1913
1859
|
|
|
1914
1860
|
// src/ETC2Encoder.ts
|
|
1915
1861
|
var ETC2Encoder = class extends Encoder {
|