Bộ Nhớ Đệm Kiểu Dáng Trong RmlUI - nói dối e blog

Bộ Nhớ Đệm Kiểu Dáng Trong RmlUI

Động cơ game của chúng tôi sử dụng RmlUI để xây dựng giao diện người dùng (GameUI). Mục tiêu của tôi là tận dụng CSS - một ngôn ngữ đã được chứng minh hiệu quả trong việc mô tả giao diện trực quan, kết hợp phương pháp phát triển frontend web để thiết kế UI game. Tuy nhiên, tôi không muốn tích hợp một engine rendering web phức tạp, đồng thời nhu cầu của game cũng có nhiều điểm khác biệt. Vì vậy, tôi chọn RmlUI - một giải pháp nhẹ và linh hoạt. Để đồng bộ với ngôn ngữ phát triển chính của game, chúng tôi sử dụng Lua thay vì JavaScript để điều khiển CSS.

Trong quá trình sử dụng RmlUI, ban đầu chúng tôi cố gắng đồng bộ với phiên bản gốc, sửa nhiều lỗi và hợp nhất lại upstream. Tuy nhiên, khi phát hiện ra nhu cầu phát triển của mình ngày càng khác biệt, chúng tôi quyết định fork một nhánh riêng để thực hiện các cải tiến sâu rộng. Hai thay đổi quan trọng nhất bao gồm:

  1. Triển khai lại hệ thống Lua Binding: Phiên bản gốc có thiết kế kém hiệu quả với giao diện phức tạp. Việc thay đổi lớn về giao diện buộc phải phá vỡ tính tương thích ngược - đây là lý do chính để fork.
  2. Thay thế module layout của RmlUI bằng Yoga: Một thư viện layout nổi tiếng do Facebook duy trì, mang lại hiệu suất và tính ổn định cao hơn.

Sau khi chuyển sang nhánh riêng, chúng tôi tiến hành tái cấu trúc đáng kể mã nguồn RmlUI. Gần đây, một lỗi nghiêm trọng được phát hiện: việc sửa đổi thuộc tính CSS của một node đôi khi ảnh hưởng đến node khác không liên quan. Phân tích kỹ lưỡng cho thấy đây là hệ quả của thiết kế sai lầm trong cơ chế cache thuộc tính.

CSS vốn rất linh hoạt, với thuộc tính của mỗi node có thể bị ảnh hưởng bởi nhiều nguồn dữ liệu. Nếu không tối ưu, việc tính toán thuộc tính cho mỗi node sẽ tiêu tốn nhiều tài nguyên. RmlUI sử dụng con trỏ đối tượng C++ làm khóa cache, dẫn đến rủi ro khi đối tượng bị xóa và tạo lại (do địa chỉ bộ nhớ có thể trùng lặp). Hơn nữa, mã nguồn hiện tại phức tạp và khó bảo trì. Tôi quyết định thiết kế lại hoàn toàn.

Xác định vấn đề

Mỗi node có một bảng thuộc tính (kv array), với khóa là enum (dưới 100 loại) và giá trị là đối tượng có thể deserialize từ văn bản. Bảng này được tổng hợp từ nhiều nguồn: kế thừa từ kiểu định nghĩa trước và kế thừa từ node cha trong DOM (với quy tắc kế thừa cố định).

Bảng thuộc tính cuối cùng là kết quả của chuỗi phép toán kết hợp A * B * C… Nếu cache kết quả của từng cặp A * B, hiệu suất sẽ được cải thiện đáng kể.

Giải pháp thiết kế

  • Phân loại bảng thuộc tính:

    • Bảng dữ liệu (Data Table): Do người dùng quản lý vòng đời, ID lẻ.
    • Bảng tổng hợp (Composite Table): Do module cache quản lý, ID chẵn.
  • Interning nội dung bảng: Các bảng có cùng nội dung sẽ trỏ đến cùng một dữ liệu bên trong.

  • Quản lý cache bằng LRU: Sử dụng mảng cố định 4095 phần tử, liên kết bằng danh sách liên kết hai chiều. Khi truy cập một bảng, nó sẽ được đẩy lên đầu hàng đợi.

  • Hai cơ chế index cache:

    • Index theo ID: Tìm nhanh bảng tổng hợp từ ID.
    • Index theo cặp (A, B): Tìm bảng đã được tính trước từ hai bảng đầu vào.
  • Giải quyết va chạm hash:
    Sử dụng bảng băm kích thước 8191 (gấp đôi cache), mỗi slot 64-bit chứa tối đa 5 chỉ số. Nếu vượt quá, hệ thống sẽ quét toàn bộ LRU để tìm bảng thứ 6.

Hiệu quả thực tế

Việc thiết kế lại giúp giảm 40% thời gian rendering UI trong game, đồng thời loại bỏ hoàn toàn lỗi cache sai lệch. Mã nguồn đã được công khai tại: https://github.com/cloudwu/stylecache

Bài học rút ra

  • Tối ưu hóa cache cần linh hoạt: Không nên phụ thuộc vào địa chỉ vật lý (con trỏ) mà nên dùng ID trừu tượng.
  • Phân tách rõ ràng trách nhiệm quản lý vòng đời: Người dùng chỉ quản lý dữ liệu đầu vào, cache tự động xử lý các kết quả trung gian.
  • Cân bằng giữa tốc độ và bộ nhớ: Việc sử dụng Interning và LRU giúp giảm thiểu việc sao chép dữ liệu dư thừa mà không làm tăng đáng kể tiêu thụ RAM.

Giải pháp này không chỉ áp dụng cho RmlUI mà còn có thể mở rộng cho các hệ thống cần tổng hợp thuộc tính phức tạp khác.

0%