Nén hình ảnh phần cứng
Hardware Image Compression
Các định dạng nén ảnh phần cứng đang có những bước đột phá mới với Metal Lossy của Apple, AFRC của ARM và PVRIC4 của ImgTec. Những định dạng này cho phép nén ảnh trực tiếp trên phần cứng trong quá trình render, mang lại hiệu quả rõ rệt và dễ dàng áp dụng hơn so với các chuẩn cũ. Lợi ích cho các developer bao gồm giảm dung lượng bộ nhớ, tăng tốc độ tải texture với những thay đổi API tối thiểu, dù vẫn có những đánh đổi nhất định về chất lượng.
Một trong những điều tôi luôn than thở về các định dạng hình ảnh phần cứng là tốc độ đổi mới chậm. Các nhà phát triển thường không sẵn lòng gửi kết cấu ở định dạng mới trừ khi định dạng đó được phổ biến rộng rãi. Nghĩa là, định dạng này phải được hỗ trợ trong phần lớn phần cứng mà họ nhắm mục tiêu,
Một trong những điều tôi luôn than thở về các định dạng hình ảnh phần cứng là tốc độ đổi mới chậm chạp. Các nhà phát triển thường không sẵn lòng gửi kết cấu ở định dạng mới trừ khi định dạng đó được phổ biến rộng rãi. Nghĩa là, định dạng này phải được hỗ trợ trong phần lớn phần cứng mà họ đang nhắm mục tiêu và phải được hỗ trợ trên tất cả các nhà cung cấp.
Ví dụ: mặc dù ATI đã giới thiệu các định dạng 3Dc vào năm 2004 với Radeon X800 (R420) và giới thiệu chúng thông qua các tiện ích mở rộng D3D9, nhưng trên thực tế, việc sử dụng chúng không trở nên phổ biến khi Direct3D 10 chuẩn hóa chúng thành BC4 và BC5 vào năm 2007, mà chỉ khi Direct3D 10 trở thành yêu cầu phần cứng tối thiểu.
Crysis là trò chơi lớn đầu tiên có kết cấu BC5, nhưng hầu hết các trò chơi đều không sẵn sàng yêu cầu phần cứng cao như vậy cho đến nhiều năm sau. Để tránh sự chậm trễ trong việc áp dụng này, các định dạng BC6 & BC7 được thiết kế với sự hợp tác giữa ATI và NVIDIA để đưa vào Direct3D 11.
Chu kỳ phát triển phần cứng vốn đã dài và để một định dạng mới được áp dụng, nó cần phải được đề xuất tiêu chuẩn hóa, điều này thường khiến quá trình này thậm chí còn kéo dài hơn.
Đây là một trong những lý do khiến tôi thấy việc nén kết cấu theo thời gian thực rất thú vị. Khi bộ mã hóa chạy trong thời gian thực, việc giới thiệu các định dạng phần cứng mới sẽ dễ dàng hơn rất nhiều vì việc áp dụng định dạng mới không còn yêu cầu phải chờ nội dung được tạo nhắm mục tiêu vào định dạng đó nữa.
Trong bài trước tôi đã đề cập đến tính năng nén phần cứng thay thế cho tính năng nén thời gian thực. Chi tiết về các định dạng này không được ghi lại ở bất kỳ đâu và việc sử dụng chúng hoàn toàn minh bạch, các ứng dụng không cần nhắm mục tiêu rõ ràng vào các định dạng này, thay vào đó, trình điều khiển sẽ nén họa tiết một cách linh hoạt trong quá trình kết xuất và tải hình ảnh lên.
Ngày nay, có ba định dạng nén hình ảnh phần cứng cạnh tranh nhau: AFRC của ARM, PVRIC4 của ImgTec và 'lossy' của Apple (vì không có tên nào hay hơn). Trong bài đăng này, tôi sẽ xem xét kỹ hơn cách sử dụng các định dạng này, chất lượng mà chúng tôi có thể mong đợi từ chúng và cách chúng hoạt động so với Spark, thư viện nén kết cấu thời gian thực của tôi.
Hãy bắt đầu với việc triển khai của Apple.
Kim loại
Apple đã giới thiệu tính năng nén kết cấu làm mất dữ liệu trong chipset A15 và M2 (có cùng thế hệ GPU). Kích hoạt nó sẽ dẫn đến tỷ lệ nén 1:2.
Tính năng nén có tổn hao của Metal rất dễ được chọn sử dụng. Bề mặt API ở mức tối thiểu, thuộc tính compressionType trên MTLTextureDescriptor nhận giá trị từ enum MTLTextureCompressionType và đặt giá trị đó thành MTLTextureCompressionTypeLossy thường là thay đổi bắt buộc duy nhất.
Bảng tập hợp tính năng kim loại chỉ ra rằng tất cả các định dạng pixel thông thường đều hỗ trợ nén có tổn hao, bao gồm các định dạng 10 bit và dấu phẩy động. Tôi cho rằng điều này khá đáng chú ý. Tôi đã chạy một số thử nghiệm và có thể xác nhận rằng điều này thực sự đúng, nhưng cho đến nay tôi chỉ tập trung thử nghiệm vào các định dạng R8, RG8 và RGBA8.
Về mặt chất lượng, định dạng R và RG hoạt động tốt hơn codec Spark EAC nhưng kém hơn codec BC4 và BC5:
| R | Tổn hao kim loại (1:2) | BC4 Trung bình (1:2) | BC4 Cao (1:2) | EAC_RG Thấp (1:2) | EAC_RG Trung bình (1:2) | EAC_RG Cao (1:2) |
|---|---|---|---|---|---|---|
| RMSE | 1.8579 | 1.8469 | 1.7149 | 2.3399 | 2.2922 | 1.8636 |
| RG | Tổn hao kim loại (1:2) | BC5 Trung bình (1:2) | BC5 Cao (1:2) | EAC_RG Thấp (1:2) | EAC_RG Trung bình (1:2) | EAC_RG Cao (1:2) |
|---|---|---|---|---|---|---|
| RMSE | 3.1757 | 3.3099 | 3.0442 | 4.2261 | 4.1592 | 3.3601 |
Không thể so sánh trực tiếp giữa codec RGBA8 bị tổn hao và các định dạng Spark có thể nhắm mục tiêu, vì tỷ lệ nén khác nhau, Metal lossy chỉ hỗ trợ tỷ lệ nén 1:2, trong khi định dạng Spark RGB(A) là 1:4, nhưng hãy bao gồm các kết quả để có đầy đủ:
Về mặt hiệu suất, các định dạng lossy hoạt động rất tốt và có xu hướng bão hòa băng thông bộ nhớ nếu kích thước của kết cấu đủ lớn. Tôi đã chạy một số thử nghiệm trên M4 Pro (16 lõi GPU). Bảng sau hiển thị kết quả tính bằng MPix/giây cho hai bộ họa tiết khác nhau với kích thước khác nhau:
| Phương thức | 4096 | 2048 | 1024 | 512 | 256 |
|---|---|---|---|---|---|
| Không nén (blit) | 41.618 | 26.680 | 43.749 | 70.111 | 44.939 |
| Tổn thất kim loại (blit) | 41.807 | 40.847 | 43.100 | 69.873 | 48.729 |
| BC7 High (GPU) | 35.563 | 42.230 | 37.082 | 34.224 | 10.985 |
Lưu ý rằng hiệu suất của các miếng vải tiêu chuẩn vẫn khá ổn định bất kể kích thước kết cấu. Mặt khác, codec Spark dường như có chi phí cố định và trở nên quan trọng hơn khi kích thước họa tiết giảm. Việc tăng tốc độ của các đốm sáng ở 512 x 512 rất thú vị và cần được điều tra thêm vì tôi không có lời giải thích thỏa đáng cho điều đó.
Cũng lưu ý rằng codec Spark cần thực hiện một bản sao bổ sung từ bộ đệm đầu ra của codec đến kết cấu nén cuối cùng. Ngay cả với công cụ phân bổ va chạm nhanh không có tính năng theo dõi nguy hiểm, vẫn có thể tránh được một số chi phí nếu Metal hỗ trợ ghi để chặn kết cấu nén, giống như Vulkan.
Cách thức hoạt động nội bộ của các định dạng mất dữ liệu khá thú vị. Các định dạng mất dữ liệu mà tôi đã kiểm tra đều sử dụng kích thước khối 8 × 4 và giống với một số tính năng của định dạng ETC và EAC. Mặc dù họ yêu cầu nén 1:2 nhưng trên thực tế, có một byte dữ liệu meta được phân bổ cho mỗi khối, do đó tổng mức sử dụng bộ nhớ cao hơn một chút so với quảng cáo.
Tôi đã thiết kế ngược hoàn toàn mã hóa khối tương ứng với một số định dạng bị mất dữ liệu, nhưng bây giờ tôi sẽ cung cấp cho bạn thông tin chi tiết. Tôi có thể ghi lại những phát hiện của mình trong một bài đăng blog khác.
Vulkan
Trên Vulkan, tiện ích mở rộng VK_EXT_image_compression_control cung cấp cho ứng dụng một cách để yêu cầu nén tốc độ cố định cho hình ảnh. Tiện ích mở rộng này đã có sẵn trên các thiết bị hàng đầu của Arm và Imagination.
Như bạn mong đợi, việc bật tính năng nén hình ảnh bị mất chất lượng trong Vulkan dài dòng hơn một chút so với trong Metal nhưng trên thực tế thì không phức tạp hơn nhiều. Điều duy nhất chúng ta cần làm là mở rộng cấu trúc VkImageCreateInfo bằng cách nối chuỗi VkImageCompressionControlEXT cấu trúc cho nó.
Chúng ta có thể sử dụng Cờ VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT để cho phép quá trình triển khai chọn bất kỳ cài đặt nén tốc độ cố định nào:
VkImageCompressionControlEXT nén_control = { 0 };
nén_control.sType = VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_Control_EXT;
nén_control.flags = VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT;
nén_control.pFixedRateFlags = nullptr;
Ngoài ra, bạn có thể chỉ định cờ tốc độ cố định rõ ràng để kiểm soát tỷ lệ nén được phép. Ví dụ:
VkFlags đã sửa_rate_flags = VK_IMAGE_COMPRESSION_FIXED_RATE_3BPC_BIT_EXT |
VK_IMAGE_COMPRESSION_FIXED_RATE_4BPC_BIT_EXT;
nén_control.flags = VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT;
nén_control.pFixedRateFlags = &fixed_rate_flags;
BPC là viết tắt của “bits per Component”, điều này hơi bất thường nhưng nhằm cho phép bạn chỉ định tỷ lệ nén một cách thống nhất bất kể số lượng kênh.
Để tham khảo, BPC của các định dạng nén khối GPU hiện có như sau:
| Định dạng | Kênh | Kích thước trên mỗi pixel | Kích thước trên mỗi kênh |
|---|---|---|---|
| BC1 | RGB | 4 bpp | ~1,33 bpc |
| BC4 | R | 4 bpp | 4 bpc |
| BC5 | RG | 8 bpp | 4 bpc |
| BC7 | RGBA | 8 bpp | 2 bpc |
| ASTC 4×4 | RGBA | 8 bpp | 2 bpc |
| ASTC 6×6 | RGBA | ~3,55 bpp | ~1,18 bpc |
VK_EXT_image_compression_control tiện ích mở rộng cũng được hiển thị trên một số trình điều khiển AMD và Qualcomm, nhưng theo tôi biết thì cả hai nhà cung cấp này đều không hỗ trợ nén hình ảnh tốc độ cố định.
Trong trường hợp của AMD, tiện ích mở rộng được hiển thị trong trình điều khiển RADV như một cách để proton vô hiệu hóa tính năng nén bộ đệm khung không mất dữ liệu trong một số trò chơi mà tiện ích mở rộng này gây ra sự cố về tính chính xác. Điều này đạt được bằng cách sử dụng cờ VK_IMAGE_COMPRESSION_DISABLED_EXT.
Tôi nghi ngờ Qualcomm có thể đang sử dụng nó theo cách tương tự, nhưng tôi không có thiết bị nào hiển thị tiện ích mở rộng này để xác nhận điều đó.
AFRC của ARM
Tính năng nén tốc độ cố định hay AFRC của ARM được công bố vào năm 2021 và lần đầu tiên được giới thiệu trên Mali-G510 vào năm 2022, nhưng thiết kế này được áp dụng rất hạn chế. Các thiết bị có AFRC chỉ trở nên phổ biến khi ra mắt Mali-G715 và Mali-G615 vào cuối năm đó.
Tôi đã thử nghiệm tính năng này trên Pixel 8 với GPU Mali-G715 và nó cho biết nó hỗ trợ các định dạng nén tốc độ cố định sau:
| Định dạng | 2 bpc | 3 bpc | 4 bpc | 5 bpc |
|---|---|---|---|---|
| R8 | 2 bpp | 3 bpp | 4 bpp | — |
| RG8 | 4 bpp | 6 bpp | 8 bpp | — |
| RGB8 | 6 bpp | — | 12 bpp | 15 bpp |
| RGBA8 | 8 bpp | 12 bpp | 16 bpp | — |
Định dạng này linh hoạt hơn đáng kể so với định dạng lossy của Metal, hỗ trợ nhiều tỷ lệ nén hơn.
Không giống như định dạng mất mát Metal, AFRC không sử dụng byte dữ liệu meta bổ sung. Tất cả các bit điều khiển/tiêu đề đều nằm trong khối đó. Hình ảnh được chia thành các khối 8×8 pixel và trong một số trường hợp, các khối này được phân chia thành các khối con có kích thước nhỏ hơn. Kích thước tính bằng Byte cho mỗi khối 8×8 này như sau:
| Định dạng | 2 bpc | 3 bpc | 4 bpc | 5 bpc | ||
|---|---|---|---|---|---|---|
| R8 | 16 | 24 | 32 | — | ||
| RG8 | 32 | 48 | 64 | — | ||
| RGB8 | 64 | — | 96 | 128 | ||
| RGBA8 | 64 | 96 | 128 | — |
| R | AFRC (1:2) | EAC_R Thấp (1:2) | EAC_R Trung bình (1:2) | EAC_R Cao (1:2) |
|---|---|---|---|---|
| RMSE | 1.4937 | 2.3399 | 2.2922 | 1.8636 |
| RG | AFRC (1:2) | EAC_RG Thấp (1:2) | EAC_RG Trung bình (1:2) | EAC_RG Cao (1:2) |
|---|---|---|---|---|
| RMSE | 2.2079 | 4.2261 | 4.1592 | 3.3601 |
| RGBA | AFRC (1:2) | AFRC (1:4) | ASTC 4×4 Thấp (1:4) | ASTC 4×4 Trung bình (1:4) | ASTC 4×4 Cao (1:4) |
|---|---|---|---|---|---|
| RMSE | 0,6679 | 3,4184 | 6,2994 | 5,9686 | 5,3637 |
Trong mọi trường hợp, RMSE thấp hơn đáng kể, nghĩa là AFRC hoạt động tốt hơn những gì bạn có thể đạt được khi nhắm mục tiêu ASTC bằng bộ mã hóa thời gian thực.
Mặc dù kết quả trung bình thấp hơn nhiều so với Spark nhưng vẫn có một số trường hợp Spark tạo ra kết quả có chất lượng cao hơn. Đây là trường hợp xảy ra với những hình ảnh rất mượt mà, trong đó quá trình nén AFRC dẫn đến các mẫu hoà sắc có thể nhìn thấy cũng tiết lộ kích thước khối:
Một trong những trường hợp phổ biến nhất đối với AFRC là sử dụng nó để nén bộ đệm khung. Khi được sử dụng theo cách này, texels ánh xạ tới các pixel và mẫu hoà sắc hầu như không đáng chú ý. Tuy nhiên, khi được sử dụng như một kết cấu, dưới độ phóng đại, nó sẽ trở nên đáng chú ý hơn nhiều. So sánh với Spark ASTC:

Trong tất cả các trường hợp khác, AFRC chiếm ưu thế hơn. Mỗi thành phần màu được mã hóa độc lập nên định dạng không bị lỗi khớp dòng như thường xảy ra ở các định dạng nén khối truyền thống.
Về mặt hiệu suất, việc bật AFRC không gây ra chi phí hiệu suất đáng kể khi tải lên kết cấu không nén, ngoại trừ ở một số kích thước kết cấu:
| Phương thức | 4096 | 2048 | 1024 | 512 | 256 |
|---|---|---|---|---|---|
| Không nén | 4.961 | 3.951 | 3.063 | 2.290 | 2.337 |
| AFRC 4 bpc | 5.508 | 3.792 | 1.771 | 2.341 | 2.318 |
| AFRC 2 bpc | 5.041 | 4.433 | 2.556 | 2.267 | 2.332 |
| Spark ASTC Q0 | 4.810 | 4.207 | 2.503 | 3.662 | 2.259 |
| Spark ASTC Q2 | 4.481 | 3.715 | 2.319 | 2.950 | 1.903 |
Thông lượng ở đây tăng theo kích thước kết cấu thay vì giữ nguyên ở mức phẳng. Lưu ý mức chi phí này ảnh hưởng như thế nào đến các blit và Spark tính toán các shader như nhau.
Không giống như phần Metal, nơi các điểm mất mát rõ ràng chiếm ưu thế Spark ở kích thước nhỏ, ở đây hình ảnh hỗn tạp hơn: Spark gần giống hoặc vượt trội so với AFRC, cho thấy mã hóa kết cấu theo thời gian thực có khả năng cạnh tranh với tính năng nén phần cứng.
Lưu ý rằng con số tuyệt đối ở đây thấp hơn nhiều so với trên M4 Pro, vì đây là những loại thiết bị rất khác nhau.
ImgTec PVRIC4
Mặc dù ImgTec được công bố lần đầu tiên hỗ trợ PVRIC4 vào năm 2018 cho GPU Series 6, tôi đã không thể chạm tay vào thiết bị hỗ trợ tính năng này cho đến khi Pixel 10 đi kèm với chipset Series D được ra mắt.
Thông báo ban đầu dường như chỉ ra rằng giống như tính năng nén có tổn hao của Metal, PVRIC4 chỉ hỗ trợ nén 50% nhưng tiện ích mở rộng quảng cáo nhiều tùy chọn hơn:
| Định dạng | 1 bpc | 2 bpc | 3 bpc | 4 bpc |
|---|---|---|---|---|
| R8 | 1 bpp | 2 bpp | 3 bpp | 4 bpp |
| RG8 | 2 bpp | 4 bpp | 6 bpp | 8 bpp |
| RGBA8 | 4 bpp | 8 bpp | 12 bpp | 16 bpp |
Thật ngạc nhiên, chất lượng đầu ra vẫn như nhau bất kể bpc. Điều tra thêm, tôi kết luận rằng trình điều khiển đã bỏ qua bpc được yêu cầu và luôn mặc định là 4 bpc (nén 1:2).
Tôi rất muốn biết ý kiến của ImgTec xem đây có phải là một lỗi đã biết hay không và liệu phần cứng có hỗ trợ các tỷ lệ nén khác hiện chưa được bật hay không.
Trong số tất cả các nhà cung cấp, định dạng khối của PVRIC4 là định dạng phức tạp nhất và tôi đạt được rất ít tiến bộ trong kỹ thuật đảo ngược nó. Điều duy nhất tôi có thể xác định được là kích thước khối là 16×16 và giống như Metal bị mất, có một byte cho mỗi khối siêu dữ liệu riêng biệt.
Về mặt chất lượng, kết quả thật đáng thất vọng. Đối với các định dạng R và RG, Spark thực sự hoạt động tốt hơn PVRIC4 khi nhắm mục tiêu các định dạng nén khối tiêu chuẩn được phần cứng này hỗ trợ:
| R | PVRIC4 (1:2) | BC4 Trung bình (1:2) | BC4 Cao (1:2) | EAC_R Thấp (1:2) | EAC_R Trung bình (1:2) | EAC_R Cao (1:2) |
|---|---|---|---|---|---|---|
| RMSE | 3.4346 | 1.8469 | 1.7149 | 2.3399 | 2,2922 | 1,8636 |
| RG | PVRIC4 (1:2) | BC5 Trung bình (1:2) | BC5 Cao (1:2) | EAC_RG Thấp (1:2) | EAC_RG Trung bình (1:2) | EAC_RG Cao (1:2) |
|---|---|---|---|---|---|---|
| RMSE | 5.4392 | 3.3099 | 3.0442 | 4.2261 | 4.1592 | 3.3601 |
Đối với RGBA, chúng tôi không thể so sánh trực tiếp vì chúng tôi đang nhắm tới các tỷ lệ nén khác nhau nhưng chất lượng cũng kém hơn đáng kể so với các nhà cung cấp khác.
| RGBA | PVRIC4 (1:2) | ASTC 4×4 Thấp (1:4) | ASTC 4×4 Trung bình (1:4) | ASTC 4×4 Cao (1:4) |
|---|---|---|---|---|
| RMSE | 2.3160 | 6.2994 | 5.9686 | 5.3637 |
Về mặt thực hiện, tôi thu được kết quả như sau:
| Phương thức | 4096 | 2048 | 1024 | 512 | 256 |
|---|---|---|---|---|---|
| Không nén | 2.299 | 2.629 | 2.643 | 1.909 | 1.178 |
| PVRIC4 4 bpc | 2.582 | 2.972 | 3.851 | 2.877 | 1.102 |
| Spark ASTC Q0 | 3.327 | 3.509 | 3.097 | 2.051 | 911 |
| Tỏa sáng ASTC Quý 2 | 3.002 | 2.759 | 2.498 | 1.485 | 634 |
Đường cong thông lượng trên thiết bị này khá khác so với Pixel 8, đạt đỉnh điểm vào khoảng 1024–2048 thay vì tăng tỷ lệ đơn điệu theo kích thước. Ở kích thước lớn, thông lượng Spark thực sự cao hơn so với tải lên kết cấu không nén. Điều này thường xảy ra trên các thiết bị có băng thông hạn chế: một blit đơn giản phải đọc toàn bộ dữ liệu đầu vào và ghi lại cùng một lượng dữ liệu ra ngoài, trong khi Spark chỉ ghi 1/4 dữ liệu đầu vào. Băng thông bộ nhớ được lưu khi ghi thường đủ để bù đắp chi phí tính toán cho việc mã hóa, dẫn đến thông lượng ròng cao hơn.
Kết luận
AFRC của ARM rõ ràng là người chiến thắng. Nó không chỉ vượt trội so với việc triển khai phần mềm như Spark mà còn vượt trội hơn tất cả các nhà cung cấp khác ở tất cả các định dạng.
| Định dạng | 1:2 RMSE | 1:4 RMSE |
|---|---|---|
| R8 Metal Lossy | 1.8579 | — |
| R8 AFRC | 1.4937 | — |
| R8 PVRIC4 | 3.4346 | — |
| Spark BC4 | 1.7149 | — |
| RG8 Tổn hao kim loại | 3.1757 | — |
| RG8 AFRC | 2.2079 | — |
| RG8 PVRIC4 | 5.4392 | — |
| Spark BC5 | 3.0442 | — |
| RGBA8 Tổn hao kim loại | 1.4947 | — |
| RGBA8 AFRC | 0.6679 | 3.4184 |
| RGBA8 PVRIC4 | 2.3160 | — |
| Spark BC7 | — | 4.2136 |
Điều đáng lưu ý là kết quả PVRIC4 của tôi có thể không phản ánh hết tiềm năng của phần cứng. Trình điều khiển dường như bỏ qua tỷ lệ nén được yêu cầu và luôn đặt mặc định là 1:2, vì vậy tôi hy vọng sẽ xem lại các kết quả này sau khi sự cố được khắc phục.
Nén phần cứng gốc là giải pháp thay thế hấp dẫn cho nén thời gian thực. Lưu ý chính là tính năng này hiện chỉ giới hạn ở các thiết bị cao cấp hiện đại, cũng là những thiết bị có nhiều bộ nhớ và băng thông dự phòng nhất.
Ngay cả khi có sẵn tính năng nén phần cứng gốc, vẫn có lý do chính đáng để tiếp tục sử dụng Spark. Đầu ra nén phần cứng khác nhau giữa các nhà cung cấp và trong một số trường hợp, như chúng ta đã thấy với PVRIC4, chất lượng không đạt được mức mà bộ mã hóa thời gian thực có thể đạt được. Nếu kết quả đầu ra nhất quán, có thể dự đoán được của tất cả các nhà cung cấp có ý nghĩa quan trọng đối với trường hợp sử dụng của bạn thì Spark vẫn là công cụ phù hợp.
Cuối cùng, cần lưu ý rằng hiện không có định dạng nén phần cứng nào trong số này được hiển thị thông qua WebGPU. Nếu điều đó thay đổi trong tương lai, việc mở rộng spark.js để hỗ trợ chúng sẽ rất đơn giản. Thư viện có thể tự động chọn định dạng tốt nhất được phần cứng cơ bản hỗ trợ mà không cần phải thay đổi ứng dụng.
Tác giả: luu