Learn how to create and manage GPU buffers in WebGPU: Uniform vs Storage, struct alignment rules, BindGroups, and transforming 3D models with matrices on the GPU.

This lesson is currently only available in Vietnamese. Please switch to Vietnamese to read the full guide.

Bài 2, chúng ta đã truyền một biến time đơn lẻ qua Uniform Buffer. Nhưng trong ứng dụng 3D thật, bạn cần truyền ma trận biến đổi (model/view/projection), thuộc tính vật liệu, mảng dữ liệu hàng nghìn phần tử — tất cả thông qua hệ thống GPUBuffer.

Bài này đào sâu cách tạo, ghi và ánh xạ buffer lên shader, đặc biệt là quy tắc căn chỉnh bộ nhớ (alignment) — nguồn gốc của phần lớn lỗi "shader không nhận đúng dữ liệu" mà người mới gặp.

1. Tạo & ghi dữ liệu vào GPUBuffer

GPUBuffer là vùng nhớ trên GPU mà shader có thể đọc (và đôi khi ghi). Khi tạo buffer, bạn phải khai báo kích thước (bytes) và usage flags cho biết buffer dùng để làm gì:

create-buffer.js
// Uniform buffer: chứa 1 ma trận 4×4 (16 float × 4 bytes = 64 bytes)
const uniformBuffer = device.createBuffer({
  size: 64,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// Ghi dữ liệu vào buffer từ CPU
const matrix = new Float32Array(16); // ma trận đơn vị
matrix[0] = matrix[5] = matrix[10] = matrix[15] = 1.0;
device.queue.writeBuffer(uniformBuffer, 0, matrix);

GPUBufferUsage.COPY_DST cho phép CPU ghi dữ liệu vào buffer qua writeBuffer(). Nếu thiếu flag này, cuộc gọi sẽ lỗi. Các usage phổ biến:

  • UNIFORM — shader đọc dữ liệu nhỏ, không đổi trong 1 draw call.
  • STORAGE — shader đọc/ghi mảng lớn, dùng cho compute shader.
  • VERTEX — chứa dữ liệu đỉnh (position, normal, UV).
  • INDEX — chứa danh sách chỉ mục tam giác.
🕳️ Cạm bẫy: Kích thước buffer phải là bội số 16
Khi tạo Uniform Buffer, size phải là bội số 16 bytes (minBindingSize alignment). Nếu bạn cần truyền 1 số f32 (4 bytes), vẫn phải khai báo size: 16 — không phải 4. Đây là ràng buộc phần cứng, vi phạm sẽ gây validation error.

2. Quy tắc căn chỉnh struct (Alignment Rules)

Đây là phần gây nhiều lỗi nhất cho người mới. Khi truyền một struct từ JS sang shader WGSL, bộ nhớ phải được căn chỉnh đúng ranh giới — nếu không, shader sẽ đọc sai dữ liệu (mà không báo lỗi!).

Quy tắc alignment của WGSL (tương tự std140 của OpenGL):

Kiểu WGSL Kích thước Alignment
f32, i32, u32 4 bytes 4
vec2<f32> 8 bytes 8
vec3<f32> 12 bytes 16 (!)
vec4<f32> 16 bytes 16
mat4x4<f32> 64 bytes 16
⚠️ Cảnh báo: vec3 chiếm 16 bytes, không phải 12!
vec3<f32> có alignment bằng 16 — nghĩa là sau nó luôn có 4 bytes padding ngầm. Nếu trên JS bạn xếp Float32Array theo kiểu [x, y, z, r, g, b, ...] mà quên padding, mọi trường phía sau sẽ bị lệch. Giải pháp: hoặc dùng vec4 (thêm 1 component dummy), hoặc thêm 4 bytes padding thủ công trên JS.
alignment.wgsl
struct Material {
  color: vec3<f32>,    // offset 0, size 12, NHƯNG alignment 16!
                        // → 4 bytes padding tự động ở đây
  roughness: f32,       // offset 16 (không phải 12!)
  metallic: f32,        // offset 20
  // → tổng struct size: pad lên bội 16 = 32 bytes
};
alignment-js.js
// ❌ SAI — quên padding sau vec3
const wrong = new Float32Array([
  1.0, 0.5, 0.2,  // color (vec3) — chỉ 12 bytes
  0.8,             // roughness — GPU đọc giá trị NÀY thành color.w (padding)!
  0.3,             // metallic — GPU đọc đây là roughness → sai hết
]);

// ✅ ĐÚNG — thêm padding sau vec3
const correct = new Float32Array([
  1.0, 0.5, 0.2,  // color (vec3)
  0.0,             // _padding (4 bytes cho alignment 16)
  0.8,             // roughness — offset 16, khớp với GPU
  0.3,             // metallic — offset 20, đúng
  0.0, 0.0,        // pad struct lên 32 bytes (bội 16)
]);

3. BindGroup & BindGroupLayout — ánh xạ buffer lên shader

Buffer đã tạo, nhưng shader chưa biết nó ở đâu. Hệ thống BindGroup là cầu nối: nó nói cho render pipeline biết "buffer này tương ứng với @group(G) @binding(B) nào trong WGSL".

bindgroup.js
// Layout mô tả CẤU TRÚC: binding 0 là uniform buffer
const layout = device.createBindGroupLayout({
  entries: [{
    binding: 0,
    visibility: GPUShaderStage.VERTEX,
    buffer: { type: 'uniform' },
  }],
});

// BindGroup gắn buffer CỤ THỂ vào layout
const bindGroup = device.createBindGroup({
  layout: layout,
  entries: [{
    binding: 0,
    resource: { buffer: uniformBuffer },
  }],
});

// Trong render pass: kích hoạt bind group
passEncoder.setBindGroup(0, bindGroup); // group 0
ℹ️ Lưu ý: Layout tự động vs. tường minh
Bạn có thể dùng pipeline.getBindGroupLayout(0) để lấy layout tự sinh từ shader (tiện cho prototype). Nhưng trong ứng dụng thật, nên khai báo layout tường minh rồi truyền vào createRenderPipeline({ layout }) — giúp chia sẻ layout giữa nhiều pipeline và kiểm soát hiệu năng tốt hơn.

4. Sân chơi tương tác: Xoay khối lập phương bằng ma trận Uniform

Dưới đây là demo kết hợp tất cả: tạo Uniform Buffer chứa ma trận model 4×4, cập nhật mỗi frame để xoay một khối lập phương. Kéo slider để thay đổi tốc độ và trục xoay:

🎨 Xoay khối lập phương — Uniform Buffer Demo

Uniform Controls

Trình duyệt chưa hỗ trợ WebGPU. Hãy dùng Chrome/Edge phiên bản mới nhất.

cube-shader.wgsl
struct Uniforms { mvp: mat4x4<f32> };
struct ColorU  { color: vec4<f32> };

@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<uniform> uc: ColorU;

struct VSOut {
  @builtin(position) pos: vec4<f32>,
};

@vertex fn vs(@location(0) p: vec4<f32>) -> VSOut {
  var o: VSOut;
  o.pos = u.mvp * p;  // nhân ma trận Model-View-Projection
  return o;
}

@fragment fn fs() -> @location(0) vec4<f32> {
  return uc.color;     // màu truyền qua Uniform Buffer
}
cube-setup.js
// 1. Tạo Uniform Buffers
const uniformBuf = device.createBuffer({
  size: 64, // mat4x4 = 16 floats × 4 bytes
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const colorBuf = device.createBuffer({
  size: 16, // vec4 = 4 floats × 4 bytes
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// 2. BindGroupLayout + BindGroup
const bgl = device.createBindGroupLayout({
  entries: [
    { binding: 0, visibility: GPUShaderStage.VERTEX,
      buffer: { type: 'uniform' } },
    { binding: 1, visibility: GPUShaderStage.FRAGMENT,
      buffer: { type: 'uniform' } },
  ],
});
const bg = device.createBindGroup({
  layout: bgl,
  entries: [
    { binding: 0, resource: { buffer: uniformBuf } },
    { binding: 1, resource: { buffer: colorBuf } },
  ],
});

// 3. Mỗi frame: ghi ma trận xoay + màu mới
function frame(t) {
  const angle = t * 0.001 * speed;
  mat4RotAxis(model, angle, rx, ry, rz);
  mat4Mul(mvp, projView, model);
  device.queue.writeBuffer(uniformBuf, 0, mvp);
  device.queue.writeBuffer(colorBuf, 0, colorData);
  // ... render pass ...
  requestAnimationFrame(frame);
}
🔬 Đào sâu: Tại sao Uniform Buffer giới hạn 64KB?
Phần cứng GPU thiết kế Uniform Buffer cho dữ liệu nhỏ, truy cập nhanh (thường nằm trong bộ nhớ đệm L1 của shader core). Giới hạn điển hình là maxUniformBufferBindingSize = 65536 bytes. Nếu cần truyền mảng lớn (hàng nghìn phần tử, bảng dữ liệu), hãy dùng Storage Buffer — chậm hơn một chút nhưng không giới hạn kích thước (thực tế tới hàng GB). Storage buffer cũng cho phép shader ghi ngược lại (read_write), là nền tảng của Compute Shader ở bài sau.

Trắc nghiệm ôn tập

Câu 1: Struct WGSL có trường color: vec3<f32> theo sau là roughness: f32. Trường roughness nằm ở offset bao nhiêu bytes?

Trắc nghiệm ôn tập

Câu 2: Bạn muốn shader cả đọc lẫn ghi vào một mảng 100.000 phần tử. Nên dùng loại buffer nào?

Trắc nghiệm ôn tập

Câu 3: BindGroup dùng để làm gì?

📖 Tài liệu tham khảo / References

Related Articles

Bài viết liên quan trong series

Lesson 2: WGSL Shader Programming & Dynamic Gradients Bài 2: Lập Trình Shaders Với WGSL — Biến Đổi Màu Sắc Dynamic Lesson 4: Pipeline State & Depth Testing Bài 4: Pipeline State & Depth Testing — Khối Rubik 3D Back to WebGPU Course Overview Quay lại Lộ trình Series WebGPU