sort.ts
src/core/sort.ts · raw · all examples · site home
Exports: SortKeyBitsSortKeys
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
// Back-to-front sort keys of splat distances and their stable CPU radix
// sort. The GPU computes the same keys (GpuSortKeys); the CPU only sorts
// them, and WgpuRadixSort sorts them into the same order.
//
// f, the float32 bits of a distance >= 0 (sign bit 0), maps to a code q
// that grows with distance: 32-bit q = f; 24-bit q = f >>> 7 (8 exponent
// and 16 mantissa bits); 16-bit q keeps 6 exponent bits (distances
// 2^-32..2^32, clamped outside) and 10 mantissa bits. key = min(mask ^ q,
// validMax), so ascending keys run back to front. Invalid splats (culled,
// non-finite distance) get `mask`: the top 8-bit digit is reserved for
// them, so they sort last.
import { defineBundle } from "./bundles";
import { TextureLayout } from "./layout";
export type SortKeyBits = 16 | 24 | 32;
const FLOAT_BITS_INFINITY = 0x7f800000;
const f32 = new Float32Array(1);
const u32 = new Uint32Array(f32.buffer);
const histogramLo = new Uint32Array(65536);
const histogramHi = new Uint32Array(65536);
let scratchIndex = new Uint32Array(0);
let scratchDigit = new Uint16Array(0);
export class SortKeys {
// The float bits 16-bit keys cover (distances 2^-32..2^32)
static readonly KEY16_LO = 95 << 23;
static readonly KEY16_HI = (159 << 23) - 1;
// Sort keys as projections write them: the 4 bytes of a key per RGBA8
// texel, read back as a Uint32Array (little-endian)
static readonly KEYS = defineBundle({ key: "unorm8x4" }, { serialize: false });
// The invalid key of a width: all ones
static invalid(bits: SortKeyBits) {
return 2 ** bits - 1;
}
static validMax(bits: SortKeyBits) {
return 0xff * 2 ** (bits - 8) - 1;
}
// Key of a distance >= 0; non-finite distances give the invalid key
static compute(distance: number, bits: SortKeyBits): number {
const mask = SortKeys.invalid(bits);
f32[0] = distance;
const f = u32[0];
if (!(f < FLOAT_BITS_INFINITY)) {
return mask;
}
const q = bits === 32 ? f
: bits === 24 ? f >>> 7
: (Math.min(Math.max(f, SortKeys.KEY16_LO), SortKeys.KEY16_HI) - SortKeys.KEY16_LO) >>> 13;
return Math.min((mask ^ q) >>> 0, SortKeys.validMax(bits));
}
// Stable ascending sort of `count` keys into `ordering` (indices; by
// default a whole capacity, as an ArrayData.INDICES channel holds). Invalid keys sort
// last; `visible` counts the others. 16-bit keys take one counting pass,
// 24-bit keys two LSD passes of 11 and 13 bits, 32-bit keys two of 16.
static sort<O extends Uint32Array = Uint32Array<ArrayBuffer>>({ keys, count, bits, ordering = new Uint32Array(TextureLayout.alignCapacity(count)) as O }: {
keys: Uint32Array;
count: number;
bits: SortKeyBits;
ordering?: O;
}): { ordering: O; visible: number } {
if (ordering.length < count) {
throw new Error(`SortKeys.sort: ordering holds ${ordering.length} of ${count} indices`);
}
if (scratchIndex.length < count) {
scratchIndex = new Uint32Array(count);
scratchDigit = new Uint16Array(count);
}
const visible = bits === 16 ? sort1x16(keys, count, ordering)
: bits === 24 ? sort11x13(keys, count, ordering)
: sort2x16(keys, count, ordering);
return { ordering, visible };
}
}
function prefixSum(histogram: Uint32Array, last: number) {
let sum = 0;
for (let d = 0; d <= last; d += 1) {
const n = histogram[d];
histogram[d] = sum;
sum += n;
}
}
// Written out per layout so each hot loop has constant shifts and masks,
// which measured 6-9% faster than one function taking the digit width
function sort1x16(keys: Uint32Array, count: number, ordering: Uint32Array) {
const histogram = histogramLo;
histogram.fill(0);
for (let i = 0; i < count; i += 1) {
histogram[keys[i] & 0xffff] += 1;
}
const visible = count - histogram[0xffff];
prefixSum(histogram, 0xffff);
for (let i = 0; i < count; i += 1) {
ordering[histogram[keys[i] & 0xffff]++] = i;
}
return visible;
}
// 11 + 13 measured 10-20% faster than 12 + 12 (bench-core.ts)
function sort11x13(keys: Uint32Array, count: number, ordering: Uint32Array) {
const lo = histogramLo;
const hi = histogramHi;
lo.fill(0, 0, 2048);
hi.fill(0, 0, 8192);
for (let i = 0; i < count; i += 1) {
const k = keys[i];
lo[k & 0x7ff] += 1;
hi[k >>> 11] += 1;
}
const visible = count - hi[0x1fff];
prefixSum(lo, 0x7ff);
prefixSum(hi, 0x1fff);
for (let i = 0; i < count; i += 1) {
const k = keys[i];
const pos = lo[k & 0x7ff]++;
scratchIndex[pos] = i;
scratchDigit[pos] = k >>> 11;
}
for (let j = 0; j < count; j += 1) {
ordering[hi[scratchDigit[j]]++] = scratchIndex[j];
}
return visible;
}
function sort2x16(keys: Uint32Array, count: number, ordering: Uint32Array) {
const lo = histogramLo;
const hi = histogramHi;
lo.fill(0);
hi.fill(0);
for (let i = 0; i < count; i += 1) {
const k = keys[i];
lo[k & 0xffff] += 1;
hi[k >>> 16] += 1;
}
const visible = count - hi[0xffff];
prefixSum(lo, 0xffff);
prefixSum(hi, 0xffff);
for (let i = 0; i < count; i += 1) {
const k = keys[i];
const pos = lo[k & 0xffff]++;
scratchIndex[pos] = i;
scratchDigit[pos] = k >>> 16;
}
for (let j = 0; j < count; j += 1) {
ordering[hi[scratchDigit[j]]++] = scratchIndex[j];
}
return visible;
}