Tệp Nguồn Trong Kho Lưu Trữ Biên Dịch Lười Biếng
Hệ thống lưu trữ tài nguyên biên dịch theo nhu cầu trong kho dữ liệu
Kho tài nguyên của engine 3D chúng tôi được xây dựng dựa trên cấu trúc Merkle Tree, được triển khai trên hệ thống tệp tin vật lý và gọi là VFS (Hệ thống tệp ảo). Cấu trúc này có nhiều điểm tương đồng với kho dữ liệu của Git. Về thiết kế hệ thống này, chúng tôi đã có nhiều bài viết chi tiết trước đây.
Ở phiên bản hiện tại, chúng tôi đã tích hợp thành công máy ảo Lua cùng toàn bộ thư viện Lua viết bằng C/C++ thành một tệp thực thi độc lập. Ứng dụng có thể khởi động không cần cấu hình, truy cập kho VFS từ xa qua mạng và tự động cập nhật hệ thống để chạy dự án từ kho dữ liệu từ xa.
Trong quá trình phát triển gần đây, chúng tôi nhận thấy một số hạn chế trong việc áp dụng Merkle Tree và đã thực hiện cải tiến đáng kể. Merkle Tree có đặc điểm là nút gốc chứa giá trị băm đại diện cho toàn bộ cây, bất kỳ thay đổi nào ở các nút con cũng sẽ ảnh hưởng đến giá trị băm gốc. Đây là cơ sở thuật toán giúp Git có thể xác định trạng thái của nhánh bất kỳ chỉ bằng một chuỗi băm duy nhất mà không cần đánh số phiên bản. Hệ thống VFS của chúng tôi cũng kế thừa nguyên lý này.
Tuy nhiên, trong phát triển game, phương pháp này gặp phải một số bất tiện. Các nhà phát triển thường chỉ lưu trữ tài nguyên nguồn gốc thay vì tài nguyên đã tối ưu hóa cho nền tảng cụ thể. Ví dụ: bạn có thể chỉ lưu trữ mã nguồn shader, sau đó biên dịch thành các phiên bản khác nhau phù hợp với Windows, iOS hoặc Android. Tương tự, bạn có thể lưu trữ hình ảnh ở định dạng PNG chung, sau đó nén sang các định dạng đặc thù như DXT, ETC, PVR tùy theo nền tảng đích.
Nếu tiếp tục lưu trữ tất cả phiên bản tài nguyên đã biên dịch trong Merkle Tree, mỗi lần chỉnh sửa tài nguyên nguồn sẽ phải lập tức tạo ra toàn bộ phiên bản cho các nền tảng khác nhau, hoặc phải duy trì nhiều cây Merkle riêng biệt cho từng nền tảng. Cả hai phương án đều gây ảnh hưởng tiêu cực đến trải nghiệm sử dụng. Ngay cả Unity cũng gặp vấn đề tương tự, dù đã cải thiện phần nào nhờ dịch vụ cache, nhưng việc chuyển đổi giữa các nền tảng vẫn tốn thời gian như việc biên dịch lại toàn bộ dự án C++.
Hệ thống VFS của chúng tôi được thiết kế theo kiến trúc C/S (Client-Server), tất cả tài nguyên đều được lưu trữ tập trung trên máy chủ, client chỉ đồng bộ những gì cần thiết khi vận hành. Máy chủ có cấu hình mạnh, hỗ trợ truyền tải và biên dịch dữ liệu đa luồng để phục vụ nhiều client cùng lúc. Chúng tôi cho rằng việc biên dịch tài nguyên nên được thực hiện theo nguyên tắc “lười” (lazy compilation) - chỉ khi nào thực sự cần mới tiến hành xử lý.
Để đạt được điều này, chúng tôi đã cải tiến cấu trúc Merkle Tree bằng cách lưu trữ tài nguyên nguồn trong thư mục gốc. Ví dụ: hình ảnh sẽ được lưu dưới dạng tệp .png. Đối với các tài nguyên cần biên dịch, chúng tôi thêm một tệp mở rộng .lk (tương tự như tệp .meta trong Unity) để mô tả quy trình biên dịch cần thực hiện. Tệp .lk luôn được đặt cùng thư mục với tài nguyên nguồn. Ví dụ: một hình ảnh có thể bao gồm hai tệp foobar.png và foobar.png.lk.
Khi chương trình cần đọc hình ảnh foobar.png, hệ thống sẽ phát hiện đồng thời tồn tại tệp foobar.png.lk trong VFS. Lúc này, hệ thống sẽ kết hợp giá trị băm của foobar.png, giá trị băm của foobar.png.lk và tên nền tảng đang sử dụng để tạo thành một chuỗi dài, sau đó tính toán giá trị băm mới (chúng tôi gọi đây là LHASH) làm khóa tra cứu trên máy chủ tài nguyên.
Nói cách khác, tài nguyên nguồn, tệp cấu hình .lk (chứa thông tin biên dịch) và tên nền tảng sẽ cùng xác định một khối dữ liệu đích. Khối dữ liệu này vẫn được lưu trữ trong kho VFS dưới dạng chỉ mục băm, nhưng hệ thống sẽ tạo thêm một tệp chỉ mục LHASH để trỏ đến khối băm thực sự. Lần sau khi cần truy xuất, hệ thống có thể trực tiếp lấy dữ liệu đã biên dịch thông qua chỉ mục LHASH.
Dữ liệu biên dịch cuối cùng vẫn được lưu trữ trong kho tài nguyên, nhưng nằm ngoài cấu trúc Merkle Tree. Điều này đảm bảo rằng các tài nguyên chưa được tạo ra sẽ không ảnh hưởng đến cấu trúc cây Merkle. Đồng thời, cơ chế LHASH giúp đảm bảo rằng bất kỳ thay đổi nào trên tài nguyên nguồn hoặc tệp .lk (chứa tham số biên dịch) đều sẽ tạo ra phiên bản mới nhất.
Ngoài ra, việc lưu trữ dữ liệu biên dịch dưới dạng cache trong kho tài nguyên còn giúp các nhà phát triển dễ dàng quản lý phiên bản của tài nguyên nguồn. Khi cần kiểm tra lịch sử thay đổi hoặc khôi phục phiên bản cũ, họ chỉ cần tập trung vào các tệp nguồn và tệp cấu hình .lk mà không cần quan tâm đến các phiên bản biên dịch dành cho từng nền tảng cụ thể.