Bao Bọc Đối Tượng Khối Bộ Nhớ Trong Lua - nói dối e blog

Bao Bọc Đối Tượng Khối Bộ Nhớ Trong Lua

Gần đây, tôi đã thực hiện một số cải tiến cho phần bọc Lua của thư viện bgfx. Do đã thay đổi ngữ nghĩa của API gốc, tôi cần ghi lại chi tiết những thay đổi này.

Trong các thư viện đồ họa 3D, các API thường yêu cầu xử lý nhiều thao tác trên các khối bộ nhớ. Việc tạo lập buffer, texture hay shader đều cần cung cấp các khối dữ liệu đầu vào. Phần lớn các khối dữ liệu này là chỉ đọc, chỉ một phần nhỏ cần hỗ trợ ghi lại. Đối với dữ liệu chỉ đọc, lớp bao bọc có thể dùng chuỗi Lua (Lua string) để thay thế, còn với dữ liệu có thể ghi thì dùng userdata.

Bgfx tự định nghĩa một cấu trúc gọi là Memory để thống nhất mô tả các đối tượng khối bộ nhớ này. Theo thiết kế của bgfx, việc tạo dựng Memory do người dùng quyết định, nhưng việc giải phóng bộ nhớ thường do bgfx quản lý thay vì người gọi API.

Cụ thể, người dùng chịu trách nhiệm tạo ra đối tượng Memory, sao chép dữ liệu vào đó, sau đó truyền cho các API của bgfx rồi không cần quan tâm nữa. Tuy nhiên, nếu bạn tạo Memory mà không truyền cho bgfx sẽ gây rò rỉ bộ nhớ (vì không có API trực tiếp để giải phóng). Ngoài ra, không thể sử dụng một đối tượng Memory nhiều lần (truyền cho bgfx nhiều lần), vì một khi đã truyền cho bgfx, bạn sẽ mất quyền kiểm soát đối tượng đó.

Cách dùng này rất thuận tiện ở cấp độ C/C++, nhưng lại gây khó khăn khi xây dựng bọc Lua. Vấn đề nằm ở việc khó đảm bảo tính an toàn trong bao bọc. Nếu bạn đóng gói Memory thành một userdata Lua, khi người dùng tạo xong nhưng vì lý do nào đó không sử dụng (có thể do lỗi làm gián đoạn luồng thực thi), bạn không có cách nào để “tiêu hóa” nó (vì không có API giải phóng trực tiếp). Việc không thể tái sử dụng đối tượng Memory cũng gây phiền phức trong quá trình phát triển.

Việc tạo dựng Memory còn gây ra một lần sao chép bộ nhớ, có thể là lãng phí. Bgfx cung cấp phương pháp tạo Memory từ tham chiếu, nhưng bạn phải tự đảm bảo vòng đời dữ liệu. Có hai phương án khả thi:

  1. Đảm bảo dữ liệu tham chiếu tồn tại ít nhất qua 2 khung hình.
  2. Cung cấp một hàm callback để giải phóng bộ nhớ.

Nếu chọn phương án 1, bạn phải tự quản lý việc giữ tham chiếu đến tất cả các đối tượng bộ nhớ Lua truyền vào bgfx, sau đó giải phóng định kỳ mỗi 2 khung hình. Với phương án 2, cần lưu ý bgfx là thư viện đa luồng, trong khi thao tác với các đối tượng Lua VM qua luồng khác cần đảm bảo an toàn luồng.

Xuất phát từ những thách thức này, ban đầu khi xây dựng bọc bgfx, tôi không trừu tượng hóa đối tượng memory mà trực tiếp xử lý tham số đầu vào tại tất cả các API liên quan. Dù tham số là bảng (table), chuỗi (string) hay userdata, tôi đều tạo tạm thời đối tượng Memory để truyền cho bgfx, không tiết lộ chi tiết kỹ thuật. Tuy nhiên, khi dự án phát triển, nhu cầu về các phương pháp tạo dữ liệu đa dạng ngày càng tăng, khiến việc bảo trì nhóm API này trở nên phức tạp. Một số phương pháp tạo phức tạp (như truyền con trỏ kết hợp offset và thông tin khác) khiến số lượng tham số tăng vọt, độ phức tạp API trở nên khó chấp nhận. Vì vậy, tôi quyết định trừu tượng hóa đối tượng memory ở cấp độ Lua để giải quyết triệt để vấn đề này.

Kết quả đạt được

Tôi đã bổ sung API mới bgfx.memory_buffer cho thư viện bọc, hỗ trợ 4 cách tạo khối bộ nhớ:

  1. Từ chuỗi mô tả cấu trúc dữ liệu và mảng bảng: Chuỗi mô tả định nghĩa kiểu dữ liệu từng đoạn (số thực/số nguyên độ dài khác nhau), bảng chứa giá trị các trường.
  2. Từ chuỗi với vị trí và độ dài tùy chọn: Tạo khối bộ nhớ chỉ đọc.
  3. Từ lightuserdata (con trỏ) với độ dài và đối tượng liên kết: Đối tượng liên kết giúp framework giữ tham chiếu, tránh mất dữ liệu.
  4. Chỉ định kích thước: Tạo khối bộ nhớ có thể ghi.

Hầu hết API cũ vẫn tương thích, nhưng một số như bgfx.create_vertex_buffer giờ yêu cầu truyền đối tượng memory, không còn chấp nhận bảng như trước. Chi tiết thay đổi có thể xem trong ví dụ minh họa.

Cơ chế triển khai

Đối tượng memory Lua không phải bao bọc trực tiếp bgfx::Memory. Nó chỉ được tạo thành đối tượng Memory của bgfx khi thực sự được gọi qua API. Điều này đảm bảo ngay cả khi tạo mà không dùng, nó vẫn có thể được thu gom rác an toàn.

Khi đối tượng này được sử dụng bởi API bgfx, framework sẽ tạo tạm thời một đối tượng Memory thông qua tham chiếu dữ liệu, đồng thời tăng bộ đếm tham chiếu. Khi bgfx không còn dùng (giải phóng trong vòng 2 khung hình), hàm callback sẽ giảm bộ đếm này.

Phương thức __gc của đối tượng kiểm tra bộ đếm tham chiếu. Chỉ khi về 0 mới được giải phóng. Nếu vẫn còn tham chiếu, đối tượng sẽ tồn tại thêm một thời gian ngắn. Làm sao để “kéo dài sự sống” cho đối tượng trong __gc? Tôi dùng thủ thuật tạo tạm thời một userdata khác trong __gc, gắn đối tượng gốc vào uservalue của nó. Hàm __gc của userdata tạm này sẽ tiếp tục kiểm tra bộ đếm, lặp lại quá trình nếu cần.

0%