diff --git a/src/d3d11/d3d11_context.cpp b/src/d3d11/d3d11_context.cpp
index 16529a217..8d42c283b 100644
--- a/src/d3d11/d3d11_context.cpp
+++ b/src/d3d11/d3d11_context.cpp
@@ -3423,6 +3423,17 @@ namespace dxvk {
       if (unlikely(shader->needsLibraryCompile()))
         m_device->requestCompileShader(shader);
 
+      // If this shader activates any bindings that have not yet been applied,
+      // mark the shader stage as dirty so it gets applied on the next draw.
+      // Don't apply it right away since any dirty bindings are likely redundant.
+      m_state.lazy.shadersUsed.set(ShaderStage);
+      m_state.lazy.bindingsUsed[ShaderStage] = pShaderModule->GetBindingMask();
+
+      if (!m_state.lazy.shadersDirty.test(ShaderStage)) {
+        if (!(m_state.lazy.bindingsDirty[ShaderStage] & m_state.lazy.bindingsUsed[ShaderStage]).empty())
+          m_state.lazy.shadersDirty.set(ShaderStage);
+      }
+
       EmitCs([
         cBuffer = std::move(buffer),
         cShader = std::move(shader)
@@ -3438,6 +3449,15 @@ namespace dxvk {
           Forwarder::move(cBuffer));
       });
     } else {
+      // Mark shader stage as inactive and clean since we'll have no active
+      // bindings. This works because if the app changes any binding at all
+      // for this stage, it will get flagged as dirty, and if another shader
+      // gets bound, it will check for any dirty bindings again.
+      m_state.lazy.shadersUsed.clr(ShaderStage);
+      m_state.lazy.shadersDirty.clr(ShaderStage);
+
+      m_state.lazy.bindingsUsed[ShaderStage].reset();
+
       EmitCs([] (DxvkContext* ctx) {
         constexpr VkShaderStageFlagBits stage = GetShaderStage(ShaderStage);
 
@@ -4516,6 +4536,9 @@ namespace dxvk {
     m_state.srv.reset();
     m_state.uav.reset();
     m_state.samplers.reset();
+
+    // Reset dirty tracking
+    m_state.lazy.reset();
   }
 
 
@@ -4623,6 +4646,36 @@ namespace dxvk {
   }
 
 
+  template<typename ContextType>
+  void D3D11CommonContext<ContextType>::RestoreUsedBindings() {
+    // Mark all bindings used since the last reset as dirty so that subsequent draws
+    // and dispatches will reapply them as necessary. Marking null bindings here may
+    // lead to some redundant CS thread traffic, but is otherwise harmless.
+    auto maxBindings = GetMaxUsedBindings();
+
+    for (uint32_t i = 0; i < uint32_t(DxbcProgramType::Count); i++) {
+      auto stage = DxbcProgramType(i);
+      auto stageInfo = maxBindings.stages[i];
+
+      m_state.lazy.bindingsDirty[stage].cbvMask |= (1u << stageInfo.cbvCount) - 1u;
+      m_state.lazy.bindingsDirty[stage].samplerMask |= (1u << stageInfo.samplerCount) - 1u;
+
+      if (stageInfo.uavCount)
+        m_state.lazy.bindingsDirty[stage].uavMask |= uint64_t(-1) >> (64u - stageInfo.uavCount);
+
+      if (stageInfo.srvCount > 64u) {
+        m_state.lazy.bindingsDirty[stage].srvMask[0] |= uint64_t(-1);
+        m_state.lazy.bindingsDirty[stage].srvMask[1] |= uint64_t(-1) >> (128u - stageInfo.srvCount);
+      } else if (stageInfo.srvCount) {
+        m_state.lazy.bindingsDirty[stage].srvMask[0] |= uint64_t(-1) >> (64u - stageInfo.srvCount);
+      }
+
+      if (m_state.lazy.shadersUsed.test(stage) && !m_state.lazy.bindingsDirty[stage].empty())
+        m_state.lazy.shadersDirty.set(stage);
+    }
+  }
+
+
   template<typename ContextType>
   void D3D11CommonContext<ContextType>::RestoreCommandListState() {
     BindFramebuffer();
diff --git a/src/d3d11/d3d11_context.h b/src/d3d11/d3d11_context.h
index a2d6c3659..32f0ee77f 100644
--- a/src/d3d11/d3d11_context.h
+++ b/src/d3d11/d3d11_context.h
@@ -967,6 +967,8 @@ namespace dxvk {
     void ResolveOmUavHazards(
             D3D11RenderTargetView*            pView);
 
+    void RestoreUsedBindings();
+
     void RestoreCommandListState();
     
     template<DxbcProgramType Stage>
diff --git a/src/d3d11/d3d11_context_state.h b/src/d3d11/d3d11_context_state.h
index 9dea34937..be42ef599 100644
--- a/src/d3d11/d3d11_context_state.h
+++ b/src/d3d11/d3d11_context_state.h
@@ -302,6 +302,30 @@ namespace dxvk {
       predicateValue = false;
     }
   };
+
+
+  /**
+   * \brief Lazy binding state
+   *
+   * Keeps track of what state needs to be
+   * re-applied to the context.
+   */
+  struct D3D11LazyBindings {
+    DxbcProgramTypeFlags shadersUsed = 0u;
+    DxbcProgramTypeFlags shadersDirty = 0u;
+
+    D3D11ShaderStageState<DxbcBindingMask> bindingsUsed;
+    D3D11ShaderStageState<DxbcBindingMask> bindingsDirty;
+
+    void reset() {
+      shadersUsed = 0u;
+      shadersDirty = 0u;
+
+      bindingsUsed.reset();
+      bindingsDirty.reset();
+    }
+  };
+
   
   /**
    * \brief Context state
@@ -325,6 +349,8 @@ namespace dxvk {
     D3D11SrvBindings    srv;
     D3D11UavBindings    uav;
     D3D11SamplerBindings samplers;
+
+    D3D11LazyBindings   lazy;
   };
 
   /**
@@ -342,7 +368,7 @@ namespace dxvk {
    * \brief Maximum used binding numbers for all context state
    */
   struct D3D11MaxUsedBindings {
-    std::array<D3D11MaxUsedStageBindings, 6> stages;
+    std::array<D3D11MaxUsedStageBindings, uint32_t(DxbcProgramType::Count)> stages;
     uint32_t  vbCount;
     uint32_t  soCount;
   };