From 2534b3ef64ed73ce42dff7b898a249f65c74ccae Mon Sep 17 00:00:00 2001 From: Sas van der Westhuizen Date: Tue, 28 Jul 2026 22:39:20 +0200 Subject: [PATCH 1/3] fix: map GraphicsProfile.Level_11_2 to a valid Direct3D feature level GraphicsProfile.Level_11_2 (0xB200) has no corresponding native Direct3D feature level: real feature levels jump from 11_1 (0xB100) to 12_0 (0xC000) (Direct3D 11.2 was an API revision that runs on FL 11_1 hardware, not a new feature level). ToFeatureLevel raw-cast the profile straight to 0xB200, so selecting Level_11_2 failed on every machine: Direct3D11 rejected it in GraphicsAdapter.IsProfileSupported (the exact-match check can never match a non-existent level), and Direct3D12 passed its unconditional IsProfileSupported check but then failed at CreateDevice(0xB200). Vulkan ignored the profile and ran, which is the tell that the fault was in the Direct3D mapping, not the enum. Map Level_11_2 to its real capability tier (FL 11_1) in ToFeatureLevel, drive the Direct3D11 IsProfileSupported check through ToFeatureLevel (instead of a raw cast), and list Level_11_2 alongside 11_0/11_1 in the shader-model 5_0 case so the compiler no longer throws for it. Direct3D11 and Direct3D12 now accept Level_11_2 and run at FL 11_1, matching Vulkan. Legitimate rejection is preserved: profiles a device genuinely cannot provide (e.g. Level_11_x on FL10-only hardware) still fail the capability check as before. Co-Authored-By: Claude Opus 4.8 --- .../Direct3D/GraphicsAdapter.Direct3D.cs | 6 +++++- .../Direct3D/GraphicsProfileHelper.cs | 12 +++++++++++- .../Direct3D/ShaderCompiler.cs | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs b/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs index 4efc67bc35..722ee67ca9 100644 --- a/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs +++ b/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs @@ -269,7 +269,11 @@ public bool IsProfileSupported(GraphicsProfile graphicsProfile) ID3D11DeviceContext* deviceContext = null; D3DFeatureLevel matchedFeatureLevel = 0; - var featureLevel = (D3DFeatureLevel) graphicsProfile; + // Use ToFeatureLevel (not a raw cast) so Level_11_2 resolves to the real FL 11_1 it maps + // to; a raw cast would ask CreateDevice for the non-existent 0xB200, which can never match + // and would wrongly report the profile as unsupported on every adapter. The exact-match + // check below still legitimately rejects real capability gaps (e.g. FL10-only hardware). + var featureLevel = graphicsProfile.ToFeatureLevel(); var featureLevels = stackalloc D3DFeatureLevel[] { featureLevel }; HResult result = d3d11.CreateDevice(pAdapter: null, D3DDriverType.Hardware, Software: IntPtr.Zero, diff --git a/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs b/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs index 10381abd93..39056a2e37 100644 --- a/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs +++ b/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs @@ -33,7 +33,17 @@ public static D3DFeatureLevel[] ToFeatureLevel(this GraphicsProfile[] profiles) /// /// A to convert. /// A Direct3D . - public static D3DFeatureLevel ToFeatureLevel(this GraphicsProfile profile) => (D3DFeatureLevel) profile; + public static D3DFeatureLevel ToFeatureLevel(this GraphicsProfile profile) => profile switch + { + // Direct3D has no feature level 11.2: D3D 11.2 was an API revision that runs on FL 11_1 + // hardware, not a new feature level (real levels jump 11_1 (0xB100) -> 12_0 (0xC000)). Map it + // to its real capability tier (11_1) so device creation succeeds and the backend runs, + // matching Vulkan; a raw cast would emit the non-existent 0xB200 and fail CreateDevice on + // both D3D11 and D3D12. Every other GraphicsProfile value equals a real D3DFeatureLevel, so a + // direct cast is correct for them. + GraphicsProfile.Level_11_2 => (D3DFeatureLevel) GraphicsProfile.Level_11_1, + _ => (D3DFeatureLevel) profile, + }; /// /// Converts a to its corresponding . diff --git a/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs b/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs index 7676823ac6..fe7a42f33d 100644 --- a/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs +++ b/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs @@ -161,7 +161,9 @@ static string ShaderProfileFromGraphicsProfile(GraphicsProfile graphicsProfile) GraphicsProfile.Level_9_3 => "4_0_level_9_3", GraphicsProfile.Level_10_0 => "4_0", GraphicsProfile.Level_10_1 => "4_1", - GraphicsProfile.Level_11_0 or GraphicsProfile.Level_11_1 => "5_0", + // Level_11_2 shares shader model 5_0 with 11_0/11_1 (it maps to FL 11_1); listing + // it here keeps it from hitting the throw below when used as the compile target. + GraphicsProfile.Level_11_0 or GraphicsProfile.Level_11_1 or GraphicsProfile.Level_11_2 => "5_0", _ => throw new ArgumentException("Graphics Profile not supported.", nameof(graphicsProfile)) }; From 6a9922f4c43aa79d6ca68ded763814962faa164a Mon Sep 17 00:00:00 2001 From: Sas van der Westhuizen Date: Wed, 29 Jul 2026 00:25:58 +0200 Subject: [PATCH 2/3] test: cover GraphicsProfile.Level_11_2 -> FL 11_1 mapping Add a regression test for the Direct3D GraphicsProfile -> D3DFeatureLevel mapping: Level_11_2 must resolve to the real FL 11_1 (not the non-existent 0xB200), and every profile must map to a defined D3DFeatureLevel. Verified red/green against the ToFeatureLevel fix (both facts fail on the raw cast, pass on the fix). Reference Silk.NET.Direct3D11 in the test project so D3DFeatureLevel is visible at compile time (Stride.Graphics hides it via PrivateAssets); the test is guarded by STRIDE_GRAPHICS_API_DIRECT3D since Stride.Graphics.Tests builds per graphics API. Co-Authored-By: Claude Opus 4.8 --- .../Stride.Graphics.Tests.csproj | 3 ++ .../TestGraphicsProfileHelper.cs | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs diff --git a/sources/engine/Stride.Graphics.Tests/Stride.Graphics.Tests.csproj b/sources/engine/Stride.Graphics.Tests/Stride.Graphics.Tests.csproj index 69fdf60fad..a9220ab5ec 100644 --- a/sources/engine/Stride.Graphics.Tests/Stride.Graphics.Tests.csproj +++ b/sources/engine/Stride.Graphics.Tests/Stride.Graphics.Tests.csproj @@ -14,6 +14,9 @@ + + + diff --git a/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs b/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs new file mode 100644 index 0000000000..545a96312a --- /dev/null +++ b/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +#if STRIDE_GRAPHICS_API_DIRECT3D + +using System; + +using Xunit; + +using Silk.NET.Core.Native; + +namespace Stride.Graphics.Tests; + +/// +/// Regression tests for the GraphicsProfile -> D3DFeatureLevel mapping in GraphicsProfileHelper. +/// Direct3D-only: the helper and D3DFeatureLevel exist only under STRIDE_GRAPHICS_API_DIRECT3D. +/// +public class TestGraphicsProfileHelper +{ + [Fact] + public void Level_11_2_MapsTo_Level_11_1() + { + // 0xB200 has no native D3D feature level; it must resolve to the real 11_1 (0xB100), + // matching Vulkan, instead of the raw-cast 0xB200 that fails CreateDevice on D3D11/D3D12. + Assert.Equal(GraphicsProfile.Level_11_1.ToFeatureLevel(), GraphicsProfile.Level_11_2.ToFeatureLevel()); + Assert.NotEqual((D3DFeatureLevel)0xB200, GraphicsProfile.Level_11_2.ToFeatureLevel()); + } + + [Fact] + public void EveryProfile_MapsToADefinedFeatureLevel() + { + // Guards against any profile (now or future) mapping to a value with no native feature level. + foreach (GraphicsProfile profile in Enum.GetValues()) + { + var featureLevel = profile.ToFeatureLevel(); + Assert.True(Enum.IsDefined(featureLevel), + $"{profile} (0x{(int)profile:X4}) maps to undefined D3DFeatureLevel 0x{(int)featureLevel:X4}"); + } + } +} + +#endif From 6e0820845f4f3a31c277095e68b334fe7be563ab Mon Sep 17 00:00:00 2001 From: Sas van der Westhuizen Date: Sun, 2 Aug 2026 22:52:29 +0200 Subject: [PATCH 3/3] refactor: map GraphicsProfile to feature levels explicitly Address review feedback on the verbose comments by removing the reason they were needed. The helper relied on GraphicsProfile values happening to equal D3D feature levels, so every conversion was a cast and the Level_11_2 special case had to be explained in prose at each site. An explicit mapping table documents itself: Level_11_1 and Level_11_2 sharing a row states the fix outright, so the call-site comment in GraphicsAdapter and the shader model comment in ShaderCompiler are both dropped, and the helper's own explanation shrinks to a one-line remark. Also removes the unused array overload, whose span reinterpret bypassed the mapping entirely and would still have produced the non-existent 0xB200, and extends the tests to pin every profile so a mis-mapping cannot pass. FromFeatureLevel is left as-is; it is a separate direction with its own unmapped-value question and no bearing on this fix. Co-Authored-By: Claude Opus 5 --- .../TestGraphicsProfileHelper.cs | 25 ++++++++---- .../Direct3D/GraphicsAdapter.Direct3D.cs | 4 -- .../Direct3D/GraphicsProfileHelper.cs | 40 ++++++++----------- .../Direct3D/ShaderCompiler.cs | 2 - 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs b/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs index 545a96312a..65f793c4fc 100644 --- a/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs +++ b/sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs @@ -17,19 +17,24 @@ namespace Stride.Graphics.Tests; /// public class TestGraphicsProfileHelper { - [Fact] - public void Level_11_2_MapsTo_Level_11_1() + [Theory] + [InlineData(GraphicsProfile.Level_9_1, D3DFeatureLevel.Level91)] + [InlineData(GraphicsProfile.Level_9_2, D3DFeatureLevel.Level92)] + [InlineData(GraphicsProfile.Level_9_3, D3DFeatureLevel.Level93)] + [InlineData(GraphicsProfile.Level_10_0, D3DFeatureLevel.Level100)] + [InlineData(GraphicsProfile.Level_10_1, D3DFeatureLevel.Level101)] + [InlineData(GraphicsProfile.Level_11_0, D3DFeatureLevel.Level110)] + [InlineData(GraphicsProfile.Level_11_1, D3DFeatureLevel.Level111)] + [InlineData(GraphicsProfile.Level_11_2, D3DFeatureLevel.Level111)] + public void Profile_MapsToExpectedFeatureLevel(GraphicsProfile profile, D3DFeatureLevel expected) { - // 0xB200 has no native D3D feature level; it must resolve to the real 11_1 (0xB100), - // matching Vulkan, instead of the raw-cast 0xB200 that fails CreateDevice on D3D11/D3D12. - Assert.Equal(GraphicsProfile.Level_11_1.ToFeatureLevel(), GraphicsProfile.Level_11_2.ToFeatureLevel()); - Assert.NotEqual((D3DFeatureLevel)0xB200, GraphicsProfile.Level_11_2.ToFeatureLevel()); + Assert.Equal(expected, profile.ToFeatureLevel()); } [Fact] public void EveryProfile_MapsToADefinedFeatureLevel() { - // Guards against any profile (now or future) mapping to a value with no native feature level. + // Guards against a profile added later without a matching entry in the mapping. foreach (GraphicsProfile profile in Enum.GetValues()) { var featureLevel = profile.ToFeatureLevel(); @@ -37,6 +42,12 @@ public void EveryProfile_MapsToADefinedFeatureLevel() $"{profile} (0x{(int)profile:X4}) maps to undefined D3DFeatureLevel 0x{(int)featureLevel:X4}"); } } + + [Fact] + public void UnknownProfile_Throws() + { + Assert.Throws(() => ((GraphicsProfile)0x1234).ToFeatureLevel()); + } } #endif diff --git a/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs b/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs index 722ee67ca9..b6f376cd41 100644 --- a/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs +++ b/sources/engine/Stride.Graphics/Direct3D/GraphicsAdapter.Direct3D.cs @@ -269,10 +269,6 @@ public bool IsProfileSupported(GraphicsProfile graphicsProfile) ID3D11DeviceContext* deviceContext = null; D3DFeatureLevel matchedFeatureLevel = 0; - // Use ToFeatureLevel (not a raw cast) so Level_11_2 resolves to the real FL 11_1 it maps - // to; a raw cast would ask CreateDevice for the non-existent 0xB200, which can never match - // and would wrongly report the profile as unsupported on every adapter. The exact-match - // check below still legitimately rejects real capability gaps (e.g. FL10-only hardware). var featureLevel = graphicsProfile.ToFeatureLevel(); var featureLevels = stackalloc D3DFeatureLevel[] { featureLevel }; diff --git a/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs b/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs index 39056a2e37..05d83eee95 100644 --- a/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs +++ b/sources/engine/Stride.Graphics/Direct3D/GraphicsProfileHelper.cs @@ -3,9 +3,9 @@ #if STRIDE_GRAPHICS_API_DIRECT3D -using Silk.NET.Core.Native; +using System; -using Stride.Core.UnsafeExtensions; +using Silk.NET.Core.Native; namespace Stride.Graphics; @@ -14,35 +14,27 @@ namespace Stride.Graphics; /// internal static class GraphicsProfileHelper { - /// - /// Converts an array of s to an array of corresponding s. - /// - /// An array of s to convert. - /// An array of Direct3D s. - public static D3DFeatureLevel[] ToFeatureLevel(this GraphicsProfile[] profiles) - { - if (profiles is null or []) - return null; - - var featureLevels = profiles.AsReadOnlySpan().ToArray(); - return featureLevels; - } - /// /// Converts a to its corresponding . /// /// A to convert. /// A Direct3D . + /// is not a known profile. + /// + /// Direct3D has no feature level 11.2, as 11.2 was an API revision running on 11_1 hardware, so + /// maps to 11_1, its real capability tier. + /// public static D3DFeatureLevel ToFeatureLevel(this GraphicsProfile profile) => profile switch { - // Direct3D has no feature level 11.2: D3D 11.2 was an API revision that runs on FL 11_1 - // hardware, not a new feature level (real levels jump 11_1 (0xB100) -> 12_0 (0xC000)). Map it - // to its real capability tier (11_1) so device creation succeeds and the backend runs, - // matching Vulkan; a raw cast would emit the non-existent 0xB200 and fail CreateDevice on - // both D3D11 and D3D12. Every other GraphicsProfile value equals a real D3DFeatureLevel, so a - // direct cast is correct for them. - GraphicsProfile.Level_11_2 => (D3DFeatureLevel) GraphicsProfile.Level_11_1, - _ => (D3DFeatureLevel) profile, + GraphicsProfile.Level_9_1 => D3DFeatureLevel.Level91, + GraphicsProfile.Level_9_2 => D3DFeatureLevel.Level92, + GraphicsProfile.Level_9_3 => D3DFeatureLevel.Level93, + GraphicsProfile.Level_10_0 => D3DFeatureLevel.Level100, + GraphicsProfile.Level_10_1 => D3DFeatureLevel.Level101, + GraphicsProfile.Level_11_0 => D3DFeatureLevel.Level110, + GraphicsProfile.Level_11_1 or GraphicsProfile.Level_11_2 => D3DFeatureLevel.Level111, + + _ => throw new ArgumentOutOfRangeException(nameof(profile), profile, "Unknown graphics profile.") }; /// diff --git a/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs b/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs index fe7a42f33d..013fcbaa05 100644 --- a/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs +++ b/sources/shaders/Stride.Shaders.Compilers/Direct3D/ShaderCompiler.cs @@ -161,8 +161,6 @@ static string ShaderProfileFromGraphicsProfile(GraphicsProfile graphicsProfile) GraphicsProfile.Level_9_3 => "4_0_level_9_3", GraphicsProfile.Level_10_0 => "4_0", GraphicsProfile.Level_10_1 => "4_1", - // Level_11_2 shares shader model 5_0 with 11_0/11_1 (it maps to FL 11_1); listing - // it here keeps it from hitting the throw below when used as the compile target. GraphicsProfile.Level_11_0 or GraphicsProfile.Level_11_1 or GraphicsProfile.Level_11_2 => "5_0", _ => throw new ArgumentException("Graphics Profile not supported.", nameof(graphicsProfile))