Biên Dịch Lười Dữ Liệu Trong Hệ Thống Tập Tin Ảo
Kỳ trước, tôi đã viết về hệ thống tập tin ảo (Virtual File System - VFS) được thiết kế cho engine game của chúng tôi. Trong hệ thống này, phương pháp quản lý tài nguyên game có một số điểm thiết kế đặc biệt thú vị mà tôi muốn chia sẻ kỹ hơn ở bài viết này.
Mục tiêu thiết kế VFS
VFS ra đời với mục đích thuận tiện đồng bộ dữ liệu từ ổ cứng máy phát triển sang thiết bị thực thi (thường là điện thoại di động). Trong các engine game truyền thống, phương pháp phổ biến là xây dựng một kho tài nguyên (asset repository) được cập nhật liên tục trong giai đoạn phát triển. Khi cần triển khai game lên thiết bị, toàn bộ kho tài nguyên này sẽ được đóng gói và tải lên. Vì các engine cổ điển thường chạy trực tiếp trên máy phát triển trong quá trình lập trình, việc tải lên (upload) dữ liệu tài nguyên không diễn ra thường xuyên.
Tuy nhiên, engine của chúng tôi được thiết kế riêng cho game di động - nơi mà việc phát triển trực tiếp trên thiết bị là không khả thi. Điều này dẫn đến sự phân tách rõ ràng giữa máy phát triển và máy đích. Để tối ưu hiệu suất làm việc, chúng tôi đã xây dựng hệ thống VFS với khả năng đồng bộ tài nguyên qua mạng.
Quản lý phiên bản thông minh
Để hỗ trợ phát triển liên tục, VFS được tích hợp cơ chế quản lý phiên bản (versioning). Chúng tôi học hỏi mô hình của Git: Mọi tập tin trong kho đều được đánh chỉ số dựa trên giá trị băm (hash) nội dung của nó. Khi nội dung thay đổi, một chỉ số mới sẽ được tạo ra. Điều đặc biệt là các tập tin giống nhau về nội dung (dù tên gọi và vị trí lưu trữ khác nhau) chỉ tồn tại duy nhất một bản trong kho.
Hệ thống VFS cho phép “ghép nối” bất kỳ thư mục nào từ hệ thống tập tin vật lý vào cây thư mục ảo. Cơ chế mod (mô-đun bổ sung) cho phép hợp nhất hai thư mục theo độ ưu tiên xác định, giúp truy cập vào cùng một thư mục ảo trên VFS.
Cây Merkle và đồng bộ thông minh
Cấu trúc thư mục VFS được xây dựng dưới dạng cây Merkle. Mỗi thư mục trong kho là một văn bản chứa danh sách tên tập tin và giá trị băm tương ứng của tất cả thành phần bên trong. Vì kho VFS không thay đổi trong quá trình game chạy, giá trị băm của thư mục gốc có thể xem như “số hiệu phiên bản” của toàn bộ kho. Mọi thay đổi trong VFS đều tạo ra phiên bản mới, cho phép đồng bộ hiệu quả (tương tự Git). Trong giai đoạn phát triển, chúng tôi có thể dễ dàng đồng bộ sự khác biệt dữ liệu từ máy tính sang điện thoại qua mạng, hoặc gửi bản cập nhật (patch) để nâng cấp kho tài nguyên trên thiết bị lên phiên bản mới nhất.
Khó khăn với dữ liệu tài nguyên
Một thách thức đặc biệt với engine game là sự tồn tại song song của hai loại dữ liệu trong kho:
- Dữ liệu tĩnh - được lưu trực tiếp trong hệ thống tập tin
- Tài nguyên - như texture, mô hình 3D… được lưu ở dạng nguồn (ví dụ: file PNG) nhưng cần xử lý ngoại tuyến trước khi sử dụng. Chẳng hạn, một file PNG trong thư mục phát triển sẽ được chuyển đổi sang định dạng ASTC nén có mất dữ liệu tùy theo thiết bị đích.
Các engine truyền thống giải quyết vấn đề này bằng cách yêu cầu nhà phát triển dùng editor để import nguồn tài nguyên vào kho. Quá trình này tạo ra tập dữ liệu có thể dùng trực tiếp trên máy phát triển. Khi cần xuất bản lên thiết bị, một quy trình chuyển đổi bổ sung (quá trình đóng gói) sẽ được thực hiện. Đây thường là quá trình tiêu tốn nhiều thời gian, khiến nhiều studio phải thiết lập hệ thống “build hàng ngày” bằng máy chủ chuyên dụng.
Giải pháp biên dịch lười của VFS
Tôi không hài lòng với phương pháp này vì:
- Khó quản lý phiên bản kho tài nguyên
- Quy trình xuất bản mất nhiều thời gian
- Dù phân tán quá trình biên dịch trong giai đoạn phát triển, thời gian xử lý vẫn ảnh hưởng đến tiến độ
Do đó, VFS áp dụng nguyên tắc “biên dịch lười” (lazy compilation): Chỉ khi nào game cần một tài nguyên cụ thể, máy chủ tập tin (fileserver) của VFS mới kích hoạt quá trình biên dịch. Fileserver không cần chạy trên máy phát triển cố định, cho phép mở rộng ngang (horizontal scaling) dễ dàng.
Quan hệ N:M giữa nguồn và đích
Một điểm đặc biệt là quan hệ giữa tập tin nguồn và dữ liệu thực thi thường không đơn giản 1:1. Một nhóm nguồn có thể tạo ra nhiều tập tin đích, hoặc ngược lại. Chẳng hạn, định dạng glTF cho phép lưu trữ texture phụ thuộc ở bên ngoài, tạo ra mối quan hệ 1:N giữa nguồn và đích. Trong VFS, tập tin tài nguyên trong nguồn dữ liệu địa phương được xử lý như một liên kết mềm (soft link) trỏ tới cây thư mục khác (gồm N tập tin).
Mô hình cây con và đồng bộ động
Nếu coi tài nguyên như một cây con (subtree), việc tính toán cây Merkle sẽ trở nên phức tạp vì cần có giá trị băm của mọi nút. Tuy nhiên, với cơ chế biên dịch lười, giá trị băm chỉ có được sau khi tài nguyên được biên dịch. Đây chính là mấu chốt vấn đề - các quy trình đóng gói truyền thống thường mất từ vài phút đến vài giờ, không phù hợp với phát triển thử nghiệm liên tục.
Giải pháp hiện tại trong VFS là chia tách dữ liệu thành hai cây con ngang cấp dưới cùng một gốc chung:
- Cây dữ liệu tĩnh
- Cây tài nguyên
Tập tin tĩnh có thể chứa liên kết mềm trỏ tới nút trong cây tài nguyên. Trong giai đoạn phát triển, các nút tài nguyên chưa sử dụng sẽ không tồn tại. Khi runtime phát hiện liên kết mềm hỏng, nó sẽ yêu cầu fileserver tạo cây tài nguyên tương ứng. Dù cây tài nguyên thay đổi khi thêm/sửa nút, cây dữ liệu tĩnh vẫn giữ nguyên. Trong quá trình chạy, hệ thống sẽ chuyển sang phiên bản gốc mới nhưng không cần làm mới toàn bộ cây tĩnh hay các phần tài nguyên đã tồn tại.
Tổng kết
Dù VFS là hệ thống bất biến, chúng tôi có thể sửa đổi thông qua việc tạo phiên bản mới. Việc tách biệt dữ liệu tĩnh và tài nguyên bằng liên kết mềm giúp tối ưu hiệu suất. Kho dữ liệu tĩnh giữ nguyên trong suốt quá trình phát triển, trong khi kho tài nguyên có thể thay đổi theo thời gian thực. Những thay đổi chỉ giới hạn ở các nút con, còn dữ liệu trong từng nhóm vẫn giữ nguyên phiên bản cho đến khi tiến trình kết thúc.