diff --git a/dxvk.conf b/dxvk.conf
index a3ddf3ea6..54f2ac0ab 100644
--- a/dxvk.conf
+++ b/dxvk.conf
@@ -464,6 +464,7 @@
 # capabilities that the applicatation queries.
 # 
 # Supported values:
+# - 0: Fixed-function only
 # - 1: Shader Model 1
 # - 2: Shader Model 2
 # - 3: Shader Model 3
diff --git a/src/d3d8/d3d8_d3d9_util.h b/src/d3d8/d3d8_d3d9_util.h
index 5be366ee9..69aeb9c07 100644
--- a/src/d3d8/d3d8_d3d9_util.h
+++ b/src/d3d8/d3d8_d3d9_util.h
@@ -19,7 +19,10 @@ namespace dxvk {
 
     // Max supported shader model is PS 1.4 and VS 1.1
     pCaps8->VertexShaderVersion = D3DVS_VERSION(1, 1);
-    pCaps8->PixelShaderVersion  = D3DPS_VERSION(1, 4);
+    // Late fixed-function capable hardware will advertise VS 1.1
+    // support, but will not advertise any support for PS
+    if (likely(caps9.PixelShaderVersion != D3DPS_VERSION(0, 0)))
+      pCaps8->PixelShaderVersion  = D3DPS_VERSION(1, 4);
 
     // Remove D3D9-specific caps:
 
diff --git a/src/d3d8/d3d8_device.cpp b/src/d3d8/d3d8_device.cpp
index b6128008b..0a5ab0f0d 100644
--- a/src/d3d8/d3d8_device.cpp
+++ b/src/d3d8/d3d8_device.cpp
@@ -58,6 +58,12 @@ namespace dxvk {
 
     if (m_d3d8Options.batching)
       m_batcher = new D3D8Batcher(this, GetD3D9());
+
+    d3d9::D3DCAPS9 caps9;
+    HRESULT res = GetD3D9()->GetDeviceCaps(&caps9);
+
+    if (unlikely(SUCCEEDED(res) && caps9.PixelShaderVersion == D3DPS_VERSION(0, 0)))
+      m_isFixedFunctionOnly = true;
   }
 
   D3D8Device::~D3D8Device() {
@@ -1784,8 +1790,8 @@ namespace dxvk {
 
     // Validate VS version for non-FF shaders
     if (pFunction != nullptr) {
-      uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
-      uint32_t minorVersion = pFunction[0] & 0xff;
+      const uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
+      const uint32_t minorVersion = pFunction[0] & 0xff;
 
       if (unlikely(majorVersion != 1 || minorVersion > 1)) {
         Logger::err(str::format("D3D8Device::CreateVertexShader: Unsupported VS version ", majorVersion, ".", minorVersion));
@@ -2023,10 +2029,10 @@ namespace dxvk {
     if (unlikely(pFunction == nullptr || pHandle == nullptr))
       return D3DERR_INVALIDCALL;
 
-    uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
-    uint32_t minorVersion = pFunction[0] & 0xff;
+    const uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
+    const uint32_t minorVersion = pFunction[0] & 0xff;
 
-    if (unlikely(majorVersion != 1 || minorVersion > 4)) {
+    if (unlikely(m_isFixedFunctionOnly || majorVersion != 1 || minorVersion > 4)) {
       Logger::err(str::format("D3D8Device::CreatePixelShader: Unsupported PS version ", majorVersion, ".", minorVersion));
       return D3DERR_INVALIDCALL;
     }
diff --git a/src/d3d8/d3d8_device.h b/src/d3d8/d3d8_device.h
index dc83a3eb7..61ce40b8d 100644
--- a/src/d3d8/d3d8_device.h
+++ b/src/d3d8/d3d8_device.h
@@ -429,6 +429,9 @@ namespace dxvk {
     // Value of D3DRS_PATCHSEGMENTS
     float                 m_patchSegments = 1.0f;
 
+    // Controls fixed-function exclusive mode (no PS support)
+    bool                  m_isFixedFunctionOnly = false;
+
     D3D8StateBlock*                            m_recorder = nullptr;
     DWORD                                      m_recorderToken = 0;
     DWORD                                      m_token    = 0;
diff --git a/src/d3d8/d3d8_interface.cpp b/src/d3d8/d3d8_interface.cpp
index 502f3cfb8..1fd9933eb 100644
--- a/src/d3d8/d3d8_interface.cpp
+++ b/src/d3d8/d3d8_interface.cpp
@@ -12,7 +12,7 @@ namespace dxvk {
 
     // Get the bridge interface to D3D9.
     if (FAILED(m_d3d9->QueryInterface(__uuidof(IDxvkD3D8InterfaceBridge), (void**)&m_bridge))) {
-      throw DxvkError("D3D8Device: ERROR! Failed to get D3D9 Bridge. d3d9.dll might not be DXVK!");
+      throw DxvkError("D3D8Interface: ERROR! Failed to get D3D9 Bridge. d3d9.dll might not be DXVK!");
     }
 
     m_bridge->SetD3D8CompatibilityMode(true);
diff --git a/src/d3d8/d3d8_main.cpp b/src/d3d8/d3d8_main.cpp
index 54109773a..86ca6118c 100644
--- a/src/d3d8/d3d8_main.cpp
+++ b/src/d3d8/d3d8_main.cpp
@@ -26,8 +26,8 @@ extern "C" {
     if (unlikely(pPixelShader == nullptr)) {
       errorMessage = "D3D8: ValidatePixelShader: Null pPixelShader";
     } else {
-      uint32_t majorVersion = (pPixelShader[0] >> 8) & 0xff;
-      uint32_t minorVersion = pPixelShader[0] & 0xff;
+      const uint32_t majorVersion = (pPixelShader[0] >> 8) & 0xff;
+      const uint32_t minorVersion = pPixelShader[0] & 0xff;
 
       if (unlikely(majorVersion != 1 || minorVersion > 4)) {
         errorMessage = dxvk::str::format("D3D8: ValidatePixelShader: Unsupported PS version ",
@@ -69,8 +69,8 @@ extern "C" {
     if (unlikely(pVertexShader == nullptr)) {
       errorMessage = "D3D8: ValidateVertexShader: Null pVertexShader";
     } else {
-      uint32_t majorVersion = (pVertexShader[0] >> 8) & 0xff;
-      uint32_t minorVersion = pVertexShader[0] & 0xff;
+      const uint32_t majorVersion = (pVertexShader[0] >> 8) & 0xff;
+      const uint32_t minorVersion = pVertexShader[0] & 0xff;
 
       if (unlikely(majorVersion != 1 || minorVersion > 1)) {
         errorMessage = dxvk::str::format("D3D8: ValidateVertexShader: Unsupported VS version ",
diff --git a/src/d3d9/d3d9_adapter.cpp b/src/d3d9/d3d9_adapter.cpp
index b40ef2454..e1bc320a4 100644
--- a/src/d3d9/d3d9_adapter.cpp
+++ b/src/d3d9/d3d9_adapter.cpp
@@ -573,17 +573,22 @@ namespace dxvk {
     // Max Stream Stride
     pCaps->MaxStreamStride           = 508; // bytes
 
-    const uint32_t majorVersion = options.shaderModel;
-    const uint32_t minorVersion = options.shaderModel != 1 ? 0 : 4;
+    // Late fixed-function capable cards, such as the GeForce 4 MX series,
+    // expose support for VS 1.1, while not advertising any PS support
+    const uint32_t majorVersionVS = options.shaderModel == 0 ? 1 : options.shaderModel;
+    const uint32_t majorVersionPS = options.shaderModel;
+    // Max supported SM1 is VS 1.1 and PS 1.4
+    const uint32_t minorVersionVS = majorVersionVS != 1 ? 0 : 1;
+    const uint32_t minorVersionPS = majorVersionPS != 1 ? 0 : 4;
 
     // Shader Versions
-    pCaps->VertexShaderVersion = D3DVS_VERSION(majorVersion, minorVersion);
-    pCaps->PixelShaderVersion  = D3DPS_VERSION(majorVersion, minorVersion);
+    pCaps->VertexShaderVersion = D3DVS_VERSION(majorVersionVS, minorVersionVS);
+    pCaps->PixelShaderVersion  = D3DPS_VERSION(majorVersionPS, minorVersionPS);
 
     // Max Vertex Shader Const
     pCaps->MaxVertexShaderConst       = MaxFloatConstantsVS;
     // Max PS1 Value
-    pCaps->PixelShader1xMaxValue      = FLT_MAX;
+    pCaps->PixelShader1xMaxValue      = options.shaderModel > 0 ? FLT_MAX : 0.0f;
     // Dev Caps 2
     pCaps->DevCaps2                   = D3DDEVCAPS2_STREAMOFFSET
                                    /* | D3DDEVCAPS2_DMAPNPATCH */
@@ -646,8 +651,8 @@ namespace dxvk {
     pCaps->PS20Caps.NumInstructionSlots      = options.shaderModel >= 2 ? 512 : 256;
 
     pCaps->VertexTextureFilterCaps           = 50332416;
-    pCaps->MaxVShaderInstructionsExecuted    = 4294967295;
-    pCaps->MaxPShaderInstructionsExecuted    = 4294967295;
+    pCaps->MaxVShaderInstructionsExecuted    = options.shaderModel >= 2 ? 4294967295 : 0;
+    pCaps->MaxPShaderInstructionsExecuted    = options.shaderModel >= 2 ? 4294967295 : 0;
 
     pCaps->MaxVertexShader30InstructionSlots = options.shaderModel == 3 ? 32768 : 0;
     pCaps->MaxPixelShader30InstructionSlots  = options.shaderModel == 3 ? 32768 : 0;
diff --git a/src/d3d9/d3d9_device.cpp b/src/d3d9/d3d9_device.cpp
index 502625556..415afebf4 100644
--- a/src/d3d9/d3d9_device.cpp
+++ b/src/d3d9/d3d9_device.cpp
@@ -3314,6 +3314,19 @@ namespace dxvk {
     if (unlikely(ppShader == nullptr))
       return D3DERR_INVALIDCALL;
 
+    const uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
+    const uint32_t minorVersion = pFunction[0] & 0xff;
+
+    // Late fixed-function capable hardware exposed support for VS 1.1
+    const uint32_t shaderModelVS = m_d3d9Options.shaderModel == 0 ? 1 : m_d3d9Options.shaderModel;
+
+    if (unlikely(majorVersion > shaderModelVS
+             || (majorVersion == 1 && minorVersion > 1)
+             || (majorVersion > 1  && minorVersion != 0))) {
+      Logger::err(str::format("D3D9DeviceEx::CreateVertexShader: Unsupported VS version ", majorVersion, ".", minorVersion));
+      return D3DERR_INVALIDCALL;
+    }
+
     DxsoModuleInfo moduleInfo;
     moduleInfo.options = m_dxsoOptions;
 
@@ -3678,6 +3691,16 @@ namespace dxvk {
     if (unlikely(ppShader == nullptr))
       return D3DERR_INVALIDCALL;
 
+    const uint32_t majorVersion = (pFunction[0] >> 8) & 0xff;
+    const uint32_t minorVersion = pFunction[0] & 0xff;
+
+    if (unlikely(majorVersion > m_d3d9Options.shaderModel
+             || (majorVersion == 1 && minorVersion > 4)
+             || (majorVersion > 1  && minorVersion != 0))) {
+      Logger::err(str::format("D3D9DeviceEx::CreatePixelShader: Unsupported PS version ", majorVersion, ".", minorVersion));
+      return D3DERR_INVALIDCALL;
+    }
+
     DxsoModuleInfo moduleInfo;
     moduleInfo.options = m_dxsoOptions;
 
diff --git a/src/d3d9/d3d9_interface.cpp b/src/d3d9/d3d9_interface.cpp
index 4ed2dfdf2..f0c0921c4 100644
--- a/src/d3d9/d3d9_interface.cpp
+++ b/src/d3d9/d3d9_interface.cpp
@@ -67,6 +67,9 @@ namespace dxvk {
       SetProcessDPIAware();
     }
 #endif
+
+    if (unlikely(m_d3d9Options.shaderModel == 0))
+      Logger::warn("D3D9InterfaceEx: WARNING! Fixed-function exclusive mode is enabled.");
   }
 
 
diff --git a/src/d3d9/d3d9_options.h b/src/d3d9/d3d9_options.h
index 4d733880f..8fee48361 100644
--- a/src/d3d9/d3d9_options.h
+++ b/src/d3d9/d3d9_options.h
@@ -34,7 +34,7 @@ namespace dxvk {
     int32_t maxFrameRate;
 
     /// Set the max shader model the device can support in the caps.
-    int32_t shaderModel;
+    uint32_t shaderModel;
 
     /// Whether or not to set the process as DPI aware in Windows when the API interface is created.
     bool dpiAware;
diff --git a/src/d3d9/d3d9_shader.cpp b/src/d3d9/d3d9_shader.cpp
index b17ab47c0..6ab5df3d7 100644
--- a/src/d3d9/d3d9_shader.cpp
+++ b/src/d3d9/d3d9_shader.cpp
@@ -98,9 +98,6 @@ namespace dxvk {
 
     DxsoModule module(reader);
 
-    if (module.info().majorVersion() > pDxbcModuleInfo->options.shaderModel)
-      throw DxvkError("GetShaderModule: Out of range of supported shader model");
-
     if (module.info().shaderStage() != ShaderStage)
       throw DxvkError("GetShaderModule: Bytecode does not match shader stage");
 
diff --git a/src/dxso/dxso_options.cpp b/src/dxso/dxso_options.cpp
index 30ea3b331..28400af72 100644
--- a/src/dxso/dxso_options.cpp
+++ b/src/dxso/dxso_options.cpp
@@ -20,8 +20,6 @@ namespace dxvk {
     strictPow            = options.strictPow;
     d3d9FloatEmulation   = options.d3d9FloatEmulation;
 
-    shaderModel          = options.shaderModel;
-
     invariantPosition    = options.invariantPosition;
 
     forceSamplerTypeSpecConstants = options.forceSamplerTypeSpecConstants;
diff --git a/src/dxso/dxso_options.h b/src/dxso/dxso_options.h
index 3145e2c61..132b99ac3 100644
--- a/src/dxso/dxso_options.h
+++ b/src/dxso/dxso_options.h
@@ -25,9 +25,6 @@ namespace dxvk {
     /// Whether or not we should care about pow(0, 0) = 1
     bool strictPow;
 
-    /// Max version of shader to support
-    uint32_t shaderModel;
-
     /// Work around a NV driver quirk
     /// Fixes flickering/z-fighting in some games.
     bool invariantPosition;