Skip to content

fix: map GraphicsProfile.Level_11_2 to a valid Direct3D feature level - #3302

Open
sasvdw wants to merge 6 commits into
stride3d:masterfrom
LazyWorksZA:fix/graphicsprofile-level-11-2-mapping
Open

fix: map GraphicsProfile.Level_11_2 to a valid Direct3D feature level#3302
sasvdw wants to merge 6 commits into
stride3d:masterfrom
LazyWorksZA:fix/graphicsprofile-level-11-2-mapping

Conversation

@sasvdw

@sasvdw sasvdw commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

PR Details

SummaryGraphicsProfile.Level_11_2 does not work on the Direct3D backends. This PR maps it to
the 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) to 12_0 (0xC000). Its
headline feature, tiled resources, is an optional capability. The application queries it after device
creation with CheckFeatureSupport(D3D11_FEATURE_D3D11_OPTIONS1)TiledResourcesTier, and Tier 1
needs FL 11_0 hardware plus a WDDM 1.3 driver. This PR only stops an enum value that we already ship
from failing.

DescriptionGraphicsProfile.Level_11_2 = 0xB200 has no native Direct3D feature level.
GraphicsProfileHelper.ToFeatureLevel cast the profile to the non-existent 0xB200. Direct3D11 then
rejected Level 11.2 in GraphicsAdapter.IsProfileSupported. Direct3D12 passed its unconditional
IsProfileSupported and then failed at CreateDevice(0xB200). Vulkan ignores the profile, so Vulkan
ran.

Level_11_2 reaches those paths from disk, not only from code. GraphicsProfile is a [DataContract]
and Level_11_2 has a [Display] name. It is therefore a selectable value for
RenderingSettings.DefaultGraphicsProfile in Game Settings, and Game.cs feeds that value into both
deviceManager.ShaderProfile and PreferredGraphicsProfile.

Five Direct3D files change. The GraphicsProfile enum stays as it is, so this PR breaks nothing and
changes no serialized data.

  • GraphicsProfileHelper.csToFeatureLevel maps Level_11_2 to 11_1 with an explicit
    table, instead of a cast that depends on the two enums sharing values. ToFeatureLevels maps a
    sequence one element at a time. It replaces the array overload, whose span reinterpretation skipped
    the mapping.
  • GraphicsAdapter.Direct3D.cs (Direct3D11 IsProfileSupported) resolves the feature level with
    ToFeatureLevel() instead of a cast, so the exact-match check tests the real FL 11_1.
  • GraphicsOutput.Direct3D11.cs and GraphicsOutput.Direct3D12.cs
    (FindClosestMatchingDisplayMode) map the caller's profile span instead of reinterpreting it. See
    the next section.
  • ShaderCompiler.cs (ShaderProfileFromGraphicsProfile) lists Level_11_2 with 11_0 and
    11_1 in the shader-model 5_0 case, so it no longer throws.

The display-mode path was a second crash

FindClosestMatchingDisplayMode exists once per backend. Both copies reinterpreted the whole profile
span as feature levels. GamePlatform passes the game's own PreferredGraphicsProfile to that method
when IsFullScreen is set. With 11.2 selected, the fullscreen path carried 0xB200 into device
creation.

  • Direct3D12 tried each level in turn and failed every attempt. It then threw from
    ThrowNoCompatibleProfile. Fullscreen therefore still rejected the profile, even with the mapping
    fixed elsewhere.
  • Direct3D11 discarded the CreateDevice HRESULT and then released the device and the context
    unconditionally. On failure both pointers are still null, so it faulted instead of reporting an
    unsupported profile. This fault is not specific to 11.2. Any unsupported profile in fullscreen
    reached the same null release, for example Level_11_1 on FL10 hardware. Direct3D11 now checks the
    HRESULT before it uses the device, which is what Direct3D12 already did.

Direct3D11 and Direct3D12 now accept Level_11_2 and run at FL 11_1, windowed and fullscreen, the
same 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

  • The round trip loses information, by design. FromFeatureLevel does not change, so a game that
    requests 11.2 reports Features.CurrentProfile == Level_11_1. RequestedProfile keeps the request.
    The case Level_11_2: arm in GraphicsDeviceFeatures.Direct3D11.cs is therefore unreachable. It is
    harmless, because it shares a row with 11_1 and 11_0.
  • IsProfileSupported(Level_11_2) now succeeds on FL 11_1 hardware.
    IsPreferredProfileAvailable walks every enum value, so the message "the highest available profile
    is [...]"
    can now name Level_11_2. This follows from treating 11.2 as the 11_1 tier. It is not a
    regression.
  • Shader macros still carry 0xB200. EffectCompiler emits STRIDE_GRAPHICS_PROFILE from the
    requested profile, plus one GRAPHICS_PROFILE_LEVEL_* macro per enum value. The ordered comparisons
    in .sdsl stay correct, because 0xB200 is above every other level. Stride.Shaders.Tests already
    pins those values. Shader-model resolution is the part that needed the fix.
  • The Null backend is the concrete reason the ShaderCompiler arm is necessary.
    Null/GraphicsDeviceFeatures.Null.cs sets RequestedProfile = CurrentProfile = Level_11_2, and
    EffectSystem feeds Features.RequestedProfile into the compiler.
  • The test project takes its own Silk.NET.Direct3D11 reference. Stride.Graphics keeps Silk.NET
    at PrivateAssets="compile", so D3DFeatureLevel does not flow to consumers. The reference is
    conditioned to the desktop frameworks, the same way Stride.Graphics conditions 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 0xB200 with
E_INVALIDARG, and accepting 0xB100. 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 broken
option 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

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

Validation status (kept honest — check the boxes above as each one is satisfied):

  • Docs: no documentation change is necessary. The profile works now, and the existing tooltip
    becomes accurate.
  • Tests: sources/engine/Stride.Graphics.Tests/TestGraphicsProfileHelper.cs covers both mappings.
    Level_11_2 resolves to FL 11_1 and not to 0xB200. Every profile maps to a defined
    D3DFeatureLevel. A sequence maps one element at a time, so no entry can leak 0xB200. Verified
    red and green: the sequence tests fail against a cast and pass with the mapping.
  • Build: Stride.Graphics builds clean under StrideGraphicsApi=Direct3D11 and under
    =Direct3D12. The two GraphicsOutput files sit behind mutually exclusive #if guards, so each
    backend needs its own build for full coverage. Stride.Shaders.Compilers builds clean. CI runs the
    full test suite.
  • Editor: outstanding. Build and run the editor with a project set to Level 11.2, windowed and
    fullscreen, to confirm that it starts. This is the mandatory personal-testing step.

sasvdw and others added 2 commits July 28, 2026 22:39
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>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Draft PR — automatic CI is skipped to save runner minutes.

  • Mark the PR ready for review to run the full automatic CI — or add a ci-run-on-draft label to run it now without leaving draft.
  • Or arm a specific opt-in suite: ci-enduser, ci-editor, ci-ios, ci-android.

@sasvdw
sasvdw marked this pull request as ready for review July 28, 2026 22:51
@Ethereal77

Copy link
Copy Markdown
Contributor

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 😁)

@sasvdw

sasvdw commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@Ethereal77

Copy link
Copy Markdown
Contributor

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 ToFeatureLevel(), and that helper already documents why the mapping 11_2 is special, then the call site of the helper does not need the same comment, as the potential bug is not there anymore.

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>
@sasvdw

sasvdw commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@Ethereal77

Copy link
Copy Markdown
Contributor

👍 Looks nice

@xen2 xen2 self-assigned this Aug 3, 2026
@xen2

xen2 commented Aug 14, 2026

Copy link
Copy Markdown
Member

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:
Is there any actual need for 11.2 in the meantime? If yes, we can merge this as a stopgap, otherwise let's not add unnecessary complexity until something better is in place.

@Ethereal77

Copy link
Copy Markdown
Contributor

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.

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 😁

Is there any actual need for 11.2 in the meantime?

I'd say not really. Direct3D Feature Level 11.2 included:

  • Tiled resources: Would be nice, but there is currently no support for that in Stride.Graphics anyway. It would need to be implemented.
  • Improvements for WARP devices (support for FL 11.1, double in shaders, 16x MSAA): Would be nice, but mostly for tests, so not really useful for everyday use.
  • Annotate graphics commands: Again, nice for better debugging, but not critical.
  • HLSL shader linking: Not really useful for us I believe, as that role is already covered by the current shader system, no?

sasvdw and others added 3 commits August 17, 2026 17:08
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>
@sasvdw

sasvdw commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GraphicsProfile.Level_11_2 is unusable on Direct3D (invalid feature-level mapping)

3 participants