2 điểm bởi GN⁺ 2024-09-03 | 1 bình luận | Chia sẻ qua WhatsApp
  • paraLLEl-GS là dự án tái hiện GS (Graphics Synthesizer) của PS2 bằng compute Vulkan, nhằm xử lý các giới hạn về độ chính xác và upscale mà GSdx, tiêu chuẩn thực tế suốt gần 20 năm, còn bỏ lại
  • GS vận hành dựa trên VRAM 4MiB và fillrate cao, nhưng các đặc tính pipeline pixel như kiểm tra alpha đích, blending có điều kiện, alpha/màu vượt quá 1.0 khiến việc tái hiện bằng API đồ họa thông thường trở nên khó khăn
  • Cách triển khai theo dõi VRAM theo đơn vị trang và khối 256 byte, rồi kết hợp snapshot CLUT, unswizzle texture và sắp xếp render pass để xử lý phản hồi framebuffer/texture
  • Các trường hợp như Tales of the Abyss, Final Fantasy X, MGS2, Valkyrie Profile 2, Shadow of the Colossus được dùng để so sánh các vấn đề về upscale, UI, blending độ chính xác cao và phản hồi texture; bài viết cũng đề cập cả các cảnh 8x·16x SSAA
  • Hiện việc xác minh chủ yếu vẫn dựa vào phát lại GS dump; đã có hack patch cho PCSX2 và thử nghiệm thời gian thực bằng mkfifo, nhưng để đến được với người dùng thực tế thì vẫn cần tích hợp vào emulator

Mục tiêu và điểm khởi đầu của paraLLEl-GS

  • paraLLEl-GS là dự án mô phỏng GS (Graphics Synthesizer) của PlayStation 2 bằng compute Vulkan
  • paraLLEl-RDP, công trình năm 2020 của cùng tác giả, đã triển khai RDP của N64 bằng compute Vulkan và lấy Angrylion làm chuẩn, hướng tới kết quả gần mức chính xác từng bit cùng khả năng upscale
  • Trên PS2, GSdx đã gần như giữ vị thế triển khai tiên tiến nhất trên thực tế trong khoảng 20 năm
  • Từng có một nỗ lực triển khai compute GS của PS2 dựa trên OpenCL vào khoảng năm 2014, nhưng không hoàn thành và hiện cũng khó tìm thấy trong kho upstream
  • Lý do phải dùng raster compute shader trên PS2 không mạnh như trên N64
    • PCSX2 có software renderer được tối ưu tốt và renderer dựa trên graphics tương đối vững
    • Software renderer không hỗ trợ upscale
    • Renderer dựa trên graphics gặp nhiều bug và glitch, đặc biệt là khi upscale
  • paraLLEl-GS tập trung vào việc tránh các vấn đề chính xác rõ ràng hơn là theo đuổi độ chính xác từng bit với phần cứng
    • Ngay cả software renderer của GSdx cũng không có vẻ là triển khai chính xác từng bit so với phần cứng, nên việc kiểm thử dựa trên so sánh trực tiếp nhanh chóng chạm trần

Vì sao GS của PS2 khó xử lý

  • GS là phần cứng vào năm 2000 với fillrate và băng thông về lý thuyết có thể xử lý hơn 1 tỷ pixel mỗi giây
  • VRAM chỉ có 4MiB nhưng được thiết kế để được stream liên tục thông qua nhiều DMA engine
  • Bản thân pipeline pixel có vài mặt còn đơn giản hơn RDP của N64
    • Một texture duy nhất
    • Combiner một chu kỳ duy nhất
    • Anti-aliasing rất cơ bản
  • Có nhiều tính năng khó triển khai bằng API đồ họa thông thường
    • Blending vượt quá 1.0: hành vi kế thừa từ PS1, trong đó 0x80 được xem như 1.0 và có thể biểu diễn tới 0xff
    • Kiểm tra alpha đích: có thể dùng alpha đích như một dạng stencil
    • Blending có điều kiện: có thể tắt blending theo điều kiện dựa trên alpha
    • Hiệu chỉnh alpha: có thể OR bit MSB trước khi ghi alpha để ép giá trị gần 1 hơn
    • Loại bỏ một phần trong alpha test: có thể bỏ màu nhưng vẫn giữ ghi độ sâu
    • AA1: theo kiểu coverage-to-alpha và gắn với việc điều khiển ghi độ sâu theo từng pixel
    • Z fixed-point 32 bit: hỗ trợ D32_UINT về mặt kỹ thuật có tồn tại, nhưng tác giả chưa thấy trường hợp dùng thực tế
  • Nếu không có programmable blending thì trên GPU desktop immediate mode sẽ cần ROV hoặc barrier theo từng pixel, khiến hiệu năng giảm mạnh
  • Cách triển khai bằng compute tạo ra tile-based deferred renderer (TBDR) riêng để né các ràng buộc này

Quy tắc raster, hàng đợi đỉnh và bố cục bộ nhớ

  • Primitive của GS được cung cấp trong clip space theo cách khá thông thường
    • VU1 thực hiện biến đổi và clipping rồi xuất nhiều thuộc tính đỉnh sang GS
  • Tọa độ và thuộc tính dùng định dạng đặc trưng của GS
    • X/Y: fixed-point không dấu 12.4
    • Z: uint 24 bit hoặc 32 bit
    • FOG: uint 8 bit
    • RGBA: giá trị 8 bit cho lighting theo từng đỉnh
    • STQ: tọa độ texture có hiệu chỉnh phối cảnh
    • UV: tọa độ không chuẩn hóa fixed-point 12.4 không có hiệu chỉnh phối cảnh
  • Quy tắc raster gần với kiểu D3D9
    • Tam giác dùng quy tắc raster top-left như GPU hiện đại
    • Tâm pixel nằm trên tọa độ nguyên như trong D3D9
    • Đường dùng thuật toán Bresenham nên khó upscale, phải xấp xỉ bằng rect hoặc parallelogram
    • Điểm được snap vào pixel gần nhất
    • Sprite là quad đơn giản với hai tọa độ
  • Hàng đợi đỉnh của GS giống immediate mode của OpenGL 1.0
    • Thiết lập RGBA, STQ và nhiều thanh ghi khác, rồi ghi vào thanh ghi XYZ sẽ tạo thành thao tác “kick” cho đỉnh
    • Cũng hỗ trợ TRIANGLE_FAN
  • Tọa độ pixel của PS2 được bố trí theo đơn vị trang
    • Mỗi trang là 8KiB
    • Trang được chia thành 32 khối
    • Với RGBA 32 bit, một trang là 64×32 pixel và 32 khối 8×8 được swizzle theo Z-order
  • Khi render màu 24 bit hoặc độ sâu 24 bit, 8 bit cao còn dư có thể dùng để chứa texture
    • Các định dạng 8H, 4HL, 4HH hữu ích cho palette 8 bit và 4 bit

Texture, CLUT, TEXFLUSH

  • Texture của GS có phần giống API hiện đại và cũng có phần rất đặc thù
    • Tâm texel nằm ở half-pixel như API hiện đại
    • Độ chính xác subtexel có vẻ là 4 bit chứ không phải 8 bit
    • Bộ lọc bilinear là bilinear thông thường, không phải cấu trúc đặc biệt như bộ lọc 3 điểm của N64
  • Các chế độ đánh địa chỉ đặc biệt làm tăng độ khó triển khai
    • REGION_CLAMP cho phép áp dụng clamp lên một vùng tùy ý bên trong texture atlas
    • REGION_REPEAT còn khó hơn vì có thể áp dụng phép toán bit như (u & MASK) | FIX cho từng tọa độ
  • Mipmapping tính LOD bằng log2 của hệ số Q đã nội suy và hệ số scale thay vì dùng đạo hàm
    • Với triển khai compute, việc không phụ thuộc vào đạo hàm là một lợi thế lớn
    • Cách này không thể hỗ trợ các tính năng như anisotropic filtering
  • CLUT là một cache 1KiB chứa palette hiện tại
    • Muốn dùng thì phải sao chép tường minh từ VRAM sang cache CLUT
    • Với màu 32 bit, cache này chứa được một palette 256 màu
    • Với 16bpp, có thể chứa 32 palette 16 màu
  • TEXFLUSH là lệnh tường minh gần với việc đồng bộ và vô hiệu hóa cache texture
    • Ban đầu tác giả định dùng TEXFLUSH làm mốc theo dõi hazard, nhưng cuối cùng phải bỏ qua
    • Có game quên gọi TEXFLUSH hoặc gọi quá nhiều
  • Cách triển khai cuối cùng chọn hướng minimal caching
    • Xem như không có cache và theo dõi hazard trực tiếp
    • Với feedback loop thì cân nhắc xử lý ngoại lệ riêng
    • GSdx có vẻ cũng đi theo hướng tương tự

Pipeline render compute Vulkan

  • Pipeline triển khai được xây dựng với giả định có đồng bộ giữa từng giai đoạn
    • Đồng bộ bản sao VRAM phía CPU với GPU
    • Thực hiện upload VRAM hoặc copy local-to-local
    • Cập nhật cache CLUT từ VRAM
    • Unswizzle VRAM thành VkImage để có thể sample trực tiếp
    • Thực hiện render
    • Đồng bộ ngược bản sao VRAM trên GPU về CPU
  • Cách hoạt động thông thường của game khá khớp với pipeline này
    • Upload texture vào VRAM
    • Upload palette vào VRAM
    • Cập nhật cache CLUT
    • Vẽ bằng texture
    • Nếu cần thì unswizzle từ VRAM sang VkImage
    • Gom batch primitive thành render pass
  • Nếu không có hazard quay ngược thì có thể trì hoãn batch và đồng bộ
    • Với kiểu renderer này, muốn có hiệu năng thì giữ được batch là rất quan trọng
  • Các trường hợp hazard chính được xử lý riêng
    • Copy lại vào VRAM mà copy trước đó đã ghi
    • Copy vào VRAM mà texture hoặc CLUT đã sample/đọc
    • Sample vùng đã render như một texture
    • Copy vào VRAM đã được render

Theo dõi theo trang và cache texture

  • Phần khó nhất trong mô phỏng GS là xử lý hazard read-after-writewrite-after-write trong VRAM
  • VRAM 4MiB trước hết được chia theo đơn vị trang
    • Trang là đơn vị của framebuffer và depth buffer nên là mốc theo dõi có ý nghĩa nhất
  • Trạng thái được theo dõi theo cấp độ trang gồm
    • pending frame buffer write
    • pending frame buffer read
  • Texture và VRAM copy có căn chỉnh 256 byte nên dùng bitmask u32 cho 32 khối
    • VRAM copy write
    • VRAM copy read
    • pending read sang cache CLUT hoặc VkImage
    • khối đã bị ghi đè bởi bất kỳ thao tác ghi nào
  • Khi render vào màu 24 bit nhưng sample 8 bit cao như texture thì có thể không có hazard
    • Vì thế cần theo dõi riêng write mask của framebuffer và read mask của texture
  • Mỗi trang giữ danh sách các VkImage liên kết
    • Khi texture của trang bị vô hiệu hóa thì image sẽ bị hủy và phải unswizzle lại từ VRAM
    • Một texture có thể trải trên nhiều trang; chỉ cần một trang bị ghi đè là cả texture bị vô hiệu hóa
  • Theo dõi đơn giản và quá bảo thủ là không đủ để game PS2 chạy đúng
    • Theo dõi ở mức khối 256 byte và xét đến write/read mask là rất quan trọng
  • Texture POT và việc không dùng REGION_CLAMP có thể tạo false positive
    • Ví dụ, nếu đặt render target 512×448 thành texture 512×512 thì phần không dùng có thể bị xem là hazard
    • Cách triển khai dùng một cách lách để bỏ qua hazard tiềm năng trong “red zone” đó

Batch CLUT và unswizzle texture

  • Muốn batch upload texture thì cũng phải batch cả upload CLUT
  • Cách triển khai có 1024 bản sao CLUT trong một snapshot ring buffer
    • Một workgroup duyệt qua các cập nhật và ghi vào SSBO
    • Tương tự cập nhật TMEM của RDP N64, nhưng cập nhật CLUT đơn giản hơn nhiều
  • Trên Vulkan, tác giả cấp phát VkImage mới, suballocate từ VkDeviceMemory rồi unswizzle bằng compute shader
  • Dùng Vulkan specialization constants để chuyên biệt hóa định dạng texture và logic swizzle
  • Hành vi đặc biệt của REGION_REPEAT cũng được xử lý ngay trong bước unswizzle
    • Nhờ vậy ubershader sau đó ít phải tự xử lý bilinear filtering thủ công cho trường hợp này hơn
  • Render target cũng đi vòng qua SSBO VRAM để trở thành texture
    • Tác giả cho rằng việc cố forward trực tiếp render target thành texture tạo ra quá nhiều bug và ngoại lệ

Thiết lập tam giác, binning, ubershader

  • paraLLEl-GS là renderer dựa trên tile giống paraLLEl-RDP
  • Việc thiết lập tam giác diễn ra trước khi binning, với đầu vào chia thành ba mảng
    • Vị trí
    • Thuộc tính theo từng đỉnh
    • Thuộc tính theo từng primitive
  • Rasterizer dựa trên barycentric, chịu ảnh hưởng mạnh từ các bài viết về graphics pipeline của Fabian Giesen và cách raster song song trong bài báo Pineda 1988
  • GS thực của PS2 dùng DDA, tức scanline rasterizer, nhưng vì chưa biết mô tả chính xác từng bit của DDA trên GS nên tác giả dùng cách barycentric
  • Cũng hỗ trợ parallelogram để triển khai wide line và sprite
  • inv_area được tính bằng RCP fixed-point tùy biến
    • RCP tiêu chuẩn của GPU có tính nhất quán giữa các triển khai thấp và độ chính xác chỉ khoảng 22.5 bit nên bị tránh
    • RCP tùy biến nhắm đến độ chính xác khoảng 24.0 bit
  • Binning thường dùng khối 32×32 pixel
    • Số primitive tối đa mỗi render pass là 64k do dùng chỉ số u16
    • Các render pass lớn được quan sát chủ yếu rơi vào khoảng 10k~30k primitive
  • GS của PS2 có fillrate cao và độ phức tạp trên mỗi pixel thấp nên có thể dùng ubershader thuần
    • Khác với N64, có thể tận dụng bindless nên độ phức tạp texture cũng giảm
  • Ubershader dùng early Z, deferred on-tile shading và lazy pixel shading
    • Chỉ thực hiện shading thật khi pixel phụ thuộc vào kết quả trước đó
    • Alpha test, color write mask, alpha blending là các nguyên nhân tạo ra phụ thuộc này
    • Màu framebuffer cuối cùng và độ sâu được ghi vào SSBO để giảm dùng băng thông GPU

Supersampling và giảm artifact khi upscale

  • Chỉ render một mẫu là chưa đủ để khai thác renderer này
  • Ví dụ ở 8x SSAA, GPU giữ 10 phiên bản VRAM
    • 1 bản VRAM single-sample
    • 1 giá trị reference của VRAM single-sample
    • 8 bản supersample
  • Khi render, nếu VRAM single-sample và reference trùng nhau thì sẽ nạp phiên bản supersample
    • Điều này quan trọng với incremental rendering
  • Khi hoàn tất tile, việc resolve multisample được thực hiện bằng clustered subgroup operation và ghi lại cả bản supersample lẫn bản single-sample
  • Supersampling cho ít răng cưa hơn upscale đơn thuần, đồng thời làm cảm nhận độ phân giải giữa thành phần 3D và UI nhất quán hơn
  • Primitive sprite luôn phải được render ở single-rate
    • Đây thường là UI hoặc thành phần tương tự; nếu upscale thì có thể sample ra ngoài rect mong muốn hoặc bị bilinear filtering làm mờ quá mức
  • Vì nhiều UI được vẽ bằng tam giác thường, một số flat primitive sẽ hạ nội suy thuộc tính về tọa độ một pixel
    • Kể cả khi có perspective, nếu mọi đỉnh có cùng Q và cùng Z thì được ước đoán là primitive UI phẳng
    • Có thể có false positive, nhưng với các game đã thử thì hoạt động đủ tốt

Kết quả theo game và các ca khó

  • Trong Tales of the Abyss, upscale của backend Vulkan trong PCSX2 cho thấy hiện tượng lệch căn bloom trên kính và các hoa văn hình vuông
    • Với 8x SSAA của paraLLEl-GS, các lỗi điển hình của upscale kém hầu như không lộ rõ
    • Screenshot đó còn có thêm hậu xử lý upscale FSR1
  • UI của Final Fantasy X cho thấy vấn đề upscale khi so sánh giữa native resolution và 4x upscale
    • Mẹo MSAA snap giúp tránh artifact hiệu quả
    • Nguyên tắc cốt lõi là không upscale UI vượt quá nearest-neighbor với bội số nguyên
  • MGS2 là trường hợp trong PCSX2 đòi hỏi blending accuracy cao
    • PCSX2 ở đường đi programmable blending phải chèn barrier theo từng primitive nên hiệu năng giảm mạnh
    • paraLLEl-GS luôn vận hành với độ chính xác blending 100%; cảnh 16x SSAA trên RX 7600 được nêu với mức 25W và 17% GPU utilization
  • Valkyrie Profile 2 có trường hợp sample alpha của chính pixel làm chỉ số palette
    • paraLLEl-GS phát hiện điều này, đặt chỉ số texture thành một giá trị đặc biệt và tham chiếu màu framebuffer đang giữ trong thanh ghi
    • Tối ưu này làm số render pass barrier giảm từ hơn 500 xuống còn 18
  • Hiệu ứng camo trong phần mở đầu MGS2 sample framebuffer làm texture nhưng dùng các tọa độ chồng lấn không khớp căn pixel
    • Có vẻ PCSX2 cũng không thêm barrier ở đây, và paraLLEl-GS xử lý theo cùng cách
  • Shadow of the Colossus là bài stress test rất nặng
    • Với blend accuracy tối đa của PCSX2, chỉ cần 2x upscale ở đoạn mở đầu là GPU có thể tụt xuống 24 FPS
    • paraLLEl-GS vẫn cho hiệu năng ổn cả ở 8x SSAA, nhưng cảnh đó vẫn là gánh nặng lớn
    • Trong trường hợp này, nút thắt cổ chai nằm ở xử lý geometry phía CPU hơn là GPU

Trạng thái hiện tại và bước tiếp theo

  • Hiện cách kiểm thử thực tế nhất là dùng GS dump
  • hack patch để PCSX2 có thể dump raw GS trace
  • Cũng có thể thử nghiệm thời gian thực ở mức thô sơ qua mkfifo
  • Để hữu ích với người dùng cuối thì vẫn cần tích hợp vào emulator dưới một hình thức nào đó
  • Vì thư viện game PS2 rất lớn nên nhiều khả năng vẫn còn ẩn nhiều bug
  • Do mang tính chất thư viện độc lập, nó cũng có những trường hợp sử dụng tiềm năng như một API render kiểu cũ

1 bình luận

 
GN⁺ 2024-09-03
Các ý kiến trên Hacker News
  • Tôi đã tưởng mình phải mất khá lâu để tìm xem từ viết tắt GS trong bài này được viết đầy đủ lúc nào. Nội dung thì thú vị, nhưng ở điểm này có cảm giác hơi bị tụt lại

    • Đó là viết tắt của Graphics Synthesizer, cái tên Sony đặt cho “GPU” của PS2
  • “Hãy cầu nguyện có programmable blending” ư; kể từ khi lần đầu học về pixel shader hồi đầu những năm 2000, tôi vẫn luôn mong có programmable blending thông qua “blending shader”
    Nhân tiện, tôi cũng muốn có programmable texture decoding thông qua “texture shader”, hữu ích cho các định dạng/nén texture tùy chỉnh, tổng hợp texture, v.v.
    Không hiểu sao GPU lại có ray tracing trước programmable blending; cái trước giống như giấc mơ đêm hè, còn cái sau thì có cảm giác chỉ là biến thêm một khối chức năng cố định thành khối lập trình được. Texture shader thì tôi vẫn đang chờ

    • Các GPU di động kế thừa dòng PowerVR có tính năng như vậy
      https://medium.com/pocket-gems/programmable-blending-on-ios-...
      https://developer.apple.com/videos/play/tech-talks/605
    • Lần cuối tôi dùng thử, GPU PowerVR di động có programmable blending, và thực ra đó là cách blending duy nhất. Trên PS Vita, đổi trạng thái blend mất khoảng 1ms nên trông không đẹp lắm
    • Tôi nghĩ VK_EXT_fragment_shader_interlock phần nào là một dạng programmable blending. Raster-order views bên DirectX cũng vậy
      Có một ví dụ hay về việc tận dụng nó: https://vulkan.org/user/pages/09.events/vulkanised-2024/vulk...
    • Ngày nay còn có mesh shader, work graphs, CUDA, và cả shader C++ thông thường. OTOY giờ xử lý toàn bộ rendering bằng compute
    • Tôi nghĩ phần lớn những thứ đó có thể được giả lập trong Vulkan hoặc DX12; các API khác thì tôi không rõ. Tuy nhiên tôi tò mò về nơi dùng thực tế của nó
      Tôi cho rằng có thể triển khai ở mức nào đó, nhưng nếu không có use case thuyết phục thì khó biện minh cho công triển khai
  • Phần tôi thích nhất ở GS là quy mô phi lý của cấu trúc bus. Tổng độ rộng 2560 bit, và cách chia cache cũng rất khéo
    PS3 ở một số khía cạnh có cảm giác như bước lùi, đặc biệt là blending

  • Tôi tò mò cách tiếp cận này so với ubershader của Dolphin ra sao

    • Về cơ bản thì gần như không phải đối tượng để so sánh. Ubershader của Dolphin làm một việc: mô phỏng blending/texturing chức năng cố định bằng phần cứng hiện đại linh hoạt
      Thực ra khi Dolphin đưa nó vào, kỹ thuật này đã cũ rồi. Dự án này là một renderer hoàn chỉnh, bao gồm cả rasterizer, và như trong bài viết cho thấy, cũng có ubershader cho blending
      Shader không vẽ tam giác; nó được gọi tại từng điểm bên trong tam giác, nhận vài đầu vào và quyết định màu của điểm đó. Đại khái giống như so sánh một trình giả lập CPU đầy đủ với việc chỉ triển khai các lệnh ADD/MUL
  • Tôi thắc mắc top-left raster nghĩa là gì