diff --git a/tools/dxvk/build.sh b/tools/dxvk/build.sh new file mode 100755 index 000000000..8243ec0fd --- /dev/null +++ b/tools/dxvk/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# CI component build; this does not change the APK or an installed Proton depot. +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +. "$root/tools/dxvk/source.env" +mkdir -p "${1:?output directory required}" +out=$(cd "$1" && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +git init -q "$work/source" +git -C "$work/source" fetch --depth=1 https://github.com/doitsujin/dxvk.git "$DXVK_REVISION" +git -C "$work/source" checkout -q --detach FETCH_HEAD +test "$(git -C "$work/source" rev-parse HEAD)" = "$DXVK_REVISION" +git -C "$work/source" submodule update --init --recursive --depth=1 +for patch in "$root"/tools/dxvk/patches/*.patch; do + git -C "$work/source" apply --check "$patch" + git -C "$work/source" apply "$patch" +done +python3 "$root/tools/dxvk/check_present_sync.py" "$work/source/src/dxvk/dxvk_presenter.cpp" +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 + meson compile -C "$work/build$arch" -j 4 + meson install -C "$work/build$arch" +done +cp "$work/source/LICENSE" "$out/LICENSE" +printf '%s\n' "$DXVK_REVISION" > "$out/source-commit" +git -C "$work/source" submodule status --recursive > "$out/submodules.txt" +git -C "$work/source" diff > "$out/downstream.patch" +tar --exclude-vcs --zstd -cf "$out/source.tar.zst" -C "$work/source" . +(cd "$out" && find x32 x64 -name '*.dll' -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS) diff --git a/tools/dxvk/check_present_sync.py b/tools/dxvk/check_present_sync.py new file mode 100644 index 000000000..ad47a8ade --- /dev/null +++ b/tools/dxvk/check_present_sync.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Replay asynchronous WSI ownership using the pinned presenter's actual selections. + +This checks semaphore selection, not the full Vulkan presenter. Real Vulkan +validation and device testing remain separate gates. Requires a native C++ compiler. +""" +import argparse +import os +from pathlib import Path +import re +import subprocess +import tempfile + + +def selection_code(source): + match = re.search( + r"// Return relevant Vulkan objects for the acquired image\s*" + r"(?P.*?)\s*m_presentPending = true;", source, re.S) + wait = re.search(r"info\.pWaitSemaphores\s*=.*?;", source, re.S) + if not match or not wait: + raise ValueError("unsupported pinned presenter layout") + return match["acquire"], wait[0] + + +HARNESS = r''' +#include +#include +#include +#include +using VkSemaphore = uint64_t; +struct Sync { VkSemaphore acquire, present; }; +struct Info { const VkSemaphore* pWaitSemaphores; }; + +struct Presenter { + std::vector m_semaphores; + std::vector m_images = {0, 1, 2, 3}; + unsigned m_frameIndex = 0, m_imageIndex = 0; + bool m_hasSwapchainMaintenance1; + explicit Presenter(bool maintenance): m_hasSwapchainMaintenance1(maintenance) { + for (unsigned i = 0; i < (maintenance ? 4u : 8u); ++i) + m_semaphores.push_back({100 + i, 1 + i}); + } + Sync acquire() { + Sync sync{}; unsigned image; + ACQUIRE_SELECTION + (void)image; + return sync; + } + VkSemaphore present() { + auto& currSync = m_semaphores.at(m_frameIndex); + Info info{}; + PRESENT_SELECTION + return *info.pWaitSemaphores; + } +}; + +int replay(bool maintenance) { + Presenter p(maintenance); + std::array outstandingByImage{}; + std::array outstandingByFence{}; + // Deliberately delay reacquisition of image 2 beyond a full frame-ring cycle. + const std::array imageOrder = { + 2,0,3,0,1,3,0,1,1,2,3,1,0,3,2,2,0,1,3,0,2 + }; + for (unsigned iteration = 0; iteration < 200; ++iteration) { + p.m_imageIndex = imageOrder[iteration % imageOrder.size()]; + if (maintenance) { + // The existing fence path waits this frame's previous present fence. + const auto completed = outstandingByFence.at(p.m_frameIndex); + for (auto& pending : outstandingByImage) + if (pending == completed) pending = 0; + } + // Acquisition and its GPU wait complete the last present of THIS image. + outstandingByImage.at(p.m_imageIndex) = 0; + const auto signal = p.acquire().present; + const auto wait = p.present(); + if (signal != wait) { + std::fprintf(stderr, "FAIL: render signal differs from present wait\n"); + return 2; + } + for (auto pending : outstandingByImage) { + if (pending == signal) { + std::fprintf(stderr, "FAIL: semaphore reused before presentation completion at frame %u\n", iteration); + return 2; + } + } + outstandingByImage.at(p.m_imageIndex) = signal; + if (maintenance) outstandingByFence.at(p.m_frameIndex) = signal; + p.m_frameIndex = (p.m_frameIndex + 1) % p.m_semaphores.size(); + } + std::printf("PASS: %s, 200 out-of-order acquisitions\n", maintenance ? "present-fence path" : "no-present-fence path"); + return 0; +} +int main() { + if (int status = replay(false)) return status; + return replay(true); +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path, help="pinned dxvk_presenter.cpp, after patching") + args = parser.parse_args() + acquire, present = selection_code(args.source.read_text()) + code = HARNESS.replace("ACQUIRE_SELECTION", acquire).replace("PRESENT_SELECTION", present) + with tempfile.TemporaryDirectory(prefix="dxvk-present-sync-") as directory: + cpp = Path(directory) / "replay.cpp" + exe = Path(directory) / "replay" + cpp.write_text(code) + subprocess.run([os.environ.get("HOST_CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", str(cpp), "-o", str(exe)], check=True) + return subprocess.run([str(exe)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch b/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch new file mode 100644 index 000000000..db327f038 --- /dev/null +++ b/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch @@ -0,0 +1,53 @@ +diff --git a/src/dxvk/dxvk_device_info.cpp b/src/dxvk/dxvk_device_info.cpp +index db09fba..8e9a76e 100644 +--- a/src/dxvk/dxvk_device_info.cpp ++++ b/src/dxvk/dxvk_device_info.cpp +@@ -118,6 +118,17 @@ namespace dxvk { + initQueueProperties(instance, adapter, deviceInfo); + initMemoryProperties(instance, adapter); + ++ m_hasBcFormats = true; ++ for (uint32_t value = VK_FORMAT_BC1_RGBA_UNORM_BLOCK; value <= VK_FORMAT_BC7_SRGB_BLOCK; value++) { ++ VkFormatProperties properties = { }; ++ vk->vkGetPhysicalDeviceFormatProperties(adapter, VkFormat(value), &properties); ++ constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT ++ | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ++ | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT; ++ if ((properties.optimalTilingFeatures & required) != required) ++ m_hasBcFormats = false; ++ } ++ + disableUnusedFeatures(instance, safeMode); + + enableFeaturesAndExtensions(); +@@ -785,6 +796,9 @@ namespace dxvk { + } + } + ++ if (!m_hasBcFormats) ++ return std::string("Device does not support the BC formats required by Direct3D"); ++ + if (!m_featuresEnabled.extDescriptorHeap.descriptorHeap && m_properties.core.properties.limits.maxPushConstantsSize < MaxTotalPushDataSize) + return str::format("Device does not support ", MaxTotalPushDataSize, " of push data"); + +@@ -886,7 +900,7 @@ namespace dxvk { + ENABLE_FEATURE(core.features, shaderResourceResidency, false), + ENABLE_FEATURE(core.features, shaderResourceMinLod, false), + ENABLE_FEATURE(core.features, tessellationShader, false), +- ENABLE_FEATURE(core.features, textureCompressionBC, true), ++ ENABLE_FEATURE(core.features, textureCompressionBC, false), + ENABLE_FEATURE(core.features, variableMultisampleRate, false), + ENABLE_FEATURE(core.features, vertexPipelineStoresAndAtomics, false), + ENABLE_FEATURE(core.features, wideLines, false), +diff --git a/src/dxvk/dxvk_device_info.h b/src/dxvk/dxvk_device_info.h +index 0a55f90..8bbc770 100644 +--- a/src/dxvk/dxvk_device_info.h ++++ b/src/dxvk/dxvk_device_info.h +@@ -394,6 +394,7 @@ namespace dxvk { + + DxvkDeviceQueueMapping m_queueMapping = { }; + ++ bool m_hasBcFormats = false; + bool m_hasMeshShader = false; + bool m_hasFmask = false; + diff --git a/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch b/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch new file mode 100644 index 000000000..bd22bdc52 --- /dev/null +++ b/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch @@ -0,0 +1,26 @@ +diff --git a/src/dxvk/dxvk_presenter.cpp b/src/dxvk/dxvk_presenter.cpp +index 643f7c5..4dc8c4d 100644 +--- a/src/dxvk/dxvk_presenter.cpp ++++ b/src/dxvk/dxvk_presenter.cpp +@@ -161,6 +161,10 @@ namespace dxvk { + + // Return relevant Vulkan objects for the acquired image + sync = m_semaphores.at(m_frameIndex); ++ // Without a present fence, reacquiring this image is the only guarantee ++ // that its previous presentation has consumed the wait semaphore. ++ if (!m_hasSwapchainMaintenance1) ++ sync.present = m_semaphores.at(m_imageIndex).present; + image = m_images.at(m_imageIndex); + + m_presentPending = true; +@@ -239,7 +243,9 @@ namespace dxvk { + + VkPresentInfoKHR info = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + info.waitSemaphoreCount = 1; +- info.pWaitSemaphores = &currSync.present; ++ info.pWaitSemaphores = m_hasSwapchainMaintenance1 ++ ? &currSync.present ++ : &m_semaphores.at(m_imageIndex).present; + info.swapchainCount = 1; + info.pSwapchains = &m_swapchain; + info.pImageIndices = &m_imageIndex; diff --git a/tools/dxvk/source.env b/tools/dxvk/source.env new file mode 100644 index 000000000..7aac170c6 --- /dev/null +++ b/tools/dxvk/source.env @@ -0,0 +1 @@ +DXVK_REVISION=25ca63f17f34bdc05a39873ee63907d3cbbfa030