Normalize swapchain backbuffers for the opt-in D3D9 single-sample path

This commit is contained in:
Leask Wong committed 2026-10-11 23:52:18 -04:00
1 parent 8d9b7084f8
commit 0fd3a1a2b8
5 files changed
+363

No files matched your search

@@ -29,6 +29,19 @@ alpha-to-coverage. It is a compatibility override, not transparent MSAA emulatio
or a universal PowerVR default. Existing low32, BC-format, present synchronization
and tiler workarounds must remain intact.
Swapchain backbuffers have a separate creation path. With the override enabled,
that path now validates a copy of the original requested descriptor and transfers
the effective sample type/quality before allocation, so automatic depth surfaces
and backbuffers agree. Validation uses the application's lockable-backbuffer flag;
the internal lockability required for GDI fallback is retained on the allocated
backbuffer. With the override disabled the original backbuffer path is unchanged.
This correction addresses official PR602 review 5486295088. Its CPU harness
compiles the actual DXVK sample decoder, surface normalizer and backbuffer method,
plus the automatic-depth descriptor and framebuffer depth-binding excerpts.
Fifteen cases cover on/off, NONMASKABLE quality, matching depth, no-depth operation,
unsupported/invalid/lockable requests, allocation cleanup and recreation. Backend
images and adapter capabilities are mocked; these are not GPU or gameplay tests.
## Independent HUD correction
The diagnostic frametimes HUD bound an R8_UINT storage texel buffer to a shader
@@ -172,6 +185,9 @@ runtime or APK. Its output directory must be new or empty (including hidden
entries). Prior or partially built outputs are refused before fetching sources,
left untouched and never included in a new DLL manifest. Use a fresh directory
after a failed build.
The recipe also requires a native C++17 compiler for
`python3 tools/dxvk/check_d3d9_single_sample.py /path/to/patched-dxvk` before DLL
compilation. Add `--sanitize` on supported hosts to run its ASan/UBSan controls.
The compositor ownership safeguards are independently proposed in
<https://github.com/Droid-Deck/DroidDeck/pull/561>. Those native changes were present
+1
View File
@@ -27,6 +27,7 @@ for patch in "$root"/tools/dxvk/patches/*.patch; do
done
python3 "$root/tools/dxvk/check_present_sync.py" "$work/source/src/dxvk/dxvk_presenter.cpp"
python3 "$root/tools/dxvk/check_hud_storage_format.py" "$work/source/src/dxvk/hud/shaders/hud_frame_time_eval.comp"
python3 "$root/tools/dxvk/check_d3d9_single_sample.py" "$work/source"
for arch in 32 64; do
meson setup "$work/build$arch" "$work/source" --cross-file "$work/source/build-win$arch.txt" \
--buildtype release --prefix "$out" --bindir "x$arch" --libdir "x$arch" -Db_ndebug=if-release --strip
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Compile pinned DXVK's actual descriptor paths against a controlled backend.
This checks CPU control flow, not GPU rendering. No Wine/game/device is started.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
CASES = ('forced_msaa_depth', 'default_msaa_depth', 'forced_nonmaskable',
'default_nonmaskable', 'forced_already_single', 'forced_no_depth',
'forced_unsupported', 'forced_invalid_type', 'forced_lockable_msaa',
'forced_lockable_single', 'forced_allocation_failure',
'default_allocation_failure', 'forced_recreate', 'ordinary_surface',
'ordinary_invalid_lockable')
def function(text, signature):
start = text.index(signature)
opening = text.index('{', start)
# Ignore braces in comments and strings while preserving source positions.
stripped = re.sub(r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'',
lambda m: ' ' * len(m.group()), text, flags=re.S)
depth = 0
for index in range(opening, len(stripped)):
depth += (stripped[index] == '{') - (stripped[index] == '}')
if depth == 0:
return text[start:index + 1]
raise ValueError('Unterminated source function: ' + signature)
def production(source):
base = source / 'src/d3d9'
common = (base / 'd3d9_common_texture.cpp').read_text()
header = (base / 'd3d9_common_texture.h').read_text()
swap = (base / 'd3d9_swapchain.cpp').read_text()
device = (base / 'd3d9_device.cpp').read_text()
util = (base / 'd3d9_util.cpp').read_text()
pieces = {
'decode': function(util, 'HRESULT DecodeMultiSampleType('),
'normalize': function(common, 'HRESULT D3D9CommonTexture::NormalizeTextureProperties('),
'backbuffers': function(swap, 'HRESULT D3D9SwapChainEx::CreateBackBuffers('),
'uses_image': function(header, 'static bool TextureUsesImage('),
}
reset = function(device, 'HRESULT D3D9DeviceEx::ResetSwapChain(')
begin = reset.index('D3D9_COMMON_TEXTURE_DESC desc;', reset.index('if (pPresentationParameters->EnableAutoDepthStencil)'))
end = reset.index('m_autoDepthStencil =', begin)
pieces['auto_depth'] = reset[begin:end].rstrip()
framebuffer = function(device, 'void D3D9DeviceEx::BindFramebuffer(')
begin = framebuffer.index('const bool mismatch =')
end = framebuffer.index(';', framebuffer.index('const bool bindDS =', begin)) + 1
pieces['depth_binding'] = framebuffer[begin:end]
return pieces
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('source', type=Path, help='Pinned DXVK tree with downstream patches applied')
parser.add_argument('--cxx', default=os.environ.get('CXX', 'c++'))
parser.add_argument('--sanitize', action='store_true', help='Enable ASan/UBSan on the CPU harness')
args = parser.parse_args()
pieces = production(args.source)
harness = (Path(__file__).parent / 'tests/d3d9_single_sample.cpp').read_text()
harness = harness.replace('/* PRODUCTION_USES_IMAGE */', pieces['uses_image'])
with tempfile.TemporaryDirectory(prefix='dxvk-d3d9-') as directory:
out = Path(directory)
(out / 'harness.cpp').write_text(harness)
include = '\n\n'.join(pieces[key] for key in ('decode', 'normalize', 'backbuffers'))
include += '\nHRESULT auto_depth(D3D9DeviceEx* device, D3DPRESENT_PARAMETERS* pPresentationParameters, D3D9_COMMON_TEXTURE_DESC* output) {\n'
include += 'auto* self = device;\n' + pieces['auto_depth'].replace('(this,', '(self,')
include += '\n*output = desc; return D3D_OK;\n}\n'
include += '\nbool depth_binds(const ImageInfo& color, const ImageInfo& dsInfo) {\n'
include += 'auto sampleCount = color.sampleCount; auto renderArea = color.extent;\n'
include += 'const bool noRT0Bound = false, ignoreRT0 = false;\n'
include += pieces['depth_binding'] + '\nreturn bindDS;\n}\n'
(out / 'd3d9_production.inc').write_text(include)
command = [args.cxx, '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', str(out / 'harness.cpp'), '-o', str(out / 'check')]
# The original CreateBackBuffers does not use Flags; that known baseline
# warning must not turn a negative control into a compiler-fixture error.
if args.sanitize:
command[1:1] = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer']
compilation = subprocess.run(command, capture_output=True, text=True)
if compilation.returncode:
raise SystemExit(compilation.stderr)
results = []
for case in CASES:
process = subprocess.run([str(out / 'check'), case], capture_output=True,
text=True, timeout=10, cwd=out,
env={'ASAN_OPTIONS': 'detect_leaks=' +
('0' if sys.platform == 'darwin' else '1')})
results.append({'case': case, 'passed': process.returncode == 0,
'exit': process.returncode, 'detail': process.stderr.strip()[-800:]})
result = {'cases': results, 'passed': sum(row['passed'] for row in results),
'failed': sum(not row['passed'] for row in results),
'production_sha256': {key: hashlib.sha256(value.encode()).hexdigest()
for key, value in pieces.items()},
'backend_and_adapter_mocked': True, 'GPU_test': False,
'sanitizers': args.sanitize,
'leak_detection': args.sanitize and sys.platform != 'darwin'}
print(json.dumps(result, indent=2))
return bool(result['failed'])
if __name__ == '__main__':
raise SystemExit(main())
@@ -64,3 +64,23 @@ index e69a39a..4478420 100644
/// Allow D3DLOCK_DISCARD
bool allowDiscard;
diff --git a/src/d3d9/d3d9_swapchain.cpp b/src/d3d9/d3d9_swapchain.cpp
--- a/src/d3d9/d3d9_swapchain.cpp
+++ b/src/d3d9/d3d9_swapchain.cpp
@@ -1055,1 +1055,16 @@ namespace dxvk {
+ if (m_parent->GetOptions()->forceSingleSample) {
+ // Backbuffers bypass the normal surface-creation entry points. Validate
+ // the application's request, then apply the same effective sample count.
+ // IsLockable above is an internal GDI fallback requirement, not the
+ // application's lockability request used by MSAA validation.
+ auto requestedDesc = desc;
+ requestedDesc.IsLockable = (Flags & D3DPRESENTFLAG_LOCKABLE_BACKBUFFER) != 0;
+ HRESULT hr = D3D9CommonTexture::NormalizeTextureProperties(
+ m_parent, D3DRTYPE_SURFACE, &requestedDesc);
+ if (FAILED(hr))
+ return hr;
+ desc.MultiSample = requestedDesc.MultiSample;
+ desc.MultisampleQuality = requestedDesc.MultisampleQuality;
+ }
+
const bool isExtended = m_parent->IsD3DCompatibile(D3DCompatibility::D3D9Ex);
+214
View File
@@ -0,0 +1,214 @@
// Backend/adapter fixture for the verbatim DXVK functions in d3d9_production.inc.
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
using HRESULT = int32_t;
using DWORD = uint32_t;
using UINT = uint32_t;
using VkSampleCountFlagBits = uint32_t;
constexpr HRESULT D3D_OK = 0, D3DERR_INVALIDCALL = -1,
D3DERR_NOTAVAILABLE = -2, D3DERR_OUTOFVIDEOMEMORY = -3;
constexpr uint32_t VK_SAMPLE_COUNT_1_BIT = 1, VK_IMAGE_ASPECT_COLOR_BIT = 1;
constexpr bool FALSE = false, TRUE = true;
#define FAILED(x) ((x) < 0)
#define unlikely(x) (x)
enum D3DMULTISAMPLE_TYPE { D3DMULTISAMPLE_NONE, D3DMULTISAMPLE_NONMASKABLE,
D3DMULTISAMPLE_16_SAMPLES = 16 };
enum D3DRESOURCETYPE { D3DRTYPE_SURFACE, D3DRTYPE_CUBETEXTURE, D3DRTYPE_VOLUMETEXTURE };
enum D3DPOOL { D3DPOOL_DEFAULT, D3DPOOL_MANAGED, D3DPOOL_SYSTEMMEM, D3DPOOL_SCRATCH };
enum class D3DCompatibility { D3D9Ex };
enum class D3D9Format { X8R8G8B8, D24S8, A8, NULL_FORMAT, ATI2,
D32_LOCKABLE, D32F_LOCKABLE, D16_LOCKABLE, S8_LOCKABLE };
constexpr DWORD D3DUSAGE_RENDERTARGET = 1, D3DUSAGE_DEPTHSTENCIL = 2,
D3DUSAGE_DYNAMIC = 4, D3DUSAGE_WRITEONLY = 8, D3DUSAGE_AUTOGENMIPMAP = 16;
constexpr DWORD D3DPRESENTFLAG_LOCKABLE_BACKBUFFER = 1, D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL = 2;
constexpr int D3DDEVTYPE_HAL = 1, VK_IMAGE_LAYOUT_UNDEFINED = 0;
struct Extent { uint32_t width, height, depth = 1; };
namespace util {
uint32_t computeMipLevelCount(Extent e) {
uint32_t n = std::max({e.width, e.height, e.depth}), levels = 0;
do { ++levels; n >>= 1; } while (n);
return levels;
}
}
bool IsPoolManaged(D3DPOOL pool) { return pool == D3DPOOL_MANAGED; }
bool IsDXTFormat(D3D9Format) { return false; }
bool IsDepthStencilFormat(D3D9Format format) { return format == D3D9Format::D24S8; }
bool IsLockableDepthStencilFormat(D3D9Format) { return false; }
struct D3D9_FORMAT_BLOCK_SIZE { uint32_t Width, Height; };
D3D9_FORMAT_BLOCK_SIZE GetFormatAlignedBlockSize(D3D9Format) { return {1, 1}; }
D3D9Format EnumerateFormat(D3D9Format format) { return format; }
struct D3D9_COMMON_TEXTURE_DESC {
uint32_t Width = 1280, Height = 720, Depth = 1, MipLevels = 1, ArraySize = 1;
D3D9Format Format = D3D9Format::X8R8G8B8;
D3DMULTISAMPLE_TYPE MultiSample = D3DMULTISAMPLE_NONE;
DWORD MultisampleQuality = 0;
D3DPOOL Pool = D3DPOOL_DEFAULT;
DWORD Usage = D3DUSAGE_RENDERTARGET;
bool Discard = false, IsBackBuffer = false, IsAttachmentOnly = false, IsLockable = false;
};
struct D3DPRESENT_PARAMETERS {
uint32_t BackBufferWidth = 1280, BackBufferHeight = 720;
D3D9Format BackBufferFormat = D3D9Format::X8R8G8B8;
D3DMULTISAMPLE_TYPE MultiSampleType = D3DMULTISAMPLE_TYPE(4);
DWORD MultiSampleQuality = 0, Flags = 0;
D3D9Format AutoDepthStencilFormat = D3D9Format::D24S8;
};
struct Options {
bool forceSingleSample = false, extraFrontbuffer = false,
disableA8RT = false, supportCubeDepthFormats = false;
};
struct FormatMapping {
uint32_t Aspect;
bool IsValid() const { return true; }
};
struct Adapter {
bool supported = true;
std::vector<D3DMULTISAMPLE_TYPE> checked;
HRESULT CheckDeviceMultiSampleType(int, D3D9Format, bool, D3DMULTISAMPLE_TYPE samples, void*) {
checked.push_back(samples);
return supported ? D3D_OK : D3DERR_NOTAVAILABLE;
}
};
struct ImageInfo { Extent extent; VkSampleCountFlagBits sampleCount; };
struct DxvkImage { ImageInfo info; };
template<typename T> using Rc = std::shared_ptr<T>;
template<typename T, unsigned N> using small_vector = std::vector<T>;
struct DxvkContext {
unsigned initialized = 0;
void initImage(const Rc<DxvkImage>&, int) { ++initialized; }
};
struct D3D9DeviceEx {
Options options;
Adapter adapter;
DxvkContext context;
unsigned attempts = 0, failAt = 0, losable = 0, live = 0, injected = 0;
std::vector<D3D9_COMMON_TEXTURE_DESC> created;
Options* GetOptions() { return &options; }
Adapter* GetAdapter() { return &adapter; }
FormatMapping LookupFormat(D3D9Format format) { return {IsDepthStencilFormat(format) ? 2u : 1u}; }
struct Unsupported { unsigned elementSize = 0; };
Unsupported unsupported;
const Unsupported* UnsupportedFormatInfo(D3D9Format) { return &unsupported; }
bool IsD3DCompatibile(D3DCompatibility) { return false; }
void IncrementLosableCounter() { ++losable; }
template<typename F> void InjectCs(F function) { ++injected; function(&context); }
};
HRESULT DecodeMultiSampleType(D3DMULTISAMPLE_TYPE, DWORD, VkSampleCountFlagBits*);
struct D3D9CommonTexture {
Rc<DxvkImage> image;
const Rc<DxvkImage>& GetImage() const { return image; }
static HRESULT NormalizeTextureProperties(D3D9DeviceEx*, D3DRESOURCETYPE, D3D9_COMMON_TEXTURE_DESC*);
/* PRODUCTION_USES_IMAGE */
};
class DxvkError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
std::string message() const { return what(); }
};
struct Logger { static void err(const std::string&) {} };
struct D3D9SwapChainEx;
struct D3D9Surface {
D3D9CommonTexture texture;
D3D9DeviceEx* device;
D3D9Surface(D3D9DeviceEx* d, D3D9_COMMON_TEXTURE_DESC* desc, bool, D3D9SwapChainEx*, void*) : device(d) {
if (++d->attempts == d->failAt) throw DxvkError("controlled allocation failure");
VkSampleCountFlagBits samples;
if (FAILED(DecodeMultiSampleType(desc->MultiSample, desc->MultisampleQuality, &samples)))
throw DxvkError("invalid backend sample count");
texture.image = std::make_shared<DxvkImage>(DxvkImage{{{desc->Width, desc->Height}, samples}});
d->created.push_back(*desc); ++d->live;
}
~D3D9Surface() { --device->live; }
D3D9CommonTexture* GetCommonTexture() { return &texture; }
};
struct D3D9SwapChainEx {
D3D9DeviceEx* m_parent;
D3DPRESENT_PARAMETERS m_presentParams;
std::vector<std::unique_ptr<D3D9Surface>> m_backBuffers;
bool flip = false;
unsigned destroys = 0;
bool SwapWithFrontBuffer() { return flip; }
void DestroyBackBuffers() { ++destroys; m_backBuffers.clear(); }
HRESULT CreateBackBuffers(uint32_t, DWORD);
};
#include "d3d9_production.inc"
void paired(bool enabled, D3DMULTISAMPLE_TYPE type, DWORD quality) {
D3D9DeviceEx device; device.options.forceSingleSample = enabled;
device.options.extraFrontbuffer = true;
D3D9SwapChainEx chain{&device, {}, {}};
chain.m_presentParams.MultiSampleType = type; chain.m_presentParams.MultiSampleQuality = quality;
assert(chain.CreateBackBuffers(2, 0) == D3D_OK);
D3D9_COMMON_TEXTURE_DESC depth;
assert(auto_depth(&device, &chain.m_presentParams, &depth) == D3D_OK);
VkSampleCountFlagBits requested, dsSamples;
assert(DecodeMultiSampleType(type, quality, &requested) == D3D_OK);
assert(DecodeMultiSampleType(depth.MultiSample, depth.MultisampleQuality, &dsSamples) == D3D_OK);
assert(chain.m_backBuffers.size() == 3 && device.context.initialized == 3);
for (const auto& surface : chain.m_backBuffers) {
const auto& image = surface->GetCommonTexture()->GetImage()->info;
assert(image.sampleCount == (enabled ? 1u : requested));
assert(depth_binds(image, {{depth.Width, depth.Height}, dsSamples}));
}
for (const auto& desc : device.created) {
assert(desc.IsLockable && desc.IsBackBuffer && !desc.IsAttachmentOnly);
assert(desc.MultiSample == (enabled ? D3DMULTISAMPLE_NONE : type));
assert(desc.MultisampleQuality == (enabled ? 0u : quality));
}
// Original presentation parameters remain available for reset/auto-depth validation.
assert(chain.m_presentParams.MultiSampleType == type && chain.m_presentParams.MultiSampleQuality == quality);
if (enabled && requested > 1) assert(device.adapter.checked.front() == D3DMULTISAMPLE_TYPE(requested));
}
int main(int argc, char** argv) {
assert(argc == 2); std::string name = argv[1];
if (name == "forced_msaa_depth") paired(true, D3DMULTISAMPLE_TYPE(4), 0);
else if (name == "default_msaa_depth") paired(false, D3DMULTISAMPLE_TYPE(4), 0);
else if (name == "forced_nonmaskable") paired(true, D3DMULTISAMPLE_NONMASKABLE, 2);
else if (name == "default_nonmaskable") paired(false, D3DMULTISAMPLE_NONMASKABLE, 2);
else if (name == "forced_already_single") paired(true, D3DMULTISAMPLE_NONE, 0);
else if (name == "forced_no_depth" || name == "forced_lockable_single") {
D3D9DeviceEx d; d.options.forceSingleSample = true;
D3D9SwapChainEx c{&d, {}, {}};
if (name == "forced_lockable_single") c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_NONE;
assert(c.CreateBackBuffers(1, name == "forced_lockable_single" ? D3DPRESENTFLAG_LOCKABLE_BACKBUFFER : 0) == D3D_OK);
assert(c.m_backBuffers.size() == 1 && d.created[0].MultiSample == D3DMULTISAMPLE_NONE);
assert(d.created[0].IsLockable && d.context.initialized == 1);
} else if (name == "forced_unsupported" || name == "forced_invalid_type" || name == "forced_lockable_msaa") {
D3D9DeviceEx d; d.options.forceSingleSample = true;
D3D9SwapChainEx c{&d, {}, {}};
if (name == "forced_unsupported") d.adapter.supported = false;
if (name == "forced_invalid_type") c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_TYPE(17);
HRESULT expected = name == "forced_unsupported" ? D3DERR_NOTAVAILABLE : D3DERR_INVALIDCALL;
assert(c.CreateBackBuffers(2, name == "forced_lockable_msaa" ? D3DPRESENTFLAG_LOCKABLE_BACKBUFFER : 0) == expected);
assert(d.attempts == 0 && d.losable == 0 && d.injected == 0 && c.m_backBuffers.empty());
} else if (name == "forced_allocation_failure" || name == "default_allocation_failure") {
D3D9DeviceEx d; d.options.forceSingleSample = name == "forced_allocation_failure"; d.failAt = 2;
D3D9SwapChainEx c{&d, {}, {}};
assert(c.CreateBackBuffers(3, 0) == D3DERR_OUTOFVIDEOMEMORY);
assert(d.attempts == 2 && d.losable == 1 && d.live == 0 && d.injected == 0 && c.m_backBuffers.empty());
} else if (name == "forced_recreate") {
D3D9DeviceEx d; d.options.forceSingleSample = true;
D3D9SwapChainEx c{&d, {}, {}};
assert(c.CreateBackBuffers(3, 0) == D3D_OK);
c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_TYPE(8);
assert(c.CreateBackBuffers(1, 0) == D3D_OK);
assert(d.live == 1 && c.m_backBuffers.size() == 1 && c.destroys == 2);
assert(d.adapter.checked.size() == 2 && d.adapter.checked.back() == D3DMULTISAMPLE_TYPE(8));
assert(c.m_backBuffers[0]->GetCommonTexture()->GetImage()->info.sampleCount == 1);
} else if (name == "ordinary_surface" || name == "ordinary_invalid_lockable") {
D3D9DeviceEx d; d.options.forceSingleSample = true;
D3D9_COMMON_TEXTURE_DESC desc; desc.MultiSample = D3DMULTISAMPLE_TYPE(4);
desc.IsLockable = name == "ordinary_invalid_lockable";
assert(D3D9CommonTexture::NormalizeTextureProperties(&d, D3DRTYPE_SURFACE, &desc)
== (desc.IsLockable ? D3DERR_INVALIDCALL : D3D_OK));
if (!desc.IsLockable) assert(desc.MultiSample == D3DMULTISAMPLE_NONE && desc.MultisampleQuality == 0);
} else assert(false);
return 0;
}