Back to Blog
App DevelopmentPublished on July 29, 2026

Cracking Windows Open: Porting Mesa's RADV Vulkan Driver to Win32 Architecture

Explore the low-level systems engineering required to run Mesa's open-source RADV AMD Vulkan driver natively on Windows. Learn how to bridge Linux DRM/KMS abstractions with the Windows Display Driver Model (WDDM) interface.

Introduction: The Open-Source GPU Driver Conundrum

For years, the Linux graphics ecosystem has enjoyed a quiet revolution. While proprietary vendor drivers once dominated high-performance rendering, open-source alternatives developed within the Mesa 3D Graphics Library have taken the crown. AMD's RADV—the open-source Vulkan driver for Radeon GPUs—is a prime example. Developed by community engineers and valve contractors, RADV frequently outpaces AMD's official proprietary Linux driver (AMDVLK) and offers superior compliance, rapid bug fixes, and deep integration with modern translation layers like DXVK and VKD3D-Proton.

However, this innovation has remained largely locked inside the Linux kernel ecosystem. Windows users running AMD hardware are tied strictly to the closed-source AMD Software: Adrenalin Edition drivers. But what if we could break that boundary? What would it take to port Mesa's RADV driver to run natively on Windows (Win32)?

Porting a Linux GPU driver to Win32 is not merely a matter of recompiling C code against POSIX shims. It requires a fundamental reconciliation between two drastically different operating system philosophies: Linux's Direct Rendering Manager (DRM / GEM) and Microsoft's Windows Display Driver Model (WDDM). In this article, we will disassemble the architectural hurdles of porting RADV to Windows, detail the translation of kernel-mode interfaces, and build a prototype allocation shim.


Understanding the Layered Architecture: RADV on Linux vs. Win32

To understand how RADV operates, we must first map its dependency chain on Linux:

  1. Vulkan API Layer: The application calls Vulkan entry points (e.g., vkCmdDrawIndexed).
  2. RADV User-Mode Driver (UMD): Translates Vulkan commands into hardware-specific AMD GPU instruction packets (PM4 packets).
  3. Mesa libdrm_amdgpu: A user-space wrapper library that formats ioctl calls for the Linux kernel.
  4. Linux Kernel amdgpu.ko (KMD): Handles GPU memory management (TTM/GEM), command submission ring buffers, display output (KMS), and hardware power states.

When attempting to run RADV on Windows, step 1 and step 2 remain largely identical because Vulkan API specifications and GPU hardware ISAs (RDNA/GCN) are OS-agnostic. The point of failure occurs at steps 3 and 4.

On Windows, user-mode drivers cannot issue raw ioctl requests to a character device like /dev/dri/card0. Instead, Windows enforces the WDDM architecture. Hardware interaction must pass through the Win32 Graphics Kernel Subsystem (dxgkrnl.sys) using specialized User-Mode Driver APIs provided by gdi32.dll or d3d12.dll kernel thunks.

+-------------------------------------------------------+
|                 Vulkan Application                    |
+-------------------------------------------------------+
                            |
                            v
+-------------------------------------------------------+
|                   RADV UMD (Mesa)                     |
+-------------------------------------------------------+
                            |
             +--------------+--------------+
             |                             |
   (Linux Path - Original)        (Win32 Port - Target)
             |                             |
             v                             v
+-------------------------+   +-------------------------+
|      libdrm_amdgpu      |   |   Win32 Translation     |
|    (ioctl /dev/dri/..)  |   |   Layer (D3DKMT / WDDM) |
+-------------------------+   +-------------------------+
             |                             |
             v                             v
+-------------------------+   +-------------------------+
|   Linux Kernel (amdgpu) |   |   dxgkrnl.sys (WDDM)    |
+-------------------------+   +-------------------------+

Key Architectural Challenges

1. Memory Management: GEM Objects vs. WDDM Allocations

Linux's amdgpu kernel driver uses the Graphics Execution Manager (GEM) and Translation Table Manager (TTM) to handle memory allocation. RADV requests memory buffers by passing parameters to amdgpu_bo_alloc(), which delegates to DRM_IOCTL_AMDGPU_GEM_CREATE.

On Windows, memory is managed via WDDM paging. Allocations must be created using the low-level kernel interface D3DKMTCreateAllocation2 or managed through DirectX Graphics Infrastructure (DXGI) shared handles.

  • Alignment & Domains: Linux DRM allows explicit placement in VRAM, GTT (system memory mapped to GPU), or CPU-visible VRAM (ReBAR). WDDM abstracts physical placement into preferred memory segments (D3DDDI_SEGMENTPREFERENCE), delegating actual paging to the OS scheduler (dxgkrnl). RADV must be updated to respect WDDM's resident/evict semantics (D3DKMTMakeResident and D3DKMTEvict).

2. Command Submission: amdgpu_cs_submit vs. D3DKMTSubmitCommand

RADV writes raw command buffers containing PM4 packets (Command Processor instructions for RDNA GPUs) directly into mapped GPU memory. On Linux, these buffer chains are submitted via amdgpu_cs_submit ioctls to specific hardware rings (GFX, Compute, SDMA).

Windows WDDM operates differently depending on whether hardware scheduling (HWS) is enabled:

  • Legacy WDDM: Commands are parsed by a kernel-mode driver (KMD) miniport before hitting the GPU hardware scheduler.
  • Modern WDDM (2.0+): User-mode drivers map hardware submission queues directly to user-space memory rings using D3DKMTCreatePagingQueue and D3DKMTSubmitCommand.

To port RADV, we must implement a translation layer that takes RADV's constructed amdgpu_cs_request structures and converts them into D3DKMT_SUBMITCOMMAND payloads that dxgkrnl.sys understands.

3. Synchronization: Timeline Semaphores & Monitored Fences

Vulkan relies heavily on timeline semaphores for asynchronous execution graph synchronization. On modern Linux kernels, these map directly to DRM Sync Objects (drm_syncobj).

On Windows, the native kernel synchronization mechanism for graphics queues is the WDDM Monitored Fence (D3DKMT_CREATESYNCHRONIZATIONOBJECT2 with type D3DDDI_MONITORED_FENCE). Fortunately, modern WDDM monitored fences behave almost identically to Vulkan timeline semaphores—both use a 64-bit monotonically increasing counter. Bridging this gap is surprisingly one of the cleanest aspects of the port.


Engineering the Translation Layer: C++ Implementation

To demonstrate how RADV can communicate with the Windows graphics subsystem without native amdgpu.ko support, let's write a C++ translation module that wraps WDDM Kernel Mode Thunks (D3DKMT) to allocate GPU memory for the RADV driver on Windows.

#include <windows.h>
#include <d3dkmthk.h>
#include <iostream>
#include <cstdint>
#include <stdexcept>

// Conceptual replacement for libdrm_amdgpu memory allocation on Win32
struct RadvWin32BufferObject {
    D3DKMT_HANDLE hDevice;
    D3DKMT_HANDLE hAllocation;
    uint64-t gpuVirtualAddress;
    void* pCpuCpuMappedAddress;
    size_t size;
};

class Win32GpuMemoryManager {
private:
    D3DKMT_HANDLE m_hAdapter;
    D3DKMT_HANDLE m_hDevice;

public:
    Win32GpuMemoryManager() {
        // 1. Enum Adapters using D3DKMT
        D3DKMT_ENUMADAPTERS enumAdapters = {};
        if (NT_SUCCESS(D3DKMTEnumAdapters(&enumAdapters)) && enumAdapters.NumAdapters > 0) {
            m_hAdapter = enumAdapters.Adapters[0].hAdapter;
        } else {
            throw std::runtime_error("Failed to locate WDDM adapter.");
        }

        // 2. Create Kernel Context Device
        D3DKMT_CREATEDEVICE createDevice = {};
        createDevice.hAdapter = m_hAdapter;
        if (!NT_SUCCESS(D3DKMTCreateDevice(&createDevice))) {
            throw std::runtime_error("Failed to create D3DKMT device context.");
        }
        m_hDevice = createDevice.hDevice;
    }

    ~Win32GpuMemoryManager() {
        if (m_hDevice) {
            D3DKMT_DESTROYDEVICE destroyDevice = { m_hDevice };
            D3DKMTDestroyDevice(&destroyDevice);
        }
    }

    RadvWin32BufferObject AllocateGpuBuffer(size_t sizeInBytes, bool isVramPreferred) {
        RadvWin32BufferObject bo = {};
        bo.hDevice = m_hDevice;
        bo.size = sizeInBytes;

        // Step A: Define Allocation Parameters
        D3DDDI_ALLOCATIONINFO2 allocInfo = {};
        allocInfo.pPrivateDriverData = nullptr;
        allocInfo.PrivateDriverDataSize = 0;

        D3DKMT_CREATEALLOCATION2 createAlloc = {};
        createAlloc.hDevice = m_hDevice;
        createAlloc.NumAllocations = 1;
        createAlloc.pAllocationInfo2 = &allocInfo;
        createAlloc.Flags.CreateResource = 0; // Pure raw buffer allocation

        if (!NT_SUCCESS(D3DKMTCreateAllocation2(&createAlloc))) {
            throw std::runtime_error("WDDM allocation creation failed.");
        }
        bo.hAllocation = allocInfo.hAllocation;

        // Step B: Map GPU Virtual Address Space (equivalent to amdgpu_bo_va_op)
        D3DKMT_MAPGPUVIRTUALADDRESS mapGpuVa = {};
        mapGpuVa.hDevice = m_hDevice;
        mapGpuVa.hAllocation = bo.hAllocation;
        mapGpuVa.Size = sizeInBytes;
        mapGpuVa.MinimumAddress = 0x10000; // Protection against null-pointer execution on GPU
        mapGpuVa.MaximumAddress = 0xFFFFFFFFFFFF0000ULL;

        if (!NT_SUCCESS(D3DKMTMapGpuVirtualAddress(&mapGpuVa))) {
            throw std::runtime_error("Failed to map GPU Virtual Address space.");
        }
        bo.gpuVirtualAddress = mapGpuVa.VirtualAddress;

        // Step C: Ensure Memory is Made Resident
        D3DKMT_MAKERESIDENT makeResident = {};
        makeResident.hDevice = m_hDevice;
        makeResident.NumAllocations = 1;
        makeResident.AllocationList = &bo.hAllocation;
        
        if (!NT_SUCCESS(D3DKMTMakeResident(&makeResident))) {
            throw std::runtime_error("Failed to make allocation resident in VRAM/System Memory.");
        }

        return bo;
    }

    void FreeGpuBuffer(RadvWin32BufferObject& bo) {
        if (bo.hAllocation) {
            D3DKMT_FREEGPUVIRTUALADDRESS freeVa = {};
            freeVa.hAdapter = m_hAdapter;
            freeVa.BaseAddress = bo.gpuVirtualAddress;
            freeVa.Size = bo.size;
            D3DKMTFreeGpuVirtualAddress(&freeVa);

            D3DKMT_DESTROYALLOCATION destroyAlloc = {};
            destroyAlloc.hDevice = m_hDevice;
            destroyAlloc.NumAllocations = 1;
            destroyAlloc.phAllocationList = &bo.hAllocation;
            D3DKMTDestroyAllocation(&destroyAlloc);

            bo.hAllocation = 0;
        }
    }
};

int main() {
    try {
        Win32GpuMemoryManager memManager;
        std::cout << "[RADV-Win32] Initialized WDDM Subsystem Wrapper successfully.\n";
        
        // Allocate 64MB buffer for RADV descriptor pools or vertex data
        auto bo = memManager.AllocateGpuBuffer(64 * 1024 * 1024, true);
        std::cout << "[RADV-Win32] Allocated 64MB GPU VA at: 0x" 
                  << std::hex << bo.gpuVirtualAddress << std::dec << "\n";
        
        memManager.FreeGpuBuffer(bo);
        std::cout << "[RADV-Win32] Deallocated GPU Memory successfully.\n";
    } catch (const std::exception& e) {
        std::cerr << "[Error] " << e.what() << "\n";
        return -1;
    }
    return 0;
}

Display Presentation: Overcoming Swapchain Bottlenecks

Once RADV renders a frame on Windows, it must present that image to the display screen. On Linux, RADV interfaces with X11 (via DRI3) or Wayland protocols. On Windows, presentation requires integration with the Win32 Desktop Window Manager (DWM).

To achieve swapchain output on Win32 without rewriting RADV's entire presentation engine, developers can leverage DXGI/D3D11 Interop.

  1. RADV renders to an internal Vulkan VkImage backed by a WDDM allocation.
  2. The underlying memory is exported using a Win32 Shared Handle (HANDLE hSharedNT).
  3. The handle is imported into a lightweight Direct3D 11 / Direct3D 12 texture context.
  4. A DXGI Swapchain (IDXGISwapChain1::Present1) presents the texture to the HWND target.

While this introduces a microscopic zero-copy interop overhead, it allows RADV to operate entirely independently of Windows' native driver user-space binaries.


Why This Engineering Effort Matters

Porting Mesa's RADV driver to Win32 is far more than an academic exercise or novelty project:

  1. Driver Debuggability: Windows driver developers are notoriously constrained by opaque, closed-source kernel miniports. Running RADV on Win32 grants developers line-by-line debugging access using tools like RenderDoc, Valgrind, and GDB/Visual Studio attached to driver source code.
  2. Custom Compiler Tuning: RADV leverages ACO (AMD Compiler Option), Valve's custom shader compiler back-end. ACO generates significantly faster code for complex compute pipelines than traditional compilers. Porting RADV brings ACO shader compilation performance to Windows workloads.
  3. Legacy Hardware Preservation: As commercial GPU vendors drop official support for older architectures (such as AMD Polaris/Vega), open-source drivers like RADV continue to receive active maintenance and Vulkan feature updates. A Win32 RADV port extends the lifespan of older hardware on modern Windows platforms.

Conclusion

The boundary between Linux and Windows system architectures is often viewed as insurmountable at the driver level. However, as Microsoft's WDDM APIs mature and user-space hardware access expands, bridging open-source graphics stacks like RADV into the Win32 ecosystem becomes increasingly viable. By mapping DRM IOCTLs to WDDM Kernel-Mode Thunks and matching Linux syncobjs with Monitored Fences, we can unleash open-source graphics performance on any operating system.

#Vulkan#Systems Programming#Mesa#Windows Kernel#Graphics Drivers