layout.ts
src/core/layout.ts · raw · all examples · site home
Exports: TexelSpanCopySpanTextureLayout
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
// How elements map to texels of a 2D array texture, shared by the CPU side
// (capacity alignment) and the GPU side (uploads, reads, copies, shaders).
// Width and height are powers of two, so texel t sits at x = t & 1023,
// y = (t >> 10) & (height - 1), layer = t >> (10 + yBits): shifts and masks
// only. The layer count is not a power of two.
// A rectangle of texels within one layer; `offset` counts texels from the
// start of the range
export interface TexelSpan {
x: number;
y: number;
layer: number;
width: number;
rows: number;
offset: number;
}
export interface CopySpan {
src: { x: number; y: number; layer: number };
dst: { x: number; y: number; layer: number };
width: number;
rows: number;
}
export class TextureLayout {
static readonly WIDTH = 1024;
static readonly MAX_PACKING = 4;
// One texture row for every packing (1, 2 or 4 elements per texel)
static readonly ROW_ELEMENTS = TextureLayout.WIDTH * TextureLayout.MAX_PACKING;
// Array textures are limited by MAX_TEXTURE_SIZE (not MAX_3D_TEXTURE_SIZE),
// >= 4096 on 100% of WebGL2 devices (web3dsurvey.com, Sep 2026)
static readonly MAX_HEIGHT = 4096;
// WebGL2 MAX_ARRAY_TEXTURE_LAYERS: 100% of devices; WebGPU baseline 2048
// (its default is 256, so createRenderer requests more)
static readonly MAX_LAYERS = 512;
readonly width: number;
readonly height: number;
readonly layers: number;
readonly xBits: number;
readonly yBits: number;
// width * height * layers
readonly texels: number;
private constructor(height: number, layers: number) {
this.width = TextureLayout.WIDTH;
this.height = height;
this.layers = layers;
this.xBits = Math.log2(this.width);
this.yBits = Math.log2(height);
this.texels = this.width * height * layers;
}
// Layout holding at least `texels` texels in 2 to 4 layers (at most 50%
// padding) until the height reaches its cap, then one more 4096-row layer
// at a time. At least 2 layers: the WebGL backend only renders to single
// layers of targets with more than one layer.
static for(texels: number): TextureLayout {
const rows = Math.max(1, Math.ceil(texels / TextureLayout.WIDTH));
const height = Math.min(TextureLayout.MAX_HEIGHT, 2 ** Math.max(0, Math.floor(Math.log2(rows / 2))));
const layers = Math.max(2, Math.ceil(rows / height));
if (layers > TextureLayout.MAX_LAYERS) {
throw new Error(`${texels} texels do not fit in an array texture`);
}
return new TextureLayout(height, layers);
}
// Capacity for `count` elements: whole rows for every packing
static alignCapacity(count: number) {
return Math.max(1, Math.ceil(count / TextureLayout.ROW_ELEMENTS)) * TextureLayout.ROW_ELEMENTS;
}
// Texels [start, start + count) as few rectangles as possible: partial
// rows at the ends, whole rows in between (one span per layer)
spans(start: number, count: number): TexelSpan[] {
const { width, height } = this;
const layerSize = width * height;
const spans: TexelSpan[] = [];
const end = start + count;
let t = start;
while (t < end) {
const layer = Math.floor(t / layerSize);
const inLayer = t - layer * layerSize;
const y = Math.floor(inLayer / width);
const x = inLayer - y * width;
if (x > 0 || end - t < width) {
const n = Math.min(width - x, end - t);
spans.push({ x, y, layer, width: n, rows: 1, offset: t - start });
t += n;
} else {
const rows = Math.min(Math.floor((end - t) / width), height - y);
spans.push({ x: 0, y, layer, width, rows, offset: t - start });
t += rows * width;
}
}
return spans;
}
// Rectangles copying texels [srcStart, srcStart + count) of `src` to
// dstStart of `dst`. Whole rows are copied together when both sides are
// row-aligned; otherwise pieces stop at the row end of either side.
static copySpans(src: TextureLayout, srcStart: number, dst: TextureLayout, dstStart: number, count: number): CopySpan[] {
const width = TextureLayout.WIDTH;
const position = (layout: TextureLayout, t: number) => {
const layerSize = width * layout.height;
const layer = Math.floor(t / layerSize);
const y = Math.floor((t - layer * layerSize) / width);
return { x: t % width, y, layer };
};
const spans: CopySpan[] = [];
let done = 0;
while (done < count) {
const s = position(src, srcStart + done);
const d = position(dst, dstStart + done);
const left = count - done;
let span: CopySpan;
if (s.x === 0 && d.x === 0 && left >= width) {
const rows = Math.min(Math.floor(left / width), src.height - s.y, dst.height - d.y);
span = { src: s, dst: d, width, rows };
} else {
span = { src: s, dst: d, width: Math.min(left, width - s.x, width - d.x), rows: 1 };
}
spans.push(span);
done += span.width * span.rows;
}
return spans;
}
}