{"id":14450,"date":"2026-10-28T09:52:19","date_gmt":"2026-10-28T16:52:19","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/directx\/?p=14450"},"modified":"2026-10-28T09:52:19","modified_gmt":"2026-10-28T16:52:19","slug":"async-commands-fallback-layer","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/directx\/async-commands-fallback-layer\/","title":{"rendered":"Async Commands Fallback Layer"},"content":{"rendered":"<p>We&#8217;re pleased to announce that Agility SDK 1.620.1 includes a fallback layer for Batched Async Commands, letting developers use the new commands on hardware that doesn&#8217;t support them natively. You can find the NuGet package <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Direct3D.D3D12\/1.620.1\">here<\/a>.<\/p>\n<h3>Background<\/h3>\n<p>Batched Async Commands were introduced in the <a href=\"https:\/\/devblogs.microsoft.com\/directx\/shader-model-6-10-agilitysdk-720-preview\/\">Agility SDK 1.720 Preview<\/a> as a new set of copy, fill, clear, and resolve commands. The legacy versions of these operations are implicitly serialized, since the original resource barrier model has no way to say that two copies don&#8217;t depend on each other. The batched versions drop that implicit serialization and take many operations in a single call, so the driver and GPU are free to overlap independent work and you only add enhanced barriers where there&#8217;s an actual dependency. They also fill in some long-standing gaps in the API. <code>ClearTextureSubresources<\/code> can clear a texture without an RTV, UAV, or descriptor heap (including block-compressed formats), <code>FillBuffers<\/code> can write formatted or raw patterns into buffers without any UAV clear setup, and <code>ClearBoundRenderTargetViews<\/code> and <code>ClearBoundDepthStencilView<\/code> clear whatever is currently bound, even in the middle of a render pass.<\/p>\n<p>The downside is that the feature is only supported on newer GPUs. At the 1.720 Preview launch, that meant AMD Radeon RX 7000 and 9000 series, Intel Arc B-Series, and NVIDIA RTX GPUs. There&#8217;s a lot of hardware still in use that isn&#8217;t on that list, and if you want to ship on all of it, you&#8217;d have to keep your existing copy and clear code around alongside the new path.\nThat&#8217;s where the fallback layer comes in.<\/p>\n<h3>Batched Async Commands Fallback<\/h3>\n<p>The goal of the fallback layer is to let you move to the new commands now, without having to maintain two separate systems while the hardware ecosystem catches up. When the fallback is enabled on a device that doesn&#8217;t support Batched Async Commands, the runtime implements the commands itself using operations that every driver already supports. On devices with native support, the calls go straight to the driver as usual, so the same code takes the fast path wherever it&#8217;s available.<\/p>\n<p>The fallback is opt-in, for a couple of reasons. First, it isn&#8217;t a perfect match for native behavior, and there are some cases it can&#8217;t handle at all (covered below). Second, it has a cost even if you never call one of the new commands. To implement fallback support for commands like <code>FillBuffers<\/code> and the bound clears, the runtime needs to know the barrier state of the resources involved and has to put back your pipeline state, root signature, bindings, and render targets when it&#8217;s done. That means tracking all of that on every direct, compute, and bundle command list while the fallback is enabled.<\/p>\n<p>The Performance section below covers what that costs. None of it is huge on its own, but it adds up over a frame, and apps that don&#8217;t use Batched Async Commands shouldn&#8217;t have to pay for it.<\/p>\n<p>The fallback itself requires Enhanced Barriers support, so on a device without Enhanced Barriers or native support, the batched commands aren&#8217;t available.<\/p>\n<h4>Example Usage<\/h4>\n<pre><code class=\"language-cpp\">\/\/ Opt in to the fallback on a device factory, before creating the device\r\nComPtr&lt;ID3D12DeviceFactory&gt; pFactory;\r\nD3D12GetInterface(CLSID_D3D12DeviceFactory, IID_PPV_ARGS(&amp;pFactory));\r\n\r\nComPtr&lt;ID3D12AsyncCommandsFallback&gt; pFallback;\r\nif (SUCCEEDED(pFactory-&gt;GetConfigurationInterface(CLSID_D3D12AsyncCommandsFallback, IID_PPV_ARGS(&amp;pFallback))))\r\n{\r\n    pFallback-&gt;SetFallbackLayerEnabled(TRUE);\r\n}\r\n\r\nComPtr&lt;ID3D12Device&gt; pDevice;\r\npFactory-&gt;CreateDevice(pAdapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&amp;pDevice));\r\n\r\n\/\/ Check whether you got native support, the fallback, or neither\r\nD3D12_FEATURE_DATA_ASYNC_COMMANDS asyncCommands = {};\r\npDevice-&gt;CheckFeatureSupport(D3D12_FEATURE_ASYNC_COMMANDS, &amp;asyncCommands, sizeof(asyncCommands));\r\n\r\nif (asyncCommands.Impl == D3D12_ASYNC_COMMANDS_IMPL_NOT_SUPPORTED)\r\n{\r\n    \/\/ Batched Async Commands unavailable, keep using the legacy path\r\n    return;\r\n}\r\n<\/code><\/pre>\n<p>The setting applies to devices that factory creates, and you can read it back as <code>D3D12_DEVICE_FLAG_ASYNC_COMMANDS_FALLBACK_ENABLED<\/code> in <code>ID3D12DeviceConfiguration::GetDesc<\/code>. If you create your device with <code>D3D12CreateDevice<\/code> instead, you can get the same interface from <code>D3D12GetInterface(CLSID_D3D12AsyncCommandsFallback, ...)<\/code>. That sets the option globally, for devices created with <code>D3D12CreateDevice<\/code>. Like other global settings, set it before creating any devices. It doesn&#8217;t carry over to device factories unless you call <code>InitializeFromGlobalState<\/code> on them.<\/p>\n<p>If you tried Batched Async Commands in the preview SDK, note that <code>D3D12_FEATURE_DATA_ASYNC_COMMANDS<\/code> has changed. The <code>BOOL Supported<\/code> field is now <code>D3D12_ASYNC_COMMANDS_IMPL Impl<\/code>, which tells you whether you got the native implementation, the fallback, or neither. Since <code>D3D12_ASYNC_COMMANDS_IMPL_NATIVE<\/code> is 2, code that compares the old field against <code>TRUE<\/code> will treat native support as unsupported, so check against <code>D3D12_ASYNC_COMMANDS_IMPL_NOT_SUPPORTED<\/code> instead.<\/p>\n<h3>Where the Fallback Differs from Native<\/h3>\n<p>Most of the batched commands map pretty directly onto existing operations. Batched copies, tile copies, and resolves run as a loop over their legacy equivalents, so you get the same results as native, just without the overlap. Fills and clears are where the fallback has to do real work, and where there are a few differences to be aware of.<\/p>\n<p>When the fallback can&#8217;t handle a call, it fails rather than silently skipping the work. The command list is removed and <code>Close<\/code> returns an error, usually <code>E_FAIL<\/code>. With the debug layer enabled, the rejected call also produces a debug-layer error that names the API the fallback couldn&#8217;t record, so if <code>Close<\/code> starts failing after you&#8217;ve switched to one of the new commands, check the debug output first. The fallback also validates its inputs more strictly than the runtime does when passing calls to a native driver, so a call that works natively can still be rejected on a fallback device.<\/p>\n<h4>ClearTextureSubresources<\/h4>\n<p>Multisampled textures can only be cleared on direct command lists; on compute command lists the call fails, though single-sample clears work fine there. The fallback is fastest when it knows a texture&#8217;s current layout, which it learns from the barriers you record earlier in the same command list. A barrier that only covers some of a texture&#8217;s subresources makes the layout unknown until the next whole-resource barrier, and until then clears take a slower copy-based path. There are also a few combinations the fallback can&#8217;t handle, and these fail outright:<\/p>\n<ul>\n<li>Partial clears of depth\/stencil textures (single-sample or multisampled) and of multisampled color textures, on devices without programmable sample positions, when the texture can&#8217;t be cleared in place. For example, this happens on compute command lists, or when the texture&#8217;s layout isn&#8217;t known.<\/li>\n<li>Multisampled clears in protected sessions or on protected textures.<\/li>\n<li>Multisampled clears with a format and sample count the device can&#8217;t render to.<\/li>\n<\/ul>\n<h4>ClearBoundRenderTargetViews and ClearBoundDepthStencilView<\/h4>\n<p>Bound clears are mostly implemented with internal draws. Afterwards, the runtime restores the state those draws changed: your pipeline (a pipeline state object, a state object set with <code>SetPipelineState1<\/code>, or a generic program set with <code>SetProgram<\/code>), root signature and root arguments, viewports, scissors, and other related state.<\/p>\n<p>In a few cases the fallback can&#8217;t reconstruct that state, for example if a bundle changed state in a way the fallback can&#8217;t track, or if <code>ExecuteIndirect<\/code> used an argument type the fallback doesn&#8217;t track. What happens next depends on whether the call is inside a render pass:<\/p>\n<ul>\n<li>Outside a render pass, the fallback uses legacy clears instead of draws.<\/li>\n<li>Inside a render pass, the clear fails, since ending the pass for a legacy clear would defeat the purpose of clearing inside it.<\/li>\n<\/ul>\n<p>Native drivers can record bound clears in bundles, but the fallback doesn&#8217;t support that yet, so a bound clear in a bundle fails on a fallback device. A few other differences to keep in mind:<\/p>\n<ul>\n<li>The internal draws count toward occlusion and pipeline statistics queries that are open at the time, so query results won&#8217;t match native.<\/li>\n<li>If the bound outputs are different sizes and a clear region extends past the smallest one, or if they have different sample counts, the clear fails inside a render pass. Outside of a render pass, the call is lowered to legacy clears instead.<\/li>\n<li>Depth clear values are normalized. NaN, negative values, negative zero, and denormals become 0, and anything above 1 (including +infinity) becomes 1.<\/li>\n<li>View information is captured when you call <code>OMSetRenderTargets<\/code> or <code>BeginRenderPass<\/code>. The resource needs to stay alive until the clear is recorded, and the runtime doesn&#8217;t check this.<\/li>\n<\/ul>\n<h4>Inexact Clear Values<\/h4>\n<p>A few paths go through a legacy view clear, which takes its clear value as floats. When a legacy view clear can&#8217;t produce your exact value, the fallback uses the closest value it can instead of failing. This applies to <code>ClearTextureSubresources<\/code> on multisampled textures, and to bound clears that fall back to legacy clears outside of a render pass, as described above. In those cases:<\/p>\n<ul>\n<li><code>UINT<\/code> and <code>SINT<\/code> values are converted to float, so values with a magnitude above 2^24 can round.<\/li>\n<li>Float subnormals are flushed to zero, keeping the sign.<\/li>\n<li>NaN values are passed through, but the stored NaN payload isn&#8217;t guaranteed.<\/li>\n<li>Depth is clamped to [0, 1].<\/li>\n<\/ul>\n<p>The debug layer doesn&#8217;t warn when this happens. Single-sample <code>ClearTextureSubresources<\/code> calls never approximate: if a view clear can&#8217;t produce the exact value, the fallback uses its copy-based path instead.<\/p>\n<p>For the full list of differences, see the <a href=\"https:\/\/microsoft.github.io\/DirectX-Specs\/d3d\/D3D12AsyncCommandsFallback.html\">Async Commands fallback spec<\/a>.<\/p>\n<h3>Testing the Fallback on Hardware with Native Support<\/h3>\n<p>If all of your development machines support Batched Async Commands natively, you&#8217;ll never actually exercise the fallback. To help with that, you can use d3dconfig to force the fallback on, even on hardware with native support. Like other d3dconfig device settings, this applies to the apps in d3dconfig&#8217;s app list, so add your app first:<\/p>\n<pre><code>d3dconfig apps --add MyGame.exe\r\nd3dconfig device force-async-commands-fallback=true\r\n<\/code><\/pre>\n<p>This is also an easy way to compare native and fallback performance for your own workload on a single machine.<\/p>\n<h3>Performance<\/h3>\n<p>To get a sense of how the fallback compares with native support and with doing this work yourself, we measured each command on a GPU with native support at batch sizes of 1, 16, and 256 operations per call, with the fallback forced on for the fallback runs. For <code>ResolveQueryDataAsync<\/code>, batch size is the number of queries resolved, and for the bound clears it&#8217;s the number of clear rectangles. The legacy versions loop over the equivalent legacy calls and batch their barriers. <code>FillBuffers<\/code> uses a compute shader fill, <code>ClearTextureSubresources<\/code> transitions the textures and uses view clears, and the bound clears use <code>ClearRenderTargetView<\/code> and <code>ClearDepthStencilView<\/code> with clear rects, recorded outside a render pass. For most commands, the fallback performs about the same as native and the legacy version on the GPU, with some extra CPU overhead for tracking state and restoring the pipeline state and bindings it changes. The main exception is <code>ClearTextureSubresources<\/code>. When the fallback can&#8217;t clear a texture in place, it copies the clear value from a staging buffer, which costs more on both the GPU and the CPU. The bound clears also cost noticeably more CPU time to record.<\/p>\n<p>Keep in mind that these are microbenchmarks. Each one times a batch of commands on its own, so they show the cost of the work itself, not the benefit of running it asynchronously. With native support, the GPU can overlap batched commands with each other and with surrounding work that doesn&#8217;t depend on them. The fallback runs them as legacy operations, so it can&#8217;t. In a real frame, the difference between the fallback and native may be larger than these numbers suggest, depending on how much independent work there is to overlap. We recommend leveraging the d3dconfig setting to force on the fallback layer to see how performance is impacted for your use case.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch.webp\"><img decoding=\"async\" class=\"alignnone wp-image-14453\" src=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-1024x423.webp\" alt=\"Async Commands cpu time by batch image\" width=\"1118\" height=\"462\" srcset=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-1024x423.webp 1024w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-300x124.webp 300w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-768x317.webp 768w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-1536x634.webp 1536w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-cpu-time-by-batch-2048x845.webp 2048w\" sizes=\"(max-width: 1118px) 100vw, 1118px\" \/><\/a>\n<a href=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch.webp\"><img decoding=\"async\" class=\"alignnone wp-image-14452\" src=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-1024x423.webp\" alt=\"AsyncCommands gpu time by batch image\" width=\"1118\" height=\"462\" srcset=\"https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-1024x423.webp 1024w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-300x124.webp 300w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-768x317.webp 768w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-1536x634.webp 1536w, https:\/\/devblogs.microsoft.com\/directx\/wp-content\/uploads\/sites\/42\/2026\/10\/blog-gpu-time-by-batch-2048x845.webp 2048w\" sizes=\"(max-width: 1118px) 100vw, 1118px\" \/><\/a><\/p>\n<p>Enabling the fallback also adds CPU overhead to recording, even on command lists that never use the new commands. Here&#8217;s how much longer common calls took to record with the fallback enabled in our testing. State-setting calls go up by a bigger proportion, but they&#8217;re very cheap to begin with:<\/p>\n<table>\n<thead>\n<tr>\n<th>Call<\/th>\n<th>Relative Change<\/th>\n<th>Extra time per call<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Draws, dispatches, copies, and legacy clears<\/td>\n<td>about the same<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td><code>ExecuteIndirect<\/code><\/td>\n<td>+20-25%<\/td>\n<td>6-8 ns<\/td>\n<\/tr>\n<tr>\n<td>Barriers on direct and compute command lists<\/td>\n<td>+5-20% (up to +75% in our worst cases)<\/td>\n<td>5-20 ns per barrier (up to 90-100 ns)<\/td>\n<\/tr>\n<tr>\n<td>Barriers on copy command lists<\/td>\n<td>about the same<\/td>\n<td>\u2014<\/td>\n<\/tr>\n<tr>\n<td>Setting root arguments<\/td>\n<td>+20-180%<\/td>\n<td>1-11 ns (up to 35 ns for root constants in bundles)<\/td>\n<\/tr>\n<tr>\n<td>Setting a pipeline state<\/td>\n<td>1.5-4x as long<\/td>\n<td>3-9 ns<\/td>\n<\/tr>\n<tr>\n<td>Changing the root signature<\/td>\n<td>5-16x as long<\/td>\n<td>25-30 ns<\/td>\n<\/tr>\n<tr>\n<td>Binding render targets<\/td>\n<td>+15-70%<\/td>\n<td>13-15 ns<\/td>\n<\/tr>\n<tr>\n<td>An empty render pass<\/td>\n<td>about +33%<\/td>\n<td>about 20 ns<\/td>\n<\/tr>\n<tr>\n<td>Executing a bundle<\/td>\n<td>about 2.8x as long<\/td>\n<td>about 100 ns<\/td>\n<\/tr>\n<tr>\n<td>Resetting or closing a bundle<\/td>\n<td>1.4-2.9x as long<\/td>\n<td>20-35 ns<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Try It Out<\/h3>\n<p>Check out the Batched Async Commands <a href=\"https:\/\/github.com\/microsoft\/DirectX-Graphics-Samples\/tree\/master\/Samples\/Desktop\/D3D12AsyncCommands\">samples<\/a>, and try them with the fallback forced on and off. We&#8217;d love to hear how it works for you.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We&#8217;re pleased to announce that Agility SDK 1.620.1 includes a fallback layer for Batched Async Commands, letting developers use the new commands on hardware that doesn&#8217;t support them natively. You can find the NuGet package here. Background Batched Async Commands were introduced in the Agility SDK 1.720 Preview as a new set of copy, fill, [&hellip;]<\/p>\n","protected":false},"author":66528,"featured_media":12651,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-14450","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-directx"],"acf":[],"blog_post_summary":"<p>We&#8217;re pleased to announce that Agility SDK 1.620.1 includes a fallback layer for Batched Async Commands, letting developers use the new commands on hardware that doesn&#8217;t support them natively. You can find the NuGet package here. Background Batched Async Commands were introduced in the Agility SDK 1.720 Preview as a new set of copy, fill, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/posts\/14450","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/users\/66528"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/comments?post=14450"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/posts\/14450\/revisions"}],"predecessor-version":[{"id":14459,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/posts\/14450\/revisions\/14459"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/media\/12651"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/media?parent=14450"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/categories?post=14450"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/directx\/wp-json\/wp\/v2\/tags?post=14450"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}