TypedArray stride parameter ?
中文标题:TypedArray 步长参数
提案速览
该提案通过向 ArrayBuffer 视图构造函数添加 stride 参数,解决了 TypedArrays 中交错数据访问困难的问题,允许视图跳过元素。它提供了新的 API 形式 new Float32Array(buffer, byteOffset, length, stride),默认步长为 1,从而无需手动索引即可单独访问颜色通道或顶点属性。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
ArrayBufferView 步长参数
作者:Shu-yu Guo, Surma
提案发起人:Shu-yu Guo
阶段:TC39 流程中的第 1 阶段。
动机
ArrayBuffer 视图(如 Uint8Array、Float32Array 等)提供了对 ArrayBuffer 的视图。这些视图允许同一块内存被解释为不同类型的数据,并允许开发者就地操作二进制数据。
所有视图的构造函数都遵循以下 API 形式:
new Float32Array(length);
new Float32Array(typedArray);
new Float32Array(object);
new Float32Array(buffer [, byteOffset [, length]]);
*) 所有具体引用 Float32Array 的地方都是任意 ArrayBuffer 视图 的占位符。
平台上 ArrayBuffer 的使用场景包括但不限于 WebGLBuffer 和 ImageData。
ImageData 示例
ImageData 是一个 Uint8ClampedArray,表示一张图像作为一维数组,包含 RGBA 顺序的数据,整数取值在 0 到 255 之间。
// ctx 是一个 CanvasRenderingContext2D
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// imageData.data 是一个 Uint8ClampedArray,数据按照以下模式:
// [ r0, g0, b0, a0, r1, g1, b1, a1, ... ]
由于交错表示,一次访问一个颜色通道很繁琐。
WebGL 示例
在 WebGL 中,顶点可以被分配多组数据。显而易见的是三维向量,比如位置、法线或颜色,但也可以扩展到纹理 ID 和其他辅助数据。这些组件中的每一个都称为顶点属性,并且被分配一个唯一的 ID。使用这个 ID,你可以在着色器中按顶点访问数据。
你可以为每个组件创建缓冲区,并通过多次调用将它们上传到 GPU。或者(也可以说更常见的是),创建一个大的缓冲区,这样你就可以一次更新所有数据。要告诉 WebGL 如何解释缓冲区,可以使用 vertexAttribPointer,它有一个 stride 参数:
// prettier-ignore
const arrayBuffer = new Float32Array([
0, 0, 0, // 顶点 0 的位置
0, 1, 0, // 顶点 0 的法线
1, 0, 0, // 顶点 0 的颜色
-1, 0, -1, // 顶点 1 的位置
0, 1, 0, // 顶点 1 的法线
0, 1, 0, // 顶点 1 的颜色
// ...
])
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.STATIC_DRAW);
gl.vertexAttribPointer(
0, // 顶点属性 0 用于位置
3, // 三维
gl.FLOAT, // 类型(也可以是 gl.BYTE、gl.SHORT 等)
false, // GPU 是否应该归一化数据?
9 * Float32Array.BYTES_PER_ELEMENT, // 步长
0 // 偏移量
);
gl.vertexAttribPointer(
1, // 顶点属性 1 用于法线
3,
gl.FLOAT,
false,
9 * Float32Array.BYTES_PER_ELEMENT,
3 * Float32Array.BYTES_PER_ELEMENT
);
// ...
由于交错的数据表示,操作给定顶点的所有顶点属性再次变得繁琐。
提议的解决方案
ArrayBuffer 视图构造函数用于在现有缓冲区上创建视图,已经接受 offset 和 length。
new Float32Array(buffer, byteOffset, length);
这允许开发者使用特定的视图类型处理 ArrayBuffer 的子集。
该提案是向构造函数添加额外的 stride 参数。
new Float32Array(buffer[, byteOffset[, length[, stride]]]);
stride 的默认值和下限是 1,并以元素数量的倍数表示。
高层 API
// prettier-ignore
const { buffer } = new Float32Array([
0, 10, 20,
1, 11, 21,
2, 12, 22
]);
const view1 = new Float32Array(
buffer, // 缓冲区
0 * Float32Array.BYTES_PER_ELEMENT, // 偏移量
3, // 长度
3 // 步长(新增!)
);
const view2 = new Float32Array(
buffer,
1 * Float32Array.BYTES_PER_ELEMENT,
3,
3
);
const view3 = new Float32Array(
buffer,
2 * Float32Array.BYTES_PER_ELEMENT,
3,
3
);
// view1 == [0, 1, 2];
// view2 == [10, 11, 12];
// view2 == [20, 21, 22];
特性检测
为了检测 stride 的支持,开发者可以检查任意 ArrayBuffer 视图实例是否暴露 stride 属性:
if ("stride" in new Float32Array()) {
// 支持 `stride`
} else {
// 不支持 `stride`
}
示例
作为一个例子,这将允许开发者单独访问 ImageData 图像的各个颜色通道作为连续数组:
// ctx 是一个 CanvasRenderingContext2D
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const redChannel = new Uint8ClampedArray(
imageData.data.buffer,
0 * Uint8ClampedArray.BYTES_PER_ELEMNT,
imageData.width * imageData.height,
4
);
// ...
const alphaChannel = new Uint8ClampedArray(
imageData.data.buffer,
3 * Uint8ClampedArray.BYTES_PER_ELEMNT,
imageData.width * imageData.height,
4
);
FAQ 和开放问题
选项对象是否比另一个参数更好?
我认为选项对象 是 更可取的,但需要对规范进行更大规模的更改。好处是它将允许开发者定义 stride,而不必显式定义 offset 或 length,这两个参数都已经有明确定义的默认值。
例如:
const view1 = new Float32Array(buffer, {
offset: 0 * Float32Array.BYTES_PER_ELEMENT,
length: 6,
stride: 3
});
支持未对齐的偏移量
目前 TypedArrays 总是对齐的,因为传递导致未对齐访问的字节偏移量会抛出异常。以表达性为代价,通过构造保证对齐有几个优点:
- 更快的性能
- 更容易生成机器代码
- 更容易在 SharedArrayBuffers 上使用,尤其是与 Atomics 一起使用时
鉴于这种情况,我们不提议放宽整数 TypedArrays 的对齐要求。
这也许对于 Float32Array 和 Float64Array 来说更像是一个开放问题。
TC39 会议记录
待定
规范
待定
实现
Polyfill
polyfill.js 中有一个_进行中_的 polyfill,使用 ES6 代理。它不是为性能而写的,而是为了便于迭代。
浏览器
无