diff --git a/.github/ISSUE_TEMPLATE/vk_release_checklist.md b/.github/ISSUE_TEMPLATE/vk_release_checklist.md new file mode 100644 index 0000000000..3f2b8d96bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/vk_release_checklist.md @@ -0,0 +1,51 @@ +--- +name: Vulkan SDK Release Checklist +about: Track the DXC-side steps to prepare and hand off a Vulkan SDK release candidate +title: 'Vulkan SDK release checklist: ' +labels: ['spirv'] +assignees: '' + +--- + +Tracking issue for preparing a DXC release candidate for the LunarG Vulkan SDK. + +**Target version:** `` — the SDK major and minor version, e.g. `1.4.360` + +## Dependencies + +- [ ] Get the SPIRV-Headers and SPIRV-Tools commits LunarG is releasing against. +- [ ] Update the `external/SPIRV-Headers` and `external/SPIRV-Tools` submodules to + those commits. + +## Build the candidate + +- [ ] Push a `release/vulkan/` branch (e.g. `release/vulkan/1.4.360`) with the + updated submodules. This triggers the release-candidate pipeline (build + SPIRV + tests + offload tests). + +## Validate + +The candidate is ready only when all of the following hold: + +- [ ] It builds against the pinned SPIRV-Headers / SPIRV-Tools commits. +- [ ] Every SPIRV test harness passes — googletest, lit, and TAEF — and the emitted + SPIRV validates under `spirv-val`. +- [ ] The offload tests pass on both software renderers: SPIRV on lavapipe (Vulkan) + and DXIL on WARP (Direct3D 12). +- [ ] The manifest (`rc-manifest.json`) records the per-suite results and the + lavapipe / WARP driver versions. + +## Release the candidate + +Each handoff to LunarG gets its own release-candidate tag; repeat as needed. + +- [ ] Tag the validated commit with the next release-candidate tag, e.g. + `vulkan-sdk-1.4.360.0rc1` (increment the `rc` number on each handoff: `rc2`, ...). + The `vulkan-sdk-` prefix marks it as a Vulkan SDK version, distinct from DXC's + own release tags. +- [ ] Report that tag to LunarG — by email, or by updating the Khronos GitLab release + issue with most recent release-candidate tag. +- [ ] If LunarG reports a problem, fix it on the release branch, re-validate, and cut + the next release-candidate tag. +- [ ] Once the Vulkan SDK is published, promote the latest release candidate to a + release tag with no `rc` suffix, e.g. `vulkan-sdk-1.4.360.0`. diff --git a/.github/workflows/offload-tests.yml b/.github/workflows/offload-tests.yml new file mode 100644 index 0000000000..4c9034d94a --- /dev/null +++ b/.github/workflows/offload-tests.yml @@ -0,0 +1,189 @@ +name: "Offload tests (software renderers)" + +# Runs the LLVM offload-test-suite against a DXC release candidate on software +# renderers, no physical GPU: +# * Vulkan (check-hlsl-vk) on lavapipe -- Mesa's pure-CPU Vulkan; runs the +# candidate's SPIR-V. +# * DirectX (check-hlsl-warp-d3d12) on WARP -- Microsoft's software D3D12 device; +# runs the candidate's DXIL. +# Reusable: +# * workflow_call -- the Vulkan SDK RC pipeline invokes it after publishing. +# * workflow_dispatch -- run it STANDALONE against an existing candidate artifact, so +# the heavy DXC build doesn't have to be repeated while iterating on the offload +# setup. Give it the run_id of a prior pipeline run and the artifact name. + +on: + workflow_call: + inputs: + artifact_name: + description: "dxc_rc_ artifact name." + type: string + required: true + run_id: + description: "ID of the run that produced the artifact." + type: string + required: true + workflow_dispatch: + inputs: + run_id: + description: "ID of the pipeline run that produced the dxc_rc artifact." + type: string + required: true + artifact_name: + description: "Artifact name from that run (e.g. dxc_rc_main)." + type: string + required: true + default: "dxc_rc_main" + +permissions: + contents: read + actions: read # download-artifact from another run uses the Actions API + +jobs: + offload-tests: + runs-on: windows-latest + steps: + - name: Download the release candidate + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + run-id: ${{ inputs.run_id }} + github-token: ${{ github.token }} + path: rc + + - name: Locate DXC_DIR + id: dxc + shell: pwsh + run: | + $dxc = Get-ChildItem rc -Recurse -Filter dxc.exe | Select-Object -First 1 + if (-not $dxc) { throw "dxc.exe not found in the RC artifact '${{ inputs.artifact_name }}'" } + "dir=$($dxc.Directory.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + Write-Host "DXC_DIR = $($dxc.Directory.FullName)" + + - name: Setup MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Install Ninja + uses: seanmiddleditch/gha-setup-ninja@v6 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # The offload suite's Vulkan backend -- and the check-hlsl-vk target -- is only + # built when find_package(Vulkan) succeeds, so install the Vulkan SDK for the + # headers + loader (vulkan-1.lib/.dll). The runtime ICD is supplied separately. + - name: Install Vulkan SDK (headers + loader) + shell: pwsh + run: | + $exe = "$env:RUNNER_TEMP\VulkanSDK.exe" + Invoke-WebRequest "https://sdk.lunarg.com/sdk/download/latest/windows/vulkan_sdk.exe?Human=true" -OutFile $exe + Start-Process $exe -ArgumentList "--accept-licenses","--default-answer","--confirm-command","install" -Wait + $sdk = (Get-ChildItem "C:\VulkanSDK" -Directory | Sort-Object Name -Descending | Select-Object -First 1).FullName + if (-not $sdk) { throw "Vulkan SDK install dir not found under C:\VulkanSDK" } + "VULKAN_SDK=$sdk" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "$sdk\Bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + Write-Host "VULKAN_SDK = $sdk" + + # Fetch Mesa and register the lavapipe (lvp, pure-CPU Vulkan) ICD in + # HKLM\SOFTWARE\Khronos\Vulkan\Drivers -- the Vulkan loader IGNORES + # VK_DRIVER_FILES / VK_ICD_FILENAMES when the process is elevated (GitHub runners + # run elevated), so the registry is the only discovery path. Use reg.exe (the + # value name is the full ICD path; a path contains ':' and '\', which PowerShell's + # New-ItemProperty -Name mangles). Pick the .x86_64.json (a 32-bit .x86.json would + # load as VK_ERROR_INCOMPATIBLE_DRIVER). The Mesa package also ships dxil.dll, + # which dxc needs to sign DXIL for the WARP/DirectX run below. + - name: Fetch Mesa and register the lavapipe Vulkan ICD + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + # Pin the Mesa release; keep in sync with vulkan-sdk-rc.yml's LAVAPIPE_VERSION, + # which the manifest records as lavapipe_version, so it matches what runs. + $ver = "26.1.2" + gh release download "$ver" -R pal1000/mesa-dist-win --pattern "*release-msvc.7z" -D "$env:RUNNER_TEMP\mesa" + $pkg = (Get-ChildItem "$env:RUNNER_TEMP\mesa" -Filter "*release-msvc.7z" | Select-Object -First 1).FullName + if (-not $pkg) { throw "Mesa $ver release-msvc.7z not found" } + 7z x "$pkg" -o"$env:RUNNER_TEMP\mesa\x" -y | Out-Null + $lvp = (Get-ChildItem "$env:RUNNER_TEMP\mesa\x" -Recurse -Filter "lvp_icd.x86_64.json" | Select-Object -First 1).FullName + if (-not $lvp) { throw "lavapipe ICD (lvp_icd.x86_64.json) not found in Mesa package" } + reg add "HKLM\SOFTWARE\Khronos\Vulkan\Drivers" /v "$lvp" /t REG_DWORD /d 0 /f /reg:64 + reg query "HKLM\SOFTWARE\Khronos\Vulkan\Drivers" /reg:64 + Write-Host "Registered lavapipe ICD: $lvp" + + - name: Checkout offload-test-suite + uses: actions/checkout@v4 + with: + repository: llvm/offload-test-suite + path: OffloadTest + fetch-depth: 1 + + - name: Checkout LLVM + uses: actions/checkout@v4 + with: + repository: llvm/llvm-project + path: llvm-project + fetch-depth: 1 + - name: Checkout golden images + uses: actions/checkout@v4 + with: + repository: llvm/offload-golden-images + path: golden-images + fetch-depth: 1 + - name: Install OffloadTest python deps + run: python -m pip install -r OffloadTest/test/requirements.txt + + # WARP_VERSION pins the WARP build the Direct3D 12 tests run against. Keep it in + # sync with vulkan-sdk-rc.yml's WARP_VERSION, which the manifest records as + # warp_version, so the manifest matches what actually ran. + - name: Configure LLVM + OffloadTest against the RC's DXC + run: >- + cmake -G Ninja -B llvm-project/build + -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=On + -DLLVM_EXTERNAL_PROJECTS=OffloadTest + -C llvm-project/clang/cmake/caches/HLSL.cmake + -DDXC_DIR="${{ steps.dxc.outputs.dir }}" + -DLLVM_EXTERNAL_OFFLOADTEST_SOURCE_DIR=${{ github.workspace }}/OffloadTest + -DGOLDENIMAGE_DIR=${{ github.workspace }}/golden-images + -DOFFLOADTEST_TEST_CLANG=Off + -DWARP_VERSION=1.0.19 + -DLLVM_LIT_ARGS="--xunit-xml-output=testresults.xunit.xml -v" + llvm-project/llvm + + - name: Build offload test dependencies + run: cmake --build llvm-project/build --target hlsl-test-depends + + # Vulkan path: the candidate's SPIR-V on lavapipe (Mesa's pure-CPU Vulkan, the + # only registered Vulkan ICD). Prepend the Mesa folder to PATH so vulkan_lvp.dll + # resolves its dependencies. VK_LOADER_DEBUG is honored even when elevated, so + # keep it for diagnostics. + - name: Run Vulkan offload tests on lavapipe + shell: pwsh + run: | + $bin = (Get-ChildItem "$env:RUNNER_TEMP\mesa\x" -Recurse -Filter "lvp_icd.x86_64.json" | Select-Object -First 1).Directory.FullName + if (-not $bin) { throw "lavapipe ICD (lvp_icd.x86_64.json) not found in Mesa package" } + $env:PATH = "$bin;$env:PATH" + $env:VK_LOADER_DEBUG = "all" + $env:LIT_OPTS = "--xunit-xml-output=$env:GITHUB_WORKSPACE\results-vk-lavapipe.xml" + cmake --build llvm-project/build --target check-hlsl-vk + + # DirectX path: the candidate's DXIL on WARP (Microsoft's software D3D12 device, + # downloaded by the suite's setup_warp at configure time). dxc needs dxil.dll to + # sign DXIL; the Mesa folder (on PATH) ships it. + - name: Run DirectX offload tests on WARP + if: always() # run even if the Vulkan pass failed + shell: pwsh + run: | + $bin = (Get-ChildItem "$env:RUNNER_TEMP\mesa\x" -Recurse -Filter "lvp_icd.x86_64.json" | Select-Object -First 1).Directory.FullName + $env:PATH = "$bin;$env:PATH" + $env:LIT_OPTS = "--xunit-xml-output=$env:GITHUB_WORKSPACE\results-d3d12-warp.xml" + cmake --build llvm-project/build --target check-hlsl-warp-d3d12 + + - name: Upload offload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: offload-results-${{ inputs.artifact_name }} + path: ${{ github.workspace }}/results-*.xml + if-no-files-found: warn diff --git a/.github/workflows/spec.md b/.github/workflows/spec.md new file mode 100644 index 0000000000..a894df24e6 --- /dev/null +++ b/.github/workflows/spec.md @@ -0,0 +1,195 @@ +--- +title: "INF-0007 - DXC Vulkan SDK Release Strategy" +params: + authors: + - damyanp: Damyan Pepper + - joaosaffran: João Saffran + sponsors: + - damyanp: Damyan Pepper + - joaosaffran: João Saffran + status: Under Consideration +--- + +* Impacted Projects: DXC + +## Introduction + +DXC is included in the Vulkan SDK. Before each SDK release, the DXC submodule +references (SPIRV-Headers, SPIRV-Tools) need to be updated and the product needs +to be tested. This process has previously been mostly performed manually. This +document details the requirements for ensuring DXC is ready for inclusion in the +Vulkan SDK and proposes the changes required in order to satisfy them. + +## Motivation + +SPIRV-Headers and SPIRV-Tools need to be kept up to date so that the most recent +SPIRV features are available in DXC. We need to verify that DXC is generating +valid SPIRV code and that there are no regressions. The process needs to be +documented and automated enough so that it does not rely on individuals with +special knowledge. Additionally, we want to align the version included in the +Vulkan SDK with a formal DXC release so that it matches up with GitHub and NuGet +releases and can be ingested into Godbolt. + +## Proposed solution + +The SPIRV-Headers and SPIRV-Tools submodules are the single source of truth for the +SPIRV revisions a candidate is built against. Once a week a workflow bumps them to the +latest upstream commit and opens a pull request to merge that update into `main`. The +pull request runs the release-candidate pipeline, building DXC against +the updated submodules and running the SPIRV tests. + +For an SDK release, the pipeline also publishes the candidate as an artifact and runs +the LLVM [offload-test-suite](https://github.com/llvm/offload-test-suite) against it, +using lavapipe as the SPIRV driver and WARP as the DXIL driver. DXC from the Vulkan SDK +is also used to compile shader targeting DirectX, so its DXIL output is worth testing too. +Each validated candidate handed to LunarG is tagged as a release candidate (its +results recorded in the manifest); the latest is promoted to a release once the Vulkan +SDK ships. + +The automation is split into three workflows, each shown below starting from its triggers: + +**1. Weekly submodule update** — keeps the SPIRV dependencies updated. + +```mermaid +flowchart TD + cron(["Trigger: weekly schedule"]) --> bump + md1(["Trigger: manual dispatch"]) --> bump + bump["Bump SPIRV-Headers / SPIRV-Tools
submodules to latest upstream"] --> changed{"Anything changed?"} + changed -->|no| stop(["Done — already current"]) + changed -->|yes| pr["Open a vk-update PR"] + pr -. "branch push starts" .-> rc(["Release-candidate pipeline"]) +``` + +**2. Release-candidate pipeline** — builds and tests DXC; for a release candidate it +also publishes the artifact and runs the offload tests. + +```mermaid +flowchart TD + push(["Trigger: push to a
vk-update or release/vulkan branch"]) --> build + md2(["Trigger: manual dispatch"]) --> build + build["Build DXC against the
submodule pointers, plus spirv-val"] --> test + test["Run every SPIRV test:
googletest, lit, TAEF, spirv-val"] --> gate{"Release candidate?
release/vulkan branch
or manual opt-in"} + gate -->|no| prchecks(["Validation only — checks surface
on the vk-update PR, merge when green"]) + gate -->|yes| publish["Publish dxc_rc artifact + manifest"] + publish -. "consumed by" .-> offload(["Offload tests"]) +``` + +**3. Offload tests** — runs the offload-test-suite against the published candidate on +software renderers, split by API. + +```mermaid +flowchart TD + called(["Trigger: called by the pipeline"]) --> get + md3(["Trigger: manual dispatch —
run_id + artifact name"]) --> get + get["Download the dxc_rc artifact"] --> conf["Build offload-test-suite against
the candidate's DXC — DXC only"] + conf --> vk["Vulkan: check-hlsl-vk on lavapipe
runs the candidate's SPIRV"] + conf --> d3d["Direct3D 12: check-hlsl-warp-d3d12 on WARP
runs the candidate's DXIL"] +``` + +### Dependency update + +A scheduled job runs once a week (and can be triggered on demand). Such advances the +SPIRV-Headers and SPIRV-Tools submodules to their latest upstream commit and, if +anything changed, commits the new pointers onto a fresh `vk-update/` branch and +opens a pull request against the default branch. Pushing that branch starts the +release-candidate pipeline, whose checks attach to the commit and show on the pull +request, so the bump is reviewed and merged only once the candidate is green. + +### Release Pipeline + +The release pipeline runs on every push to a `vk-update/` branch (the weekly job +creates these) or a `release/vulkan/` branch (cut by hand for a release; +see [Release Steps](#Release Steps)), and can be started manually from the GitHub UI. +The build and test stages run on every push; the publish and offload stages run only +for a release candidate — a `release/vulkan/` branch, or a manual run that +opts in. + +1. **Build.** DXC is checked out with its submodules, so it builds against exactly + the SPIRV the pointers name, configured with SPIRV code generation and the SPIRV + tests enabled. The build also produces the tools the tests need, including + `spirv-val`. + +2. **Test.** Googletest unit tests, the lit CodeGenSPIRV tests, and the TAEF tests are + all run in this stage; the binary's output is then checked with `spirv-val`. Each + result is recorded in the manifest, including failed tests. The logs also contain + further information where each file is located and what was exected. This stage is + non-blocking — a release candidate is published even if some tests fail. + +3. **Publish.** The DXC binaries (`dxc`, `dxv`, and `dxcompiler.dll`), the manifest, + and the test reports are uploaded as a single artifact named `dxc_rc_`. + +4. **Offload tests.** A separate job runs the LLVM offload-test-suite against the + published artifact, with DXC as the only compiler (the in-tree clang compiler is + disabled). The Vulkan tests (`check-hlsl-vk`) on lavapipe and the Direct3D 12 + tests (`check-hlsl-warp-d3d12`) on WARP (developers still use the dxc which ships + in the Vulkan SDK to compile dx shaders). Because it consumes the published artifact + rather than the build tree, this job can also be run on its own against any earlier candidate. + +### Release manifest + +The manifest records the DXC commit, the SPIRV-Headers and SPIRV-Tools commits the +candidate was built against and the tests results: + +```json +{ + "dxc_commit": "", + "lavapipe_version": "", + "warp_version": "", + "spirv_dependencies": { + "SPIRV-Headers": "", + "SPIRV-Tools": "" + }, + "test_suites": [ + { "name": "spirv-unit", "passed": 105, "failed": 0 }, + { "name": "spirv-codegen", "passed": 1564, "failed": 0 }, + { "name": "spirv-taef", "passed": 1, "failed": 0 }, + { "name": "spirv-val", "passed": 6, "failed": 0 } + ] +} +``` + +### Release tagging + +A release is prepared on a branch and shipped to LunarG as a series of release +candidate tags. The branch carries the Vulkan SDK's major and minor version; each tag +records the full Vulkan SDK version and a release-candidate number, and is prefixed +`vulkan-sdk-` so it reads as a Vulkan SDK version rather than being confused with DXC's +own release tags (`v..YYMM.`). This matches the `vulkan-sdk-` +tags SPIRV-Tools already uses for the SDK. For SDK version `1.4.360`, for example, the +release branch is `release/vulkan/1.4.360`, the candidates sent to LunarG are tagged +`vulkan-sdk-1.4.360.0rc1`, `vulkan-sdk-1.4.360.0rc2`, and so on, and once the Vulkan +SDK is published the latest candidate is promoted to a release tag with no `rc` suffix: +`vulkan-sdk-1.4.360.0`. + +### Release Steps + +These steps are performed by whoever is currently responsible for monitoring the +llvm-build, and may be repeated as needed: + +1. File an issue in DXC repository using the `vk_release_checklist.md` issue template, + assign this to yourself. This should be handed off at the end of rotation. +2. Update the SPIRV-Headers and SPIRV-Tools submodules to the commits specified by + LunarG. +3. Create the `release/vulkan/` branch — named for the SDK major and minor + version, e.g. `release/vulkan/1.4.360` — which triggers the pipeline. +4. Check whether the resulting candidate is validated (see + [Release Candidate readiness](#release-candidate-readiness)). +5. Tag the validated commit with the next release-candidate tag (e.g. + `vulkan-sdk-1.4.360.0rc1`) and report that tag to LunarG — by email, or by updating + the Khronos GitLab release issue with the validated commit. +6. If LunarG reports a problem, fix it on the release branch and re-validate, then cut + the next release-candidate tag (`...rc2`, and so on) and report it. +7. Once the Vulkan SDK is published, promote the latest release-candidate tag to a + release tag with no `rc` suffix, e.g. `vulkan-sdk-1.4.360.0`. + +### Release Candidate readiness + +The following must be true and validated for a release candidate to be considered +ready for the Vulkan SDK. + +* It builds against the SPIRV-Headers and SPIRV-Tools commits the submodules are + pinned to. +* Every SPIRV test harness passes — the googletest, lit, and TAEF tests — and the + SPIRV the binary emits validates under `spirv-val`. +* The shaders the candidate compiles execute on the offload-test-suite under both + software renderers: the SPIRV on lavapipe (Vulkan) and the DXIL on WARP (Direct3D 12). diff --git a/.github/workflows/vulkan-sdk-rc.yml b/.github/workflows/vulkan-sdk-rc.yml new file mode 100644 index 0000000000..89f26ade6c --- /dev/null +++ b/.github/workflows/vulkan-sdk-rc.yml @@ -0,0 +1,159 @@ +name: "Vulkan SDK release candidate" + +# Prototype for INF-0007: prepare + validate DXC for a Vulkan SDK release. +# +# Builds DXC against the checked-out SPIRV-Headers / SPIRV-Tools submodule pointers +# (the source of truth for which SPIR-V ships) and runs every SPIR-V test +# (lit / TAEF / googletest) + spirv-val. Triggers: +# * push to vk-update/** or release/vulkan/** -- the weekly-spirv-update workflow +# opens vk-update PRs; release/vulkan branches are cut by hand for a release. +# * workflow_dispatch -- manual run for any ref. +# +# The dxc_rc_ artifact and the offload-test-suite run (lavapipe + WARP) are +# the release-candidate deliverable -- produced only on release/vulkan/** pushes or +# a manual opt-in (publish_artifact), not on the weekly vk-update validation runs. + +on: + push: + branches: + - "release/vulkan/**" + - "vk-update/**" + workflow_dispatch: + inputs: + publish_artifact: + description: "Upload the dxc_rc artifact (and run the offload tests) for this build." + type: boolean + default: true + +permissions: + contents: read + +jobs: + build-and-validate: + runs-on: windows-latest + outputs: + version: ${{ steps.version.outputs.version }} + publish: ${{ steps.publish_gate.outputs.publish }} + env: + # TAEF test framework, restored from nuget (see the "Install TAEF" step). + # Not on nuget.org -- served from the public feed microsoft/terminal uses. + TAEF_VERSION: "10.100.251104001" + TAEF_FEED: "https://pkgs.dev.azure.com/shine-oss/terminal/_packaging/TerminalDependencies%40Local/nuget/v3/index.json" + # Software drivers the offload tests run against; recorded in the manifest and + # pinned in offload-tests.yml so the manifest matches what actually runs. lavapipe + # is a Mesa release (pal1000/mesa-dist-win tag); WARP is the offload suite's pin. + LAVAPIPE_VERSION: "26.1.2" + WARP_VERSION: "1.0.19" + steps: + - name: Checkout DXC + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # The SPIRV-Headers / SPIRV-Tools submodule pointers are the source of + # truth for the SPIR-V deps, so check them out as committed. + submodules: true + + # The SDK version is the trailing segment of the release/vulkan/ + # branch; it names the published artifact (dxc_rc_). vk-update and + # other refs fall back to the sanitized ref name. + - name: Determine SDK version + id: version + shell: bash + run: | + ref="${{ github.ref_name }}" + version="${ref#release/vulkan/}" # strip the branch prefix if present + version="${version//\//-}" # sanitize any remaining slashes + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "SDK version: $version" + + # The artifact + offload run are the release-candidate deliverable: produced on + # release/vulkan/** pushes, or on a manual run that opts in. Weekly vk-update + # validation runs build + test only. + - name: Decide whether to publish the candidate + id: publish_gate + shell: bash + run: | + publish=false + if [[ "${{ github.ref }}" == refs/heads/release/vulkan/* ]]; then + publish=true + elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.publish_artifact }}" == "true" ]]; then + publish=true + fi + echo "publish=$publish" >> "$GITHUB_OUTPUT" + echo "publish candidate (artifact + offload): $publish" + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Ninja + uses: seanmiddleditch/gha-setup-ninja@v6 + + # Puts cl.exe / link.exe and the MSVC headers on PATH so the Ninja + # generator can find the compiler. Required for every build/test step below. + - name: Setup MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + # DXC's Windows test targets do find_package(TAEF REQUIRED), and TAEF isn't + # on the runner. Restore the Microsoft.Taef nuget package (nuget.exe is + # preinstalled on windows runners) and export the include dir + TE.exe so + # build_and_test.py can pre-seed FindTAEF.cmake. We discover the paths rather + # than hardcode them because the binaries folder name varies between TAEF + # versions (e.g. build/Binaries/x64 vs build/Binaries/amd64). + - name: Install TAEF (test framework) + shell: pwsh + run: | + nuget install Microsoft.Taef -Version $env:TAEF_VERSION -Source $env:TAEF_FEED -OutputDirectory "$env:RUNNER_TEMP\taef" -ExcludeVersion + $root = "$env:RUNNER_TEMP\taef\Microsoft.Taef" + $te = Get-ChildItem $root -Recurse -Filter TE.exe | Where-Object { $_.FullName -match '\\(x64|amd64)\\' } | Select-Object -First 1 + $inc = Get-ChildItem $root -Recurse -Filter Wex.Common.h | Select-Object -First 1 + if (-not $te -or -not $inc) { throw "Could not locate TE.exe / Wex.Common.h in restored TAEF package" } + "TAEF_EXECUTABLE=$($te.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "TAEF_INCLUDE_DIR=$($inc.Directory.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "TAEF $env:TAEF_VERSION: TE.exe=$($te.FullName) include=$($inc.Directory.FullName)" + + # --allow-test-failures: record failures in the manifest but still exit 0, + # so a test regression doesn't block publishing the candidate. Gate on the + # manifest's "validated" field downstream instead. + - name: Build DXC and validate SPIR-V output + run: >- + python utils/vulkan-sdk/build_and_test.py --build-dir build --allow-test-failures + --lavapipe-version "${{ env.LAVAPIPE_VERSION }}" + --warp-version "${{ env.WARP_VERSION }}" + + - name: Upload release candidate + # Only on the deliverable path (release branch / manual opt-in). always() so + # a problem in an earlier step doesn't skip the upload; a genuine build + # failure leaves no dxc.exe, so if-no-files-found: warn keeps that from + # masking the real error. + if: ${{ always() && steps.publish_gate.outputs.publish == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: dxc_rc_${{ steps.version.outputs.version }} + # The DXC tools the offload-tests job needs as DXC_DIR (dxc.exe needs + # dxcompiler.dll to run; DXC_DIR holds dxc + dxv), the manifest, and the + # per-suite JUnit reports. + path: | + build/bin/dxc.exe + build/bin/dxv.exe + build/bin/dxcompiler.dll + build/rc-manifest.json + build/spirv-*-results.xml + if-no-files-found: warn + + # Run the offload-test-suite against the candidate on software renderers: Vulkan on + # lavapipe, DirectX on WARP. Factored into a reusable workflow so it can also be run + # standalone against an existing artifact (offload-tests.yml, workflow_dispatch) + # without rebuilding DXC. Only on the deliverable path (release branch / manual opt-in). + offload-tests: + needs: build-and-validate + if: ${{ needs.build-and-validate.outputs.publish == 'true' }} + permissions: + contents: read + actions: read # the reusable workflow downloads the artifact via the Actions API + uses: ./.github/workflows/offload-tests.yml + with: + artifact_name: dxc_rc_${{ needs.build-and-validate.outputs.version }} + run_id: ${{ github.run_id }} diff --git a/.github/workflows/weekly-spirv-update.yml b/.github/workflows/weekly-spirv-update.yml new file mode 100644 index 0000000000..8e39b08ec0 --- /dev/null +++ b/.github/workflows/weekly-spirv-update.yml @@ -0,0 +1,68 @@ +name: "Weekly SPIR-V submodule update" + +# Keeps DXC's SPIR-V dependencies current. Once a week (or on demand) it bumps the +# SPIRV-Headers / SPIRV-Tools submodules to their latest upstream commit and, if +# anything changed, opens a PR on a vk-update/ branch. The submodule pointers +# are the source of truth for which SPIR-V DXC ships. +# +# Pushing that branch triggers the "Vulkan SDK release candidate" pipeline +# (vulkan-sdk-rc.yml), which builds + tests DXC against the new SPIR-V; its checks +# land on the PR, so the bump is merged only when green. This workflow only produces +# the commit + PR; the RC pipeline does the build/test. + +on: + schedule: + - cron: "0 6 * * 1" # Mondays 06:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + bump: + runs-on: ubuntu-latest + steps: + - name: Checkout DXC + uses: actions/checkout@v4 + with: + submodules: true + # Push with a PAT, not GITHUB_TOKEN: pushes made with GITHUB_TOKEN do not + # trigger other workflows, and we need the vk-update branch to trigger the + # RC pipeline. WEEKLY_BUMP_TOKEN needs contents:write + pull-requests:write. + token: ${{ secrets.WEEKLY_BUMP_TOKEN }} + + - name: Bump SPIR-V submodules to latest upstream + id: bump + run: | + git submodule update --remote external/SPIRV-Headers external/SPIRV-Tools + if git diff --quiet -- external/SPIRV-Headers external/SPIRV-Tools; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "SPIR-V submodules already at latest upstream; nothing to do." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "SPIRV-Headers -> $(git -C external/SPIRV-Headers rev-parse HEAD)" + echo "SPIRV-Tools -> $(git -C external/SPIRV-Tools rev-parse HEAD)" + fi + + # Commit the bumped pointers onto a fresh vk-update branch and open a PR against + # the default branch. Pushing the branch triggers vulkan-sdk-rc.yml; its checks + # attach to this commit and show on the PR. + - name: Open PR with the bump + if: steps.bump.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.WEEKLY_BUMP_TOKEN }} + run: | + date="$(date -u +%Y%m%d)" + branch="vk-update/$date" + git config user.name "dxc-rc-bot" + git config user.email "dxc-rc-bot@users.noreply.github.com" + git checkout -b "$branch" + git add external/SPIRV-Headers external/SPIRV-Tools + git commit -m "Weekly SPIR-V submodule bump ($date)" + git push origin "$branch" + gh pr create \ + --base "${{ github.event.repository.default_branch }}" \ + --head "$branch" \ + --title "Weekly SPIR-V submodule bump ($date)" \ + --body "Automated weekly bump of SPIRV-Headers and SPIRV-Tools to the latest upstream commit. The release-candidate pipeline (build + SPIR-V tests) runs on this branch and reports here; merge once the candidate is green." \ + || echo "PR for $branch may already exist; skipping create." diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b0e1cbeaf2..51db6b680b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -177,3 +177,176 @@ stages: testResultsFormat: 'JUnit' testResultsFiles: '**/testresults.xunit.xml' condition: succeededOrFailed() + +- stage: VulkanSDKReleaseCandidate + displayName: 'Vulkan SDK release candidate' + dependsOn: [] # independent of Build; runs in parallel + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/release/vulkan/') + variables: + HLSL_SRC_DIR: '$(Build.SourcesDirectory)' + HLSL_BLD_DIR: '$(Agent.BuildDirectory)' + platform: x64 + # Software drivers the offload tests run against (recorded against the candidate). + LAVAPIPE_VERSION: '26.1.2' # pal1000/mesa-dist-win release tag + WARP_VERSION: '1.0.19' # offload-test-suite WARP nuget (LKG) + + jobs: + - job: Build + displayName: 'Build DXC for SPIR-V' + timeoutInMinutes: 120 + pool: + vmImage: windows-2022 + strategy: + matrix: + Release: + configuration: Release + Debug: + configuration: Debug + steps: + - checkout: self + clean: true + submodules: true + - script: | + call utils\hct\hctstart.cmd %HLSL_SRC_DIR% %HLSL_BLD_DIR% + call utils\hct\hctbuild.cmd -vs2022 -$(platform) -$(configuration) -spirv -spirvtest -show-cmake-log + displayName: 'Building (SPIR-V)' + - script: | + call utils\hct\hctstart.cmd %HLSL_SRC_DIR% %HLSL_BLD_DIR% + call utils\hct\hcttest.cmd -$(configuration) spirv + displayName: 'SPIR-V Tests' + condition: succeededOrFailed() + # Publish the Release binaries (DXC_DIR) for the offload job. dxil.dll is needed + # to sign DXIL for the WARP/Direct3D 12 run. + - pwsh: | + $bin = "$(HLSL_BLD_DIR)\$(configuration)\bin" + $dst = "$(Build.ArtifactStagingDirectory)\dxc" + New-Item -ItemType Directory -Force -Path $dst | Out-Null + foreach ($f in @("dxc.exe","dxv.exe","dxcompiler.dll","dxil.dll")) { + if (Test-Path "$bin\$f") { Copy-Item "$bin\$f" $dst } else { Write-Warning "missing $f" } + } + displayName: 'Stage DXC binaries' + condition: and(succeeded(), eq(variables['configuration'], 'Release')) + - task: PublishPipelineArtifact@1 + displayName: 'Publish DXC binaries' + condition: and(succeeded(), eq(variables['configuration'], 'Release')) + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)\dxc' + artifact: 'dxc_vulkan' + + - job: OffloadTests + displayName: 'Offload tests (lavapipe + WARP)' + dependsOn: Build + timeoutInMinutes: 180 + pool: + vmImage: windows-2022 + variables: + # vcvars persists the MSVC env to later steps so cmake/ninja find cl.exe. + VS_DEV: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat' + workspace: + clean: all + steps: + - checkout: none + + - task: DownloadPipelineArtifact@2 + displayName: 'Download DXC binaries' + inputs: + artifact: 'dxc_vulkan' + path: '$(Agent.TempDirectory)\dxc' + + - pwsh: | + $dxc = Get-ChildItem "$(Agent.TempDirectory)\dxc" -Recurse -Filter dxc.exe | Select-Object -First 1 + if (-not $dxc) { throw "dxc.exe not found in the published binaries" } + Write-Host "##vso[task.setvariable variable=DXC_DIR]$($dxc.Directory.FullName)" + Write-Host "DXC_DIR = $($dxc.Directory.FullName)" + displayName: 'Locate DXC_DIR' + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.11' + - script: choco install ninja -y + displayName: 'Install Ninja' + - task: BatchScript@1 + displayName: 'Set up MSVC environment' + inputs: + filename: '$(VS_DEV)' + modifyEnvironment: true + + # The offload suite's Vulkan backend (check-hlsl-vk) is only built when + # find_package(Vulkan) succeeds, so install the Vulkan SDK (headers + loader). + - pwsh: | + $exe = "$(Agent.TempDirectory)\VulkanSDK.exe" + Invoke-WebRequest "https://sdk.lunarg.com/sdk/download/latest/windows/vulkan_sdk.exe?Human=true" -OutFile $exe + Start-Process $exe -ArgumentList "--accept-licenses","--default-answer","--confirm-command","install" -Wait + $sdk = (Get-ChildItem "C:\VulkanSDK" -Directory | Sort-Object Name -Descending | Select-Object -First 1).FullName + if (-not $sdk) { throw "Vulkan SDK install dir not found under C:\VulkanSDK" } + Write-Host "##vso[task.setvariable variable=VULKAN_SDK]$sdk" + Write-Host "##vso[task.prependpath]$sdk\Bin" + Write-Host "VULKAN_SDK = $sdk" + displayName: 'Install Vulkan SDK' + + # lavapipe (Mesa's CPU Vulkan) registered in the registry -- the Vulkan loader + # ignores VK_DRIVER_FILES when the process is elevated, so the registry is the + # only discovery path. The Mesa folder also ships dxil.dll (DXIL signing). + - pwsh: | + $ver = "$(LAVAPIPE_VERSION)" + $url = "https://github.com/pal1000/mesa-dist-win/releases/download/$ver/mesa3d-$ver-release-msvc.7z" + Invoke-WebRequest $url -OutFile "$(Agent.TempDirectory)\mesa.7z" + 7z x "$(Agent.TempDirectory)\mesa.7z" -o"$(Agent.TempDirectory)\mesa" -y | Out-Null + $lvp = (Get-ChildItem "$(Agent.TempDirectory)\mesa" -Recurse -Filter "lvp_icd.x86_64.json" | Select-Object -First 1).FullName + if (-not $lvp) { throw "lavapipe ICD (lvp_icd.x86_64.json) not found in Mesa package" } + reg add "HKLM\SOFTWARE\Khronos\Vulkan\Drivers" /v "$lvp" /t REG_DWORD /d 0 /f /reg:64 + Write-Host "##vso[task.setvariable variable=MESA_BIN]$(Split-Path $lvp)" + Write-Host "Registered lavapipe ICD: $lvp" + displayName: 'Install lavapipe (Mesa) and register the ICD' + + - pwsh: | + git clone --depth 1 https://github.com/llvm/offload-test-suite OffloadTest + git clone --depth 1 https://github.com/llvm/llvm-project llvm-project + git clone --depth 1 https://github.com/llvm/offload-golden-images golden-images + python -m pip install -r OffloadTest/test/requirements.txt + displayName: 'Checkout offload-test-suite + LLVM' + workingDirectory: '$(Agent.TempDirectory)' + + - script: > + cmake -G Ninja -B llvm-project/build + -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=On + -DLLVM_EXTERNAL_PROJECTS=OffloadTest + -C llvm-project/clang/cmake/caches/HLSL.cmake + -DDXC_DIR="$(DXC_DIR)" + -DLLVM_EXTERNAL_OFFLOADTEST_SOURCE_DIR=$(Agent.TempDirectory)/OffloadTest + -DGOLDENIMAGE_DIR=$(Agent.TempDirectory)/golden-images + -DOFFLOADTEST_TEST_CLANG=Off + -DWARP_VERSION=$(WARP_VERSION) + -DLLVM_LIT_ARGS="--xunit-xml-output=testresults.xunit.xml -v" + llvm-project/llvm + displayName: 'Configure LLVM + OffloadTest against DXC' + workingDirectory: '$(Agent.TempDirectory)' + + - script: cmake --build llvm-project/build --target hlsl-test-depends + displayName: 'Build offload test dependencies' + workingDirectory: '$(Agent.TempDirectory)' + + # Vulkan path: candidate's SPIR-V on lavapipe. PATH gets the Mesa folder so the + # ICD DLLs resolve their dependencies; VK_LOADER_DEBUG is kept for diagnostics. + - pwsh: | + $env:PATH = "$(MESA_BIN);$env:PATH" + $env:VK_LOADER_DEBUG = "all" + $env:LIT_OPTS = "--xunit-xml-output=$(Agent.TempDirectory)/results-vk-lavapipe.xml" + cmake --build llvm-project/build --target check-hlsl-vk + displayName: 'Vulkan offload tests on lavapipe' + workingDirectory: '$(Agent.TempDirectory)' + + # Direct3D 12 path: candidate's DXIL on WARP (downloaded by the suite's setup_warp). + - pwsh: | + $env:PATH = "$(MESA_BIN);$env:PATH" + $env:LIT_OPTS = "--xunit-xml-output=$(Agent.TempDirectory)/results-d3d12-warp.xml" + cmake --build llvm-project/build --target check-hlsl-warp-d3d12 + displayName: 'DirectX offload tests on WARP' + condition: succeededOrFailed() + workingDirectory: '$(Agent.TempDirectory)' + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(Agent.TempDirectory)/results-*.xml' diff --git a/external/SPIRV-Headers b/external/SPIRV-Headers index ad9184e76a..daa093dd29 160000 --- a/external/SPIRV-Headers +++ b/external/SPIRV-Headers @@ -1 +1 @@ -Subproject commit ad9184e76a66b1001c29db9b0a3e87f646c64de0 +Subproject commit daa093dd29aab8cbb6562b808370562f56e399fb diff --git a/external/SPIRV-Tools b/external/SPIRV-Tools index 0539c81f69..f25a3b067e 160000 --- a/external/SPIRV-Tools +++ b/external/SPIRV-Tools @@ -1 +1 @@ -Subproject commit 0539c81f69a3daeb706fd3477dca61435b475156 +Subproject commit f25a3b067eed21f6ce43186942eec3c407cade02 diff --git a/utils/vulkan-sdk/README.md b/utils/vulkan-sdk/README.md new file mode 100644 index 0000000000..3bc2ce6a6e --- /dev/null +++ b/utils/vulkan-sdk/README.md @@ -0,0 +1,109 @@ +# DXC Vulkan SDK release-candidate pipeline (prototype) + +A prototype for [hlsl-specs#878](https://github.com/microsoft/hlsl-specs/issues/878) +/ INF-0007. It automates the DXC-owned slice of the LunarG Vulkan SDK release +checklist: keeping DXC's SPIR-V dependencies current, building DXC for SPIR-V, +validating its output, and producing a candidate for downstream testing. + +## Source of truth + +The `external/SPIRV-Headers` and `external/SPIRV-Tools` **submodule pointers** are +the single source of truth for which SPIR-V DXC is built against. There is no +separate pin file — whatever the submodules are committed at is what gets built, +tested, and shipped. + +## The pipeline + +Everything lives in one workflow, **`vulkan-sdk-rc.yml`**, with three triggers: + +- **schedule (weekly)** — runs the `bump` job: advances the SPIRV-Headers / + SPIRV-Tools submodules to their latest upstream commit and, if anything changed, + commits that bump onto a fresh `vk-update/` branch and opens a PR against + the default branch. Pushing that branch runs build-and-validate, so the bump's + test results attach to the commit and show on the PR — merged only once it is + green. (Uses a PAT — `WEEKLY_BUMP_TOKEN`, contents:write + pull-requests:write — + because pushes/PRs made with the default `GITHUB_TOKEN` don't trigger workflows.) +- **push to `vk-update/**` or `release/vulkan/**`** — runs build-and-validate (and, + on a release branch, the deliverable steps below). +- **workflow_dispatch (manual)** — `update_deps` runs the `bump` instead of + building; otherwise it builds, with `publish_artifact` controlling the deliverable. + +`build-and-validate` always: + +1. **Checkout** the repo with `submodules: true`, so DXC builds against exactly the + SPIR-V the submodule pointers name. +2. **Build** — `build_and_test.py` configures DXC with `ENABLE_SPIRV_CODEGEN=ON` + `SPIRV_BUILD_TESTS=ON` and builds `dxc`. On Windows the configure also needs + TAEF (DXC's test framework); the workflow restores the `Microsoft.Taef` nuget + package (from the public feed microsoft/terminal uses — not on nuget.org) and + pre-seeds `FindTAEF.cmake`. No DXC tests are disabled. +3. **Validate** — runs every SPIR-V test the DXC repo provides — the lit tests, the + TAEF tests, and the googletest unit tests — plus a standalone `spirv-val` check + of dxc's output. Each is non-fatal; the per-suite results (plus the DXC commit, the + submodule SPIR-V commits, and the lavapipe / WARP driver versions) are recorded in + `rc-manifest.json`, and `--allow-test-failures` keeps failures from blocking + publication. There is no pass/fail flag — whether to release is the team's call, + read from the recorded results. + +The **deliverable** steps run only on `release/vulkan/**` pushes or a manual opt-in +(`publish_artifact`) — not on the weekly `vk-update` validation runs: + +4. **Publish** — uploads `dxc.exe` + `dxv.exe` + `dxcompiler.dll`, `rc-manifest.json` + (DXC commit + the submodule SPIR-V commits + lavapipe / WARP versions + per-suite + results), and the JUnit reports as the `dxc_rc_` artifact. +5. **Offload tests** — the `offload-tests` job runs the suite against the candidate + on software renderers (see below). + +So the weekly bump → `vk-update` PR → build/test chain is automatic; an actual SDK +release candidate is a `release/vulkan/` branch, which additionally +publishes the artifact and runs the offload tests. The submodule commit is the +candidate. + +## Bumping the submodules manually + +The weekly job does this for you, but to bump + review locally: + +```sh +git submodule update --remote external/SPIRV-Headers external/SPIRV-Tools +git diff --submodule -- external/SPIRV-Headers external/SPIRV-Tools # review the bump +python utils/vulkan-sdk/build_and_test.py --build-dir build # build + validate +``` + +## Run it locally + +```sh +git submodule update --init external/SPIRV-Headers external/SPIRV-Tools +python utils/vulkan-sdk/build_and_test.py --build-dir build +``` + +## Downstream: execution on software renderers (OffloadTest) + +`offload-tests.yml` clones `llvm/offload-test-suite`, builds it with `DXC_DIR` pointed +at the candidate's `dxc`, and runs it on software renderers — no physical GPU — split +by API: + +- **Vulkan** (`check-hlsl-vk`) on **lavapipe** (`lvp`, Mesa's pure-CPU Vulkan) — runs + the candidate's **SPIR-V**. lavapipe comes from the Windows Mesa build + (`pal1000/mesa-dist-win`) and is **registered in the registry** + (`HKLM\SOFTWARE\Khronos\Vulkan\Drivers`, via `reg.exe`) because the Vulkan loader + **ignores `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` when the process is elevated** — and + GitHub-hosted runners run elevated — so the registry is the only discovery path it + trusts. The Vulkan SDK is installed so `find_package(Vulkan)` succeeds and the + `check-hlsl-vk` target is built. +- **DirectX** (`check-hlsl-warp-d3d12`) on **WARP** (Microsoft's software D3D12 device, + which the suite downloads via its `setup_warp`) — runs the candidate's **DXIL**. + +Results land in `results-vk-lavapipe.xml` and `results-d3d12-warp.xml`. The Mesa folder +is put on `PATH` (it ships `dxil.dll`, which `dxc` needs to sign DXIL for the WARP run, +and the lavapipe driver's dependencies). + +This workflow is **reusable**: the RC pipeline invokes it via `workflow_call`, and you +can run it **standalone** from the Actions UI (`workflow_dispatch`) against an existing +candidate — give it the `run_id` of a prior pipeline run and the `artifact_name` (e.g. +`dxc_rc_main`). That lets you iterate on the offload/WARP setup without rebuilding DXC. + +## Not yet prototyped (discussion points for the spec) + +- Tagging the validated commit `vulkan-sdk-X.Y.ZZZ.w` and aligning it with a + formal DXC GitHub/NuGet release (the Godbolt goal in INF-0007). +- Publishing to a durable, cross-org consumable location (vs. a workflow artifact). diff --git a/utils/vulkan-sdk/build_and_test.py b/utils/vulkan-sdk/build_and_test.py new file mode 100644 index 0000000000..bada5446e8 --- /dev/null +++ b/utils/vulkan-sdk/build_and_test.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +"""Build DXC for SPIR-V and run *every* SPIR-V test, across all harnesses. + +Steps 2 & 3 of the RC pipeline. Configures DXC with SPIR-V codegen enabled, builds +it, then runs every SPIR-V test we can find regardless of harness: + + * gtest -- ClangSPIRVTests: the SPIR-V backend unit suite (links SPIRV-Tools, + runs spirv-val internally). + * lit -- tools/clang/test/CodeGenSPIRV/: ~1300 FileCheck tests driven through + the built dxc, the actual codegen corpus. + * TAEF -- the SPIR-V tests inside ClangHLSLTests (e.g. RewriterTest::RunSpirv), + selected by a "*Spirv*" name filter. + +Each harness runs independently and is non-fatal: results (pass/fail counts + +failing test names) are recorded per-suite in rc-manifest.json, alongside the DXC +commit, the SPIRV submodule commits, and the lavapipe / WARP driver versions. With +--allow-test-failures the script still exits 0 so the pipeline publishes the release +candidate regardless; whether a candidate is good enough to release is the team's +call, read from the recorded results. + +The cmake flags mirror the existing GCP build (gcp-pipelines/x86_64-linux-clang.yml). +Stdlib only. Builds whatever the SPIRV-Headers/SPIRV-Tools submodules are checked +out at -- the submodule pointers are the source of truth. +""" + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +CODEGEN_SPIRV_DIR = REPO_ROOT / "tools" / "clang" / "test" / "CodeGenSPIRV" + +# Keep this list in sync with gcp-pipelines/x86_64-linux-clang.yml. +# +# On Windows this configure pulls in TAEF-dependent unittests (gated by the +# default HLSL_INCLUDE_TESTS=ON in PredefinedParams.cmake). The pipeline restores +# TAEF and points FindTAEF.cmake at it via the TAEF_* env vars handled in +# configure() -- so no TAEF flags belong in this list. See vulkan-sdk-rc.yml. +SPIRV_CMAKE_FLAGS = [ + "-DCMAKE_BUILD_TYPE=Release", + "-DENABLE_SPIRV_CODEGEN=ON", + "-DSPIRV_BUILD_TESTS=ON", + "-DLLVM_ENABLE_WERROR=On", +] +# spirv-val is built separately, from a standalone SPIRV-Tools configure (see +# build_spirv_val). DXC's in-tree build can't compile the SPIRV-Tools executables: +# its global CMAKE_INCLUDE_CURRENT_DIR puts each target's source dir on the include +# path, so tools/io.cpp's `#include ` picks up SPIRV-Tools' own tools/io.h +# instead of the CRT header. A separate configure (no that flag) builds it cleanly, +# and we drop the binary into build/bin so the SPIR-V tests find it on PATH. +SPIRV_TOOLS_SRC = REPO_ROOT / "external" / "SPIRV-Tools" +SPIRV_HEADERS_SRC = REPO_ROOT / "external" / "SPIRV-Headers" + +# Built unconditionally; a failure here is fatal (no binary => no release candidate). +# lit.cfg registers substitutions for the whole DX tool family (%dxc %dxv %dxa +# %dxopt %dxr %dxl, plus %batch -> dxc_batch on Windows) and escapes every +# substitution value up front -- so any of these we DON'T build is None and crashes +# lit on *every* test, even though CodeGenSPIRV itself only invokes %dxc. lit.cfg +# also shells out to llvm-config, and error-path RUN lines use `not`/`count`. +# ClangSPIRVTests is the gtest suite. +REQUIRED_TARGETS = ["dxc", "dxv", "dxa", "dxopt", "dxr", "dxl", + "FileCheck", "llvm-config", "not", "count", "ClangSPIRVTests"] +if sys.platform == "win32": + REQUIRED_TARGETS.append("dxc_batch") # %batch substitution, Windows-only +# Best-effort: ClangHLSLTests hosts the TAEF SPIR-V tests. If it fails to build we +# note it and carry on. +OPTIONAL_TARGETS = ["ClangHLSLTests"] + +GTEST_TARGET = "ClangSPIRVTests" +TAEF_DLL = "ClangHLSLTests" + + +def run(cmd, **kwargs): + printable = " ".join(str(c) for c in cmd) + print(f"\n$ {printable}", flush=True) + result = subprocess.run([str(c) for c in cmd], **kwargs) + if result.returncode != 0: + raise SystemExit(f"command failed ({result.returncode}): {printable}") + return result + + +# --------------------------------------------------------------------------- # +# Configure / build +# --------------------------------------------------------------------------- # + +def configure(build_dir): + cache = REPO_ROOT / "cmake" / "caches" / "PredefinedParams.cmake" + cmd = [ + "cmake", "-S", str(REPO_ROOT), "-B", str(build_dir), "-GNinja", + *SPIRV_CMAKE_FLAGS, "-C", str(cache), + ] + # On Windows the configure needs TAEF (find_package(TAEF REQUIRED) in the + # HLSL/dxilconv unittests). The pipeline restores the Microsoft.Taef nuget + # package and exports these env vars; forwarding them pre-seeds FindTAEF.cmake's + # find_path/find_program cache entries so it skips searching (the package's + # binaries dir is named differently across versions). The TAEF .libs are then + # found relative to TAEF_INCLUDE_DIR, so we don't need to point at them. + for cache_var in ("TAEF_INCLUDE_DIR", "TAEF_EXECUTABLE"): + value = os.environ.get(cache_var) + if value: + cmd.append(f"-D{cache_var}={value}") + run(cmd, cwd=str(REPO_ROOT)) + + +def build(build_dir, targets, jobs): + cmd = ["cmake", "--build", str(build_dir), "--target", *targets] + if jobs: + cmd += ["-j", str(jobs)] + run(cmd, cwd=str(REPO_ROOT)) + + +def build_optional(build_dir, target, jobs): + """Build a target, returning True/False instead of raising on failure.""" + cmd = ["cmake", "--build", str(build_dir), "--target", target] + if jobs: + cmd += ["-j", str(jobs)] + print(f"\n$ {' '.join(str(c) for c in cmd)}", flush=True) + ok = subprocess.run([str(c) for c in cmd], cwd=str(REPO_ROOT)).returncode == 0 + if not ok: + print(f"(optional target '{target}' failed to build; dependent tests will be skipped)") + return ok + + +def find_file(build_dir, name): + for path in build_dir.rglob(name): + return path + return None + + +def build_spirv_val(main_build_dir, jobs): + """Build spirv-val from a standalone SPIRV-Tools configure and drop it into + build/bin so the SPIR-V tests can invoke it on PATH. Best-effort: a failure is + logged but does not abort the pipeline (it can't build in-tree -- see the note + on SPIRV_TOOLS_SRC above). Returns the path to the binary, or None.""" + if not SPIRV_TOOLS_SRC.is_dir() or not SPIRV_HEADERS_SRC.is_dir(): + print("(spirv-val: SPIRV-Tools/SPIRV-Headers not checked out; skipping)") + return None + st_build = main_build_dir / "spirv-tools-standalone" + configure = [ + "cmake", "-S", str(SPIRV_TOOLS_SRC), "-B", str(st_build), "-GNinja", + "-DCMAKE_BUILD_TYPE=Release", "-DSPIRV_SKIP_TESTS=ON", + # Forward slashes: SPIRV-Tools embeds this path into a cmake string (the + # --extinst-grammar command), where Windows backslashes become invalid + # escapes ("\a", ...). as_posix() avoids that. + f"-DSPIRV-Headers_SOURCE_DIR={SPIRV_HEADERS_SRC.as_posix()}", + ] + print(f"\n$ {' '.join(configure)}", flush=True) + if subprocess.run(configure, cwd=str(REPO_ROOT)).returncode != 0: + print("WARNING: standalone SPIRV-Tools configure failed; spirv-val unavailable.") + return None + if not build_optional(st_build, "spirv-val", jobs): + print("WARNING: standalone spirv-val build failed; spirv-val unavailable.") + return None + + exe = find_file(st_build, "spirv-val.exe") or find_file(st_build, "spirv-val") + if not exe: + print("WARNING: standalone build produced no spirv-val binary.") + return None + dest = main_build_dir / "bin" / exe.name + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(exe, dest) + print(f"spirv-val -> {dest}") + return dest + + +# --------------------------------------------------------------------------- # +# Test harnesses -- each returns a suite-result dict, never raises on failure +# --------------------------------------------------------------------------- # + +def _suite(name, harness, status, passed=None, failed=None, total=None, + failed_tests=None, returncode=None, note=None): + return { + "name": name, "harness": harness, "status": status, + "passed": passed, "failed": failed, "total": total, + "failed_tests": failed_tests or [], "returncode": returncode, "note": note, + } + + +def suite_gtest(build_dir): + """ClangSPIRVTests -- the SPIR-V backend unit suite.""" + binary = find_file(build_dir, GTEST_TARGET + ".exe") or find_file(build_dir, GTEST_TARGET) + if not binary: + return _suite("spirv-unit", "gtest", "skipped", note=f"{GTEST_TARGET} not found") + xml = build_dir / "spirv-unit-results.xml" + proc = _capture([str(binary), f"--gtest_output=xml:{xml}"]) + passed = _int(re.search(r"\[\s*PASSED\s*\] (\d+) test", proc.stdout)) + failed = _int(re.search(r"\[\s*FAILED\s*\] (\d+) test", proc.stdout)) + # Each failure is reported as "Suite.Case [file.cpp:line]". + names = _gtest_failures_with_files(proc.stdout) + return _suite("spirv-unit", "gtest", "ran", passed, failed, + (passed or 0) + (failed or 0), names, proc.returncode) + + +def suite_lit(build_dir): + """The CodeGenSPIRV FileCheck corpus, via the built llvm-lit.""" + litpy = find_file(build_dir, "llvm-lit.py") or find_file(build_dir, "llvm-lit") + if not litpy: + return _suite("spirv-codegen", "lit", "skipped", note="llvm-lit not found") + if not find_file(build_dir, "FileCheck.exe") and not find_file(build_dir, "FileCheck"): + return _suite("spirv-codegen", "lit", "skipped", note="FileCheck not built") + xml = build_dir / "spirv-codegen-results.xml" + proc = _capture([sys.executable, str(litpy), "-v", "--no-progress-bar", + f"--xunit-xml-output={xml}", str(CODEGEN_SPIRV_DIR)]) + out = proc.stdout + + def tally(label): + m = re.search(rf"{label}\s*:\s*(\d+)", out) + return int(m.group(1)) if m else 0 + + # lit's summary uses "Expected Passes", "Unexpected Failures", etc. -- NOT + # plain "Passed"/"Failed". XFAIL (Expected Failures) and Unsupported are not + # failures; XPASS (Unexpected Passes), Unresolved and Timed Out are. + passed = tally("Expected Passes") + failed = (tally("Unexpected Failures") + tally("Unexpected Passes") + + tally("Unresolved Tests") + tally("Timed Out Tests")) + # Names of the genuinely-bad results (FAIL / XPASS / UNRESOLVED / TIMEOUT). + names = _dedup(re.findall(r"^(?:FAIL|XPASS|UNRESOLVED|TIMEOUT): .*?:: (\S+)", + out, re.MULTILINE)) + ran = bool(re.search(r"Expected Passes\s*:|Testing Time:", out)) + return _suite("spirv-codegen", "lit", "ran" if ran else "skipped", + passed, failed, passed + failed, names, proc.returncode, + note=None if ran else "no lit summary found") + + +def suite_taef(build_dir): + """SPIR-V tests inside the TAEF ClangHLSLTests dll (e.g. RewriterTest::RunSpirv).""" + te = os.environ.get("TAEF_EXECUTABLE") + if not te or not Path(te).is_file(): + return _suite("spirv-taef", "taef", "skipped", note="te.exe (TAEF_EXECUTABLE) unavailable") + dll = find_file(build_dir, TAEF_DLL + ".dll") + if not dll: + return _suite("spirv-taef", "taef", "skipped", note=f"{TAEF_DLL}.dll not built") + # te.exe needs dxcompiler.dll (build/bin) on PATH and HLSL_SRC_DIR to locate + # test data files (GetPathToHlslDataFile). Run only SPIR-V-named tests. + env = dict(os.environ) + env["HLSL_SRC_DIR"] = str(REPO_ROOT) + env["PATH"] = str(dll.parent) + os.pathsep + env.get("PATH", "") + proc = _capture([te, str(dll), "/select:@Name='*Spirv*'"], env=env) + total = _int(re.search(r"Total[=:]\s*(\d+)", proc.stdout)) + passed = _int(re.search(r"Passed[=:]\s*(\d+)", proc.stdout)) + failed = _int(re.search(r"Failed[=:]\s*(\d+)", proc.stdout)) + return _suite("spirv-taef", "taef", "ran", passed, failed, total, + _taef_failures_with_files(proc.stdout), proc.returncode) + + +# Trivially-valid shaders, one per stage, used to exercise the standalone +# spirv-val explicitly. (The CodeGenSPIRV corpus validates in-process via the +# linked SPIRV-Tools library and never shells out to spirv-val, so without this +# the spirv-val binary is built but never actually runs.) +SPIRV_VAL_SHADERS = [ + ("ps_6_0", "float4 main() : SV_Target { return float4(1, 0, 0, 1); }"), + ("vs_6_0", "float4 main(float4 p : POSITION) : SV_Position { return p; }"), + ("cs_6_0", "[numthreads(1, 1, 1)] void main() { }"), +] + + +def suite_spirv_val(build_dir): + """Compile shaders with the built dxc and validate the SPIR-V with the + standalone spirv-val -- an explicit, logged validator run against dxc output.""" + spirv_val = (find_file(build_dir / "bin", "spirv-val.exe") + or find_file(build_dir, "spirv-val.exe") or find_file(build_dir, "spirv-val")) + dxc = (find_file(build_dir / "bin", "dxc.exe") + or find_file(build_dir, "dxc.exe") or find_file(build_dir, "dxc")) + if not spirv_val: + return _suite("spirv-val", "spirv-val", "skipped", note="spirv-val not built") + if not dxc: + return _suite("spirv-val", "spirv-val", "skipped", note="dxc not found") + + # Print the version first so the log unambiguously shows spirv-val running. + _capture([str(spirv_val), "--version"]) + + tmp = build_dir / "spirv-val-tmp" + tmp.mkdir(parents=True, exist_ok=True) + passed = failed = 0 + failed_tests = [] + for i, (profile, src) in enumerate(SPIRV_VAL_SHADERS): + hlsl = tmp / f"case{i}_{profile}.hlsl" + spv = tmp / f"case{i}_{profile}.spv" + hlsl.write_text(src) + comp = _capture([str(dxc), "-T", profile, "-E", "main", "-spirv", + "-fspv-target-env=vulkan1.0", "-Fo", str(spv), str(hlsl)]) + if comp.returncode != 0 or not spv.is_file(): + failed += 1 + failed_tests.append(f"{profile}:compile") + continue + val = _capture([str(spirv_val), "--target-env", "vulkan1.0", str(spv)]) + if val.returncode == 0: + passed += 1 + else: + failed += 1 + failed_tests.append(f"{profile}:validate") + return _suite("spirv-val", "spirv-val", "ran", passed, failed, + passed + failed, failed_tests, 0 if failed == 0 else 1) + + +def _capture(cmd, env=None): + print(f"\n$ {' '.join(cmd)}", flush=True) + proc = subprocess.run(cmd, cwd=str(REPO_ROOT), text=True, + capture_output=True, env=env) + print(proc.stdout) + if proc.stderr: + print(proc.stderr, file=sys.stderr) + return proc + + +def _int(match): + return int(match.group(1)) if match else None + + +def _dedup(seq): + out = [] + for item in seq: + if item not in out: + out.append(item) + return out + + +def _gtest_failures_with_files(out): + """Pair each failed gtest case with the source file:line of its failure. + gtest prints `path\\foo.cpp(70): error:` (Windows) or `path/foo.cpp:70: Failure` + just before the matching `[ FAILED ] Suite.Case` line.""" + failures, loc = [], None + for line in out.splitlines(): + # The end-of-run "[ FAILED ] N tests, listed below:" block repeats the + # failures without file info -- stop before it so we don't duplicate them. + if re.match(r"\[\s*FAILED\s*\]\s+\d+\s+tests?\b", line): + break + m = re.match(r"\s*(\S+\.(?:cpp|cc|cxx|h|hpp|inc))[:(](\d+)\)?:\s*(?:error|Failure)", + line) + if m: + loc = f"{Path(m.group(1)).name}:{m.group(2)}" + continue + m = re.match(r"\[\s*FAILED\s*\]\s+(\S+\.\S+)", line) + if m: + failures.append(f"{m.group(1)} [{loc}]" if loc else m.group(1)) + loc = None + return _dedup(failures) + + +def _taef_failures_with_files(out): + """Best-effort: name each failed TAEF test and its source file. A TAEF test + `Class::Method` lives in `Class.cpp` by convention (e.g. RewriterTest.cpp).""" + names = _dedup(re.findall(r"(\w+(?:::\w+)+)\s*\[Failed\]", out)) + return [f"{n} [{n.split('::')[0]}.cpp]" for n in names] + + +# --------------------------------------------------------------------------- # +# Manifest +# --------------------------------------------------------------------------- # + +def read_pinned_commits(): + """The SPIR-V dependency commits are whatever the submodules are checked out + at -- the submodule pointers are the source of truth.""" + deps = {} + for name in ("SPIRV-Headers", "SPIRV-Tools"): + sub = REPO_ROOT / "external" / name + r = subprocess.run(["git", "-C", str(sub), "rev-parse", "HEAD"], + text=True, capture_output=True) + deps[name] = r.stdout.strip() if r.returncode == 0 else "unknown" + return deps + + +def git_head(): + r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(REPO_ROOT), + text=True, capture_output=True) + return r.stdout.strip() if r.returncode == 0 else "unknown" + + +def suite_clean(s): + return s["status"] == "ran" and not s["failed"] and s["returncode"] == 0 + + +# The SPIR-V test frameworks, each with a human title so the log says which is +# which at a glance. Order = run order. +SUITES = [ + ("googletest unit tests (ClangSPIRVTests)", suite_gtest), + ("lit tests (CodeGenSPIRV corpus)", suite_lit), + ("TAEF tests (ClangHLSLTests, *Spirv*)", suite_taef), + ("spirv-val (validate generated SPIR-V)", suite_spirv_val), +] + + +def run_suite(title, fn, build_dir): + """Run one framework inside a titled, collapsible log group, then print a + one-line verdict (visible without expanding the group).""" + print(f"\n::group::{title}", flush=True) + result = fn(build_dir) + result["title"] = title + print("::endgroup::", flush=True) + if result["status"] != "ran": + print(f">>> [SKIPPED] {title} -- {result['note']}", flush=True) + else: + verdict = "PASS" if suite_clean(result) else "FAIL" + print(f">>> [{verdict}] {title}: " + f"{result['passed']} passed, {result['failed']} failed", flush=True) + return result + + +def write_manifest(build_dir, dxc_sha, suites, lavapipe_version, warp_version): + # No top-level pass/fail verdict: whether a candidate is good enough to release + # is the team's decision, read from the per-suite results below -- not a flag the + # tooling sets. lavapipe_version / warp_version are the software drivers the offload + # tests run the candidate against: lavapipe (Mesa) for SPIRV, WARP for DXIL. + manifest = { + "dxc_commit": dxc_sha, + "lavapipe_version": lavapipe_version, + "warp_version": warp_version, + "spirv_dependencies": read_pinned_commits(), + "test_suites": suites, + } + out = build_dir / "rc-manifest.json" + out.write_text(json.dumps(manifest, indent=2) + "\n") + print(f"\nWrote release-candidate manifest -> {out}") + return out + + +# --------------------------------------------------------------------------- # + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--build-dir", default=str(REPO_ROOT / "build"), + help="Build directory (default: ./build).") + parser.add_argument("--jobs", "-j", type=int, default=0, + help="Parallel build jobs (default: let the generator decide).") + parser.add_argument("--skip-build", action="store_true", + help="Skip configure/build; just run the existing test binaries.") + parser.add_argument("--allow-test-failures", action="store_true", + help="Record test failures in the manifest but still exit 0, " + "so the pipeline publishes the release candidate anyway.") + parser.add_argument("--lavapipe-version", default="", + help="lavapipe (Mesa) version the Vulkan offload tests run " + "against (recorded in the manifest).") + parser.add_argument("--warp-version", default="", + help="WARP version the Direct3D 12 offload tests run against " + "(recorded in the manifest).") + args = parser.parse_args(argv) + + build_dir = Path(args.build_dir).resolve() + + if not args.skip_build: + if shutil.which("ninja") is None: + raise SystemExit("ninja not found on PATH; install it or adjust the generator.") + configure(build_dir) + build(build_dir, REQUIRED_TARGETS, args.jobs) # fatal on failure + for target in OPTIONAL_TARGETS: + build_optional(build_dir, target, args.jobs) + # spirv-val, built standalone and dropped into build/bin (on the tests' PATH). + build_spirv_val(build_dir, args.jobs) + + # Run every SPIR-V framework. Each is independent and non-fatal, and its output + # is grouped under a clear title (see run_suite) so the log names which is which. + suites = [run_suite(title, fn, build_dir) for title, fn in SUITES] + manifest = write_manifest(build_dir, git_head(), suites, + args.lavapipe_version, args.warp_version) + + print("\n==================== SPIR-V test summary ====================") + any_problem = False + for s in suites: + label = s.get("title", s["name"]) + if s["status"] != "ran": + any_problem = True + print(f" [SKIPPED] {label} -- {s['note']}") + else: + ok = suite_clean(s) + any_problem = any_problem or not ok + print(f" [{'PASS' if ok else 'FAIL'}] {label}: " + f"{s['passed']} passed, {s['failed']} failed") + for name in s["failed_tests"]: + print(f" FAILED: {name}") + print("=============================================================") + print(f"Release candidate manifest: {manifest}") + + if any_problem and not args.allow_test_failures: + return 1 + if any_problem: + print("\nPublishing release candidate anyway (--allow-test-failures); " + "the per-suite failures are recorded in the manifest for the team to review.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())