Nội dung trên trang web này đã được dịch bằng trí tuệ nhân tạo (AI) hoặc công nghệ dịch máy và có thể có lỗi.

Skip to content

Triển lãm hồi tưởng về kim loại

Chúng tôi đã triển khai thành công bộ xử lý đồ họa Metal đến hàng triệu người dùng, và tôi muốn chia sẻ một chút về điều đó. Có nhiều ý kiến khác nhau về Metal trong ngành - một số người cho rằng Metal sẽ không cần thiết nếu Apple dành nhiều sự chú ý hơn cho OpenGL và Vulkan, một số người lại cho rằng đây là API đồ họa dễ sử dụng nhất từ trước đến nay. Tại sao phải bận tâm đến Metal, một số người hỏi, nếu bạn có thể viết mã OpenGL hoặc Vulkan, và sử dụng MoltenGL hoặc MoltenVK để đạt được hiệu quả tương tự? Dưới đây là suy nghĩ của tôi về API này.

Tại sao lại là Metal?

Khi Apple công bố Metal tại WWDC năm 2014, phản ứng ban đầu của tôi là phớt lờ nó. Nó chỉ có sẵn trên phần cứng mới nhất, mà phần lớn người dùng của chúng tôi không sở hữu. Và mặc dù Apple khẳng định Metal giải quyết các vấn đề về hiệu năng CPU, việc tối ưu hóa cho thị trường nhỏ nhất sẽ khiến khoảng cách giữa các thiết bị nhanh nhất và chậm nhất ngày càng gia tăng. Vào thời điểm đó, chúng tôi chỉ chạy OpenGL ES 2 trên Apple và cũng bắt đầu chuyển sang Android.

Hai năm rưỡi sau, đây là thị phần của Metal đối với người dùng của chúng tôi:

Điều này hấp dẫn hơn nhiều so với trước đây. Việc triển khai Metal vẫn không giúp ích gì cho các thiết bị cũ nhất, nhưng thị trường GL trên iOS đang ngày càng thu hẹp. Hơn nữa, nội dung chạy trên các thiết bị cũ thường khác với nội dung chạy trên các thiết bị mới nhất, vì vậy việc dành một chút nỗ lực để làm cho các thiết bị này chạy nhanh hơn là hoàn toàn hợp lý. Do mã Metal trên iOS của bạn sẽ chạy trên Mac với rất ít thay đổi, nên việc sử dụng nó trên Mac cũng có thể hợp lý ngay cả khi bạn tập trung vào thiết bị di động (hiện tại chúng tôi chỉ phát hành các bản dựng Metal trên iOS).

Tôi nghĩ rằng việc phân tích thị phần chi tiết hơn một chút là rất đáng giá. Trên iOS, chúng tôi hỗ trợ Metal cho iOS 8.3+. Mặc dù có một số người dùng không thể chạy Metal do hạn chế phiên bản hệ điều hành, phần lớn 25% người dùng vẫn sử dụng GL chỉ đơn giản là đang dùng các thiết bị cũ có phần cứng SGX. Họ cũng không có các tính năng OpenGL ES 3, và chúng tôi chấp nhận chạy một đường dẫn render cấp thấp hơn ở đó (mặc dù, chúng tôi rất mong tất cả thiết bị đều chuyển sang Metal. May mắn thay, sự phân chia giữa GL và Metal sẽ chỉ cải thiện). Trên Mac, API Metal mới hơn và hệ điều hành đóng vai trò khá quan trọng. Bạn phải sử dụng OS X 10.11 trở lên để dùng Metal, và một nửa người dùng của chúng tôi đơn giản là đang dùng hệ điều hành cũ hơn. Điều này ít liên quan đến phần cứng mà chủ yếu là phần mềm (95% người dùng Mac của chúng tôi chạy OpenGL 3.2 trở lên).

Vì vậy, xét về thị phần, vẫn còn các lựa chọn khác không cần chuyển sang Metal. Một trong số đó là sử dụng MoltenGL, vốn sẽ tận dụng mã OpenGL hiện có nhưng được cho là nhanh hơn; một lựa chọn khác là chuyển sang Vulkan (để cải thiện hiệu năng trên PC và sau này là Android); hoặc sử dụng MoltenVK. Tôi đã đánh giá sơ bộ MoltenGL và không thực sự hài lòng với kết quả. Phải tốn khá nhiều công sức để làm cho mã nguồn của chúng ta chạy được, và mặc dù hiệu suất tốt hơn một chút so với OpenGL tiêu chuẩn, tôi vẫn mong đợi nhiều hơn. Về MoltenVK, tôi cho rằng việc cố gắng triển khai một API cấp thấp như một lớp trên một API khác là sai lầm. Bạn sẽ gặp phải sự không tương thích về giao diện, dẫn đến hiệu suất không tối ưu. Có thể nó sẽ tốt hơn API cấp cao mà bạn từng sử dụng, nhưng nó khó có thể nhanh nhất có thể, điều này được cho là lý do bạn chọn API cấp thấp ngay từ đầu! Một khía cạnh quan trọng khác là việc triển khai Metal đơn giản hơn nhiều so với Vulkan - sẽ nói chi tiết hơn về điều này sau - vì vậy theo một nghĩa nào đó, tôi ưa thích một lớp bọc Metal -> Vulkan hơn là Vulkan -> Metal.

Cũng cần lưu ý rằng dường như không có trình điều khiển GL trên iOS 10 trên các mẫu iPhone mới nhất. GL được triển khai trên nền tảng Metal, điều này có nghĩa là việc sử dụng OpenGL chỉ thực sự giúp bạn tiết kiệm một chút công sức phát triển - không nhiều lắm, xét rằng lời hứa “viết một lần, chạy mọi nơi” mà OpenGL mang lại không thực sự hiệu quả trên thiết bị di động.

Chuyển đổi

Tôi có thể nói rằng việc chuyển đổi sang Metal nói chung rất dễ dàng. Chúng tôi có nhiều kinh nghiệm làm việc với các API đồ họa khác nhau, từ các API cấp cao như Direct3D 9/11 đến các API cấp thấp trên các nền tảng console. Điều này mang lại lợi thế độc đáo là có thể sử dụng thoải mái một API như Metal, vốn vừa có mức độ cao vừa để lại một số tác vụ như đồng bộ hóa CPU-GPU cho nhà phát triển ứng dụng thực hiện.

Rào cản duy nhất là biên dịch các shader của chúng tôi. Một khi việc đó hoàn tất và đến lúc viết mã, rõ ràng là API này rất đơn giản và dễ hiểu đến mức mã gần như tự viết ra. Tôi đã làm cho phiên bản port hiển thị hầu hết các thành phần một cách chưa tối ưu chạy được trong khoảng 10 giờ trong một ngày, và dành thêm hai tuần để dọn dẹp mã nguồn, sửa các vấn đề xác thực, phân tích hiệu suất và tối ưu hóa, cũng như hoàn thiện chung. Việc triển khai API trong khung thời gian này nói lên rất nhiều về chất lượng của API và bộ công cụ. Tôi tin rằng có một số yếu tố góp phần:

  • Bạn có thể phát triển mã theo từng giai đoạn, với phản hồi tốt ở mọi bước. Mã của chúng tôi ban đầu bỏ qua toàn bộ đồng bộ hóa CPU-GPU, xử lý một số phần thiết lập trạng thái một cách không tối ưu, sử dụng theo dõi tham chiếu tích hợp cho tài nguyên, và không bao giờ chạy CPU và GPU song song để tránh gặp sự cố. Giai đoạn tối ưu hóa/hoàn thiện sau đó đã chuyển đổi điều này thành một sản phẩm có thể triển khai, mà không bao giờ mất khả năng hiển thị trong quá trình đó.
  • Các công cụ đã sẵn sàng cho bạn; chúng hoạt động và hoạt động tốt. Điều này không phải là điều bất ngờ đối với những người quen với Direct3D 11, nhưng đây là lần đầu tiên trên nền tảng di động mà tôi có trình phân tích hiệu suất CPU, trình phân tích hiệu suất GPU, trình gỡ lỗi GPU và lớp xác thực API GPU, tất cả đều hoạt động ăn ý với nhau, phát hiện hầu hết các vấn đề trong quá trình phát triển và giúp tối ưu hóa mã nguồn.
  • Mặc dù API này ở mức độ thấp hơn so với Direct3D 11 và để lại một số quyết định cấp thấp quan trọng cho nhà phát triển (như cấu hình pass render hoặc đồng bộ hóa), nó vẫn sử dụng mô hình tài nguyên truyền thống, trong đó mỗi tài nguyên được tạo ra với các “cờ sử dụng” nhất định nhưng không yêu cầu rào cản pipeline hay chuyển đổi bố cục, cùng với mô hình gán truyền thống, nơi mỗi giai đoạn shader có nhiều khe cắm mà bạn có thể gán tài nguyên một cách tự do. Cả hai mô hình này đều quen thuộc, dễ hiểu và chỉ cần một lượng mã rất hạn chế để bắt đầu nhanh chóng.

Một yếu tố khác giúp ích là giao diện API của chúng tôi đã sẵn sàng cho các API kiểu Metal. Nó rất gọn nhẹ nhưng vẫn cung cấp đủ chi tiết (chẳng hạn như các lần render) để có thể dễ dàng viết một triển khai hiệu quả. Trong suốt quá trình triển khai, tôi không bao giờ phải lưu/khôi phục trạng thái (nhiều giao diện API gặp vấn đề này, đặc biệt do coi việc thiết lập mục tiêu render là thay đổi trạng thái và việc gán tài nguyên/trạng thái duy trì qua các lần đó) hoặc đưa ra các quyết định phức tạp về vòng đời tài nguyên/đồng bộ hóa. Phần mã duy nhất “phức tạp” cần thiết để hiển thị là phần tạo trạng thái đường ống hiển thị bằng cách băm các bit cần thiết để tạo ra nó - các đối tượng trạng thái đường ống không phải là một phần của trừu tượng hóa API của chúng tôi. Ngay cả điều đó cũng khá đơn giản và nhanh chóng. Tôi sẽ viết thêm về giao diện API của chúng tôi trong một bài đăng riêng.

Vậy, một tuần để biên dịch các shader, hai tuần để có được một bản triển khai được tối ưu hóa và hoàn thiện ((Ừ, được rồi, và có lẽ một tuần để sửa một vài lỗi được phát hiện trong quá trình thử nghiệm)) - kết quả ra sao? Kết quả rất tuyệt vời. Metal hoàn toàn thực hiện được lời hứa về hiệu suất. Đầu tiên, hiệu suất phân phối đơn luồng tốt hơn đáng kể so với OpenGL (giảm thời gian phân phối vẽ trong khung hình render của chúng tôi xuống 2-3 lần tùy thuộc vào khối lượng công việc), và điều này được thực hiện ngay cả khi triển khai OpenGL của chúng tôi đã được tối ưu hóa khá tốt về mặt giảm thiểu thiết lập trạng thái dư thừa và tương tác tốt với trình điều khiển bằng cách sử dụng các đường dẫn nhanh. Nhưng không chỉ dừng lại ở đó - việc sử dụng đa luồng trong Metal rất đơn giản miễn là mã render của bạn đã sẵn sàng cho điều đó. Chúng tôi chưa chuyển sang phân phối vẽ đa luồng nhưng đã bắt đầu chuyển đổi một số phần khác chuẩn bị tài nguyên sang thực hiện ngoài luồng render, điều này, khác với OpenGL, gần như không tốn công sức.

Ngoài ra, Metal cho phép chúng tôi khắc phục một số vấn đề hiệu suất khác bằng cách cung cấp các công cụ dễ tiếp cận và đáng tin cậy. Một phần quan trọng trong mã render của chúng tôi là hệ thống tính toán dữ liệu ánh sáng trên CPU trong không gian thế giới và tải lên các vùng của một texture 3D (điều mà chúng tôi phải mô phỏng trên phần cứng OpenGL ES 2). Các bản cập nhật là từng phần nên chúng tôi không thể sao chép toàn bộ texture và phải dựa vào cách trình điều khiển triển khai tính năng cập nhật vùng (glTexSubImage3D). Tại một thời điểm, chúng tôi đã thử sử dụng PBO để cải thiện hiệu suất cập nhật nhưng gặp phải các vấn đề ổn định nghiêm trọng trên cả hai nền tảng Android và iOS. Trên Metal, có hai cách tích hợp sẵn để tải lên một vùng - MTLTexture.replaceRegion mà bạn có thể sử dụng nếu GPU hiện không đang đọc texture, hoặc MTLBlitCommandEncoder (copyFromBufferToTexture  hoặc copyFromTextureToTexture) có thể tải lên vùng một cách không đồng bộ đúng lúc để GPU bắt đầu sử dụng texture. 

Cả hai phương pháp này đều chậm hơn tôi mong muốn. Phương pháp đầu tiên thực sự không khả thi vì chúng tôi phải hỗ trợ cập nhật từng phần hiệu quả, và nó hoạt động hoàn toàn trên CPU bằng cách sử dụng một cơ chế dịch địa chỉ trông có vẻ rất chậm. Phương pháp thứ hai hoạt động nhưng dường như sử dụng một loạt các blit 2D để lấp đầy kết cấu 3D, việc này khá tốn kém để thiết lập các lệnh ở phía CPU và cũng có chi phí GPU rất cao vì lý do nào đó. Nếu đây là OpenGL thì sẽ kết thúc tại đây - trên thực tế, hiệu suất của hai phương pháp này gần như tương đương với chi phí quan sát được của một cập nhật tương tự trong OpenGL. May mắn thay, vì đây là Metal, nó có thể truy cập dễ dàng vào các shader tính toán - và một shader tính toán cực kỳ đơn giản đã cho phép chúng tôi thực hiện việc tải lên từ bộ đệm sang texture 3D một cách rất nhanh trên cả CPU và GPU, và cơ bản đã giải quyết triệt để các vấn đề hiệu suất trong phần mã này((Các con số này áp dụng cho 128 KB dữ liệu được cập nhật mỗi khung hình (hai vùng 32x16x32 RGBA8) trên A10)):

Như một nhận xét chung cuối cùng, việc duy trì mã Metal cũng khá dễ dàng. Tất cả các tính năng bổ sung mà chúng tôi phải thêm vào cho đến nay đều dễ dàng hơn so với bất kỳ API nào khác mà chúng tôi hỗ trợ, và tôi kỳ vọng xu hướng này sẽ tiếp tục. Có một chút lo ngại rằng việc thêm một API nữa sẽ đòi hỏi phải bảo trì liên tục, nhưng so với OpenGL thì việc này thực sự không đòi hỏi nhiều công sức. Thực tế, vì chúng tôi sẽ không phải hỗ trợ OpenGL ES 3 trên iOS nữa, điều này có nghĩa là chúng tôi cũng có thể đơn giản hóa một số mã OpenGL mà chúng tôi đang có.

Tính ổn định

Hiện tại trên iOS, Metal hoạt động rất ổn định. Tôi không chắc tình hình lúc ra mắt vào năm 2014 như thế nào, hay tình hình trên Mac hiện nay ra sao, nhưng cả trình điều khiển lẫn công cụ cho iOS đều khá ổn định.

Chúng tôi đã gặp một vấn đề về trình điều khiển trên iOS 10 liên quan đến việc tải các shader được biên dịch bằng Xcode 7 (chúng tôi đã khắc phục bằng cách chuyển sang Xcode 8), và một lỗi treo trình điều khiển trên iOS 9 do sử dụng sai API `nextDrawable`. Ngoài ra, chúng tôi chưa gặp bất kỳ lỗi hành vi nào hay các trường hợp treo hệ thống. Đối với một API tương đối mới, Metal đã thể hiện sự ổn định đáng kể trên mọi mặt.

Ngoài ra, các công cụ đi kèm với Metal rất đa dạng và phong phú; cụ thể, bạn có thể sử dụng:

  • Một lớp xác thực khá toàn diện giúp phát hiện các vấn đề phổ biến khi sử dụng API. Về cơ bản, nó tương tự như chế độ gỡ lỗi Direct3D - điều này quen thuộc với Direct3D nhưng gần như chưa từng có trong thế giới OpenGL (theo lý thuyết, API "ARB_debug_callback" được thiết kế để giải quyết vấn đề này; tuy nhiên, trên thực tế, nó hầu như không khả dụng và khi có, cũng không thực sự hữu ích)
  • Một công cụ gỡ lỗi GPU hoạt động, hiển thị tất cả các lệnh bạn đã gửi cùng với trạng thái của chúng, nội dung của render target, nội dung của texture, v.v. Tôi không biết liệu nó có công cụ gỡ lỗi shader hoạt động hay không vì tôi chưa bao giờ cần đến nó, và việc kiểm tra bộ đệm có thể dễ dàng hơn một chút, nhưng nó chủ yếu hoàn thành công việc.
  • Một công cụ phân tích hiệu suất GPU hoạt động, hiển thị số liệu hiệu suất theo từng pass (thời gian, băng thông) và thời gian thực thi theo từng shader. Vì GPU hoạt động theo mô hình tiler nên tất nhiên bạn không thể mong đợi thời gian thực thi theo từng drawcall. Việc có mức độ hiển thị này - đặc biệt khi xét đến sự thiếu hụt hoàn toàn thông tin thời gian GPU trong các API đồ họa trên iOS - là rất tuyệt vời.
  • Một công cụ theo dõi dòng thời gian CPU/GPU (Metal System Trace) hoạt động tốt, hiển thị lịch trình công việc render của CPU và GPU (tương tự GPUView nhưng thực sự dễ sử dụng), ngoại trừ một số điểm đặc thù về giao diện người dùng.
  • Một trình biên dịch shader ngoại tuyến giúp xác thực cú pháp shader của bạn, thỉnh thoảng đưa ra các cảnh báo hữu ích và chuyển đổi shader của bạn thành một blob nhị phân có tốc độ tải khá nhanh trong thời gian chạy và được tối ưu hóa khá tốt trước đó (do đó giảm thời gian tải vì trình biên dịch trình điều khiển có thể nhanh hơn).

Nếu bạn đến từ thế giới Direct3D hoặc console, bạn có thể coi tất cả những điều này là điều hiển nhiên. Tin tôi đi, trong OpenGL, từng tính năng này đều là điều bất thường và được đón nhận với sự hào hứng, đặc biệt là trên thiết bị di động, nơi bạn đã quen với việc phải đối phó với các trình điều khiển thỉnh thoảng bị hỏng, không có xác thực, không có trình gỡ lỗi GPU, không có trình phân tích GPU hữu ích, không có khả năng thu thập dữ liệu lập lịch GPU và buộc phải làm việc với ngôn ngữ shader dựa trên văn bản mà mỗi nhà cung cấp lại có trình phân tích cú pháp hơi khác nhau.

Kết luận

Metal là một API tuyệt vời để viết mã và phát hành ứng dụng. Nó dễ sử dụng, có hiệu suất ổn định, và sở hữu các trình điều khiển vững chắc cùng bộ công cụ đáng tin cậy. Nó vượt trội hơn OpenGL ở mọi khía cạnh ngoại trừ tính tương thích, nhưng thực tế là bạn thực sự chỉ nên sử dụng OpenGL trên ba nền tảng (iOS, Android và Mac). Hai trong số ba nền tảng đó hiện đã hỗ trợ Metal; hơn nữa, lời hứa về khả năng tương thích của OpenGL phần lớn không được thực hiện, vì mã bạn viết trên một nền tảng thường không hoạt động trên nền tảng khác vì nhiều lý do khác nhau.

Nếu bạn đang sử dụng một công cụ của bên thứ ba như Unity hoặc UE4, Metal đã được hỗ trợ ở đó; nếu không và bạn thích lập trình đồ họa hoặc quan tâm sâu sắc đến hiệu suất và coi trọng iOS hoặc Mac, tôi khuyên bạn nên thử Metal. Bạn sẽ không thất vọng đâu.