Một Trường Hợp Không Phù Hợp Để Sử Dụng Hệ Thống - nói dối e blog

Một Trường Hợp Không Phù Hợp Để Sử Dụng Hệ Thống

Một trường hợp không phù hợp để áp dụng mô hình System
Trong khung làm việc ECS, các System thường tập trung xử lý các Component cụ thể. Trong đa số trường hợp, việc xử lý một cấu trúc dữ liệu đơn giản trên một lượng lớn đối tượng theo khối sẽ hiệu quả hơn đáng kể so với việc xử lý từng đối tượng phức tạp có cấu trúc dữ liệu tổ hợp (thường là tập hợp các cấu trúc đơn giản) từng lần một. Tuy nhiên, mọi quy luật đều có ngoại lệ. Gần đây chúng tôi phát hiện ra một tình huống đặc biệt không phù hợp với nguyên tắc này – đó là tính toán xương trong hoạt ảnh xương (skeletal animation).

Quy trình tính toán xương bao gồm nhiều bước như giải nén dữ liệu xương, trộn nhiều tư thế xương (pose blending), tính toán IK (Inverse Kinematics), v.v. Ban đầu, chúng tôi tuân thủ nguyên tắc ECS bằng cách phân chia từng bước xử lý trên thành các System riêng biệt. Ví dụ, không phải đối tượng nào cũng cần tính toán IK, nên IK System chỉ cần duyệt một tập con các đối tượng; tương tự, không phải mọi hoạt ảnh đều cần trộn nhiều tư thế, v.v.

Tuy nhiên, trong quá trình triển khai, chúng tôi gặp phải vấn đề nghiêm trọng:
Các phép toán liên quan đến xương cần một vùng nhớ đệm (buffer) tạm thời có thời gian sống ngắn. Mặc dù cấu trúc dữ liệu xương lâu dài chiếm ít bộ nhớ, nhưng khi cần xử lý trung gian, dữ liệu compact này phải được giải nén vào buffer trung gian. Các thao tác như blending hay IK đều dựa trên buffer này, và sau khi hoàn tất tính toán đến bước skinned mesh, buffer này không còn giá trị sử dụng.

Nếu cứng nhắc áp dụng nguyên tắc ECS, chia nhỏ từng bước xử lý thành các System riêng biệt, mỗi System sẽ tạo ra một buffer trung gian riêng, dẫn đến lãng phí tài nguyên bộ nhớ nghiêm trọng. Thực tế, mỗi luồng xử lý (thread) chỉ cần duy trì duy nhất một buffer trung gian. Vì sau khi xử lý xong một đối tượng, buffer này có thể được tái sử dụng cho đối tượng tiếp theo mà không gây xung đột.

Do đó, gần đây chúng tôi đã điều chỉnh thiết kế bằng cách tập trung toàn bộ chuỗi thuật toán liên quan đến xương vào một System duy nhất.

Về vấn đề bọc gói (wrapping) mã Lua:
Nếu toàn bộ quy trình được thực hiện tại tầng C/C++, buffer trung gian hoàn toàn có thể khai báo trên stack và tự động giải phóng sau khi xử lý xong. Tuy nhiên, khi bọc hàm C thành API Lua, không gian stack C không thể giữ dữ liệu riêng tư. Nếu bọc buffer này thành userdata trả về Lua, sẽ phát sinh nguy cơ bị lạm dụng, gây áp lực cho quá trình garbage collection (GC).

Giải pháp của chúng tôi là bọc buffer này vào userdata ẩn trong upvalue của API C, đảm bảo lớp ứng dụng không thể truy cập trực tiếp. Chuỗi API xử lý xương này hoạt động giống như các API OpenGL, duy trì trạng thái nội tại trong Lua VM. Mỗi nhóm thao tác bắt đầu bằng một lời gọi tường minh – với xương, đó là thao tác giải nén dữ liệu xương vào buffer tạm.

So với chi phí tính toán xương cao ngất ngưỡng, chi phí chuyển từ Lua sang C là không đáng kể. Vì vậy chúng tôi không thiết kế API kiểu thư viện toán học (cho phép thực hiện nhiều phép toán toán học trong một lần gọi), mà giữ nguyên mô hình các API độc lập.

Tôi tin rằng trong tương lai sẽ còn nhiều tình huống tương tự không phù hợp với cách chia System theo từng bước. Khi một chuỗi thao tác trên đối tượng yêu cầu tài nguyên tạm thời đắt đỏ (có thể là bộ nhớ, tài nguyên GPU hay thậm chí là tài nguyên mạng), chúng ta cần xem xét lại mô hình phân chia System truyền thống.

0%