From de08feeffb1e08227b2f7ae884464175a5a21379 Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Mon, 31 Jul 2023 23:33:02 +1000
Subject: [PATCH] More abstraction

---
 src/common/CMakeLists.txt          | 165 ----------
 src/core/CMakeLists.txt            | 186 ++++++++++-
 src/core/core.vcxproj              |   6 -
 src/core/core.vcxproj.filters      |  18 -
 src/core/gpu/d3d11_device.cpp      | 505 +++++++++++++++++++++++++++++
 src/core/gpu/d3d11_device.h        | 112 ++++++-
 src/core/gpu/d3d11_pipeline.cpp    | 242 --------------
 src/core/gpu/d3d11_pipeline.h      |  53 ---
 src/core/gpu/d3d11_shader.cpp      | 113 -------
 src/core/gpu/d3d11_shader.h        |  40 ---
 src/core/gpu/gpu_device.cpp        | 189 +++++++++++
 src/core/gpu/gpu_device.h          | 345 +++++++++++++++++++-
 src/core/gpu/gpu_pipeline.h        | 199 ------------
 src/core/gpu/gpu_shader.h          |  29 --
 src/frontend-common/CMakeLists.txt |  33 --
 15 files changed, 1325 insertions(+), 910 deletions(-)
 delete mode 100644 src/core/gpu/d3d11_pipeline.cpp
 delete mode 100644 src/core/gpu/d3d11_pipeline.h
 delete mode 100644 src/core/gpu/d3d11_shader.cpp
 delete mode 100644 src/core/gpu/d3d11_shader.h
 delete mode 100644 src/core/gpu/gpu_pipeline.h
 delete mode 100644 src/core/gpu/gpu_shader.h

diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 370a89849..829244272 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -15,8 +15,6 @@ add_library(common
   fifo_queue.h
   file_system.cpp
   file_system.h
-  gpu_texture.cpp
-  gpu_texture.h
   image.cpp
   image.h
   hash_combine.h
@@ -66,28 +64,6 @@ target_link_libraries(common PRIVATE stb libchdr zlib minizip Zstd::Zstd "${CMAK
 
 if(WIN32)
   target_sources(common PRIVATE
-    d3d12/context.cpp
-    d3d12/context.h
-    d3d12/descriptor_heap_manager.cpp
-    d3d12/descriptor_heap_manager.h
-    d3d12/shader_cache.cpp
-    d3d12/shader_cache.h
-    d3d12/staging_texture.cpp
-    d3d12/staging_texture.h
-    d3d12/stream_buffer.cpp
-    d3d12/stream_buffer.h
-    d3d12/texture.cpp
-    d3d12/texture.h
-    d3d12/util.cpp
-    d3d12/util.h
-    d3d11/shader_cache.cpp
-    d3d11/shader_cache.h
-    d3d11/shader_compiler.cpp
-    d3d11/shader_compiler.h
-    d3d11/stream_buffer.cpp
-    d3d11/stream_buffer.h
-    d3d11/texture.cpp
-    d3d11/texture.h
     http_downloader_winhttp.cpp
     http_downloader_winhttp.h
     thirdparty/StackWalker.cpp
@@ -113,147 +89,6 @@ if(ANDROID)
   target_link_libraries(common PRIVATE log)
 endif()
 
-if(USE_X11)
-  target_sources(common PRIVATE
-      gl/x11_window.cpp
-      gl/x11_window.h
-  )
-  target_compile_definitions(common PRIVATE "-DUSE_X11=1")
-  target_include_directories(common PRIVATE "${X11_INCLUDE_DIR}" "${X11_Xrandr_INCLUDE_PATH}")
-  target_link_libraries(common PRIVATE "${X11_LIBRARIES}" "${X11_Xrandr_LIB}")
-endif()
-
-if(USE_WAYLAND)
-  target_compile_definitions(common PRIVATE "-DUSE_WAYLAND=1")
-elseif(SUPPORTS_WAYLAND)
-  message(WARNING "Wayland support for renderers is disabled.\nDuckStation will FAIL to start on Wayland.")
-endif()
-
-if(USE_DRMKMS)
-  target_sources(common PRIVATE
-    drm_display.cpp
-    drm_display.h
-  )
-  target_link_libraries(common PUBLIC Libdrm::Libdrm)
-endif()
-
-if(ENABLE_OPENGL)
-  target_sources(common PRIVATE
-    gl/context.cpp
-    gl/context.h
-    gl/program.cpp
-    gl/program.h
-    gl/shader_cache.cpp
-    gl/shader_cache.h
-    gl/stream_buffer.cpp
-    gl/stream_buffer.h
-    gl/texture.cpp
-    gl/texture.h
-  )
-  target_compile_definitions(common PUBLIC "WITH_OPENGL=1")
-  target_link_libraries(common PRIVATE glad)
-
-  if(WIN32)
-    target_sources(common PRIVATE
-      gl/context_wgl.cpp
-      gl/context_wgl.h
-    )
-  endif()
-
-  if(USE_EGL)
-    target_sources(common PRIVATE
-      gl/context_egl.cpp
-      gl/context_egl.h
-    )
-    target_compile_definitions(common PRIVATE "-DUSE_EGL=1")
-
-    if(USE_X11)
-      target_sources(common PRIVATE
-        gl/context_egl_x11.cpp
-        gl/context_egl_x11.h
-      )
-
-      # We set EGL_NO_X11 because otherwise X comes in with its macros and breaks
-      # a bunch of files from compiling, if we include the EGL headers. This just
-      # makes the data types opaque, we can still use it with X11 if needed.
-      target_compile_definitions(common PRIVATE "-DEGL_NO_X11=1")
-    endif()
-    if(ANDROID AND USE_EGL)
-      target_sources(common PRIVATE
-        gl/context_egl_android.cpp
-        gl/context_egl_android.h
-      )
-    endif()
-    if(USE_DRMKMS)
-      target_compile_definitions(common PRIVATE "-DUSE_GBM=1")
-      target_sources(common PRIVATE
-        gl/context_egl_gbm.cpp
-        gl/context_egl_gbm.h
-      )
-      target_link_libraries(common PUBLIC GBM::GBM)
-    endif()
-    if(USE_FBDEV)
-      target_compile_definitions(common PRIVATE "-DUSE_FBDEV=1")
-      target_sources(common PRIVATE
-        gl/context_egl_fbdev.cpp
-        gl/context_egl_fbdev.h
-      )
-    endif()
-  endif()
-
-  if(USE_X11)
-    target_sources(common PRIVATE
-      gl/context_glx.cpp
-      gl/context_glx.h
-    )
-    target_compile_definitions(common PRIVATE "-DUSE_GLX=1")
-  endif()
-
-  if(USE_WAYLAND)
-    target_sources(common PRIVATE
-      gl/context_egl_wayland.cpp
-      gl/context_egl_wayland.h
-    )
-  endif()
-
-  if(APPLE)
-    target_sources(common PRIVATE
-      gl/context_agl.mm
-      gl/context_agl.h
-    )
-  endif()
-endif()
-
-if(ENABLE_VULKAN)
-  target_sources(common PRIVATE
-    vulkan/builders.cpp
-    vulkan/builders.h
-    vulkan/context.cpp
-    vulkan/context.h
-    vulkan/loader.h
-    vulkan/loader.cpp
-    vulkan/shader_cache.cpp
-    vulkan/shader_cache.h
-    vulkan/shader_compiler.cpp
-    vulkan/shader_compiler.h
-    vulkan/stream_buffer.cpp
-    vulkan/stream_buffer.h
-    vulkan/swap_chain.cpp
-    vulkan/swap_chain.h
-    vulkan/texture.cpp
-    vulkan/texture.h
-    vulkan/util.cpp
-    vulkan/util.h
-  )
-  target_compile_definitions(common PUBLIC "WITH_VULKAN=1")
-  target_link_libraries(common PRIVATE glslang)
-
-  if(APPLE)
-    # Needed for Vulkan Swap Chain.
-    target_link_libraries(common PRIVATE "objc")
-  endif()
-endif()
-
 if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
   # We need -lrt for shm_unlink
   target_link_libraries(common PRIVATE rt)
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 158af6b30..e6c7e8cbe 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -54,8 +54,6 @@ add_library(core
     gte_types.h
     host.cpp
     host.h
-    host_display.cpp
-    host_display.h
     host_interface_progress_callback.cpp
     host_interface_progress_callback.h
     host_settings.h
@@ -122,6 +120,47 @@ target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
 target_link_libraries(core PUBLIC Threads::Threads common util zlib)
 target_link_libraries(core PRIVATE stb xxhash imgui rapidjson tinyxml2)
 
+target_sources(core PRIVATE
+  gpu/gpu_device.cpp
+  gpu/gpu_device.h
+  gpu/gpu_pipeline.h
+  gpu/gpu_shader.h
+  gpu/gpu_texture.cpp
+  gpu/gpu_texture.h
+  gpu/postprocessing_chain.cpp
+  gpu/postprocessing_chain.h
+  gpu/postprocessing_shader.cpp
+  gpu/postprocessing_shader.h
+  gpu/postprocessing_shadergen.cpp
+  gpu/postprocessing_shadergen.h
+)
+
+if(WIN32)
+  target_sources(core PRIVATE
+    d3d12/context.cpp
+    d3d12/context.h
+    d3d12/descriptor_heap_manager.cpp
+    d3d12/descriptor_heap_manager.h
+    d3d12/shader_cache.cpp
+    d3d12/shader_cache.h
+    d3d12/staging_texture.cpp
+    d3d12/staging_texture.h
+    d3d12/stream_buffer.cpp
+    d3d12/stream_buffer.h
+    d3d12/texture.cpp
+    d3d12/texture.h
+    d3d12/util.cpp
+    d3d12/util.h
+    d3d11/shader_cache.cpp
+    d3d11/shader_cache.h
+    d3d11/shader_compiler.cpp
+    d3d11/shader_compiler.h
+    d3d11/stream_buffer.cpp
+    d3d11/stream_buffer.h
+    d3d11/texture.cpp
+    d3d11/texture.h
+  )
+endif()
 if(WIN32)
   target_sources(core PRIVATE
     gpu_hw_d3d12.cpp
@@ -132,10 +171,116 @@ if(WIN32)
   target_link_libraries(core PRIVATE winmm.lib)
 endif()
 
-if(ENABLE_CUBEB)
-  target_compile_definitions(core PUBLIC "WITH_CUBEB=1")
+if(USE_X11)
+  target_sources(common PRIVATE
+      gl/x11_window.cpp
+      gl/x11_window.h
+  )
+  target_compile_definitions(common PRIVATE "-DUSE_X11=1")
+  target_include_directories(common PRIVATE "${X11_INCLUDE_DIR}" "${X11_Xrandr_INCLUDE_PATH}")
+  target_link_libraries(common PRIVATE "${X11_LIBRARIES}" "${X11_Xrandr_LIB}")
 endif()
 
+if(USE_WAYLAND)
+  target_compile_definitions(common PRIVATE "-DUSE_WAYLAND=1")
+elseif(SUPPORTS_WAYLAND)
+  message(WARNING "Wayland support for renderers is disabled.\nDuckStation will FAIL to start on Wayland.")
+endif()
+
+if(USE_DRMKMS)
+  target_sources(common PRIVATE
+    drm_display.cpp
+    drm_display.h
+  )
+  target_link_libraries(common PUBLIC Libdrm::Libdrm)
+endif()
+
+if(ENABLE_OPENGL)
+  target_sources(common PRIVATE
+    gpu/gl/context.cpp
+    gpu/gl/context.h
+    gpu/gl/program.cpp
+    gpu/gl/program.h
+    gpu/gl/shader_cache.cpp
+    gpu/gl/shader_cache.h
+    gpu/gl/stream_buffer.cpp
+    gpu/gl/stream_buffer.h
+    gpu/gl/texture.cpp
+    gpu/gl/texture.h
+  )
+  target_compile_definitions(common PUBLIC "WITH_OPENGL=1")
+  target_link_libraries(common PRIVATE glad)
+
+  if(WIN32)
+    target_sources(common PRIVATE
+      gl/context_wgl.cpp
+      gl/context_wgl.h
+    )
+  endif()
+
+  if(USE_EGL)
+    target_sources(common PRIVATE
+      gl/context_egl.cpp
+      gl/context_egl.h
+    )
+    target_compile_definitions(common PRIVATE "-DUSE_EGL=1")
+
+    if(USE_X11)
+      target_sources(common PRIVATE
+        gl/context_egl_x11.cpp
+        gl/context_egl_x11.h
+      )
+
+      # We set EGL_NO_X11 because otherwise X comes in with its macros and breaks
+      # a bunch of files from compiling, if we include the EGL headers. This just
+      # makes the data types opaque, we can still use it with X11 if needed.
+      target_compile_definitions(common PRIVATE "-DEGL_NO_X11=1")
+    endif()
+    if(ANDROID AND USE_EGL)
+      target_sources(common PRIVATE
+        gl/context_egl_android.cpp
+        gl/context_egl_android.h
+      )
+    endif()
+    if(USE_DRMKMS)
+      target_compile_definitions(common PRIVATE "-DUSE_GBM=1")
+      target_sources(common PRIVATE
+        gl/context_egl_gbm.cpp
+        gl/context_egl_gbm.h
+      )
+      target_link_libraries(common PUBLIC GBM::GBM)
+    endif()
+    if(USE_FBDEV)
+      target_compile_definitions(common PRIVATE "-DUSE_FBDEV=1")
+      target_sources(common PRIVATE
+        gl/context_egl_fbdev.cpp
+        gl/context_egl_fbdev.h
+      )
+    endif()
+  endif()
+
+  if(USE_X11)
+    target_sources(common PRIVATE
+      gl/context_glx.cpp
+      gl/context_glx.h
+    )
+    target_compile_definitions(common PRIVATE "-DUSE_GLX=1")
+  endif()
+
+  if(USE_WAYLAND)
+    target_sources(common PRIVATE
+      gl/context_egl_wayland.cpp
+      gl/context_egl_wayland.h
+    )
+  endif()
+
+  if(APPLE)
+    target_sources(common PRIVATE
+      gpu/gl/context_agl.mm
+      gpu/gl/context_agl.h
+    )
+  endif()
+endif()
 if(ENABLE_OPENGL)
   target_sources(core PRIVATE
     gpu_hw_opengl.cpp
@@ -144,6 +289,35 @@ if(ENABLE_OPENGL)
   target_link_libraries(core PRIVATE glad)
 endif()
 
+if(ENABLE_VULKAN)
+  target_sources(common PRIVATE
+    gpu/vulkan/builders.cpp
+    gpu/vulkan/builders.h
+    gpu/vulkan/context.cpp
+    gpu/vulkan/context.h
+    gpu/vulkan/loader.h
+    gpu/vulkan/loader.cpp
+    gpu/vulkan/shader_cache.cpp
+    gpu/vulkan/shader_cache.h
+    gpu/vulkan/shader_compiler.cpp
+    gpu/vulkan/shader_compiler.h
+    gpu/vulkan/stream_buffer.cpp
+    gpu/vulkan/stream_buffer.h
+    gpu/vulkan/swap_chain.cpp
+    gpu/vulkan/swap_chain.h
+    gpu/vulkan/texture.cpp
+    gpu/vulkan/texture.h
+    gpu/vulkan/util.cpp
+    gpu/vulkan/util.h
+  )
+  target_compile_definitions(common PUBLIC "WITH_VULKAN=1")
+  target_link_libraries(common PRIVATE glslang)
+
+  if(APPLE)
+    # Needed for Vulkan Swap Chain.
+    target_link_libraries(common PRIVATE "objc")
+  endif()
+endif()
 if(ENABLE_VULKAN)
   target_sources(core PRIVATE
     gpu_hw_vulkan.cpp
@@ -151,6 +325,10 @@ if(ENABLE_VULKAN)
   )
 endif()
 
+if(ENABLE_CUBEB)
+  target_compile_definitions(core PUBLIC "WITH_CUBEB=1")
+endif()
+
 if(${CPU_ARCH} STREQUAL "x64")
   target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../dep/xbyak/xbyak")
   target_compile_definitions(core PUBLIC "WITH_RECOMPILER=1" "WITH_MMAP_FASTMEM=1")
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 19f685eb4..22fe45004 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -37,8 +37,6 @@
     <ClCompile Include="gpu\d3d11\shader_compiler.cpp" />
     <ClCompile Include="gpu\d3d11\stream_buffer.cpp" />
     <ClCompile Include="gpu\d3d11_device.cpp" />
-    <ClCompile Include="gpu\d3d11_pipeline.cpp" />
-    <ClCompile Include="gpu\d3d11_shader.cpp" />
     <ClCompile Include="gpu\d3d11_texture.cpp" />
     <ClCompile Include="gpu\d3d12\context.cpp" />
     <ClCompile Include="gpu\d3d12\descriptor_heap_manager.cpp" />
@@ -163,8 +161,6 @@
     <ClInclude Include="gpu\d3d11\shader_compiler.h" />
     <ClInclude Include="gpu\d3d11\stream_buffer.h" />
     <ClInclude Include="gpu\d3d11_device.h" />
-    <ClInclude Include="gpu\d3d11_pipeline.h" />
-    <ClInclude Include="gpu\d3d11_shader.h" />
     <ClInclude Include="gpu\d3d_shaders.h" />
     <ClInclude Include="gpu\d3d11_texture.h" />
     <ClInclude Include="gpu\d3d12\context.h" />
@@ -201,8 +197,6 @@
       <ExcludedFromBuild>true</ExcludedFromBuild>
     </ClInclude>
     <ClInclude Include="gpu\gpu_device.h" />
-    <ClInclude Include="gpu\gpu_pipeline.h" />
-    <ClInclude Include="gpu\gpu_shader.h" />
     <ClInclude Include="gpu\gpu_texture.h" />
     <ClInclude Include="gpu\imgui_impl_dx12.h" />
     <ClInclude Include="gpu\imgui_impl_opengl3.h" />
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index d4e9fd66a..3109cb210 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -188,12 +188,6 @@
     <ClCompile Include="gpu\d3d11_texture.cpp">
       <Filter>gpu</Filter>
     </ClCompile>
-    <ClCompile Include="gpu\d3d11_shader.cpp">
-      <Filter>gpu</Filter>
-    </ClCompile>
-    <ClCompile Include="gpu\d3d11_pipeline.cpp">
-      <Filter>gpu</Filter>
-    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="types.h" />
@@ -401,18 +395,6 @@
     <ClInclude Include="gpu\d3d_shaders.h">
       <Filter>gpu</Filter>
     </ClInclude>
-    <ClInclude Include="gpu\gpu_pipeline.h">
-      <Filter>gpu</Filter>
-    </ClInclude>
-    <ClInclude Include="gpu\gpu_shader.h">
-      <Filter>gpu</Filter>
-    </ClInclude>
-    <ClInclude Include="gpu\d3d11_shader.h">
-      <Filter>gpu</Filter>
-    </ClInclude>
-    <ClInclude Include="gpu\d3d11_pipeline.h">
-      <Filter>gpu</Filter>
-    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <Filter Include="gpu">
diff --git a/src/core/gpu/d3d11_device.cpp b/src/core/gpu/d3d11_device.cpp
index fcc54b4bc..a167e4e9f 100644
--- a/src/core/gpu/d3d11_device.cpp
+++ b/src/core/gpu/d3d11_device.cpp
@@ -558,6 +558,9 @@ bool D3D11Device::SetFullscreen(bool fullscreen, u32 width, u32 height, float re
 
 bool D3D11Device::CreateResources()
 {
+  if (!GPUDevice::CreateResources())
+    return false;
+
   HRESULT hr;
 
   m_display_vertex_shader =
@@ -1304,3 +1307,505 @@ float D3D11Device::GetAndResetAccumulatedGPUTime()
   m_accumulated_gpu_time = 0.0f;
   return value;
 }
+
+D3D11Framebuffer::D3D11Framebuffer(ComPtr<ID3D11RenderTargetView> rtv, ComPtr<ID3D11DepthStencilView> dsv)
+  : m_rtv(std::move(rtv)), m_dsv(std::move(dsv))
+{
+}
+
+D3D11Framebuffer::~D3D11Framebuffer() = default;
+
+void D3D11Framebuffer::SetDebugName(const std::string_view& name)
+{
+  Panic("Implement me");
+}
+
+std::unique_ptr<GPUFramebuffer> D3D11Device::CreateFramebuffer(GPUTexture* rt, u32 rt_layer, u32 rt_level,
+                                                               GPUTexture* ds, u32 ds_layer, u32 ds_level)
+{
+  ComPtr<ID3D11RenderTargetView> rtv;
+  ComPtr<ID3D11DepthStencilView> dsv;
+  HRESULT hr;
+
+  if (rt)
+  {
+    D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {};
+    rtv_desc.Format = static_cast<D3D11Texture*>(rt)->GetDXGIFormat();
+    if (rt->IsMultisampled())
+    {
+      Assert(rt_level == 0);
+      if (rt->GetLayers() > 1)
+      {
+        rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY;
+        rtv_desc.Texture2DMSArray.ArraySize = rt->GetLayers();
+        rtv_desc.Texture2DMSArray.FirstArraySlice = rt_layer;
+      }
+      else
+      {
+        rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS;
+      }
+    }
+    else
+    {
+      if (rt->GetLayers() > 1)
+      {
+        rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
+        rtv_desc.Texture2DArray.ArraySize = rt->GetLayers();
+        rtv_desc.Texture2DArray.FirstArraySlice = rt_layer;
+        rtv_desc.Texture2DArray.MipSlice = rt_level;
+      }
+      else
+      {
+        rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
+        rtv_desc.Texture2D.MipSlice = rt_level;
+      }
+    }
+
+    if (FAILED(hr = m_device->CreateRenderTargetView(static_cast<D3D11Texture*>(rt)->GetD3DTexture(), &rtv_desc,
+                                                     rtv.GetAddressOf())))
+    {
+      Log_ErrorPrintf("CreateRenderTargetView() failed: %08X", hr);
+      return {};
+    }
+  }
+
+  if (ds)
+  {
+    D3D11_DEPTH_STENCIL_VIEW_DESC dsv_desc = {};
+    dsv_desc.Format = static_cast<D3D11Texture*>(ds)->GetDXGIFormat();
+    if (ds->IsMultisampled())
+    {
+      Assert(rt_level == 0);
+      if (ds->GetLayers() > 1)
+      {
+        dsv_desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2DMSARRAY;
+        dsv_desc.Texture2DMSArray.ArraySize = ds->GetLayers();
+        dsv_desc.Texture2DMSArray.FirstArraySlice = rt_layer;
+      }
+      else
+      {
+        dsv_desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2DMS;
+      }
+    }
+    else
+    {
+      if (ds->GetLayers() > 1)
+      {
+        dsv_desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2DARRAY;
+        dsv_desc.Texture2DArray.ArraySize = ds->GetLayers();
+        dsv_desc.Texture2DArray.FirstArraySlice = rt_layer;
+        dsv_desc.Texture2DArray.MipSlice = rt_level;
+      }
+      else
+      {
+        dsv_desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
+        dsv_desc.Texture2D.MipSlice = rt_level;
+      }
+    }
+
+    if (FAILED(hr = m_device->CreateDepthStencilView(static_cast<D3D11Texture*>(ds)->GetD3DTexture(), &dsv_desc,
+                                                     dsv.GetAddressOf())))
+    {
+      Log_ErrorPrintf("CreateDepthStencilView() failed: %08X", hr);
+      return {};
+    }
+  }
+
+  return std::unique_ptr<GPUFramebuffer>(new D3D11Framebuffer(std::move(rtv), std::move(dsv)));
+}
+
+D3D11Sampler::D3D11Sampler(ComPtr<ID3D11SamplerState> ss) : m_ss(std::move(ss)) {}
+
+D3D11Sampler::~D3D11Sampler() = default;
+
+void D3D11Sampler::SetDebugName(const std::string_view& name)
+{
+  Panic("Not implemented");
+}
+
+std::unique_ptr<GPUSampler> D3D11Device::CreateSampler(const GPUSampler::Config& config)
+{
+  static constexpr std::array<D3D11_TEXTURE_ADDRESS_MODE, static_cast<u8>(GPUSampler::AddressMode::MaxCount)> ta = {{
+    D3D11_TEXTURE_ADDRESS_WRAP,   // Repeat
+    D3D11_TEXTURE_ADDRESS_CLAMP,  // ClampToEdge
+    D3D11_TEXTURE_ADDRESS_BORDER, // ClampToBorder
+  }};
+
+  D3D11_SAMPLER_DESC desc = {};
+  desc.AddressU = ta[static_cast<u8>(config.address_u.GetValue())];
+  desc.AddressV = ta[static_cast<u8>(config.address_v.GetValue())];
+  desc.AddressW = ta[static_cast<u8>(config.address_w.GetValue())];
+  desc.BorderColor[0] = static_cast<float>(config.border_color & 0xFF) / 255.0f;
+  desc.BorderColor[1] = static_cast<float>((config.border_color >> 8) & 0xFF) / 255.0f;
+  desc.BorderColor[2] = static_cast<float>((config.border_color >> 16) & 0xFF) / 255.0f;
+  desc.BorderColor[3] = static_cast<float>((config.border_color >> 24) & 0xFF) / 255.0f;
+  desc.MinLOD = static_cast<float>(config.min_lod);
+  desc.MaxLOD = static_cast<float>(config.max_lod);
+
+  if (config.anisotropy > 0)
+  {
+    desc.Filter = D3D11_FILTER_ANISOTROPIC;
+    desc.MaxAnisotropy = config.anisotropy;
+  }
+  else
+  {
+    static constexpr u8 filter_count = static_cast<u8>(GPUSampler::Filter::MaxCount);
+    static constexpr D3D11_FILTER filters[filter_count][filter_count][filter_count] = {
+      {
+        {D3D11_FILTER_MIN_MAG_MIP_POINT, D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT},
+        {D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT, D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT},
+      },
+      {
+        {D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR, D3D11_FILTER_MIN_POINT_MAG_MIP_LINEAR},
+        {D3D11_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR, D3D11_FILTER_MIN_MAG_MIP_LINEAR},
+      }};
+
+    desc.Filter = filters[static_cast<u8>(config.mip_filter.GetValue())][static_cast<u8>(config.min_filter.GetValue())]
+                         [static_cast<u8>(config.mag_filter.GetValue())];
+    desc.MaxAnisotropy = 1;
+  }
+
+  // TODO: Pool?
+  ComPtr<ID3D11SamplerState> ss;
+  const HRESULT hr = m_device->CreateSamplerState(&desc, ss.GetAddressOf());
+  if (FAILED(hr))
+  {
+    Log_ErrorPrintf("CreateSamplerState() failed: %08X", hr);
+    return {};
+  }
+
+  return std::unique_ptr<GPUSampler>(new D3D11Sampler(std::move(ss)));
+}
+
+D3D11Shader::D3D11Shader(Stage stage, Microsoft::WRL::ComPtr<ID3D11DeviceChild> shader, std::vector<u8> bytecode)
+  : GPUShader(stage), m_shader(std::move(shader)), m_bytecode(std::move(bytecode))
+{
+}
+
+D3D11Shader::~D3D11Shader() = default;
+
+ID3D11VertexShader* D3D11Shader::GetVertexShader() const
+{
+  DebugAssert(m_stage == Stage::Vertex);
+  return static_cast<ID3D11VertexShader*>(m_shader.Get());
+}
+
+ID3D11PixelShader* D3D11Shader::GetPixelShader() const
+{
+  DebugAssert(m_stage == Stage::Pixel);
+  return static_cast<ID3D11PixelShader*>(m_shader.Get());
+}
+
+ID3D11ComputeShader* D3D11Shader::GetComputeShader() const
+{
+  DebugAssert(m_stage == Stage::Compute);
+  return static_cast<ID3D11ComputeShader*>(m_shader.Get());
+}
+
+void D3D11Shader::SetDebugName(const std::string_view& name)
+{
+  Panic("Implement me");
+}
+
+std::unique_ptr<GPUShader> D3D11Device::CreateShaderFromBinary(GPUShader::Stage stage, gsl::span<const u8> data)
+{
+  ComPtr<ID3D11DeviceChild> shader;
+  std::vector<u8> bytecode;
+  switch (stage)
+  {
+    case GPUShader::Stage::Vertex:
+      shader = D3D11::ShaderCompiler::CreateVertexShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
+      bytecode.resize(data.size());
+      std::memcpy(bytecode.data(), data.data(), data.size());
+      break;
+
+    case GPUShader::Stage::Pixel:
+      shader = D3D11::ShaderCompiler::CreatePixelShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
+      break;
+
+    case GPUShader::Stage::Compute:
+      shader = D3D11::ShaderCompiler::CreateComputeShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
+      break;
+
+    default:
+      UnreachableCode();
+      break;
+  }
+
+  if (!shader)
+    return {};
+
+  return std::unique_ptr<GPUShader>(new D3D11Shader(stage, std::move(shader), std::move(bytecode)));
+}
+
+std::unique_ptr<GPUShader> D3D11Device::CreateShaderFromSource(GPUShader::Stage stage, const std::string_view& source,
+                                                               std::vector<u8>* out_binary /* = nullptr */)
+{
+  // TODO: This shouldn't be dependent on build type.
+#ifdef _DEBUG
+  constexpr bool debug = true;
+#else
+  constexpr bool debug = false;
+#endif
+
+  ComPtr<ID3DBlob> blob;
+  switch (stage)
+  {
+    case GPUShader::Stage::Vertex:
+      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Vertex, m_device->GetFeatureLevel(),
+                                                  source, debug);
+      break;
+
+    case GPUShader::Stage::Pixel:
+      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Pixel, m_device->GetFeatureLevel(),
+                                                  source, debug);
+      break;
+
+    case GPUShader::Stage::Compute:
+      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Compute, m_device->GetFeatureLevel(),
+                                                  source, debug);
+      break;
+
+    default:
+      UnreachableCode();
+      break;
+  }
+
+  if (out_binary)
+  {
+    const size_t size = blob->GetBufferSize();
+    out_binary->resize(size);
+    std::memcpy(out_binary->data(), blob->GetBufferPointer(), size);
+  }
+
+  return CreateShaderFromBinary(
+    stage, gsl::span<const u8>(static_cast<const u8*>(blob->GetBufferPointer()), blob->GetBufferSize()));
+}
+
+D3D11Pipeline::D3D11Pipeline(ComPtr<ID3D11RasterizerState> rs, ComPtr<ID3D11DepthStencilState> ds,
+                             ComPtr<ID3D11BlendState> bs, ComPtr<ID3D11InputLayout> il, ComPtr<ID3D11VertexShader> vs,
+                             ComPtr<ID3D11PixelShader> ps, D3D11_PRIMITIVE_TOPOLOGY topology)
+  : m_rs(std::move(rs)), m_ds(std::move(ds)), m_bs(std::move(bs)), m_il(std::move(il)), m_vs(std::move(vs)),
+    m_ps(std::move(ps)), m_topology(topology)
+{
+}
+
+D3D11Pipeline::~D3D11Pipeline() = default;
+
+void D3D11Pipeline::SetDebugName(const std::string_view& name)
+{
+  UnreachableCode();
+}
+
+void D3D11Pipeline::Bind(ID3D11DeviceContext* context)
+{
+  context->IASetInputLayout(GetInputLayout());
+  context->IASetPrimitiveTopology(GetPrimitiveTopology());
+  context->RSSetState(GetRasterizerState());
+  context->OMSetDepthStencilState(GetDepthStencilState(), 0);
+  context->OMSetBlendState(GetBlendState(), nullptr, 0xFFFFFFFFu);
+  context->VSSetShader(GetVertexShader(), nullptr, 0);
+  context->PSSetShader(GetPixelShader(), nullptr, 0);
+}
+
+D3D11Device::ComPtr<ID3D11RasterizerState> D3D11Device::GetRasterizationState(const GPUPipeline::RasterizationState& rs)
+{
+  ComPtr<ID3D11RasterizerState> drs;
+
+  const auto it = m_rasterization_states.find(rs.key);
+  if (it != m_rasterization_states.end())
+  {
+    drs = it->second;
+    return drs;
+  }
+
+  static constexpr std::array<D3D11_CULL_MODE, static_cast<u32>(GPUPipeline::CullMode::MaxCount)> cull_mapping = {{
+    D3D11_CULL_NONE,  // None
+    D3D11_CULL_FRONT, // Front
+    D3D11_CULL_BACK,  // Back
+  }};
+
+  D3D11_RASTERIZER_DESC desc = {};
+  desc.FillMode = D3D11_FILL_SOLID;
+  desc.CullMode = cull_mapping[static_cast<u8>(rs.cull_mode.GetValue())];
+  desc.ScissorEnable = TRUE;
+  // desc.MultisampleEnable ???
+
+  HRESULT hr = m_device->CreateRasterizerState(&desc, drs.GetAddressOf());
+  if (FAILED(hr))
+    Log_ErrorPrintf("Failed to create depth state with %08X", hr);
+
+  m_rasterization_states.emplace(rs.key, drs);
+  return drs;
+}
+
+D3D11Device::ComPtr<ID3D11DepthStencilState> D3D11Device::GetDepthState(const GPUPipeline::DepthState& ds)
+{
+  ComPtr<ID3D11DepthStencilState> dds;
+
+  const auto it = m_depth_states.find(ds.key);
+  if (it != m_depth_states.end())
+  {
+    dds = it->second;
+    return dds;
+  }
+
+  static constexpr std::array<D3D11_COMPARISON_FUNC, static_cast<u32>(GPUPipeline::DepthFunc::MaxCount)> func_mapping =
+    {{
+      D3D11_COMPARISON_NEVER,         // Never
+      D3D11_COMPARISON_ALWAYS,        // Always
+      D3D11_COMPARISON_LESS,          // Less
+      D3D11_COMPARISON_LESS_EQUAL,    // LessEqual
+      D3D11_COMPARISON_GREATER,       // Greater
+      D3D11_COMPARISON_GREATER_EQUAL, // GreaterEqual
+      D3D11_COMPARISON_EQUAL,         // Equal
+    }};
+
+  D3D11_DEPTH_STENCIL_DESC desc = {};
+  desc.DepthEnable = ds.depth_test != GPUPipeline::DepthFunc::Never;
+  desc.DepthFunc = func_mapping[static_cast<u8>(ds.depth_test.GetValue())];
+  desc.DepthWriteMask = ds.depth_write ? D3D11_DEPTH_WRITE_MASK_ALL : D3D11_DEPTH_WRITE_MASK_ZERO;
+
+  HRESULT hr = m_device->CreateDepthStencilState(&desc, dds.GetAddressOf());
+  if (FAILED(hr))
+    Log_ErrorPrintf("Failed to create depth state with %08X", hr);
+
+  m_depth_states.emplace(ds.key, dds);
+  return dds;
+}
+
+D3D11Device::ComPtr<ID3D11BlendState> D3D11Device::GetBlendState(const GPUPipeline::BlendState& bs)
+{
+  ComPtr<ID3D11BlendState> dbs;
+
+  const auto it = m_blend_states.find(bs.key);
+  if (it != m_blend_states.end())
+  {
+    dbs = it->second;
+    return dbs;
+  }
+
+  static constexpr std::array<D3D11_BLEND, static_cast<u32>(GPUPipeline::BlendFunc::MaxCount)> blend_mapping = {{
+    D3D11_BLEND_ZERO,           // Zero
+    D3D11_BLEND_ONE,            // One
+    D3D11_BLEND_SRC_COLOR,      // SrcColor
+    D3D11_BLEND_INV_SRC_COLOR,  // InvSrcColor
+    D3D11_BLEND_DEST_COLOR,     // DstColor
+    D3D11_BLEND_INV_DEST_COLOR, // InvDstColor
+    D3D11_BLEND_SRC_ALPHA,      // SrcAlpha
+    D3D11_BLEND_INV_SRC_ALPHA,  // InvSrcAlpha
+    D3D11_BLEND_SRC1_ALPHA,     // SrcAlpha1
+    D3D11_BLEND_INV_SRC1_ALPHA, // InvSrcAlpha1
+    D3D11_BLEND_DEST_ALPHA,     // DstAlpha
+    D3D11_BLEND_INV_DEST_ALPHA, // InvDstAlpha
+  }};
+
+  static constexpr std::array<D3D11_BLEND_OP, static_cast<u32>(GPUPipeline::BlendOp::MaxCount)> op_mapping = {{
+    D3D11_BLEND_OP_ADD,          // Add
+    D3D11_BLEND_OP_SUBTRACT,     // Subtract
+    D3D11_BLEND_OP_REV_SUBTRACT, // ReverseSubtract
+    D3D11_BLEND_OP_MIN,          // Min
+    D3D11_BLEND_OP_MAX,          // Max
+  }};
+
+  D3D11_BLEND_DESC blend_desc = {};
+  D3D11_RENDER_TARGET_BLEND_DESC& tgt_desc = blend_desc.RenderTarget[0];
+  tgt_desc.BlendEnable = bs.enable;
+  tgt_desc.RenderTargetWriteMask = bs.write_mask;
+  if (bs.enable)
+  {
+    tgt_desc.SrcBlend = blend_mapping[static_cast<u8>(bs.src_blend.GetValue())];
+    tgt_desc.DestBlend = blend_mapping[static_cast<u8>(bs.dst_blend.GetValue())];
+    tgt_desc.BlendOp = op_mapping[static_cast<u8>(bs.blend_op.GetValue())];
+    tgt_desc.SrcBlendAlpha = blend_mapping[static_cast<u8>(bs.src_alpha_blend.GetValue())];
+    tgt_desc.DestBlendAlpha = blend_mapping[static_cast<u8>(bs.dst_alpha_blend.GetValue())];
+    tgt_desc.BlendOpAlpha = op_mapping[static_cast<u8>(bs.alpha_blend_op.GetValue())];
+  }
+
+  HRESULT hr = m_device->CreateBlendState(&blend_desc, dbs.GetAddressOf());
+  if (FAILED(hr))
+    Log_ErrorPrintf("Failed to create blend state with %08X", hr);
+
+  m_blend_states.emplace(bs.key, dbs);
+  return dbs;
+}
+
+D3D11Device::ComPtr<ID3D11InputLayout> D3D11Device::GetInputLayout(const GPUPipeline::InputLayout& il,
+                                                                   const D3D11Shader* vs)
+{
+  ComPtr<ID3D11InputLayout> dil;
+  const auto it = m_input_layouts.find(il);
+  if (it != m_input_layouts.end())
+  {
+    dil = it->second;
+    return dil;
+  }
+
+  static constexpr std::array<const char*, static_cast<u32>(GPUPipeline::VertexAttribute::Semantic::MaxCount)>
+    semantics = {{
+      "POSITION", // Position
+      "TEXCOORD", // Texcoord
+      "COLOR",    // Color
+    }};
+
+  static constexpr u32 MAX_COMPONENTS = 4;
+  static constexpr const DXGI_FORMAT
+    format_mapping[static_cast<u8>(GPUPipeline::VertexAttribute::Type::MaxCount)][MAX_COMPONENTS] = {
+      {DXGI_FORMAT_R32_FLOAT, DXGI_FORMAT_R32G32_FLOAT, DXGI_FORMAT_R32G32B32_FLOAT,
+       DXGI_FORMAT_R32G32B32A32_FLOAT},                                                                       // Float
+      {DXGI_FORMAT_R8_UINT, DXGI_FORMAT_R8G8_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_UINT},           // UInt8
+      {DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_SINT},           // SInt8
+      {DXGI_FORMAT_R8_UNORM, DXGI_FORMAT_R8G8_UNORM, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_UNORM},        // UNorm8
+      {DXGI_FORMAT_R16_UINT, DXGI_FORMAT_R16G16_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_UINT},    // UInt16
+      {DXGI_FORMAT_R16_SINT, DXGI_FORMAT_R16G16_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_SINT},    // SInt16
+      {DXGI_FORMAT_R16_UNORM, DXGI_FORMAT_R16G16_UNORM, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_UNORM}, // UNorm16
+      {DXGI_FORMAT_R32_UINT, DXGI_FORMAT_R32G32_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R32G32B32A32_UINT},    // UInt32
+      {DXGI_FORMAT_R32_SINT, DXGI_FORMAT_R32G32_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R32G32B32A32_SINT},    // SInt32
+    };
+
+  D3D11_INPUT_ELEMENT_DESC* elems =
+    static_cast<D3D11_INPUT_ELEMENT_DESC*>(alloca(sizeof(D3D11_INPUT_ELEMENT_DESC) * il.vertex_attributes.size()));
+  for (size_t i = 0; i < il.vertex_attributes.size(); i++)
+  {
+    const GPUPipeline::VertexAttribute& va = il.vertex_attributes[i];
+    Assert(va.components > 0 && va.components < MAX_COMPONENTS);
+
+    D3D11_INPUT_ELEMENT_DESC& elem = elems[i];
+    elem.SemanticName = semantics[static_cast<u8>(va.semantic.GetValue())];
+    elem.SemanticIndex = va.semantic_index;
+    elem.Format = format_mapping[static_cast<u8>(va.type.GetValue())][va.components - 1];
+    elem.InputSlot = 0;
+    elem.AlignedByteOffset = va.offset;
+    elem.InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
+    elem.InstanceDataStepRate = 0;
+  }
+
+  HRESULT hr = m_device->CreateInputLayout(elems, static_cast<UINT>(il.vertex_attributes.size()),
+                                           vs->GetBytecode().data(), vs->GetBytecode().size(), dil.GetAddressOf());
+  if (FAILED(hr))
+    Log_ErrorPrintf("Failed to create input layout with %08X", hr);
+
+  m_input_layouts.emplace(il, dil);
+  return dil;
+}
+
+std::unique_ptr<GPUPipeline> D3D11Device::CreatePipeline(const GPUPipeline::GraphicsConfig& config)
+{
+  ComPtr<ID3D11RasterizerState> rs = GetRasterizationState(config.rasterization);
+  ComPtr<ID3D11DepthStencilState> ds = GetDepthState(config.depth);
+  ComPtr<ID3D11BlendState> bs = GetBlendState(config.blend);
+
+  static constexpr std::array<D3D11_PRIMITIVE_TOPOLOGY, static_cast<u32>(GPUPipeline::Primitive::MaxCount)> primitives =
+    {{
+      D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,     // Points
+      D3D11_PRIMITIVE_TOPOLOGY_LINELIST,      // Lines
+      D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST,  // Triangles
+      D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, // TriangleStrips
+    }};
+
+  return std::unique_ptr<GPUPipeline>(
+    new D3D11Pipeline(std::move(rs), std::move(ds), std::move(bs), nullptr,
+                      static_cast<const D3D11Shader*>(config.vertex_shader)->GetVertexShader(),
+                      static_cast<const D3D11Shader*>(config.pixel_shader)->GetPixelShader(),
+                      primitives[static_cast<u8>(config.primitive)]));
+}
diff --git a/src/core/gpu/d3d11_device.h b/src/core/gpu/d3d11_device.h
index c6bb1b8aa..63de54a52 100644
--- a/src/core/gpu/d3d11_device.h
+++ b/src/core/gpu/d3d11_device.h
@@ -8,7 +8,6 @@
 #include "d3d11/stream_buffer.h"
 #include "d3d11_texture.h"
 #include "gpu_device.h"
-#include "gpu_pipeline.h"
 #include "postprocessing_chain.h"
 #include <d3d11.h>
 #include <dxgi.h>
@@ -19,8 +18,107 @@
 #include <vector>
 #include <wrl/client.h>
 
-class D3D11Pipeline;
-class D3D11Shader;
+class D3D11Device;
+
+class D3D11Framebuffer final : public GPUFramebuffer
+{
+  friend D3D11Device;
+
+  template<typename T>
+  using ComPtr = Microsoft::WRL::ComPtr<T>;
+
+public:
+  ~D3D11Framebuffer() override;
+
+  ALWAYS_INLINE ID3D11RenderTargetView* GetRTV() const { return m_rtv.Get(); }
+  ALWAYS_INLINE ID3D11DepthStencilView* GetDSV() const { return m_dsv.Get(); }
+
+  void SetDebugName(const std::string_view& name) override;
+
+private:
+  D3D11Framebuffer(ComPtr<ID3D11RenderTargetView> rtv, ComPtr<ID3D11DepthStencilView> dsv);
+
+  ComPtr<ID3D11RenderTargetView> m_rtv;
+  ComPtr<ID3D11DepthStencilView> m_dsv;
+};
+
+class D3D11Sampler final : public GPUSampler
+{
+  friend D3D11Device;
+
+  template<typename T>
+  using ComPtr = Microsoft::WRL::ComPtr<T>;
+
+public:
+  ~D3D11Sampler() override;
+
+  ALWAYS_INLINE ID3D11SamplerState* GetSamplerState() const { return m_ss.Get(); }
+
+  void SetDebugName(const std::string_view& name) override;
+
+private:
+  D3D11Sampler(ComPtr<ID3D11SamplerState> ss);
+
+  ComPtr<ID3D11SamplerState> m_ss;
+};
+
+class D3D11Shader final : public GPUShader
+{
+  friend D3D11Device;
+
+public:
+  ~D3D11Shader() override;
+
+  ID3D11VertexShader* GetVertexShader() const;
+  ID3D11PixelShader* GetPixelShader() const;
+  ID3D11ComputeShader* GetComputeShader() const;
+
+  ALWAYS_INLINE const std::vector<u8>& GetBytecode() const { return m_bytecode; }
+
+  void SetDebugName(const std::string_view& name) override;
+
+private:
+  D3D11Shader(Stage stage, Microsoft::WRL::ComPtr<ID3D11DeviceChild> shader, std::vector<u8> bytecode);
+
+  Microsoft::WRL::ComPtr<ID3D11DeviceChild> m_shader;
+  std::vector<u8> m_bytecode; // only for VS
+};
+
+class D3D11Pipeline final : public GPUPipeline
+{
+  friend D3D11Device;
+
+  template<typename T>
+  using ComPtr = Microsoft::WRL::ComPtr<T>;
+
+public:
+  ~D3D11Pipeline() override;
+
+  void SetDebugName(const std::string_view& name) override;
+
+  ALWAYS_INLINE ID3D11RasterizerState* GetRasterizerState() const { return m_rs.Get(); }
+  ALWAYS_INLINE ID3D11DepthStencilState* GetDepthStencilState() const { return m_ds.Get(); }
+  ALWAYS_INLINE ID3D11BlendState* GetBlendState() const { return m_bs.Get(); }
+  ALWAYS_INLINE ID3D11InputLayout* GetInputLayout() const { return m_il.Get(); }
+  ALWAYS_INLINE ID3D11VertexShader* GetVertexShader() const { return m_vs.Get(); }
+  ALWAYS_INLINE ID3D11PixelShader* GetPixelShader() const { return m_ps.Get(); }
+  ALWAYS_INLINE D3D11_PRIMITIVE_TOPOLOGY GetPrimitiveTopology() const { return m_topology; }
+
+  void Bind(ID3D11DeviceContext* context);
+
+private:
+  D3D11Pipeline(ComPtr<ID3D11RasterizerState> rs, ComPtr<ID3D11DepthStencilState> ds, ComPtr<ID3D11BlendState> bs,
+                ComPtr<ID3D11InputLayout> il, ComPtr<ID3D11VertexShader> vs, ComPtr<ID3D11PixelShader> ps,
+                D3D11_PRIMITIVE_TOPOLOGY topology);
+
+  ComPtr<ID3D11RasterizerState> m_rs;
+  ComPtr<ID3D11DepthStencilState> m_ds;
+  ComPtr<ID3D11BlendState> m_bs;
+  ComPtr<ID3D11InputLayout> m_il;
+  ComPtr<ID3D11VertexShader> m_vs;
+  ComPtr<ID3D11PixelShader> m_ps;
+  D3D11_PRIMITIVE_TOPOLOGY m_topology;
+};
 
 class D3D11Device final : public GPUDevice
 {
@@ -62,6 +160,8 @@ public:
                                             GPUTexture::Type type, GPUTexture::Format format,
                                             const void* data = nullptr, u32 data_stride = 0,
                                             bool dynamic = false) override;
+  std::unique_ptr<GPUSampler> CreateSampler(const GPUSampler::Config& config) override;
+
   bool DownloadTexture(GPUTexture* texture, u32 x, u32 y, u32 width, u32 height, void* out_data,
                        u32 out_data_stride) override;
   bool SupportsTextureFormat(GPUTexture::Format format) const override;
@@ -70,6 +170,9 @@ public:
   void ResolveTextureRegion(GPUTexture* dst, u32 dst_x, u32 dst_y, u32 dst_layer, u32 dst_level, GPUTexture* src,
                             u32 src_x, u32 src_y, u32 src_layer, u32 src_level, u32 width, u32 height) override;
 
+  std::unique_ptr<GPUFramebuffer> CreateFramebuffer(GPUTexture* rt, u32 rt_layer, u32 rt_level, GPUTexture* ds,
+                                                    u32 ds_layer, u32 ds_level) override;
+
   std::unique_ptr<GPUShader> CreateShaderFromBinary(GPUShader::Stage stage, gsl::span<const u8> data) override;
   std::unique_ptr<GPUShader> CreateShaderFromSource(GPUShader::Stage stage, const std::string_view& source,
                                                     std::vector<u8>* out_binary = nullptr) override;
@@ -92,7 +195,8 @@ private:
   using RasterizationStateMap = std::unordered_map<u8, ComPtr<ID3D11RasterizerState>>;
   using DepthStateMap = std::unordered_map<u8, ComPtr<ID3D11DepthStencilState>>;
   using BlendStateMap = std::unordered_map<u32, ComPtr<ID3D11BlendState>>;
-  using InputLayoutMap = std::unordered_map<GPUPipeline::InputLayout, ComPtr<ID3D11InputLayout>, GPUPipeline::InputLayoutHash>;
+  using InputLayoutMap =
+    std::unordered_map<GPUPipeline::InputLayout, ComPtr<ID3D11InputLayout>, GPUPipeline::InputLayoutHash>;
 
   static constexpr u32 DISPLAY_UNIFORM_BUFFER_SIZE = 64;
   static constexpr u32 IMGUI_VERTEX_BUFFER_SIZE = 4 * 1024 * 1024;
diff --git a/src/core/gpu/d3d11_pipeline.cpp b/src/core/gpu/d3d11_pipeline.cpp
deleted file mode 100644
index dade73170..000000000
--- a/src/core/gpu/d3d11_pipeline.cpp
+++ /dev/null
@@ -1,242 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#include "d3d11_pipeline.h"
-#include "d3d11_device.h"
-#include "d3d11_shader.h"
-
-#include "common/assert.h"
-#include "common/log.h"
-
-#include <array>
-#include <malloc.h>
-
-Log_SetChannel(D3D11Device);
-
-D3D11Pipeline::D3D11Pipeline(ComPtr<ID3D11RasterizerState> rs, ComPtr<ID3D11DepthStencilState> ds,
-                             ComPtr<ID3D11BlendState> bs, ComPtr<ID3D11InputLayout> il, ComPtr<ID3D11VertexShader> vs,
-                             ComPtr<ID3D11PixelShader> ps, D3D11_PRIMITIVE_TOPOLOGY topology)
-  : m_rs(std::move(rs)), m_ds(std::move(ds)), m_bs(std::move(bs)), m_il(std::move(il)), m_vs(std::move(vs)),
-    m_ps(std::move(ps)), m_topology(topology)
-{
-}
-
-D3D11Pipeline::~D3D11Pipeline() = default;
-
-void D3D11Pipeline::SetDebugName(const std::string_view& name)
-{
-  UnreachableCode();
-}
-
-void D3D11Pipeline::Bind(ID3D11DeviceContext* context)
-{
-  context->IASetInputLayout(GetInputLayout());
-  context->IASetPrimitiveTopology(GetPrimitiveTopology());
-  context->RSSetState(GetRasterizerState());
-  context->OMSetDepthStencilState(GetDepthStencilState(), 0);
-  context->OMSetBlendState(GetBlendState(), nullptr, 0xFFFFFFFFu);
-  context->VSSetShader(GetVertexShader(), nullptr, 0);
-  context->PSSetShader(GetPixelShader(), nullptr, 0);
-}
-
-D3D11Device::ComPtr<ID3D11RasterizerState> D3D11Device::GetRasterizationState(const GPUPipeline::RasterizationState& rs)
-{
-  ComPtr<ID3D11RasterizerState> drs;
-
-  const auto it = m_rasterization_states.find(rs.key);
-  if (it != m_rasterization_states.end())
-  {
-    drs = it->second;
-    return drs;
-  }
-
-  static constexpr std::array<D3D11_CULL_MODE, static_cast<u32>(GPUPipeline::CullMode::MaxCount)> cull_mapping = {{
-    D3D11_CULL_NONE,  // None
-    D3D11_CULL_FRONT, // Front
-    D3D11_CULL_BACK,  // Back
-  }};
-
-  D3D11_RASTERIZER_DESC desc = {};
-  desc.FillMode = D3D11_FILL_SOLID;
-  desc.CullMode = cull_mapping[static_cast<u8>(rs.cull_mode.GetValue())];
-  desc.ScissorEnable = TRUE;
-  // desc.MultisampleEnable ???
-
-  HRESULT hr = m_device->CreateRasterizerState(&desc, drs.GetAddressOf());
-  if (FAILED(hr))
-    Log_ErrorPrintf("Failed to create depth state with %08X", hr);
-
-  m_rasterization_states.emplace(rs.key, drs);
-  return drs;
-}
-
-D3D11Device::ComPtr<ID3D11DepthStencilState> D3D11Device::GetDepthState(const GPUPipeline::DepthState& ds)
-{
-  ComPtr<ID3D11DepthStencilState> dds;
-
-  const auto it = m_depth_states.find(ds.key);
-  if (it != m_depth_states.end())
-  {
-    dds = it->second;
-    return dds;
-  }
-
-  static constexpr std::array<D3D11_COMPARISON_FUNC, static_cast<u32>(GPUPipeline::DepthFunc::MaxCount)> func_mapping =
-    {{
-      D3D11_COMPARISON_NEVER,         // Never
-      D3D11_COMPARISON_ALWAYS,        // Always
-      D3D11_COMPARISON_LESS,          // Less
-      D3D11_COMPARISON_LESS_EQUAL,    // LessEqual
-      D3D11_COMPARISON_GREATER,       // Greater
-      D3D11_COMPARISON_GREATER_EQUAL, // GreaterEqual
-      D3D11_COMPARISON_EQUAL,         // Equal
-    }};
-
-  D3D11_DEPTH_STENCIL_DESC desc = {};
-  desc.DepthEnable = ds.depth_test != GPUPipeline::DepthFunc::Never;
-  desc.DepthFunc = func_mapping[static_cast<u8>(ds.depth_test.GetValue())];
-  desc.DepthWriteMask = ds.depth_write ? D3D11_DEPTH_WRITE_MASK_ALL : D3D11_DEPTH_WRITE_MASK_ZERO;
-
-  HRESULT hr = m_device->CreateDepthStencilState(&desc, dds.GetAddressOf());
-  if (FAILED(hr))
-    Log_ErrorPrintf("Failed to create depth state with %08X", hr);
-
-  m_depth_states.emplace(ds.key, dds);
-  return dds;
-}
-
-D3D11Device::ComPtr<ID3D11BlendState> D3D11Device::GetBlendState(const GPUPipeline::BlendState& bs)
-{
-  ComPtr<ID3D11BlendState> dbs;
-
-  const auto it = m_blend_states.find(bs.key);
-  if (it != m_blend_states.end())
-  {
-    dbs = it->second;
-    return dbs;
-  }
-
-  static constexpr std::array<D3D11_BLEND, static_cast<u32>(GPUPipeline::BlendFunc::MaxCount)> blend_mapping = {{
-    D3D11_BLEND_ZERO,           // Zero
-    D3D11_BLEND_ONE,            // One
-    D3D11_BLEND_SRC_COLOR,      // SrcColor
-    D3D11_BLEND_INV_SRC_COLOR,  // InvSrcColor
-    D3D11_BLEND_DEST_COLOR,     // DstColor
-    D3D11_BLEND_INV_DEST_COLOR, // InvDstColor
-    D3D11_BLEND_SRC_ALPHA,      // SrcAlpha
-    D3D11_BLEND_INV_SRC_ALPHA,  // InvSrcAlpha
-    D3D11_BLEND_SRC1_ALPHA,     // SrcAlpha1
-    D3D11_BLEND_INV_SRC1_ALPHA, // InvSrcAlpha1
-    D3D11_BLEND_DEST_ALPHA,     // DstAlpha
-    D3D11_BLEND_INV_DEST_ALPHA, // InvDstAlpha
-  }};
-
-  static constexpr std::array<D3D11_BLEND_OP, static_cast<u32>(GPUPipeline::BlendOp::MaxCount)> op_mapping = {{
-    D3D11_BLEND_OP_ADD,          // Add
-    D3D11_BLEND_OP_SUBTRACT,     // Subtract
-    D3D11_BLEND_OP_REV_SUBTRACT, // ReverseSubtract
-    D3D11_BLEND_OP_MIN,          // Min
-    D3D11_BLEND_OP_MAX,          // Max
-  }};
-
-  D3D11_BLEND_DESC blend_desc = {};
-  D3D11_RENDER_TARGET_BLEND_DESC& tgt_desc = blend_desc.RenderTarget[0];
-  tgt_desc.BlendEnable = bs.enable;
-  tgt_desc.RenderTargetWriteMask = bs.write_mask;
-  if (bs.enable)
-  {
-    tgt_desc.SrcBlend = blend_mapping[static_cast<u8>(bs.src_blend.GetValue())];
-    tgt_desc.DestBlend = blend_mapping[static_cast<u8>(bs.dst_blend.GetValue())];
-    tgt_desc.BlendOp = op_mapping[static_cast<u8>(bs.blend_op.GetValue())];
-    tgt_desc.SrcBlendAlpha = blend_mapping[static_cast<u8>(bs.src_alpha_blend.GetValue())];
-    tgt_desc.DestBlendAlpha = blend_mapping[static_cast<u8>(bs.dst_alpha_blend.GetValue())];
-    tgt_desc.BlendOpAlpha = op_mapping[static_cast<u8>(bs.alpha_blend_op.GetValue())];
-  }
-
-  HRESULT hr = m_device->CreateBlendState(&blend_desc, dbs.GetAddressOf());
-  if (FAILED(hr))
-    Log_ErrorPrintf("Failed to create blend state with %08X", hr);
-
-  m_blend_states.emplace(bs.key, dbs);
-  return dbs;
-}
-
-D3D11Device::ComPtr<ID3D11InputLayout> D3D11Device::GetInputLayout(const GPUPipeline::InputLayout& il,
-                                                                   const D3D11Shader* vs)
-{
-  ComPtr<ID3D11InputLayout> dil;
-  const auto it = m_input_layouts.find(il);
-  if (it != m_input_layouts.end())
-  {
-    dil = it->second;
-    return dil;
-  }
-
-  static constexpr std::array<const char*, static_cast<u32>(GPUPipeline::VertexAttribute::Semantic::MaxCount)>
-    semantics = {{
-      "POSITION", // Position
-      "TEXCOORD", // Texcoord
-      "COLOR",    // Color
-    }};
-
-  static constexpr u32 MAX_COMPONENTS = 4;
-  static constexpr const DXGI_FORMAT
-    format_mapping[static_cast<u8>(GPUPipeline::VertexAttribute::Type::MaxCount)][MAX_COMPONENTS] = {
-      {DXGI_FORMAT_R32_FLOAT, DXGI_FORMAT_R32G32_FLOAT, DXGI_FORMAT_R32G32B32_FLOAT,
-       DXGI_FORMAT_R32G32B32A32_FLOAT},                                                                       // Float
-      {DXGI_FORMAT_R8_UINT, DXGI_FORMAT_R8G8_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_UINT},           // UInt8
-      {DXGI_FORMAT_R8_SINT, DXGI_FORMAT_R8G8_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_SINT},           // SInt8
-      {DXGI_FORMAT_R8_UNORM, DXGI_FORMAT_R8G8_UNORM, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R8G8B8A8_UNORM},        // UNorm8
-      {DXGI_FORMAT_R16_UINT, DXGI_FORMAT_R16G16_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_UINT},    // UInt16
-      {DXGI_FORMAT_R16_SINT, DXGI_FORMAT_R16G16_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_SINT},    // SInt16
-      {DXGI_FORMAT_R16_UNORM, DXGI_FORMAT_R16G16_UNORM, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R16G16B16A16_UNORM}, // UNorm16
-      {DXGI_FORMAT_R32_UINT, DXGI_FORMAT_R32G32_UINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R32G32B32A32_UINT},    // UInt32
-      {DXGI_FORMAT_R32_SINT, DXGI_FORMAT_R32G32_SINT, DXGI_FORMAT_UNKNOWN, DXGI_FORMAT_R32G32B32A32_SINT},    // SInt32
-    };
-
-  D3D11_INPUT_ELEMENT_DESC* elems =
-    static_cast<D3D11_INPUT_ELEMENT_DESC*>(alloca(sizeof(D3D11_INPUT_ELEMENT_DESC) * il.vertex_attributes.size()));
-  for (size_t i = 0; i < il.vertex_attributes.size(); i++)
-  {
-    const GPUPipeline::VertexAttribute& va = il.vertex_attributes[i];
-    Assert(va.components > 0 && va.components < MAX_COMPONENTS);
-
-    D3D11_INPUT_ELEMENT_DESC& elem = elems[i];
-    elem.SemanticName = semantics[static_cast<u8>(va.semantic.GetValue())];
-    elem.SemanticIndex = va.semantic_index;
-    elem.Format = format_mapping[static_cast<u8>(va.type.GetValue())][va.components - 1];
-    elem.InputSlot = 0;
-    elem.AlignedByteOffset = va.offset;
-    elem.InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
-    elem.InstanceDataStepRate = 0;
-  }
-
-  HRESULT hr = m_device->CreateInputLayout(elems, static_cast<UINT>(il.vertex_attributes.size()),
-                                           vs->GetBytecode().data(), vs->GetBytecode().size(), dil.GetAddressOf());
-  if (FAILED(hr))
-    Log_ErrorPrintf("Failed to create input layout with %08X", hr);
-
-  m_input_layouts.emplace(il, dil);
-  return dil;
-}
-
-std::unique_ptr<GPUPipeline> D3D11Device::CreatePipeline(const GPUPipeline::GraphicsConfig& config)
-{
-  ComPtr<ID3D11RasterizerState> rs = GetRasterizationState(config.rasterization);
-  ComPtr<ID3D11DepthStencilState> ds = GetDepthState(config.depth);
-  ComPtr<ID3D11BlendState> bs = GetBlendState(config.blend);
-
-  static constexpr std::array<D3D11_PRIMITIVE_TOPOLOGY, static_cast<u32>(GPUPipeline::Primitive::MaxCount)> primitives =
-    {{
-      D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,     // Points
-      D3D11_PRIMITIVE_TOPOLOGY_LINELIST,      // Lines
-      D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST,  // Triangles
-      D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, // TriangleStrips
-    }};
-
-  return std::unique_ptr<GPUPipeline>(
-    new D3D11Pipeline(std::move(rs), std::move(ds), std::move(bs), nullptr,
-                      static_cast<const D3D11Shader*>(config.vertex_shader)->GetD3DVertexShader(),
-                      static_cast<const D3D11Shader*>(config.pixel_shader)->GetD3DPixelShader(),
-                      primitives[static_cast<u8>(config.primitive)]));
-}
diff --git a/src/core/gpu/d3d11_pipeline.h b/src/core/gpu/d3d11_pipeline.h
deleted file mode 100644
index fbffc8e68..000000000
--- a/src/core/gpu/d3d11_pipeline.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#pragma once
-
-#include "gpu_pipeline.h"
-
-#include "common/windows_headers.h"
-
-#include <memory>
-
-#include <d3d11.h>
-#include <wrl/client.h>
-
-#include "gsl/span"
-
-class D3D11Device;
-
-class D3D11Pipeline final : public GPUPipeline
-{
-  friend D3D11Device;
-
-  template<typename T>
-  using ComPtr = Microsoft::WRL::ComPtr<T>;
-
-public:
-  ~D3D11Pipeline() override;
-
-  void SetDebugName(const std::string_view& name) override;
-
-  ALWAYS_INLINE ID3D11RasterizerState* GetRasterizerState() const { return m_rs.Get(); }
-  ALWAYS_INLINE ID3D11DepthStencilState* GetDepthStencilState() const { return m_ds.Get(); }
-  ALWAYS_INLINE ID3D11BlendState* GetBlendState() const { return m_bs.Get(); }
-  ALWAYS_INLINE ID3D11InputLayout* GetInputLayout() const { return m_il.Get(); }
-  ALWAYS_INLINE ID3D11VertexShader* GetVertexShader() const { return m_vs.Get(); }
-  ALWAYS_INLINE ID3D11PixelShader* GetPixelShader() const { return m_ps.Get(); }
-  ALWAYS_INLINE D3D11_PRIMITIVE_TOPOLOGY GetPrimitiveTopology() const { return m_topology; }
-
-  void Bind(ID3D11DeviceContext* context);
-
-private:
-  D3D11Pipeline(ComPtr<ID3D11RasterizerState> rs, ComPtr<ID3D11DepthStencilState> ds, ComPtr<ID3D11BlendState> bs,
-                ComPtr<ID3D11InputLayout> il, ComPtr<ID3D11VertexShader> vs, ComPtr<ID3D11PixelShader> ps,
-                D3D11_PRIMITIVE_TOPOLOGY topology);
-
-  ComPtr<ID3D11RasterizerState> m_rs;
-  ComPtr<ID3D11DepthStencilState> m_ds;
-  ComPtr<ID3D11BlendState> m_bs;
-  ComPtr<ID3D11InputLayout> m_il;
-  ComPtr<ID3D11VertexShader> m_vs;
-  ComPtr<ID3D11PixelShader> m_ps;
-  D3D11_PRIMITIVE_TOPOLOGY m_topology;
-};
diff --git a/src/core/gpu/d3d11_shader.cpp b/src/core/gpu/d3d11_shader.cpp
deleted file mode 100644
index cc124678b..000000000
--- a/src/core/gpu/d3d11_shader.cpp
+++ /dev/null
@@ -1,113 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#include "d3d11_shader.h"
-#include "d3d11/shader_compiler.h"
-#include "d3d11_device.h"
-
-#include "common/assert.h"
-
-D3D11Shader::D3D11Shader(Stage stage, Microsoft::WRL::ComPtr<ID3D11DeviceChild> shader)
-  : GPUShader(stage), m_shader(std::move(shader))
-{
-}
-
-D3D11Shader::~D3D11Shader() = default;
-
-ID3D11VertexShader* D3D11Shader::GetD3DVertexShader() const
-{
-  DebugAssert(m_stage == Stage::Vertex);
-  return static_cast<ID3D11VertexShader*>(m_shader.Get());
-}
-
-ID3D11PixelShader* D3D11Shader::GetD3DPixelShader() const
-{
-  DebugAssert(m_stage == Stage::Pixel);
-  return static_cast<ID3D11PixelShader*>(m_shader.Get());
-}
-
-ID3D11ComputeShader* D3D11Shader::GetD3DComputeShader() const
-{
-  DebugAssert(m_stage == Stage::Compute);
-  return static_cast<ID3D11ComputeShader*>(m_shader.Get());
-}
-
-void D3D11Shader::SetDebugName(const std::string_view& name)
-{
-  Panic("Implement me");
-}
-
-std::unique_ptr<GPUShader> D3D11Device::CreateShaderFromBinary(GPUShader::Stage stage, gsl::span<const u8> data)
-{
-  ComPtr<ID3D11DeviceChild> shader;
-  std::vector<u8> bytecode;
-  switch (stage)
-  {
-    case GPUShader::Stage::Vertex:
-      shader = D3D11::ShaderCompiler::CreateVertexShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
-      bytecode.resize(data.size());
-      std::memcpy(bytecode.data(), data.data(), data.size());
-      break;
-
-    case GPUShader::Stage::Pixel:
-      shader = D3D11::ShaderCompiler::CreatePixelShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
-      break;
-
-    case GPUShader::Stage::Compute:
-      shader = D3D11::ShaderCompiler::CreateComputeShader(D3D11Device::GetD3DDevice(), data.data(), data.size());
-      break;
-
-    default:
-      UnreachableCode();
-      break;
-  }
-
-  if (!shader)
-    return {};
-
-  return std::unique_ptr<GPUShader>(new D3D11Shader(stage, std::move(shader), std::move(bytecode)));
-}
-
-std::unique_ptr<GPUShader> D3D11Device::CreateShaderFromSource(GPUShader::Stage stage, const std::string_view& source,
-                                                               std::vector<u8>* out_binary /* = nullptr */)
-{
-  // TODO: This shouldn't be dependent on build type.
-#ifdef _DEBUG
-  constexpr bool debug = true;
-#else
-  constexpr bool debug = false;
-#endif
-
-  ComPtr<ID3DBlob> blob;
-  switch (stage)
-  {
-    case GPUShader::Stage::Vertex:
-      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Vertex, m_device->GetFeatureLevel(),
-                                                  source, debug);
-      break;
-
-    case GPUShader::Stage::Pixel:
-      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Pixel, m_device->GetFeatureLevel(),
-                                                  source, debug);
-      break;
-
-    case GPUShader::Stage::Compute:
-      blob = D3D11::ShaderCompiler::CompileShader(D3D11::ShaderCompiler::Type::Compute, m_device->GetFeatureLevel(),
-                                                  source, debug);
-      break;
-
-    default:
-      UnreachableCode();
-      break;
-  }
-
-  if (out_binary)
-  {
-    const size_t size = blob->GetBufferSize();
-    out_binary->resize(size);
-    std::memcpy(out_binary->data(), blob->GetBufferPointer(), size);
-  }
-
-  return CreateShaderFromBinary(
-    stage, gsl::span<const u8>(static_cast<const u8*>(blob->GetBufferPointer()), blob->GetBufferSize()));
-}
diff --git a/src/core/gpu/d3d11_shader.h b/src/core/gpu/d3d11_shader.h
deleted file mode 100644
index e37406a69..000000000
--- a/src/core/gpu/d3d11_shader.h
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#pragma once
-
-#include "gpu_shader.h"
-
-#include "common/windows_headers.h"
-
-#include <memory>
-#include <vector>
-
-#include <d3d11.h>
-#include <wrl/client.h>
-
-#include "gsl/span"
-
-class D3D11Device;
-
-class D3D11Shader final : public GPUShader
-{
-  friend D3D11Device;
-
-public:
-  ~D3D11Shader() override;
-
-  ID3D11VertexShader* GetD3DVertexShader() const;
-  ID3D11PixelShader* GetD3DPixelShader() const;
-  ID3D11ComputeShader* GetD3DComputeShader() const;
-
-  ALWAYS_INLINE const std::vector<u8>& GetBytecode() const { return m_bytecode; }
-
-  void SetDebugName(const std::string_view& name) override;
-
-private:
-  D3D11Shader(Stage stage, Microsoft::WRL::ComPtr<ID3D11DeviceChild> shader, std::vector<u8> bytecode);
-
-  Microsoft::WRL::ComPtr<ID3D11DeviceChild> m_shader;
-  std::vector<u8> m_bytecode; // only for VS
-};
diff --git a/src/core/gpu/gpu_device.cpp b/src/core/gpu/gpu_device.cpp
index d3894b6f2..fb0e69627 100644
--- a/src/core/gpu/gpu_device.cpp
+++ b/src/core/gpu/gpu_device.cpp
@@ -21,6 +21,10 @@
 #include <vector>
 Log_SetChannel(GPUDevice);
 
+// FIXME
+#include "common/windows_headers.h"
+#include "d3d_shaders.h"
+
 std::unique_ptr<GPUDevice> g_host_display;
 
 size_t GPUPipeline::InputLayoutHash::operator()(const InputLayout& il) const
@@ -103,12 +107,182 @@ RenderAPI GPUDevice::GetPreferredAPI()
 #endif
 }
 
+bool GPUDevice::CreateResources()
+{
+  GPUSampler::Config config = {};
+  config.address_u = GPUSampler::AddressMode::ClampToEdge;
+  config.address_v = GPUSampler::AddressMode::ClampToEdge;
+  config.address_w = GPUSampler::AddressMode::ClampToEdge;
+  config.min_filter = GPUSampler::Filter::Nearest;
+  config.mag_filter = GPUSampler::Filter::Nearest;
+  if (!(m_point_sampler = CreateSampler(config)))
+    return false;
+
+  config.min_filter = GPUSampler::Filter::Linear;
+  config.mag_filter = GPUSampler::Filter::Linear;
+  if (!(m_linear_sampler = CreateSampler(config)))
+    return false;
+
+  if (!CreateImGuiResources())
+    return false;
+
+  return true;
+}
+
 void GPUDevice::DestroyResources()
 {
+  DestroyImGuiResources();
+
   m_cursor_texture.reset();
+  m_linear_sampler.reset();
+  m_point_sampler.reset();
+}
+
+bool GPUDevice::CreateImGuiResources()
+{
+  std::unique_ptr<GPUShader> imgui_vs = CreateShaderFromBinary(GPUShader::Stage::Vertex, s_imgui_vs_bytecode);
+  std::unique_ptr<GPUShader> imgui_ps = CreateShaderFromBinary(GPUShader::Stage::Pixel, s_imgui_ps_bytecode);
+  if (!imgui_vs || !imgui_ps)
+  {
+    Log_ErrorPrintf("Failed to create ImGui shaders.");
+    return false;
+  }
+
+  static constexpr GPUPipeline::VertexAttribute attributes[] = {
+    GPUPipeline::VertexAttribute::Make(GPUPipeline::VertexAttribute::Semantic::Position, 0,
+                                       GPUPipeline::VertexAttribute::Type::Float, 2, offsetof(ImDrawVert, pos)),
+    GPUPipeline::VertexAttribute::Make(GPUPipeline::VertexAttribute::Semantic::Texcoord, 0,
+                                       GPUPipeline::VertexAttribute::Type::Float, 2, offsetof(ImDrawVert, uv)),
+    GPUPipeline::VertexAttribute::Make(GPUPipeline::VertexAttribute::Semantic::Color, 0,
+                                       GPUPipeline::VertexAttribute::Type::UNorm8, 4, offsetof(ImDrawVert, col)),
+  };
+
+  GPUPipeline::GraphicsConfig config;
+  config.layout = GPUPipeline::Layout::SingleTexture;
+  config.primitive = GPUPipeline::Primitive::Triangles;
+  config.input_layout.vertex_attributes = attributes;
+  config.input_layout.vertex_stride = sizeof(ImDrawVert);
+  config.vertex_shader = imgui_vs.get();
+  config.pixel_shader = imgui_ps.get();
+  config.rasterization = GPUPipeline::RasterizationState::GetNoCullState();
+  config.depth = GPUPipeline::DepthState::GetNoTestsState();
+  config.blend = GPUPipeline::BlendState::GetAlphaBlendingState();
+  config.color_format = GPUTexture::Format::RGBA8; // FIXME m_window_info.surface_format;
+  config.depth_format = GPUTexture::Format::Unknown;
+  config.samples = 1;
+  config.per_sample_shading = false;
+
+  m_imgui_pipeline = CreatePipeline(config);
+  if (!m_imgui_pipeline)
+  {
+    Log_ErrorPrintf("Failed to compile ImGui pipeline.");
+    return false;
+  }
+
+  return true;
+}
+
+void GPUDevice::DestroyImGuiResources()
+{
   m_imgui_font_texture.reset();
 }
 
+void GPUDevice::MapVertexBuffer(u32 vertex_size, u32 vertex_count, void** map_ptr, u32* map_space, u32* map_base_vertex)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::UnmapVertexBuffer(u32 used_vertex_count)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::MapIndexBuffer(u32 index_count, u16** map_ptr, u32* map_space, u32* map_base_index)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::UnmapIndexBuffer(u32 used_index_count)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::UploadVertexBuffer(const void* vertices, u32 vertex_size, u32 vertex_count, u32* base_vertex)
+{
+  void* map;
+  u32 space;
+  MapVertexBuffer(vertex_size, vertex_count, &map, &space, base_vertex);
+  std::memcpy(map, vertices, vertex_size * vertex_count);
+  UnmapVertexBuffer(vertex_count);
+}
+
+void GPUDevice::UploadIndexBuffer(const u16* indices, u32 index_count, u32* base_index)
+{
+  u16* map;
+  u32 space;
+  MapIndexBuffer(index_count, &map, &space, base_index);
+  std::memcpy(map, indices, sizeof(u16) * index_count);
+  UnmapIndexBuffer(index_count);
+}
+
+void GPUDevice::PushUniformBuffer(const void* data, u32 data_size)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetFramebuffer(GPUFramebuffer* fb)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetPipeline(GPUPipeline* pipeline)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetTextureSampler(u32 slot, GPUTexture* texture, GPUSampler* sampler)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetViewport(u32 x, u32 y, u32 width, u32 height)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetScissor(u32 x, u32 y, u32 width, u32 height)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::SetViewportAndScissor(u32 x, u32 y, u32 width, u32 height)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::Draw(u32 base_vertex, u32 vertex_count)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
+void GPUDevice::DrawIndexed(u32 base_index, u32 index_count, u32 base_vertex)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+}
+
 void GPUDevice::CopyTextureRegion(GPUTexture* dst, u32 dst_x, u32 dst_y, u32 dst_layer, u32 dst_level, GPUTexture* src,
                                   u32 src_x, u32 src_y, u32 src_layer, u32 src_level, u32 width, u32 height)
 {
@@ -146,6 +320,21 @@ std::unique_ptr<GPUPipeline> GPUDevice::CreatePipeline(const GPUPipeline::Graphi
   return {};
 }
 
+std::unique_ptr<GPUSampler> GPUDevice::CreateSampler(const GPUSampler::Config& config)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+  return {};
+}
+
+std::unique_ptr<GPUFramebuffer> GPUDevice::CreateFramebuffer(GPUTexture* rt, u32 rt_layer, u32 rt_level, GPUTexture* ds,
+                                                             u32 ds_layer, u32 ds_level)
+{
+  // TODO: REMOVE ME
+  UnreachableCode();
+  return {};
+}
+
 bool GPUDevice::ParseFullscreenMode(const std::string_view& mode, u32* width, u32* height, float* refresh_rate)
 {
   if (!mode.empty())
diff --git a/src/core/gpu/gpu_device.h b/src/core/gpu/gpu_device.h
index c48f2687c..55bbf7e4a 100644
--- a/src/core/gpu/gpu_device.h
+++ b/src/core/gpu/gpu_device.h
@@ -2,10 +2,10 @@
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #pragma once
-#include "gpu_pipeline.h"
-#include "gpu_shader.h"
+
 #include "gpu_texture.h"
 
+#include "common/bitfield.h"
 #include "common/rectangle.h"
 #include "common/types.h"
 #include "common/window_info.h"
@@ -28,7 +28,307 @@ enum class RenderAPI : u32
   OpenGLES
 };
 
-// Interface to the frontend's renderer.
+class GPUFramebuffer
+{
+public:
+  GPUFramebuffer() = default;
+  virtual ~GPUFramebuffer() = default;
+
+  virtual void SetDebugName(const std::string_view& name) = 0;
+};
+
+class GPUSampler
+{
+public:
+  enum class Filter
+  {
+    Nearest,
+    Linear,
+
+    MaxCount
+  };
+
+  enum class AddressMode
+  {
+    Repeat,
+    ClampToEdge,
+    ClampToBorder,
+
+    MaxCount
+  };
+
+  struct Config
+  {
+    BitField<u64, Filter, 0, 1> min_filter;
+    BitField<u64, Filter, 1, 1> mag_filter;
+    BitField<u64, Filter, 2, 1> mip_filter;
+    BitField<u64, AddressMode, 3, 2> address_u;
+    BitField<u64, AddressMode, 5, 2> address_v;
+    BitField<u64, AddressMode, 7, 2> address_w;
+    BitField<u64, u8, 9, 5> anisotropy;
+    BitField<u64, u8, 14, 4> min_lod;
+    BitField<u64, u8, 18, 4> max_lod;
+    BitField<u64, u32, 32, 32> border_color;
+    u64 key;
+  };
+
+  GPUSampler() = default;
+  virtual ~GPUSampler() = default;
+
+  virtual void SetDebugName(const std::string_view& name) = 0;
+};
+
+class GPUShader
+{
+public:
+  enum class Stage
+  {
+    Vertex,
+    Pixel,
+    Compute
+  };
+
+  GPUShader(Stage stage) : m_stage(stage) {}
+  virtual ~GPUShader() = default;
+
+  ALWAYS_INLINE Stage GetStage() const { return m_stage; }
+
+  virtual void SetDebugName(const std::string_view& name) = 0;
+
+protected:
+  Stage m_stage;
+};
+
+class GPUPipeline
+{
+public:
+  enum class Layout : u8
+  {
+    // 128 byte UBO via push constants, 1 texture.
+    SingleTexture,
+
+    MaxCount
+  };
+
+  enum class Primitive : u8
+  {
+    Points,
+    Lines,
+    Triangles,
+    TriangleStrips,
+
+    MaxCount
+  };
+
+  union VertexAttribute
+  {
+    enum class Semantic : u8
+    {
+      Position,
+      Texcoord,
+      Color,
+
+      MaxCount
+    };
+
+    enum class Type : u8
+    {
+      Float,
+      UInt8,
+      SInt8,
+      UNorm8,
+      UInt16,
+      SInt16,
+      UNorm16,
+      UInt32,
+      SInt32,
+
+      MaxCount
+    };
+
+    BitField<u32, Semantic, 0, 3> semantic;
+    BitField<u32, u8, 4, 8> semantic_index;
+    BitField<u32, Type, 12, 4> type;
+    BitField<u32, u8, 16, 3> components;
+    BitField<u32, u8, 19, 8> offset;
+    u32 key;
+
+    // clang-format off
+    ALWAYS_INLINE VertexAttribute& operator=(const VertexAttribute& rhs) { key = rhs.key; return *this; }
+    ALWAYS_INLINE bool operator==(const VertexAttribute& rhs) const { return key == rhs.key; }
+    ALWAYS_INLINE bool operator!=(const VertexAttribute& rhs) const { return key != rhs.key; }
+    ALWAYS_INLINE bool operator<(const VertexAttribute& rhs) const { return key < rhs.key; }
+    // clang-format on
+
+    static constexpr VertexAttribute Make(Semantic semantic, u8 semantic_index, Type type, u8 components, u8 offset)
+    {
+      VertexAttribute ret = {};
+#if 0
+      ret.semantic = semantic;
+      ret.semantic_index = semantic_index;
+      ret.type = type;
+      ret.components = components;
+      ret.offset = offset;
+#else
+      // Nasty :/ can't access an inactive element of a union here..
+      ret.key = (static_cast<u32>(semantic) & 0x7) | ((static_cast<u32>(semantic_index) & 0xff) << 4) |
+                ((static_cast<u32>(type) & 0xf) << 12) | ((static_cast<u32>(components) & 0x7) << 16) |
+                ((static_cast<u32>(offset) & 0xff) << 19);
+#endif
+      return ret;
+    }
+  };
+
+  struct InputLayout
+  {
+    gsl::span<const VertexAttribute> vertex_attributes;
+    u32 vertex_stride;
+
+    bool operator==(const InputLayout& rhs) const;
+    bool operator!=(const InputLayout& rhs) const;
+  };
+
+  struct InputLayoutHash
+  {
+    size_t operator()(const InputLayout& il) const;
+  };
+
+  enum class CullMode : u8
+  {
+    None,
+    Front,
+    Back,
+
+    MaxCount
+  };
+
+  enum class DepthFunc : u8
+  {
+    Never,
+    Always,
+    Less,
+    LessEqual,
+    Greater,
+    GreaterEqual,
+    Equal,
+
+    MaxCount
+  };
+
+  enum class BlendFunc : u8
+  {
+    Zero,
+    One,
+    SrcColor,
+    InvSrcColor,
+    DstColor,
+    InvDstColor,
+    SrcAlpha,
+    InvSrcAlpha,
+    SrcAlpha1,
+    InvSrcAlpha1,
+    DstAlpha,
+    InvDstAlpha,
+
+    MaxCount
+  };
+
+  enum class BlendOp : u8
+  {
+    Add,
+    Subtract,
+    ReverseSubtract,
+    Min,
+    Max,
+
+    MaxCount
+  };
+
+  union RasterizationState
+  {
+    BitField<u8, CullMode, 0, 2> cull_mode;
+    u8 key;
+
+    // clang-format off
+    ALWAYS_INLINE RasterizationState& operator=(const RasterizationState& rhs) { key = rhs.key; return *this; }
+    ALWAYS_INLINE bool operator==(const RasterizationState& rhs) const { return key == rhs.key; }
+    ALWAYS_INLINE bool operator!=(const RasterizationState& rhs) const { return key != rhs.key; }
+    ALWAYS_INLINE bool operator<(const RasterizationState& rhs) const { return key < rhs.key; }
+    // clang-format on
+
+    static RasterizationState GetNoCullState();
+  };
+
+  struct DepthState
+  {
+    BitField<u8, DepthFunc, 0, 3> depth_test;
+    BitField<u8, bool, 4, 1> depth_write;
+    u8 key;
+
+    // clang-format off
+    ALWAYS_INLINE DepthState& operator=(const DepthState& rhs) { key = rhs.key; return *this; }
+    ALWAYS_INLINE bool operator==(const DepthState& rhs) const { return key == rhs.key; }
+    ALWAYS_INLINE bool operator!=(const DepthState& rhs) const { return key != rhs.key; }
+    ALWAYS_INLINE bool operator<(const DepthState& rhs) const { return key < rhs.key; }
+    // clang-format on
+
+    static DepthState GetNoTestsState();
+    static DepthState GetAlwaysWriteState();
+  };
+
+  struct BlendState
+  {
+    BitField<u32, bool, 0, 1> enable;
+    BitField<u32, BlendFunc, 1, 4> src_blend;
+    BitField<u32, BlendFunc, 5, 4> src_alpha_blend;
+    BitField<u32, BlendFunc, 9, 4> dst_blend;
+    BitField<u32, BlendFunc, 13, 4> dst_alpha_blend;
+    BitField<u32, BlendOp, 17, 3> blend_op;
+    BitField<u32, BlendOp, 20, 3> alpha_blend_op;
+    BitField<u32, bool, 24, 1> write_r;
+    BitField<u32, bool, 25, 1> write_g;
+    BitField<u32, bool, 26, 1> write_b;
+    BitField<u32, bool, 27, 1> write_a;
+    BitField<u32, u8, 24, 4> write_mask;
+    u32 key;
+
+    // clang-format off
+    ALWAYS_INLINE BlendState& operator=(const BlendState& rhs) { key = rhs.key; return *this; }
+    ALWAYS_INLINE bool operator==(const BlendState& rhs) const { return key == rhs.key; }
+    ALWAYS_INLINE bool operator!=(const BlendState& rhs) const { return key != rhs.key; }
+    ALWAYS_INLINE bool operator<(const BlendState& rhs) const { return key < rhs.key; }
+    // clang-format on
+
+    static BlendState GetNoBlendingState();
+    static BlendState GetAlphaBlendingState();
+  };
+
+  struct GraphicsConfig
+  {
+    Layout layout;
+
+    Primitive primitive;
+    InputLayout input_layout;
+
+    RasterizationState rasterization;
+    DepthState depth;
+    BlendState blend;
+
+    const GPUShader* vertex_shader;
+    const GPUShader* pixel_shader;
+
+    GPUTexture::Format color_format;
+    GPUTexture::Format depth_format;
+    u32 samples;
+    bool per_sample_shading;
+  };
+
+  GPUPipeline() = default;
+  virtual ~GPUPipeline() = default;
+
+  virtual void SetDebugName(const std::string_view& name) = 0;
+};
+
 class GPUDevice
 {
 public:
@@ -86,7 +386,7 @@ public:
   virtual bool IsFullscreen() = 0;
   virtual bool SetFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) = 0;
   virtual AdapterAndModeList GetAdapterAndModeList() = 0;
-  virtual bool CreateResources() = 0;
+  virtual bool CreateResources();
   virtual void DestroyResources();
 
   virtual bool SetPostProcessingChain(const std::string_view& config) = 0;
@@ -94,11 +394,37 @@ public:
   /// Call when the window size changes externally to recreate any resources.
   virtual void ResizeWindow(s32 new_window_width, s32 new_window_height) = 0;
 
+  /// Vertex/index buffer abstraction.
+  virtual void MapVertexBuffer(u32 vertex_size, u32 vertex_count, void** map_ptr, u32* map_space, u32* map_base_vertex);
+  virtual void UnmapVertexBuffer(u32 used_vertex_count);
+  virtual void MapIndexBuffer(u32 index_count, u16** map_ptr, u32* map_space, u32* map_base_index);
+  virtual void UnmapIndexBuffer(u32 used_index_count);
+
+  void UploadVertexBuffer(const void* vertices, u32 vertex_size, u32 vertex_count, u32* base_vertex);
+  void UploadIndexBuffer(const u16* indices, u32 index_count, u32* base_index);
+
+  /// Uniform buffer abstraction.
+  virtual void PushUniformBuffer(const void* data, u32 data_size);
+
+  /// Drawing setup abstraction.
+  virtual void SetFramebuffer(GPUFramebuffer* fb);
+  virtual void SetPipeline(GPUPipeline* pipeline);
+  virtual void SetTextureSampler(u32 slot, GPUTexture* texture, GPUSampler* sampler);
+  virtual void SetViewport(u32 x, u32 y, u32 width, u32 height);
+  virtual void SetScissor(u32 x, u32 y, u32 width, u32 height);
+  void SetViewportAndScissor(u32 x, u32 y, u32 width, u32 height);
+
+  // Drawing abstraction.
+  virtual void Draw(u32 base_vertex, u32 vertex_count);
+  virtual void DrawIndexed(u32 base_index, u32 index_count, u32 base_vertex);
+
   /// Creates an abstracted RGBA8 texture. If dynamic, the texture can be updated with UpdateTexture() below.
   virtual std::unique_ptr<GPUTexture> CreateTexture(u32 width, u32 height, u32 layers, u32 levels, u32 samples,
                                                     GPUTexture::Type type, GPUTexture::Format format,
                                                     const void* data = nullptr, u32 data_stride = 0,
                                                     bool dynamic = false) = 0;
+  virtual std::unique_ptr<GPUSampler> CreateSampler(const GPUSampler::Config& config);
+
   virtual bool DownloadTexture(GPUTexture* texture, u32 x, u32 y, u32 width, u32 height, void* out_data,
                                u32 out_data_stride) = 0;
   virtual void CopyTextureRegion(GPUTexture* dst, u32 dst_x, u32 dst_y, u32 dst_layer, u32 dst_level, GPUTexture* src,
@@ -107,6 +433,10 @@ public:
                                     GPUTexture* src, u32 src_x, u32 src_y, u32 src_layer, u32 src_level, u32 width,
                                     u32 height);
 
+  /// Framebuffer abstraction.
+  virtual std::unique_ptr<GPUFramebuffer> CreateFramebuffer(GPUTexture* rt, u32 rt_layer, u32 rt_level, GPUTexture* ds,
+                                                            u32 ds_layer, u32 ds_level);
+
   /// Shader abstraction.
   virtual std::unique_ptr<GPUShader> CreateShaderFromBinary(GPUShader::Stage stage, gsl::span<const u8> data);
   virtual std::unique_ptr<GPUShader> CreateShaderFromSource(GPUShader::Stage stage, const std::string_view& source,
@@ -230,8 +560,14 @@ protected:
   std::tuple<s32, s32, s32, s32> CalculateSoftwareCursorDrawRect() const;
   std::tuple<s32, s32, s32, s32> CalculateSoftwareCursorDrawRect(s32 cursor_x, s32 cursor_y) const;
 
+  bool CreateImGuiResources();
+  void DestroyImGuiResources();
+
   WindowInfo m_window_info;
 
+  std::unique_ptr<GPUSampler> m_point_sampler;
+  std::unique_ptr<GPUSampler> m_linear_sampler;
+
   u64 m_last_frame_displayed_time = 0;
 
   s32 m_mouse_position_x = 0;
@@ -252,6 +588,7 @@ protected:
   s32 m_display_texture_view_width = 0;
   s32 m_display_texture_view_height = 0;
 
+  std::unique_ptr<GPUPipeline> m_imgui_pipeline;
   std::unique_ptr<GPUTexture> m_imgui_font_texture;
 
   std::unique_ptr<GPUTexture> m_cursor_texture;
diff --git a/src/core/gpu/gpu_pipeline.h b/src/core/gpu/gpu_pipeline.h
deleted file mode 100644
index abbfb7457..000000000
--- a/src/core/gpu/gpu_pipeline.h
+++ /dev/null
@@ -1,199 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#pragma once
-
-#include "gpu_texture.h"
-
-#include "common/bitfield.h"
-#include "common/types.h"
-
-#include "gsl/span"
-
-#include <string_view>
-
-class GPUShader;
-
-class GPUPipeline
-{
-public:
-  enum class Layout : u8
-  {
-    // 128 byte UBO via push constants, 1 texture.
-    SingleTexture,
-
-    MaxCount
-  };
-
-  enum class Primitive : u8
-  {
-    Points,
-    Lines,
-    Triangles,
-    TriangleStrips,
-
-    MaxCount
-  };
-
-  union VertexAttribute
-  {
-    enum class Semantic : u8
-    {
-      Position,
-      Texcoord,
-      Color,
-
-      MaxCount
-    };
-
-    enum class Type : u8
-    {
-      Float,
-      UInt8,
-      SInt8,
-      UNorm8,
-      UInt16,
-      SInt16,
-      UNorm16,
-      UInt32,
-      SInt32,
-
-      MaxCount
-    };
-
-    BitField<u32, Semantic, 0, 3> semantic;
-    BitField<u32, u8, 4, 8> semantic_index;
-    BitField<u32, Type, 12, 4> type;
-    BitField<u32, u8, 16, 2> components;
-    BitField<u32, u8, 18, 8> offset;
-    u32 key;
-  };
-
-  struct InputLayout
-  {
-    gsl::span<const VertexAttribute> vertex_attributes;
-    u32 vertex_stride;
-
-    bool operator==(const InputLayout& rhs) const;
-    bool operator!=(const InputLayout& rhs) const;
-  };
-
-  struct InputLayoutHash
-  {
-    size_t operator()(const InputLayout& il) const;
-  };
-
-  enum class CullMode : u8
-  {
-    None,
-    Front,
-    Back,
-
-    MaxCount
-  };
-
-  enum class DepthFunc : u8
-  {
-    Never,
-    Always,
-    Less,
-    LessEqual,
-    Greater,
-    GreaterEqual,
-    Equal,
-
-    MaxCount
-  };
-
-  enum class BlendFunc : u8
-  {
-    Zero,
-    One,
-    SrcColor,
-    InvSrcColor,
-    DstColor,
-    InvDstColor,
-    SrcAlpha,
-    InvSrcAlpha,
-    SrcAlpha1,
-    InvSrcAlpha1,
-    DstAlpha,
-    InvDstAlpha,
-
-    MaxCount
-  };
-
-  enum class BlendOp : u8
-  {
-    Add,
-    Subtract,
-    ReverseSubtract,
-    Min,
-    Max,
-
-    MaxCount
-  };
-
-  union RasterizationState
-  {
-    BitField<u8, CullMode, 0, 2> cull_mode;
-    u8 key;
-
-    static RasterizationState GetNoCullState();
-  };
-
-  struct DepthState
-  {
-    BitField<u8, DepthFunc, 0, 3> depth_test;
-    BitField<u8, bool, 4, 1> depth_write;
-    u8 key;
-
-    static DepthState GetNoTestsState();
-    static DepthState GetAlwaysWriteState();
-  };
-
-  struct BlendState
-  {
-    BitField<u32, bool, 0, 1> enable;
-    BitField<u32, BlendFunc, 1, 4> src_blend;
-    BitField<u32, BlendFunc, 5, 4> src_alpha_blend;
-    BitField<u32, BlendFunc, 9, 4> dst_blend;
-    BitField<u32, BlendFunc, 13, 4> dst_alpha_blend;
-    BitField<u32, BlendOp, 17, 3> blend_op;
-    BitField<u32, BlendOp, 20, 3> alpha_blend_op;
-    BitField<u32, bool, 24, 1> write_r;
-    BitField<u32, bool, 25, 1> write_g;
-    BitField<u32, bool, 26, 1> write_b;
-    BitField<u32, bool, 27, 1> write_a;
-    BitField<u32, u8, 24, 4> write_mask;
-    u32 key;
-
-    static BlendState GetNoBlendingState();
-    static BlendState GetAlphaBlendingState();
-  };
-
-  struct GraphicsConfig
-  {
-    Layout layout;
-
-    Primitive primitive;
-    InputLayout input_layout;
-
-    RasterizationState rasterization;
-    DepthState depth;
-    BlendState blend;
-
-    const GPUShader* vertex_shader;
-    const GPUShader* pixel_shader;
-
-    GPUTexture::Format color_format;
-    GPUTexture::Format depth_format;
-    u32 samples;
-    bool per_sample_shading;
-  };
-
-  GPUPipeline() = default;
-  virtual ~GPUPipeline() = default;
-
-  virtual void SetDebugName(const std::string_view& name) = 0;
-};
diff --git a/src/core/gpu/gpu_shader.h b/src/core/gpu/gpu_shader.h
deleted file mode 100644
index 84183edec..000000000
--- a/src/core/gpu/gpu_shader.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
-// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
-
-#pragma once
-
-#include <string_view>
-
-#include "common/types.h"
-
-class GPUShader
-{
-public:
-  enum class Stage
-  {
-    Vertex,
-    Pixel,
-    Compute
-  };
-
-  GPUShader(Stage stage) : m_stage(stage) {}
-  virtual ~GPUShader() = default;
-
-  ALWAYS_INLINE Stage GetStage() const { return m_stage; }
-
-  virtual void SetDebugName(const std::string_view& name) = 0;
-
-protected:
-  Stage m_stage;
-};
diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt
index 1496aad3f..2e66254fd 100644
--- a/src/frontend-common/CMakeLists.txt
+++ b/src/frontend-common/CMakeLists.txt
@@ -19,12 +19,6 @@ add_library(frontend-common
   imgui_overlays.cpp
   imgui_overlays.h
   platform_misc.h
-  postprocessing_chain.cpp
-  postprocessing_chain.h
-  postprocessing_shader.cpp
-  postprocessing_shader.h
-  postprocessing_shadergen.cpp
-  postprocessing_shadergen.h
 )
 
 target_link_libraries(frontend-common PUBLIC core common imgui tinyxml2 rapidjson scmversion)
@@ -39,16 +33,8 @@ endif()
 
 if(WIN32)
   target_sources(frontend-common PRIVATE
-    d3d11_host_display.cpp
-    d3d11_host_display.h
-    d3d12_host_display.cpp
-    d3d12_host_display.h
     dinput_source.cpp
     dinput_source.h
-    imgui_impl_dx11.cpp
-    imgui_impl_dx11.h
-    imgui_impl_dx12.cpp
-    imgui_impl_dx12.h
     win32_raw_input_source.cpp
     win32_raw_input_source.h
     xaudio2_audio_stream.cpp
@@ -59,25 +45,6 @@ if(WIN32)
   target_link_libraries(frontend-common PRIVATE d3d11.lib dxgi.lib)
 endif()
 
-if(ENABLE_OPENGL)
-  target_sources(frontend-common PRIVATE
-    opengl_host_display.cpp
-    opengl_host_display.h
-    imgui_impl_opengl3.cpp
-    imgui_impl_opengl3.h
-  )
-  target_link_libraries(frontend-common PRIVATE glad)
-endif()
-
-if(ENABLE_VULKAN)
-  target_sources(frontend-common PRIVATE
-    imgui_impl_vulkan.cpp
-    imgui_impl_vulkan.h
-    vulkan_host_display.cpp
-    vulkan_host_display.h
-  )
-endif()
-
 if(WIN32)
   target_sources(frontend-common PRIVATE
     platform_misc_win32.cpp