Vấn Đề Thiết Kế Giao Diện Direct3D12
Cách đây không lâu, tôi đã dành nguyên một đêm để vật lộn với một lỗi nghiêm trọng liên quan đến Direct3D12. Đây là một vấn đề thú vị và đáng để chia sẻ chi tiết.
Sự cố bắt nguồn từ hàm ID3D12Device::GetAdapterLuid()
. Khi biên dịch bằng MinGW64 GCC, chỉ cần gọi hàm API này, bảng vtable của đối tượng d3d12device
lập tức bị phá vỡ. Mọi thao tác gọi API tiếp theo trên thiết bị này đều dẫn đến crash chương trình.
Do hàm này được triển khai trong d3d12.dll
(không có mã nguồn mở), tôi đành phải dùng gdb để debug. Vấn đề nằm ở điểm trả về của hàm - kiểu LUID
(một cấu trúc dữ liệu). Trong C/C++, việc trả về cấu trúc từ hàm không có quy chuẩn thống nhất về cách gọi. Điều này vi phạm nguyên tắc thiết kế COM, nơi lẽ ra nên tránh kiểu thiết kế API như vậy.
Theo quy tắc COM, mọi API bắt buộc phải trả về HRESULT
, chỉ có vài ngoại lệ đặc biệt được phép trả về kiểu số nguyên ULONG
. Thực tế, các phiên bản D3D trước đây đều tuân thủ nghiêm ngặt quy tắc này. Ví dụ, D3D9 có API tương tự với thiết kế hợp lý hơn:
|
|
Có thể các kỹ sư mới của Microsoft đã vô tình lãng quên các quy tắc COM khi thiết kế lại D3D12, đồng thời không tính đến sự tồn tại của các trình biên dịch C++ khác ngoài Visual C++. Trong triển khai d3d12.dll
, khi trả về cấu trúc, VC++ quy định địa chỉ vùng nhớ trả về sẽ được truyền như một tham số ẩn trên stack.
Nói cách khác, hàm API thực chất có dạng:
|
|
Tuy nhiên, GCC lại tối ưu hóa việc trả về cấu trúc nhỏ bằng cách sử dụng thanh ghi thay vì stack. Nếu cấu trúc ≤ 64bit, GCC sẽ trả về trực tiếp qua thanh ghi, không truyền địa chỉ stack. Điều này dẫn đến mâu thuẫn:
- Mã biên dịch bởi GCC chỉ truyền con trỏ
this
quarcx
, coirdx
là giá trị vô nghĩa d3d12.dll
lại hiểurdx
là địa chỉ vùng nhớ trả về, dẫn đến việc ghi đè dữ liệu lên chính vtable của đối tượng
Giải pháp khả thi
GCC cung cấp tùy chọn -fpcc-struct-return
để vô hiệu hóa tối ưu hóa này, nhưng vẫn tồn tại bất đồng về ABI:
- GCC truyền địa chỉ trả về như tham số đầu tiên
- VC++ lại đặt nó ở vị trí thứ hai, sau con trỏ
this
Trước khi Microsoft cập nhật SDK, tôi đã xây dựng một hàm hỗ trợ:
|
|
Vấn đề tương tự cũng xuất hiện ở các API như ID3D12DescriptorHeap
, và nghiêm trọng hơn ở ID3D12Resource::GetDesc()
- hàm trả về cấu trúc D3D12_RESOURCE_DESC
. Đặc biệt, API này được sử dụng trực tiếp trong hàm inline UpdateSubresources
của tệp d3dx12.h
, khiến lỗi trở nên phổ biến hơn.
Tôi đã thử nghiệm một giải pháp proxy:
|
|
Tuy nhiên, cách này không hiệu quả khi gọi UpdateSubresources
vì hàm này nội bộ lại gọi ID3D12GraphicsCommandList::CopyTextureRegion
- API được triển khai trong d3d12.dll
, tiếp tục gây ra xung đột giao thức.
Bài học thiết kế
Việc Microsoft đặt mã triển khai quan trọng trong các tệp .h
công khai là một quyết định thiết kế tồi. Điều này không chỉ gây ra lỗi tương thích nghiêm trọng, mà còn làm phức tạp hóa việc sửa chữa khi sự cố xảy ra. Vấn đề này phơi bày sự thiếu sót trong việc kiểm tra tính tương thích đa nền tảng khi thiết kế API hệ thống.
Hiện tại, giải pháp duy nhất là:
- Sử dụng Visual C++ thay vì GCC
- Tự sửa đổi tệp
d3dx12.h
để thay thế các hàm có vấn đề - Tránh sử dụng các hàm inline phụ thuộc vào
GetDesc()