fix: map GraphicsProfile.Level_11_2 to a valid Direct3D feature level - #3302
fix: map GraphicsProfile.Level_11_2 to a valid Direct3D feature level#3302sasvdw wants to merge 6 commits into
Conversation
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
|
🤖 Draft PR — automatic CI is skipped to save runner minutes.
|
|
LGTM. Good catch! A little nitpick: The comments look a bit too verbose for my taste (even though I tend to write big explanatory comments 😁) |
|
@Ethereal77, happy to take suggestions... Was a really tough one from a comments point of view. There's a genuine footgun highlighted in the comments that is bound to cause issues if someone just looks at the changes at surface level. |
|
I agree with you. Better documentation is always preferable, specially around conflicting code. But, I think that if you have already created and used a helper In the shader compiler, as it does not use the helper, the comment makes sense. |
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 <noreply@anthropic.com>
|
@Ethereal77, after further consideration, I think I've landed on something more concise while also adding tests to ensure everything stays up to date. I also removed the comment from the shader compiler because it's an entirely different case and should be straightforward knowledge for anyone with an understanding of this domain. |
|
👍 Looks nice |
|
If you don't mind, I would prefer to hold on for this one and take time to properly review the feature level system (inherited from D3D11 when D3D11 was our "main" API model), as this doesn't translate well cross-API. Also this level seems to translate weirdly as it seems to be a D3D software-side change, not HW support. I think later we might need to go more toward Vulkan model (capabilities, which D3D11/12 also had anyway on top of feature level), and maybe a shader model choice. Now, my question regarding what to do with this PR is: |
That would be really nice. If we design it well it would simplify a lot of logic. I hope however we won't go overboard and end up with the caps inferno of D3D9 times 😁
I'd say not really. Direct3D Feature Level 11.2 included:
|
FindClosestMatchingDisplayMode exists once per Direct3D backend, and both
reinterpreted the caller's whole profile span as feature levels rather than
mapping it. That is the same defect this PR fixes elsewhere, on a path that
takes the game's own PreferredGraphicsProfile: GamePlatform passes the list
here whenever IsFullScreen is set.
With Level_11_2 in that list the span carries 0xB200 into device creation:
- Direct3D12 tries each level in turn, fails every attempt and throws from
ThrowNoCompatibleProfile, so fullscreen still rejected the profile.
- Direct3D11 discarded the CreateDevice HRESULT entirely, then released the
device and context unconditionally. On failure both are still null, so it
faulted rather than reporting the profile as unsupported. That one is not
specific to 11.2 -- any genuinely unsupported profile in fullscreen, say
Level_11_1 on feature level 10 hardware, reached the same null release.
ToFeatureLevels restores the deleted sequence overload as an element-wise map
through the single switch, so every entry is resolved the same way a lone
profile is, and Direct3D11 now checks the HRESULT before using the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pins the two facts the display-mode fix depends on: a sequence maps element-wise so no entry can carry 0xB200 through as a feature level, and an unknown profile anywhere in the sequence throws rather than being passed on. Also states outright that Level_11_1 and Level_11_2 resolve to one feature level. That is what makes them interchangeable to GraphicsAdapter's device probe, so IsProfileSupported now answers alike for both. Verified red against an element-wise map that keeps the old raw cast, green with the mapping in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Silk.NET.Direct3D11 reference added for TestGraphicsProfileHelper was unconditional, so the Android, iOS and macOS test frameworks pulled a package they cannot use. Condition it the way Stride.Graphics conditions its own Direct3D references, and record why the test project needs its own reference at all: Stride.Graphics keeps Silk.NET at PrivateAssets="compile", so D3DFeatureLevel does not flow to consumers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
I've discovered some more silent casts, and I've made those changes. The PR description has also been updated to reflect this. The gist is that this change isn't so much introducing DirectX3D 11.2, but rather making the config parameter we're exposing "safe" for consumers in Game Studio. There's a larger scope of changes that can express hardware by capability in the long term. |
PR Details
Summary —
GraphicsProfile.Level_11_2does not work on the Direct3D backends. This PR maps it tothe feature level it represents (FL 11_1), so it behaves like every other profile.
This PR unlocks no Direct3D 11.2 capabilities. There is no feature level 11_2. Direct3D 11.2 was a
runtime revision for Windows 8.1, and the level enum goes from
11_1 (0xB100)to12_0 (0xC000). Itsheadline feature, tiled resources, is an optional capability. The application queries it after device
creation with
CheckFeatureSupport(D3D11_FEATURE_D3D11_OPTIONS1)→TiledResourcesTier, and Tier 1needs FL 11_0 hardware plus a WDDM 1.3 driver. This PR only stops an enum value that we already ship
from failing.
Description —
GraphicsProfile.Level_11_2 = 0xB200has no native Direct3D feature level.GraphicsProfileHelper.ToFeatureLevelcast the profile to the non-existent0xB200. Direct3D11 thenrejected Level 11.2 in
GraphicsAdapter.IsProfileSupported. Direct3D12 passed its unconditionalIsProfileSupportedand then failed atCreateDevice(0xB200). Vulkan ignores the profile, so Vulkanran.
Level_11_2reaches those paths from disk, not only from code.GraphicsProfileis a[DataContract]and
Level_11_2has a[Display]name. It is therefore a selectable value forRenderingSettings.DefaultGraphicsProfilein Game Settings, andGame.csfeeds that value into bothdeviceManager.ShaderProfileandPreferredGraphicsProfile.Five Direct3D files change. The
GraphicsProfileenum stays as it is, so this PR breaks nothing andchanges no serialized data.
GraphicsProfileHelper.cs—ToFeatureLevelmapsLevel_11_2to11_1with an explicittable, instead of a cast that depends on the two enums sharing values.
ToFeatureLevelsmaps asequence one element at a time. It replaces the array overload, whose span reinterpretation skipped
the mapping.
GraphicsAdapter.Direct3D.cs(Direct3D11IsProfileSupported) resolves the feature level withToFeatureLevel()instead of a cast, so the exact-match check tests the real FL 11_1.GraphicsOutput.Direct3D11.csandGraphicsOutput.Direct3D12.cs(
FindClosestMatchingDisplayMode) map the caller's profile span instead of reinterpreting it. Seethe next section.
ShaderCompiler.cs(ShaderProfileFromGraphicsProfile) listsLevel_11_2with11_0and11_1in the shader-model5_0case, so it no longer throws.The display-mode path was a second crash
FindClosestMatchingDisplayModeexists once per backend. Both copies reinterpreted the whole profilespan as feature levels.
GamePlatformpasses the game's ownPreferredGraphicsProfileto that methodwhen
IsFullScreenis set. With 11.2 selected, the fullscreen path carried0xB200into devicecreation.
ThrowNoCompatibleProfile. Fullscreen therefore still rejected the profile, even with the mappingfixed elsewhere.
CreateDeviceHRESULT and then released the device and the contextunconditionally. On failure both pointers are still
null, so it faulted instead of reporting anunsupported profile. This fault is not specific to 11.2. Any unsupported profile in fullscreen
reached the same null release, for example
Level_11_1on FL10 hardware. Direct3D11 now checks theHRESULT before it uses the device, which is what Direct3D12 already did.
Direct3D11 and Direct3D12 now accept
Level_11_2and run at FL 11_1, windowed and fullscreen, thesame as Vulkan. The code still rejects what it must reject. A profile that a device cannot provide
fails the capability check as before. Only the invalid-value failure goes away.
Consequences to know about
FromFeatureLeveldoes not change, so a game thatrequests 11.2 reports
Features.CurrentProfile == Level_11_1.RequestedProfilekeeps the request.The
case Level_11_2:arm inGraphicsDeviceFeatures.Direct3D11.csis therefore unreachable. It isharmless, because it shares a row with
11_1and11_0.IsProfileSupported(Level_11_2)now succeeds on FL 11_1 hardware.IsPreferredProfileAvailablewalks every enum value, so the message "the highest available profileis [...]" can now name
Level_11_2. This follows from treating 11.2 as the 11_1 tier. It is not aregression.
0xB200.EffectCompileremitsSTRIDE_GRAPHICS_PROFILEfrom therequested profile, plus one
GRAPHICS_PROFILE_LEVEL_*macro per enum value. The ordered comparisonsin
.sdslstay correct, because0xB200is above every other level.Stride.Shaders.Testsalreadypins those values. Shader-model resolution is the part that needed the fix.
ShaderCompilerarm is necessary.Null/GraphicsDeviceFeatures.Null.cssetsRequestedProfile = CurrentProfile = Level_11_2, andEffectSystemfeedsFeatures.RequestedProfileinto the compiler.Silk.NET.Direct3D11reference.Stride.Graphicskeeps Silk.NETat
PrivateAssets="compile", soD3DFeatureLeveldoes not flow to consumers. The reference isconditioned to the desktop frameworks, the same way
Stride.Graphicsconditions its own.Motivation and context — see #3301. The failure does not depend on hardware. A WARP
software-renderer proof in the repro shows WARP reporting FL 12_1, rejecting
0xB200withE_INVALIDARG, and accepting0xB100. The mapping has been wrong since the 2018 open-source commit.A June-2025 documentation pass added the
[Display("Level 11.2 ~ …")]tooltip, which makes the brokenoption look legitimate in Game Settings.
Related Issue
Fixes #3301. Repro (minimal code-only game and WARP proof):
https://github.com/sasvdw/stride-graphicsprofile-level112-repro
Types of changes
Checklist
Validation status (kept honest — check the boxes above as each one is satisfied):
becomes accurate.
sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cscovers both mappings.Level_11_2resolves to FL 11_1 and not to0xB200. Every profile maps to a definedD3DFeatureLevel. A sequence maps one element at a time, so no entry can leak0xB200. Verifiedred and green: the sequence tests fail against a cast and pass with the mapping.
Stride.Graphicsbuilds clean underStrideGraphicsApi=Direct3D11and under=Direct3D12. The twoGraphicsOutputfiles sit behind mutually exclusive#ifguards, so eachbackend needs its own build for full coverage.
Stride.Shaders.Compilersbuilds clean. CI runs thefull test suite.
fullscreen, to confirm that it starts. This is the mandatory personal-testing step.