diff --git a/.github/workflows/build-windows-full-msbuild.yml b/.github/workflows/build-windows-full-msbuild.yml index f52a4b7a0..987aaa903 100644 --- a/.github/workflows/build-windows-full-msbuild.yml +++ b/.github/workflows/build-windows-full-msbuild.yml @@ -34,7 +34,9 @@ concurrency: jobs: Windows-MSBuild: name: Windows Full (VS MSBuild, ${{ github.event.inputs.build-type || 'Debug' }}) - if: github.event_name != 'pull_request' || github.event.pull_request.draft != true + if: >- + (github.event_name != 'schedule' || vars.STRIDE_ENABLE_SCHEDULED_CI == 'true') + && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) runs-on: windows-2025-vs2026 timeout-minutes: 60 steps: diff --git a/.github/workflows/changes.yml b/.github/workflows/changes.yml index 7d25d81c7..2450a053d 100644 --- a/.github/workflows/changes.yml +++ b/.github/workflows/changes.yml @@ -72,6 +72,37 @@ jobs: return bool(files) print('comment_only=' + str(comment_only()).lower()) EOF + # Users get the samples from the published content template packs (StrideSamplesVersion.props), not from + # the repository, so a sample change only reaches them through a samples release (release-samples.yml). + # Editing samples is normal work, so this only warns, as a reminder that one is pending. The engine-versioned + # template sources and prose don't count. + - name: Warn on sample changes (samples release needed) + if: github.event_name == 'pull_request' + run: | + python3 - <<'EOF' + import json, re + files = [f for page in json.load(open('/tmp/pr-files.json')) for f in page] + def is_content(name): + return (name.startswith('samples/') + and not name.startswith(('samples/NewGame/', 'samples/Library/')) + and not name.lower().endswith('.md')) + changed = [f for f in files if is_content(f['filename']) or is_content(f.get('previous_filename') or '')] + if changed: + names = [f['filename'] for f in changed] + shown = ', '.join(names[:5]) + (f' and {len(names) - 5} more' if len(names) > 5 else '') + # The PR diff shows an annotation only on a file of the new version, on a line inside the diff: the + # first changed sample file that still exists and has a text diff, at the start of its first changed + # block. Deleted and binary-only changes leave the warning on the run summary only. + where = '' + for f in changed: + hunk = re.search(r'^@@ -\d+(?:,\d+)? \+(\d+)', f.get('patch') or '', re.M) + if f['status'] != 'removed' and hunk and int(hunk.group(1)) > 0: + where = f"file={f['filename']},line={hunk.group(1)}," + break + print('::warning ' + where + 'title=Samples changed::This PR changes the samples (' + shown + '). ' + 'Users only get them with the next samples release: after merging, run release-samples.yml ' + '(before the next engine release if the engine needs them; see docs/build/versioning.md).') + EOF - uses: dorny/paths-filter@v4 if: github.event_name == 'pull_request' id: filter diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 48bf4f078..4f553d857 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -81,6 +81,8 @@ jobs: build-type: Debug # Release has a bug and cannot build ### Runtimes ### + # The `ios` filter is narrower than `runtime` on purpose: iOS runners are costly, macOS-Tests-Game + # covers the shared engine surface per-PR, and master pushes run this per-commit anyway. iOS-Runtime: needs: changes if: (github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'ci-run-on-draft')) && needs.changes.outputs.ios == 'true' && (github.event_name != 'workflow_dispatch' || inputs.ios) diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml index 952069b2a..2fc81e4ad 100644 --- a/.github/workflows/release-deploy.yml +++ b/.github/workflows/release-deploy.yml @@ -125,24 +125,62 @@ jobs: } echo '```' >> $env:GITHUB_STEP_SUMMARY + # The content template packs (Samples, Starters, AssetPacks) are not part of an engine release: the engine + # names their exact version (sources/templates/StrideSamplesVersion.props), which release-samples.yml only commits + # once it is published. Refuse an engine whose content is not on the feed (every New Project would break) or + # that is stable while naming prerelease content. Samples changed since the published content (its + # samples/ tag) only warn: the engine ships with the published content, which is right for work not + # released yet, and the release manager judges whether a change is one the new engine needs. + - name: Check the content version + shell: bash + run: | + SHA="${{ steps.src.outputs.sha }}" + VERSION="${{ steps.version.outputs.version }}" + FILE=sources/templates/StrideSamplesVersion.props + SV=$(git show "$SHA:$FILE" | sed -n 's|.*<_StrideCommittedSamplesVersion>\([^<]*\)/dev/null; then + echo "::warning::No samples/$SV tag; skipped the check for sample changes since the published content." + exit 0 + fi + CHANGED=$(git diff --name-only "samples/$SV" "$SHA" -- samples ':(exclude)samples/NewGame' ':(exclude)samples/Library' ':(exclude)*.md') + if [ -n "$CHANGED" ]; then + COUNT=$(printf '%s\n' "$CHANGED" | wc -l) + echo "::warning title=Samples changed::$COUNT sample file(s) changed since the published content $SV (tag samples/$SV). This engine ships with the published $SV content. If the engine needs those changes, publish a samples release first (see the run summary)." + { + echo "## Sample changes not in the published content ($SV)" + echo "" + echo "This engine ships with the published $SV content. Fine for work not released yet; if the new engine needs these changes, publish a samples release with release-samples.yml (it updates SamplesVersion), then release again." + echo "" + echo '```' + printf '%s\n' "$CHANGED" | sed -n '1,50p' + [ "$COUNT" -gt 50 ] && echo "... and $((COUNT - 50)) more" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + - name: Push NuGet packages shell: pwsh run: | - # Content-versioned template packages (Samples, Starters) follow StrideSamplesVersion and - # only change when the samples are bumped (see plans/samples-version-bump.md). Skip the - # upload when that version is already on nuget.org - re-publishing the same version is a - # no-op and these packages are large, so this avoids a wasted upload every release. - function Test-NuGetPublished([string]$id, [string]$version) { - $url = "https://api.nuget.org/v3-flatcontainer/$($id.ToLowerInvariant())/index.json" - try { return ((Invoke-RestMethod -Uri $url -TimeoutSec 60 -ErrorAction Stop).versions -contains $version) } - catch { return $false } # 404 (never published) or transient: fall through to push (--skip-duplicate stays safe) - } - $packages = Get-ChildItem -Path bin/packages -Filter "*.nupkg" - $contentVersionedIds = @('Stride.Templates.Samples', 'Stride.Templates.Games.Starters', 'Stride.Templates.AssetPacks') - $contentVersioned = $packages | Where-Object { $_.Name -match 'Stride\.Templates\.(Samples|Games\.Starters|AssetPacks)\.' } $gameStudio = $packages | Where-Object { $_.Name -match 'GameStudio' } - $main = $packages | Where-Object { $_.Name -notmatch 'GameStudio' -and $_.Name -notmatch 'Stride\.Templates\.(Samples|Games\.Starters|AssetPacks)\.' } + $main = $packages | Where-Object { $_.Name -notmatch 'GameStudio' } echo "::group::Pushing main packages ($($main.Count))" foreach ($pkg in $main) { @@ -151,20 +189,6 @@ jobs: } echo "::endgroup::" - if ($contentVersioned) { - echo "::group::Pushing content-versioned template packages (Samples, Starters, AssetPacks)" - foreach ($pkg in $contentVersioned) { - $id = $contentVersionedIds | Where-Object { $pkg.Name.StartsWith($_ + '.') } | Select-Object -First 1 - if ($id -and (Test-NuGetPublished $id $pkg.BaseName.Substring($id.Length + 1))) { - echo "Skipping $($pkg.Name) (already published; samples unchanged since last release)." - continue - } - echo "Pushing $($pkg.Name)..." - dotnet nuget push $pkg.FullName --api-key $env:STRIDE_NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate - } - echo "::endgroup::" - } - if ($gameStudio) { echo "::group::Pushing GameStudio (last, so dependencies are already available)" foreach ($pkg in $gameStudio) { diff --git a/.github/workflows/release-samples.yml b/.github/workflows/release-samples.yml new file mode 100644 index 000000000..31cdc5d17 --- /dev/null +++ b/.github/workflows/release-samples.yml @@ -0,0 +1,351 @@ +name: Release Samples + +# Builds, signs, and (optionally) deploys the content template packs (Stride.Templates.Samples, +# Stride.Templates.Games.Starters, Stride.Templates.AssetPacks) to NuGet.org. +# +# Versioned independently of the engine: StrideSamplesVersion in sources/templates/StrideSamplesVersion.props is the +# published content version the engine uses. This workflow is the only thing that changes it: it packs the samples +# of the branch under the next content version, publishes them, and only then commits that version to the branch, +# so the branch never names content that is not on the feed. The packs are stamped for this branch's committed engine +# version (MajorMinor.Patch + NuGetVersionSuffix), the engine release they precede. +# A deploy pushes to NuGet.org, tags samples/ at the built commit, creates a GitHub Release and commits +# SamplesVersion to the branch. A run without deploy publishes and commits nothing. + +on: + workflow_dispatch: + inputs: + version: + description: Content version to publish (e.g. 4.4.1, or 4.4.1-beta1 for content not ready yet). Empty = the committed SamplesVersion's next stable number, or .0 for older-line content. + default: '' + type: string + sign: + description: Code sign the packages + default: true + type: boolean + deploy: + description: Deploy to NuGet.org, create a GitHub Release and commit SamplesVersion to the branch + default: false + type: boolean + +concurrency: + group: release-samples-${{ github.ref }} + cancel-in-progress: false + +jobs: + Validate: + runs-on: ubuntu-latest + steps: + - name: Check deploy requires signing + if: ${{ inputs.deploy && !inputs.sign }} + run: | + echo "::error::Cannot deploy unsigned packages. Enable 'sign' to deploy." + exit 1 + + - name: Check release permissions + if: ${{ inputs.sign || inputs.deploy }} + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + run: | + ORG=$(echo "${{ github.repository }}" | cut -d/ -f1) + TEAM="stride-release-managers" + USER="${{ github.actor }}" + if ! gh api "orgs/$ORG/teams/$TEAM/memberships/$USER" --silent 2>/dev/null; then + echo "::error::User $USER is not a member of $ORG/$TEAM. Sign/deploy requires stride-release-managers team membership." + exit 1 + fi + + Package: + name: Package + needs: Validate + runs-on: windows-2025-vs2026 + environment: production # signing secrets live here + outputs: + version: ${{ steps.version.outputs.version }} + engine: ${{ steps.check.outputs.engine }} + steps: + - uses: actions/checkout@v7 + with: + lfs: true + fetch-depth: 0 # full history + tags for the re-publish guard + fetch-tags: true + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + # The content version: the input, or the committed one's next number (4.4.0 -> 4.4.1, 4.4.0-beta7 -> 4.4.0), or + # .0 when the committed content is from an older line. Its major.minor is this branch's engine line + # (a line's content is released from that line's branch only), its patch part is a free counter. + - name: Choose the content version + id: version + shell: pwsh + run: | + $file = Get-Content sources/shared/SharedAssemblyInfo.cs -Raw + $committed = [regex]::Match((Get-Content sources/templates/StrideSamplesVersion.props -Raw), '<_StrideCommittedSamplesVersion>([^<]*)." + exit 1 + } + if (($version -replace '^(\d+\.\d+).*', '$1') -ne $engineLine) { + echo "::error::Content version $version is not on this branch's engine line $engineLine. Release $($version -replace '^(\d+\.\d+).*', '$1').x content from the branch of that line." + exit 1 + } + if ([System.Management.Automation.SemanticVersion]$version -le [System.Management.Automation.SemanticVersion]$committed) { + echo "::error::Content version $version does not come after the committed $committed." + exit 1 + } + echo "version=$version" >> $env:GITHUB_OUTPUT + echo "::notice::Content version: $committed -> $version" + + # A version already released means the input is wrong (or the samples were published already); fail rather + # than re-publish different content under it. A re-run on the tagged commit is allowed (completes a partial push). + - name: Guard against re-publishing an existing version + shell: bash + run: | + V="${{ steps.version.outputs.version }}" + TAG="samples/$V" + if git rev-parse "$TAG" >/dev/null 2>&1 && [ -z "$(git tag --points-at HEAD --list "$TAG")" ]; then + echo "::error::Content version $V is already released (tag $TAG on another commit). Pick the next number." + exit 1 + fi + PUBLISHED=0 + for id in stride.templates.samples stride.templates.games.starters stride.templates.assetpacks; do + if curl -sf "https://api.nuget.org/v3-flatcontainer/$id/index.json" | grep -q "\"$V\""; then + PUBLISHED=$((PUBLISHED + 1)) + fi + done + if [ "$PUBLISHED" = 3 ]; then + echo "::error::Content version $V is already on nuget.org. Pick the next number." + exit 1 + fi + + - name: Pack content templates + run: | + dotnet build build\Stride.build ` + -t:PackageSamples ` + -p:StrideSamplesVersion=${{ steps.version.outputs.version }} ` + -p:StrideSign=${{ inputs.sign }} + + # The engine the packs target is their Stride.Core dependency (the marker the CLI picks templates by). Read it + # from the packs themselves and check it is the branch's committed engine version. + - name: Check the packages + id: check + shell: pwsh + run: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + $file = Get-Content sources/shared/SharedAssemblyInfo.cs -Raw + $field = { param($name) [regex]::Match($file, "$name = `"([^`"]*)`";").Groups[1].Value } + $expected = "$(& $field 'MajorMinor').$(& $field 'Patch')$(& $field 'NuGetVersionSuffix')" + foreach ($id in 'Stride.Templates.Samples', 'Stride.Templates.Games.Starters', 'Stride.Templates.AssetPacks') { + $pkg = "bin/packages/$id.${{ steps.version.outputs.version }}.nupkg" + if (-not (Test-Path $pkg)) { + echo "::error::$id.${{ steps.version.outputs.version }}.nupkg was not produced" + exit 1 + } + $zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $pkg)) + try { + $entry = $zip.Entries | Where-Object { $_.FullName -like '*.nuspec' } | Select-Object -First 1 + $reader = [System.IO.StreamReader]::new($entry.Open()) + $nuspec = $reader.ReadToEnd() + $reader.Dispose() + } finally { $zip.Dispose() } + $engine = [regex]::Match($nuspec, '> $env:GITHUB_OUTPUT + echo "::notice title=Target engine::Samples ${{ steps.version.outputs.version }} target engine $expected" + "## Samples ${{ steps.version.outputs.version }} for engine $expected" >> $env:GITHUB_STEP_SUMMARY + + # The release-shaped packs are exercised by the smoke tests (pack -> dotnet new install -> instantiate) against + # this checkout's build, since the stamped engine may not be published yet. The same properties keep the + # tests' own pack identical to the one above. + - name: Smoke test + run: | + dotnet test sources\tools\Stride.Templates.Tests ` + -c Release ` + -p:StridePackageBuild=true ` + -p:StrideSamplesVersion=${{ steps.version.outputs.version }} ` + --logger "trx;LogFileName=templates.trx" --results-directory TestResults + + - name: Sign + if: ${{ inputs.sign }} + run: | + dotnet build build\Stride.build -t:SignPackage -p:StrideSign=true + env: + StrideSignTenantId: ${{ secrets.STRIDE_SIGN_TENANT_ID }} + StrideSignClientId: ${{ secrets.STRIDE_SIGN_CLIENT_ID }} + StrideSignClientSecret: ${{ secrets.STRIDE_SIGN_CLIENT_SECRET }} + StrideSignKeyVaultCertificate: ${{ secrets.STRIDE_SIGN_KEYVAULT_CERTIFICATE }} + StrideSignKeyVaultName: ${{ secrets.STRIDE_SIGN_KEYVAULT_NAME }} + + - name: Upload packages + uses: actions/upload-artifact@v7 + with: + name: stride-samples-${{ steps.version.outputs.version }} + path: | + bin/packages/Stride.Templates.Samples.${{ steps.version.outputs.version }}.nupkg + bin/packages/Stride.Templates.Games.Starters.${{ steps.version.outputs.version }}.nupkg + bin/packages/Stride.Templates.AssetPacks.${{ steps.version.outputs.version }}.nupkg + if-no-files-found: error + + # Surface the version in the run UI even when Deploy is skipped. + Version: + name: Packaged samples ${{ needs.Package.outputs.version }} for engine ${{ needs.Package.outputs.engine }} + needs: Package + if: ${{ always() && needs.Package.result == 'success' }} + runs-on: ubuntu-latest + steps: + - run: echo "::notice title=Packaged samples::${{ needs.Package.outputs.version }} for engine ${{ needs.Package.outputs.engine }}" + + Deploy: + name: Deploy ${{ needs.Package.outputs.version }} + if: ${{ inputs.deploy && inputs.sign }} + needs: Package + runs-on: ubuntu-latest # NuGet push doesn't need Windows + environment: production # requires manual approval in GitHub settings + permissions: + contents: write + steps: + - name: Check release permissions + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + run: | + ORG=$(echo "${{ github.repository }}" | cut -d/ -f1) + TEAM="stride-release-managers" + USER="${{ github.actor }}" + if ! gh api "orgs/$ORG/teams/$TEAM/memberships/$USER" --silent 2>/dev/null; then + echo "::error::User $USER is not a member of $ORG/$TEAM. Deploy requires stride-release-managers team membership." + exit 1 + fi + + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.GH_PAT }} # push the samples/ tag and the SamplesVersion commit + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Download packages + uses: actions/download-artifact@v8 + with: + name: stride-samples-${{ needs.Package.outputs.version }} + path: bin/packages + + - name: Push to NuGet.org + shell: bash + run: | + # --skip-duplicate: a re-run after a partial push completes the set instead of failing on what went through. + for pkg in bin/packages/*.nupkg; do + echo "Pushing $(basename "$pkg")..." + dotnet nuget push "$pkg" --api-key "$STRIDE_NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate + done + env: + STRIDE_NUGET_API_KEY: ${{ secrets.STRIDE_NUGET_API_KEY }} + + - name: Tag release + run: | + TAG="samples/${{ needs.Package.outputs.version }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists, skipping" + else + git tag "$TAG" "${{ github.sha }}" # the commit the packages were built from + git push origin "$TAG" + fi + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + run: | + TAG="samples/${{ needs.Package.outputs.version }}" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists, skipping" + else + # The samples are released apart from the engine: the changelog is the sample commits since the + # previous samples/* tag, so engine commits don't leak into it. + PREV=$(git describe --tags --abbrev=0 --match 'samples/*' "$TAG^" 2>/dev/null || true) + RANGE="$TAG" + [ -n "$PREV" ] && RANGE="$PREV..$TAG" + NOTES=$(git log --no-merges --pretty='- %s (%h)' "$RANGE" -- samples ':(exclude)samples/NewGame' ':(exclude)samples/Library') + [ -z "$NOTES" ] && NOTES="_No sample changes since the previous samples release._" + { + echo "For Stride engine ${{ needs.Package.outputs.engine }}." + echo "" + echo "## What's changed in the Stride samples" + echo "" + echo "$NOTES" + if [ -n "$PREV" ]; then + echo "" + echo "**Full samples changelog**: ${{ github.server_url }}/${{ github.repository }}/compare/$PREV...$TAG" + fi + } > samples-notes.md + gh release create "$TAG" \ + --title "Stride samples ${{ needs.Package.outputs.version }}" \ + --notes-file samples-notes.md \ + --latest=false \ + bin/packages/*.nupkg + fi + + # Last, once the content is on the feed: the branch now names it, so engines built from here on use it. Goes + # to the current branch tip (it may have moved since the build). Needs GH_PAT to push past branch protection, + # like the engine's version bump in release-deploy.yml. + - name: Commit the content version + run: | + V="${{ needs.Package.outputs.version }}" + FILE=sources/templates/StrideSamplesVersion.props + git checkout "${{ github.ref_name }}" + git pull --ff-only origin "${{ github.ref_name }}" + sed -i "s|<_StrideCommittedSamplesVersion>[^<]*$V$V" "$FILE"; then + echo "::error::Could not set the content version $V in $FILE. The samples are published; set it by hand." + exit 1 + fi + if git diff --quiet -- "$FILE"; then + echo "The content version is already $V, nothing to commit" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Commit only the version file (never -a), so an unexpected working-tree change can't ride along. + git commit -m "Samples: content version $V (released samples/$V)" -- "$FILE" + git pull --rebase origin "${{ github.ref_name }}" + git push origin "HEAD:${{ github.ref_name }}" + + - name: Publish summary + if: success() + shell: bash + run: | + V="${{ needs.Package.outputs.version }}" + { + echo "## Published Stride samples $V" + echo "" + echo "For engine ${{ needs.Package.outputs.engine }}." + echo "" + for id in Stride.Templates.Samples Stride.Templates.Games.Starters Stride.Templates.AssetPacks; do + echo "- [$id](https://www.nuget.org/packages/$id/$V)" + done + echo "- [GitHub Release](${{ github.server_url }}/${{ github.repository }}/releases/tag/samples/$V)" + echo "" + echo "SamplesVersion is now $V on ${{ github.ref_name }}; engines built from there use this content." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1fbc9887..c09921ea2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,6 @@ on: description: Build prerequisites installer default: true type: boolean - version-suffix: - description: Prerelease suffix, no leading dash (e.g. "beta" -> 4.4.123-beta). Empty = stable. - default: '' - type: string deploy: description: Deploy to NuGet.org and create GitHub Release default: false @@ -133,7 +129,6 @@ jobs: -p:StridePlatforms=$($platforms.Replace(';','%3B')) ` -p:StrideGraphicsApiDependentBuildAll=true ` -p:StrideSign=${{ inputs.sign }} ` - -p:StrideVersionSuffix=${{ inputs.version-suffix }} ` -p:StrideBuildPrerequisitesInstaller=${{ inputs.build-prerequisites-installer }} ` -p:StrideSentryDsn=${{ vars.STRIDE_SENTRY_DSN }} ` -p:StrideSentryEnvironment=${{ vars.STRIDE_SENTRY_DSN != '' && 'release' || '' }} ` diff --git a/.github/workflows/test-android-game.yml b/.github/workflows/test-android-game.yml index dfe9381ed..cdceda58f 100644 --- a/.github/workflows/test-android-game.yml +++ b/.github/workflows/test-android-game.yml @@ -77,6 +77,7 @@ concurrency: jobs: Android-Tests-Game: name: Android (Vulkan, ${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}) + if: github.event_name != 'schedule' || vars.STRIDE_ENABLE_SCHEDULED_CI == 'true' # Linux host: KVM-accelerated x86_64 Android emulator is the only path that runs in a # reasonable amount of time on a hosted runner. macOS-14+ Apple Silicon runners don't # support nested virt for AOSP emulator; older macOS-13 Intel HAXM is being retired. diff --git a/.github/workflows/test-windows-game.yml b/.github/workflows/test-windows-game.yml index b70935c37..9b18f1eb1 100644 --- a/.github/workflows/test-windows-game.yml +++ b/.github/workflows/test-windows-game.yml @@ -98,6 +98,7 @@ jobs: STRIDE_GRAPHICS_SOFTWARE_RENDERING: "1" STRIDE_TESTS_RENDERDOC: "error" STRIDE_MAX_PARALLELISM: "8" + STRIDE_VULKAN_SYNC_VALIDATION: ${{ matrix.graphics-api == 'Vulkan' && '1' || '' }} # Crash dump collection (covers 3 crash types) — all dumps land under TestResults/ # alongside the .trx logs and the blame-hang dumps from `dotnet test`. STRIDE_TESTS_CRASH_DUMPS: "1" # Type 1: SEHException logging + minidump via FirstChanceException diff --git a/build/GenerateDevPackages.cs b/build/GenerateDevPackages.cs index 5d7f65ab0..35834a6ef 100644 --- a/build/GenerateDevPackages.cs +++ b/build/GenerateDevPackages.cs @@ -59,14 +59,14 @@ if (string.IsNullOrEmpty(solution)) if (string.IsNullOrEmpty(version)) { - // Package versions use the committed MajorMinor.Patch (see StrideVersionTasks.cs). The -devN suffix comes from - // the generated overlay when present. + // Dev package versions are the committed MajorMinor.Patch without its prerelease suffix (see StrideVersionTasks.cs), + // plus the -devN suffix from the generated overlay when present. var generatedFile = Path.Combine(strideRoot, "sources", "shared", "SharedAssemblyInfo.Generated.cs"); var plainFile = Path.Combine(strideRoot, "sources", "shared", "SharedAssemblyInfo.cs"); var plainText = File.ReadAllText(plainFile); var mmMatch = Regex.Match(plainText, @"MajorMinor\s*=\s*""([^""]+)"""); var patchMatch = Regex.Match(plainText, @"\bPatch\s*=\s*""([^""]+)"""); - var suffixMatch = Regex.Match(File.ReadAllText(File.Exists(generatedFile) ? generatedFile : plainFile), @"NuGetVersionSuffix\s*=\s*""([^""]*)"""); + var suffixMatch = File.Exists(generatedFile) ? Regex.Match(File.ReadAllText(generatedFile), @"NuGetVersionSuffix\s*=\s*""([^""]*)""") : Match.Empty; if (!mmMatch.Success || !patchMatch.Success) throw new Exception("Could not determine version from SharedAssemblyInfo"); version = mmMatch.Groups[1].Value + "." + patchMatch.Groups[1].Value + (suffixMatch.Success ? suffixMatch.Groups[1].Value : ""); } @@ -232,8 +232,11 @@ if (freshPackages.Length == 0) // fell back to whatever stale package the NuGet cache still held. Every packed project that had a stub before // must have produced one now, else stop and leave the feed as it was. var fresh = freshPackages.Select(Path.GetFileName).ToHashSet(StringComparer.OrdinalIgnoreCase); +// The content template packs (Stride.Templates.Samples / .Games.Starters / .AssetPacks) are only packed on request +// (StridePackContentTemplates), so a stamp that lists them from such a build says nothing about this pack. var missing = (adopt.Length > 0 ? new List() : projects) .Select(StubName) + .Where(name => !name.StartsWith("Stride.Templates.", StringComparison.OrdinalIgnoreCase)) .Where(name => previousStamp.Contains(name, StringComparer.OrdinalIgnoreCase) && !fresh.Contains(name)) .ToList(); if (missing.Count > 0) diff --git a/build/Stride.Samples.build b/build/Stride.Samples.build index a9dc9a2a1..10281bd08 100644 --- a/build/Stride.Samples.build +++ b/build/Stride.Samples.build @@ -1,23 +1,33 @@ + dotnet msbuild build/Stride.Samples.build -t:UpgradeSamples. --> $(MSBuildThisFileDirectory)..\ @@ -31,6 +41,18 @@ + + + + + + <_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.Games.Starters\Stride.Templates.Games.Starters.csproj" /> + <_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.Samples\Stride.Templates.Samples.csproj" /> + <_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.AssetPacks\Stride.Templates.AssetPacks.csproj" /> + + - + + Debug - - + - + - + - $(SampleVersion)$(StrideWorktreeSuffix) + $([System.Text.RegularExpressions.Regex]::Replace('$(SamplesEngineVersion)', '-.*$', ''))$(StrideWorktreeSuffix) <_StrideSampleCsproj Include="$(StrideRoot)samples\**\*.csproj" /> @@ -85,27 +108,27 @@ - - - + before opening them in GameStudio on a worktree. Real csproj edits; run SamplesToReleaseEngine + before committing. A no-op on samples not at the clean version (run UpgradeSamples first). --> + + + - - - + + + - - + SamplesToReleaseEngine to finalize at the clean version before committing. SamplesEngineVersion defaults to + the committed release version, suffix included; -p:SamplesEngineVersion= overrides. -p:SamplePackages= limits + the migrated .sdpkg set. Part of PrepareSamplesForRelease; on its own it does not touch the content version. --> + + $(StrideRoot)samples\StrideSamples.slnx - + + + - - - + + + + <_StrideContentPackArgs>-c $(Configuration) -p:StridePackContentTemplates=true -p:StrideSamplesVersion=$(StrideSamplesVersion) + <_StrideContentPackArgs Condition="'$(StridePackageBuild)' == 'true'">$(_StrideContentPackArgs) -p:StridePackageBuild=true + + + + + + + + + + + + + + + + + diff --git a/build/Stride.build b/build/Stride.build index 9a120674f..27ad1b520 100644 --- a/build/Stride.build +++ b/build/Stride.build @@ -113,11 +113,8 @@ Example: dotnet build build/Stride.build -t:Package - - - @@ -151,12 +148,12 @@ Example: dotnet build build/Stride.build -t:Package - + - + <_StridePackageTargetPrerequisitesInstaller Condition="'$(StrideBuildPrerequisitesInstaller)' == 'true'">PrerequisitesInstaller @@ -199,9 +196,21 @@ Example: dotnet build build/Stride.build -t:Package - + + + + + + diff --git a/build/Stride.slnx b/build/Stride.slnx index 5f3ddcbd8..0a4810917 100644 --- a/build/Stride.slnx +++ b/build/Stride.slnx @@ -39,6 +39,7 @@ + @@ -200,6 +201,7 @@ + diff --git a/docs/build/versioning.md b/docs/build/versioning.md index 83684446a..481adfb86 100644 --- a/docs/build/versioning.md +++ b/docs/build/versioning.md @@ -10,7 +10,7 @@ The source of truth is [`sources/shared/SharedAssemblyInfo.cs`](../../sources/sh |---|---|---| | `PublicVersion` | `4.4.0` | 3-part `major.minor.patch` display/package version. Committed and bumped per release (see below). | | `AssemblyVersion` | `4.4.0.0` | Assembly binding identity, pinned per `major.minor` so the git height never churns it. Bump together with `PublicVersion`'s `major.minor`. | -| `NuGetVersionSuffix` | `` / `-beta` / `-dev2` | Prerelease tag. Empty for a stable release; set by the worktree system (`-devN`) or a release prerelease (`-beta`). | +| `NuGetVersionSuffix` | `` / `-beta1` / `-dev2` | Prerelease tag. Committed: empty for a stable release, `-betaN` before a prerelease (only package builds apply it). Dev builds replace it with the worktree suffix (`-devN`). | | `BuildMetadata` | `+g` | Set during package builds. | `NuGetVersion = PublicVersion + NuGetVersionSuffix`; `StrideVersion.NuGetVersion` (the compiled const) is what the package upgrader stamps into consumer projects. @@ -25,6 +25,7 @@ Two rules: - **Bump per release.** The release pipeline refuses to publish a version whose `releases/` tag already exists on another commit (see [Release flow](#release-flow)), so a forgotten bump fails the deploy rather than silently re-publishing. - **A format change ⇒ a numeric (`Patch`) bump.** Asset upgraders gate on the *numeric* version and ignore the prerelease suffix (`-beta1`, `-dev3`, custom), so a format change must advance the number (e.g. `4.4.0` → `4.4.1`) for the gate to fire. Successive prereleases without a format change can stay at the same number (`4.4.0-beta1`, `4.4.0-beta2`). +- **A prerelease is a committed suffix.** Commit `NuGetVersionSuffix = "-beta2"` before running the release, like a `Patch` bump, and `""` before the stable release. The release workflows have no suffix input. Only package builds apply the committed suffix: dev and CI builds stay at `MajorMinor.Patch` (+ `-devN`), so all the betas of one version share one dev version (and one NuGet cache slot per checkout). ## Per-checkout dev versions (`-devN`) @@ -45,7 +46,7 @@ Multiple checkouts of Stride on one machine (git worktrees *or* independent clon 1. **Version** = the committed value in `SharedAssemblyInfo.cs` (`StrideGitVersion` reads it and adds `+g`). 2. **Package** builds all platforms unsigned → `bin/packages/*.nupkg`. -3. **Deploy** (only when `sign && deploy`) pushes to nuget.org, then **creates and pushes the `releases/` tag** — so the tag is a *consequence* of a successful release; re-publishing a not-bumped version is refused because its tag already exists (the forget-to-bump guard). Idempotent: `--skip-duplicate`, plus content-versioned template packages are pre-checked against the nuget flat-container index and skipped if already published. +3. **Deploy** (only when `sign && deploy`) checks the content version the engine names is published (see [Samples & template package versions](#samples--template-package-versions)), pushes to nuget.org, then **creates and pushes the `releases/` tag** — so the tag is a *consequence* of a successful release; re-publishing a not-bumped version is refused because its tag already exists (the forget-to-bump guard). Idempotent: `--skip-duplicate`. 4. **Bump** (stable deploys only, unless `bump-version: false`) — after publishing, the workflow commits `Patch+1` to the branch and pushes it, so the branch opens the next dev version. Skipped for prereleases (they keep the same number) and idempotent re-runs (tag already existed). So you just dispatch `release.yml` on the branch: it publishes the committed version, tags it, and (for a stable release) advances the source to the next patch — no manual version-bump commit. You only edit the version by hand to start a new major/minor cycle or a beta. (The bump-commit push needs the checkout token — `GH_PAT` — to have push rights to the branch, i.e. bypass branch protection.) @@ -62,26 +63,49 @@ Install: `dotnet tool install -g Stride.Cli`. ## Samples & template package versions -The in-repo samples are committed referencing a **clean release version** (e.g. `4.4.0`) — which is typically still *unreleased* at commit time, since the bump rides the release that publishes it (the matching packages only appear on nuget.org once `release.yml` deploys). Locally, only the `-devN` packages exist. So to build/run/edit a sample in your checkout (including opening it in GameStudio) you switch it to the local dev version, and switch back before committing — standalone targets in [`build/Stride.Samples.build`](../../build/Stride.Samples.build): +The in-repo samples are committed referencing the **release version**, the committed one with its suffix (e.g. `4.4.0` or `4.4.0-beta2`) — which is typically still *unreleased* at commit time, since the bump rides the release that publishes it (the matching packages only appear on nuget.org once `release.yml` deploys). Locally, only the `-devN` packages exist. So to build/run/edit a sample in your checkout (including opening it in GameStudio) you switch it to the local dev version, and switch back before committing — standalone targets in [`build/Stride.Samples.build`](../../build/Stride.Samples.build): ```bash -dotnet msbuild build/Stride.Samples.build -t:SamplesToDevVersion # before editing/building locally +dotnet msbuild build/Stride.Samples.build -t:SamplesToDevEngine # before editing/building locally # ... edit / build / run (e.g. in GameStudio) ... -dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseVersion # before committing +dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseEngine # before committing ``` -These do **real csproj edits** via `SetStrideVersionInProjects`: `SamplesToDevVersion` rewrites every `Stride.*` `PackageReference` from the committed clean version to this checkout's dev build (e.g. `4.4.0` → `4.4.0-dev2`) so restore/build/GameStudio resolve the local packages; `SamplesToReleaseVersion` rewrites them back. So while you work, the sample's csproj shows as modified — finalize with `SamplesToReleaseVersion` before committing. There is no eval-time override or generated props: the version in the csproj is always the real, resolvable one. +These do **real csproj edits** via `SetStrideVersionInProjects`: `SamplesToDevEngine` rewrites every `Stride.*` `PackageReference` from the committed clean version to this checkout's dev build (e.g. `4.4.0` → `4.4.0-dev2`) so restore/build/GameStudio resolve the local packages; `SamplesToReleaseEngine` rewrites them back. So while you work, the sample's csproj shows as modified — finalize with `SamplesToReleaseEngine` before committing. There is no eval-time override or generated props: the version in the csproj is always the real, resolvable one. -The release-time bump runs every sample through the **real package upgraders**: each upgrades from its committed version (e.g. `4.3.x`) to this checkout's dev build — migrating assets to the current format, rewriting `Stride.*` to `4.4.0-devN`, and restoring from the local dev feed. It leaves the csprojs at the dev version (so they still open in GameStudio) and records the content-version authority (`StrideSamplesVersion.props`); run `SamplesToReleaseVersion` to finalize at the clean version before committing: +### Template package versions + +Two kinds of template packages ([`Stride.Templates.Common.targets`](../../sources/templates/Stride.Templates.Common.targets)): + +- `Stride.Templates.Games` (NewGame + Library) is **engine-versioned** (`$(StrideNuGetVersion)`): small, packed by every build, published with every engine release. +- `Stride.Templates.Samples`, `Stride.Templates.Games.Starters` and `Stride.Templates.AssetPacks` are **content-versioned**: ~730 MB of samples that rarely change, so they have their own version and their own release. The version is `StrideSamplesVersion` in [`StrideSamplesVersion.props`](../../sources/templates/StrideSamplesVersion.props), kept out of `SharedAssemblyInfo.cs` so a samples release rebuilds only `Stride.Assets` (which embeds the file as a resource, outside its reference assembly, so its dependents don't rebuild), not every assembly. It is the **exact** content version this engine uses — never composed with the engine's prerelease suffix — and takes the engine line's major.minor with a patch part that is a content counter (`4.4.0`, `4.4.1`, …) unrelated to the engine's patch, not an engine prerelease label. Content with a stable number published for a beta engine serves the stable engine too, with no new samples release: New Project rewrites the `Stride.*` references to the running engine (a plain `dotnet new` keeps the beta it was stamped for). A prerelease content number (`4.4.1-beta1`) is only for content known not to be ready: a stable engine refuses to name it. + +**Consumers** — the GameStudio bridge (`DotNetNewTemplateBridge`) and the `stride` CLI share one resolver ([`ContentTemplateResolver`](../../sources/engine/Stride.Assets/Templates/ContentTemplateResolver.cs)): the exact `StrideSamplesVersion`, installed from the package sources once if missing (the AssetPacks are only ever obtained this way). Starters and Samples are also pinned dependencies of `Stride.GameStudio` (`[4.4.0]`, so an install fails loudly if the content is not on the feed), which is what installs them with Stride and lets the CLI read the content version of any installed engine. The one exception is a developer's own checkout: a `-devN` engine prefers this checkout's pack of the content, exactly `StrideSamplesVersion` plus its suffix (`4.4.0-beta7-dev4`), when one exists. Everything is an exact version: the resolver never takes a higher one, and the install asks NuGet for exactly that version. From local package feeds (`bin/packages`, nugetdev), a dev engine copies only that dev pack into the NuGet cache, never a local file carrying a published version; an engine without a checkout suffix (CI) copies the plain version, which is how CI tests use the branch's content. The CLI does the same for `Stride.Templates.Games`: it uses exactly the version the engine's `Stride.GameStudio` depends on, as GameStudio uses its own. Content packed for an older engine is upgraded on instantiation (New Project keeps the packed version so the package upgraders run when the project is loaded; `stride new` runs the asset compiler's `upgrade` verb); with plain `dotnet new` the project is upgraded the first time it is opened. + +**A normal build never packs the content** (it just answers GameStudio's dependency query with the committed version). To pack this checkout's samples, e.g. while editing them, set `StridePackContentTemplates=true` (`PrepareSamplesForRelease` and `release-samples.yml` do, and CI builds default to it) — the packs are always worktree-suffixed (`4.4.0-beta7-dev2`) so they can never be mistaken for the published content (`-p:StrideSamplesVersion=` packs another number, which only `release-samples.yml` uses). Turning the flag off is enough to go back to the published content: the next build removes this checkout's dev packs (`CleanContentTemplates`, which also runs by hand). It also warns about a pack in a local feed that carries a published version number, since a restore may take it over the published package: ```bash -dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion # -p:SampleVersion= to override (e.g. a -beta) -dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseVersion # finalize to the clean version, then commit +dotnet build sources/templates/Stride.Templates.Samples -p:StridePackContentTemplates=true # or set it in build/Stride.Local.props +dotnet msbuild build/Stride.Samples.build -t:CleanContentTemplates # remove the dev packs now ``` -**Template package versions** ([`Stride.Templates.Common.targets`](../../sources/templates/Stride.Templates.Common.targets)): +### Preparing and releasing the samples -- `Stride.Templates.Games` (NewGame + Library) → **engine-versioned** (`$(StrideNuGetVersion)`), always rebuilt and pushed (small). -- `Stride.Templates.Samples` + `Stride.Templates.Games.Starters` + `Stride.Templates.AssetPacks` → **content-versioned** at the committed `StrideSamplesVersion` ([`StrideSamplesVersion.props`](../../sources/templates/StrideSamplesVersion.props)); they only change (and re-publish) when the samples are actually bumped. The GameStudio bridge resolves the highest published version `<=` the engine version for these. +`StrideSamplesVersion` is never edited by hand. Sample changes are merged like any other change, and the branch keeps naming the content already on the feed, so nothing breaks in between. [`release-samples.yml`](../../.github/workflows/release-samples.yml) publishes them under a new number and only then commits that number. A samples release, start to finish: -A user instantiating any template gets their installed engine version stamped in (the package upgrader rewrites `Stride.*` references on instantiation), so a lagging template package still produces a project on the user's current Stride. +1. **Bring the samples up to the engine** when the engine changed under them: run `PrepareSamplesForRelease` (below), open the samples in GameStudio and fix what the upgraders left, run `SamplesToReleaseEngine`, and merge the result with a normal PR. Its CI packs the content and runs the template, editor and sample tests on it. Plain sample edits skip this step. +2. **Dispatch `release-samples.yml`** on the branch (`stride-release-managers` only) with `sign` and `deploy` and the `version` to publish (`X.Y.Z`, or a typed prerelease for content not ready yet). The packs are stamped for the branch's committed engine version (suffix included, e.g. `4.4.0-beta8`), shown in the run summary. An empty `version` takes the committed one's next number (`4.4.0` → `4.4.1`; the older prerelease `4.4.0-beta7` → `4.4.0`), or `.0` when the committed content is from an older line (`4.4.2` on a 4.5 branch → `4.5.0`). The major.minor must be the branch's engine line: `4.4.x` content is released from the 4.4 branch only. A newer engine may keep naming older-line content (it is upgraded on New Project) until its own first samples release. The patch part is a counter with no relation to the engine's patch (engine `4.4.0` can use samples `4.4.1`). +3. **The workflow** checks the number (valid, after the committed one, not already released), packs the three packages release-shaped (`PackageSamples` in `build/Stride.build`), runs the template smoke tests against the repo build, and signs. With `deploy`, it pushes them, tags `samples/` on the built commit, creates a GitHub Release, and last commits `StrideSamplesVersion` = `` to the branch. Without `deploy`, nothing is published or committed. +4. **Release the engine** as usual. Engines built from then on use the new content. + +`PrepareSamplesForRelease` does the mechanical part of step 1: `SamplesToDevEngine`, `UpgradeSamples` (every sample through the **real package upgraders**: migrate assets to the current format, rewrite `Stride.*` to this checkout's dev build, restore from the local dev feed), `SamplesToReleaseEngine`, and a `-devN` pack of the content to try it. It then prints the part that stays manual on purpose. + +```bash +dotnet msbuild build/Stride.Samples.build -t:PrepareSamplesForRelease # -p:SamplesEngineVersion= for the engine version the samples reference +``` + +**Checks at engine release**: an engine naming content that is not on the feed would break every New Project, so `release-deploy.yml` refuses to deploy an engine whose `StrideSamplesVersion` is not published, or that is stable while naming prerelease content. Samples changed since the published content (its `samples/` tag) only produce a warning, with the file list in the run summary: the engine ships with the published content, which is fine for work not released yet, and the release manager decides whether a samples release should come first. A PR that changes the samples gets a warning from the change-detection job ([`changes.yml`](../../.github/workflows/changes.yml)) as a reminder that a samples release is pending; local builds never check this (local content packs are always `-devN`). + +CI builds (`CI`, `GITHUB_ACTIONS` or `TF_BUILD` set, except engine release builds) pack the content by default, so the tests that instantiate templates (`Stride.Templates.Tests`, `tests/editor`, `tests/enduser/Stride.Samples.Tests`, `Stride.Packaging.Tests`) run on this checkout's samples and don't depend on the content being published. Locally they use the published content unless the flag is on. The flag is only ever a normal property (Local.props, command line, CI default), never set on a single `ProjectReference`: that would build the template projects, and everything they reference, a second time with different global properties. + +GameStudio never downloads content while it starts. A missing Starters or Samples package is downloaded in the background and its templates appear in New Project once ready; AssetPacks are downloaded when the New Game dialog first offers them. diff --git a/samples/Games/JumpyJet/JumpyJet.Game/CharacterScript.cs b/samples/Games/JumpyJet/JumpyJet.Game/CharacterScript.cs index 9a9304322..b34951135 100644 --- a/samples/Games/JumpyJet/JumpyJet.Game/CharacterScript.cs +++ b/samples/Games/JumpyJet/JumpyJet.Game/CharacterScript.cs @@ -124,7 +124,7 @@ namespace JumpyJet if (!isRunning) continue; - var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; // apply impulse on the touch/space if (Input.IsKeyPressed(Keys.Space) || UserTappedScreen()) diff --git a/samples/Games/JumpyJet/JumpyJet.Game/PipesScript.cs b/samples/Games/JumpyJet/JumpyJet.Game/PipesScript.cs index 2371ba173..22467489a 100644 --- a/samples/Games/JumpyJet/JumpyJet.Game/PipesScript.cs +++ b/samples/Games/JumpyJet/JumpyJet.Game/PipesScript.cs @@ -66,7 +66,7 @@ namespace JumpyJet if (!isScrolling) return; - var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; for (int i = 0; i < pipeSets.Count; i++) { diff --git a/samples/Games/SpaceEscape/SpaceEscape.Game/Background/BackgroundScript.cs b/samples/Games/SpaceEscape/SpaceEscape.Game/Background/BackgroundScript.cs index 9f1c6e9b6..96842d804 100644 --- a/samples/Games/SpaceEscape/SpaceEscape.Game/Background/BackgroundScript.cs +++ b/samples/Games/SpaceEscape/SpaceEscape.Game/Background/BackgroundScript.cs @@ -78,7 +78,7 @@ namespace SpaceEscape.Background if (!isScrolling) return; - var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; // Check if needed to remove the first block var firstBlock = levelBlocks[0]; diff --git a/samples/Graphics/AnimatedModel/AnimatedModel.Game/RotateEntity.cs b/samples/Graphics/AnimatedModel/AnimatedModel.Game/RotateEntity.cs index 58f5143a5..2562d1ebe 100644 --- a/samples/Graphics/AnimatedModel/AnimatedModel.Game/RotateEntity.cs +++ b/samples/Graphics/AnimatedModel/AnimatedModel.Game/RotateEntity.cs @@ -24,7 +24,7 @@ namespace AnimatedModel rotationSpeed = 200f * Input.PointerEvents.Sum(x => x.DeltaPosition.X); rotationSpeed *= 0.93f; - var elapsedTime = (float) Game.UpdateTime.Elapsed.TotalSeconds; + var elapsedTime = (float) Game.UpdateTime.WarpElapsed.TotalSeconds; Entity.Transform.Rotation *= Quaternion.RotationY(rotationSpeed * elapsedTime); } } diff --git a/samples/Graphics/SpriteStudioDemo/SpriteStudioDemo.Game/PlayerScript.cs b/samples/Graphics/SpriteStudioDemo/SpriteStudioDemo.Game/PlayerScript.cs index 0322c1b09..792ba69a9 100644 --- a/samples/Graphics/SpriteStudioDemo/SpriteStudioDemo.Game/PlayerScript.cs +++ b/samples/Graphics/SpriteStudioDemo/SpriteStudioDemo.Game/PlayerScript.cs @@ -103,7 +103,7 @@ namespace SpriteStudioDemo if (inputState == InputState.RunLeft || inputState == InputState.RunRight) { // Update Agent's position - var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; Entity.Transform.Position.X += ((inputState == InputState.RunRight) ? AgentMoveDistance : -AgentMoveDistance) * dt; diff --git a/samples/Input/TouchInputs/TouchInputs.Game/TouchInputsScript.cs b/samples/Input/TouchInputs/TouchInputs.Game/TouchInputsScript.cs index 318aa1ead..3e83a6517 100644 --- a/samples/Input/TouchInputs/TouchInputs.Game/TouchInputsScript.cs +++ b/samples/Input/TouchInputs/TouchInputs.Game/TouchInputsScript.cs @@ -81,7 +81,7 @@ namespace TouchInputs public override void Update() { - var currentTime = Game.DrawTime.Total; + var currentTime = Game.UpdateTime.Total; keyDown = ""; keyEvents = ""; @@ -248,7 +248,7 @@ namespace TouchInputs { var screenSize = spriteBatch.VirtualResolution.Value; var position = tuple.Item1; - var duration = Game.DrawTime.Total - tuple.Item2; + var duration = Game.UpdateTime.Total - tuple.Item2; var scale = (float)(0.2f * (1f - duration.TotalSeconds / displayPointerDuration.TotalSeconds)); var pointerScreenPosition = new Vector2(position.X * screenSize.X, position.Y * screenSize.Y); diff --git a/samples/Particles/ParticlesSample/ParticlesSample.Game/PrefabInstance.cs b/samples/Particles/ParticlesSample/ParticlesSample.Game/PrefabInstance.cs index fae389fba..8737391a1 100644 --- a/samples/Particles/ParticlesSample/ParticlesSample.Game/PrefabInstance.cs +++ b/samples/Particles/ParticlesSample/ParticlesSample.Game/PrefabInstance.cs @@ -115,7 +115,7 @@ namespace ParticlesSample while (secondsCountdown > 0f) { await Script.NextFrame(); - secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; } // Remove diff --git a/samples/Particles/ParticlesSample/ParticlesSample.Game/RotateEntity.cs b/samples/Particles/ParticlesSample/ParticlesSample.Game/RotateEntity.cs index fe460ef02..39d875b8f 100644 --- a/samples/Particles/ParticlesSample/ParticlesSample.Game/RotateEntity.cs +++ b/samples/Particles/ParticlesSample/ParticlesSample.Game/RotateEntity.cs @@ -24,7 +24,7 @@ namespace ParticlesSample rotationSpeed = 200f * Input.PointerEvents.Sum(x => x.DeltaPosition.X); rotationSpeed *= 0.93f; - var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; Entity.Transform.Rotation *= Quaternion.RotationY(rotationSpeed * elapsedTime); } } diff --git a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/AnimationController.cs b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/AnimationController.cs index b610bf064..66d320e05 100644 --- a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/AnimationController.cs +++ b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/AnimationController.cs @@ -123,7 +123,7 @@ namespace FirstPersonShooter.Player // Update current animation var currentTicks = TimeSpan.FromTicks((long)(currentTime * currentClip.Duration.Ticks)); - var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor); + var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor); var currentClipFinished = (updatedTicks >= currentClip.Duration.Ticks); diff --git a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/WeaponScript.cs b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/WeaponScript.cs index 7b3b8e10e..f60dec6f9 100644 --- a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/WeaponScript.cs +++ b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Player/WeaponScript.cs @@ -56,7 +56,7 @@ namespace FirstPersonShooter.Player while (secondsCountdown > 0f) { await Script.NextFrame(); - secondsCountdown -= (float) Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float) Game.UpdateTime.WarpElapsed.TotalSeconds; } remainingBullets = 9; @@ -77,7 +77,7 @@ namespace FirstPersonShooter.Player bool didReload; reloadEvent.TryReceive(out didReload); - cooldownRemaining = (cooldownRemaining > 0) ? (cooldownRemaining - (float)this.Game.UpdateTime.Elapsed.TotalSeconds) : 0f; + cooldownRemaining = (cooldownRemaining > 0) ? (cooldownRemaining - (float)this.Game.UpdateTime.WarpElapsed.TotalSeconds) : 0f; if (cooldownRemaining > 0) return; // Can't shoot yet diff --git a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Trigger/TriggerScript.cs b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Trigger/TriggerScript.cs index ee6bd968a..bbb66f8e3 100644 --- a/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Trigger/TriggerScript.cs +++ b/samples/Templates/FirstPersonShooter/FirstPersonShooter/FirstPersonShooter.Game/Trigger/TriggerScript.cs @@ -59,7 +59,7 @@ namespace FirstPersonShooter while (secondsCountdown > 0f) { await Script.NextFrame(); - secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; } // Remove diff --git a/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/Gameplay/CoinRotation.cs b/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/Gameplay/CoinRotation.cs index 4c61dd90e..e8c11a7af 100644 --- a/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/Gameplay/CoinRotation.cs +++ b/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/Gameplay/CoinRotation.cs @@ -19,7 +19,7 @@ public class CoinRotation : SyncScript public override void Update() { - animationTimer += (float) Game.UpdateTime.Elapsed.TotalSeconds; + animationTimer += (float) Game.UpdateTime.WarpElapsed.TotalSeconds; if (animationTimer >= animationInterval) { diff --git a/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/PlayerController.cs b/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/PlayerController.cs index 273f1684e..d136ea48e 100644 --- a/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/PlayerController.cs +++ b/samples/Templates/Platformer2D/Platformer2D/Platformer2D.Game/PlayerController.cs @@ -93,7 +93,7 @@ public class PlayerController : SyncScript private void HandleAnimation(bool isMoving) { - animationTimer += (float) Game.UpdateTime.Elapsed.TotalSeconds; + animationTimer += (float) Game.UpdateTime.WarpElapsed.TotalSeconds; if (CharacterComponent.IsGrounded && !isMoving) { diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Guard.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Guard.fbx deleted file mode 100644 index 0769b8117..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Guard.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb81f0291d24d00db1dbaaa27feffa02112aa2bea36b3ce72d6b5d9590e94488 -size 697936 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Idle.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Idle.fbx deleted file mode 100644 index 54565c6f5..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Idle.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a4baf16263d68608d2b305656b7211d6379db2907aac7f0462997855a056db5 -size 1220368 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_End.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_End.fbx deleted file mode 100644 index 19c1ec38e..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_End.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8aeb50db7fe129ada00bc43b0287cf85ad4d50c1948881429d7d78b7ca43b93 -size 678384 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Loop.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Loop.fbx deleted file mode 100644 index b73f6a254..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Loop.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:733a32e804520e5cb31ef0f2c896c1ff052a40efddd3316f026cd87c65adef64 -size 707040 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Start.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Start.fbx deleted file mode 100644 index 951c5209f..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Jump_Start.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:297263f9e24d27c1751a47a0fc8ebd94d62dca7c44ee60b1aaee9a9fc7e83730 -size 567584 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Punch.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Punch.fbx deleted file mode 100644 index 46f9a3efc..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Punch.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cbf666ae6e21d726de3142bb392ec3bc474264f1b9b98f7b4bb9fd7c349230d -size 626160 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Run.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Run.fbx deleted file mode 100644 index 7abfcbda1..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Run.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3480e7724f490cacfce3595b006c49f7cdb44a8d7a49b83335bdb94d6a35a113 -size 666704 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Walk.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Walk.fbx deleted file mode 100644 index e06cb1e90..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Animations/Walk.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5c41391150ff639bf50d57e8c303936c02bcddcb3feecbf59961b25bcecb60a2 -size 835840 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Model/StrideMannequin.fbx b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Model/StrideMannequin.fbx deleted file mode 100644 index 7e170455a..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/Resources/Model/StrideMannequin.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e91084566966add6697b6aeaa00135e00b412d9865efce8e23d8b254cb689c3 -size 34488508 diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/BasicCameraController.cs b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/BasicCameraController.cs deleted file mode 100644 index d27a23eb4..000000000 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/BasicCameraController.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) -// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System; -using Stride.Core; -using Stride.Core.Mathematics; -using Stride.Engine; -using Stride.Input; - -namespace ThirdPersonPlatformer -{ - /// - /// A script that allows to move and rotate an entity through keyboard, mouse and touch input to provide basic camera navigation. - /// - /// - /// The entity can be moved using W, A, S, D, Q and E, arrow keys or dragging/scaling using multi-touch. - /// Rotation is achieved using the Numpad, the mouse while holding the right mouse button, or dragging using single-touch. - /// - public class BasicCameraController : SyncScript - { - private const float MaximumPitch = MathUtil.PiOverTwo * 0.99f; - - private Vector3 upVector; - private Vector3 translation; - private float yaw; - private float pitch; - - public Vector3 KeyboardMovementSpeed { get; set; } = new Vector3(5.0f); - - public Vector3 TouchMovementSpeed { get; set; } = new Vector3(40, 40, 20); - - public float SpeedFactor { get; set; } = 5.0f; - - public Vector2 KeyboardRotationSpeed { get; set; } = new Vector2(3.0f); - - public Vector2 MouseRotationSpeed { get; set; } = new Vector2(90.0f, 60.0f); - - public Vector2 TouchRotationSpeed { get; set; } = new Vector2(60.0f, 40.0f); - - public override void Start() - { - base.Start(); - - // Default up-direction - upVector = Vector3.UnitY; - - // Configure touch input - if (!Platform.IsWindowsDesktop) - { - Input.Gestures.Add(new GestureConfigDrag()); - Input.Gestures.Add(new GestureConfigComposite()); - } - } - - public override void Update() - { - ProcessInput(); - UpdateTransform(); - } - - private void ProcessInput() - { - translation = Vector3.Zero; - yaw = 0; - pitch = 0; - - // Move with keyboard - if (Input.IsKeyDown(Keys.W) || Input.IsKeyDown(Keys.Up)) - { - translation.Z = -KeyboardMovementSpeed.Z; - } - else if (Input.IsKeyDown(Keys.S) || Input.IsKeyDown(Keys.Down)) - { - translation.Z = KeyboardMovementSpeed.Z; - } - - if (Input.IsKeyDown(Keys.A) || Input.IsKeyDown(Keys.Left)) - { - translation.X = -KeyboardMovementSpeed.X; - } - else if (Input.IsKeyDown(Keys.D) || Input.IsKeyDown(Keys.Right)) - { - translation.X = KeyboardMovementSpeed.X; - } - - if (Input.IsKeyDown(Keys.Q)) - { - translation.Y = -KeyboardMovementSpeed.Y; - } - else if (Input.IsKeyDown(Keys.E)) - { - translation.Y = KeyboardMovementSpeed.Y; - } - - // Alternative translation speed - if (Input.IsKeyDown(Keys.LeftShift) || Input.IsKeyDown(Keys.RightShift)) - { - translation *= SpeedFactor; - } - - // Rotate with keyboard - if (Input.IsKeyDown(Keys.NumPad2)) - { - pitch = KeyboardRotationSpeed.X; - } - else if (Input.IsKeyDown(Keys.NumPad8)) - { - pitch = -KeyboardRotationSpeed.X; - } - - if (Input.IsKeyDown(Keys.NumPad4)) - { - yaw = KeyboardRotationSpeed.Y; - } - else if (Input.IsKeyDown(Keys.NumPad6)) - { - yaw = -KeyboardRotationSpeed.Y; - } - - // Rotate with mouse - if (Input.IsMouseButtonDown(MouseButton.Right)) - { - Input.LockMousePosition(); - Game.IsMouseVisible = false; - - yaw = -Input.MouseDelta.X * MouseRotationSpeed.X; - pitch = -Input.MouseDelta.Y * MouseRotationSpeed.Y; - } - else - { - Input.UnlockMousePosition(); - Game.IsMouseVisible = true; - } - - // Handle gestures - foreach (var gestureEvent in Input.GestureEvents) - { - switch (gestureEvent.Type) - { - // Rotate by dragging - case GestureType.Drag: - var drag = (GestureEventDrag)gestureEvent; - var dragDistance = drag.DeltaTranslation; - yaw = -dragDistance.X * TouchRotationSpeed.X; - pitch = -dragDistance.Y * TouchRotationSpeed.Y; - break; - - // Move along z-axis by scaling and in xy-plane by multi-touch dragging - case GestureType.Composite: - var composite = (GestureEventComposite)gestureEvent; - translation.X = -composite.DeltaTranslation.X * TouchMovementSpeed.X; - translation.Y = -composite.DeltaTranslation.Y * TouchMovementSpeed.Y; - translation.Z = -(float)Math.Log(composite.DeltaScale + 1) * TouchMovementSpeed.Z; - break; - } - } - } - - private void UpdateTransform() - { - var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; - - translation *= elapsedTime; - yaw *= elapsedTime; - pitch *= elapsedTime; - - // Get the local coordinate system - var rotation = Matrix.RotationQuaternion(Entity.Transform.Rotation); - - // Enforce the global up-vector by adjusting the local x-axis - var right = Vector3.Cross(rotation.Forward, upVector); - var up = Vector3.Cross(right, rotation.Forward); - - // Stabilize - right.Normalize(); - up.Normalize(); - - // Adjust pitch. Prevent it from exceeding up and down facing. Stabilize edge cases. - var currentPitch = MathUtil.PiOverTwo - (float)Math.Acos(Vector3.Dot(rotation.Forward, upVector)); - pitch = MathUtil.Clamp(currentPitch + pitch, -MaximumPitch, MaximumPitch) - currentPitch; - - // Move in local coordinates - Entity.Transform.Position += Vector3.TransformCoordinate(translation, rotation); - - // Yaw around global up-vector, pitch and roll in local space - Entity.Transform.Rotation *= Quaternion.RotationAxis(right, pitch) * Quaternion.RotationAxis(upVector, yaw); - } - } -} diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/AnimationController.cs b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/AnimationController.cs index 174e2d15c..a3ab4816a 100644 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/AnimationController.cs +++ b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/AnimationController.cs @@ -139,8 +139,7 @@ namespace ThirdPersonPlatformer.Player animationClipWalkLerp2 = AnimationRun; } - // Use DrawTime rather than UpdateTime - var time = Game.DrawTime; + var time = Game.UpdateTime; // This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration long blendedMaxDuration = 0; blendedMaxDuration = @@ -150,7 +149,7 @@ namespace ThirdPersonPlatformer.Player currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero - : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % + : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) % blendedMaxDuration); currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration); @@ -160,7 +159,7 @@ namespace ThirdPersonPlatformer.Player { var speedFactor = 1; var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpStart.Duration.Ticks)); - var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor); + var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor); if (updatedTicks < AnimationJumpStart.Duration.Ticks) { @@ -177,10 +176,9 @@ namespace ThirdPersonPlatformer.Player private void UpdateAirborne() { - // Use DrawTime rather than UpdateTime - var time = Game.DrawTime; + var time = Game.UpdateTime; var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpMid.Duration.Ticks)); - currentTicks = TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % + currentTicks = TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) % AnimationJumpMid.Duration.Ticks); currentTime = ((double)currentTicks.Ticks / (double)AnimationJumpMid.Duration.Ticks); } @@ -189,7 +187,7 @@ namespace ThirdPersonPlatformer.Player { var speedFactor = 1; var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpEnd.Duration.Ticks)); - var updatedTicks = currentTicks.Ticks + (long) (Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor); + var updatedTicks = currentTicks.Ticks + (long) (Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor); if (updatedTicks < AnimationJumpEnd.Duration.Ticks) { diff --git a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/PlayerController.cs b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/PlayerController.cs index 498bfeeae..2df7f0dbe 100644 --- a/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/PlayerController.cs +++ b/samples/Templates/ThirdPersonPlatformer/ThirdPersonPlatformer/ThirdPersonPlatformer.Game/Player/PlayerController.cs @@ -61,7 +61,6 @@ namespace ThirdPersonPlatformer.Player /// public override void Update() { - // var dt = Game.UpdateTime.Elapsed.Milliseconds * 0.001; Move(MaxRunSpeed); Jump(); diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Core/Utils.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Core/Utils.cs index e6527c090..58e6d5694 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Core/Utils.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Core/Utils.cs @@ -77,7 +77,7 @@ namespace TopDownRPG.Core while (secondsCountdown > 0f) { await script.Script.NextFrame(); - secondsCountdown -= (float)script.Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float)script.Game.UpdateTime.WarpElapsed.TotalSeconds; } // Remove diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CoinScript.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CoinScript.cs index 366162b3b..206b4371f 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CoinScript.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CoinScript.cs @@ -43,7 +43,7 @@ namespace TopDownRPG.Gameplay public void UpdateAnimation() { - var dt = (float) Game.UpdateTime.Elapsed.TotalSeconds; + var dt = (float) Game.UpdateTime.WarpElapsed.TotalSeconds; Entity.Transform.Rotation *= Quaternion.RotationMatrix(Matrix.RotationY(spinSpeed * (float)(dt * Math.PI))); if (!activated) diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CrateScript.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CrateScript.cs index 84b1ed9e6..53f28347f 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CrateScript.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/CrateScript.cs @@ -46,7 +46,7 @@ namespace TopDownRPG.Gameplay if (!activated) return; - var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; animationTime += dt * 8; var coinHeight = Math.Max(0, Math.Sin(animationTime)); diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/LootCoinScript.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/LootCoinScript.cs index 9d3d164fd..07932bb25 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/LootCoinScript.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Gameplay/LootCoinScript.cs @@ -44,7 +44,7 @@ namespace TopDownRPG.Gameplay public void UpdateAnimation() { - var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; if (!activated) return; diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/AnimationController.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/AnimationController.cs index 9dfe874a3..535e5c2cb 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/AnimationController.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/AnimationController.cs @@ -121,8 +121,7 @@ namespace TopDownRPG.Player animationClipWalkLerp2 = AnimationRun; } - // Use DrawTime rather than UpdateTime - var time = Game.DrawTime; + var time = Game.UpdateTime; // This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration long blendedMaxDuration = 0; blendedMaxDuration = @@ -132,7 +131,7 @@ namespace TopDownRPG.Player currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero - : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % + : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) % blendedMaxDuration); currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration); @@ -142,7 +141,7 @@ namespace TopDownRPG.Player { var speedFactor = 1; var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationPunch.Duration.Ticks)); - var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor); + var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor); if (updatedTicks < AnimationPunch.Duration.Ticks) { diff --git a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/PlayerController.cs b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/PlayerController.cs index 9d1543a58..6372b16da 100644 --- a/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/PlayerController.cs +++ b/samples/Templates/TopDownRPG/TopDownRPG/TopDownRPG.Game/Player/PlayerController.cs @@ -123,7 +123,7 @@ namespace TopDownRPG.Player private void Attack() { - var dt = (float) Game.UpdateTime.Elapsed.TotalSeconds; + var dt = (float) Game.UpdateTime.WarpElapsed.TotalSeconds; attackCooldown = (attackCooldown > 0) ? attackCooldown - dt : 0f; PunchCollision.Enabled = (attackCooldown > 0); diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/DeltaTimeDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/DeltaTimeDemo.cs index abfe9c572..4c31223cf 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/DeltaTimeDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/DeltaTimeDemo.cs @@ -30,7 +30,7 @@ namespace CSharpBeginner.Code public override void Update() { /// We can access Delta time through the static 'Game' object. - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; // We update the total time totalTime += deltaTime; diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/KeyboardInputDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/KeyboardInputDemo.cs index 2b0ef4193..f48dc6f69 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/KeyboardInputDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/KeyboardInputDemo.cs @@ -27,7 +27,7 @@ namespace CSharpBeginner.Code DebugText.Print("Hold the 1 key down to rotate the blue teapot", new Int2(340, 500)); if (Input.IsKeyDown(Keys.D1)) { - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.3f * deltaTime); } diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/LerpDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/LerpDemo.cs index 211508ced..6c457833d 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/LerpDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/LerpDemo.cs @@ -30,7 +30,7 @@ namespace CSharpBeginner.Code public override void Update() { // Keep track of elapsed time - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; elapsedTime += deltaTime; // In order to make use of the lerp method, we need to calculate the 'interpolation value': a value going from 0 to 1. diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/MouseInputDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/MouseInputDemo.cs index 53010f74a..bf3d553f8 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/MouseInputDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/MouseInputDemo.cs @@ -30,7 +30,7 @@ namespace CSharpBeginner.Code DebugText.Print("Hold the left mouse button down to rotate the blue teapot", new Int2(400, 600)); if (Input.IsMouseButtonDown(MouseButton.Left)) { - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.4f * deltaTime); } diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/RemoveEntitiesDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/RemoveEntitiesDemo.cs index d177b4d69..7b4f4c8de 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/RemoveEntitiesDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/RemoveEntitiesDemo.cs @@ -49,7 +49,7 @@ namespace CSharpBeginner.Code public override void Update() { // We use a simple timer - timer += (float)Game.UpdateTime.Elapsed.TotalSeconds; + timer += (float)Game.UpdateTime.WarpElapsed.TotalSeconds; if (timer > currentTimer) { // If the entities exist, we remove them from the scene diff --git a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/VirtualButtonsDemo.cs b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/VirtualButtonsDemo.cs index 8d101f26b..42b02eb27 100644 --- a/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/VirtualButtonsDemo.cs +++ b/samples/Tutorials/CSharpBeginner/CSharpBeginner/CSharpBeginner.Game/Code/VirtualButtonsDemo.cs @@ -49,7 +49,7 @@ namespace CSharpBeginner.Code // Note: Gamepad sticks can be a negative value. For this example we only check if the value is higher than 0 if (forward > 0) { - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.6f * forward * deltaTime); } diff --git a/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/11_Navigation/NavigateCharacter.cs b/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/11_Navigation/NavigateCharacter.cs index 60f463d3e..c2e607a7f 100644 --- a/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/11_Navigation/NavigateCharacter.cs +++ b/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/11_Navigation/NavigateCharacter.cs @@ -48,7 +48,7 @@ namespace CSharpIntermediate.Code return; } - var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; var curPosition = RegularCharacter.Transform.WorldMatrix.TranslationVector; var nextWaypointPosition = waypoints[waypointIndex]; var distanceToWaypoint = Vector3.Distance(curPosition, nextWaypointPosition); diff --git a/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/Utils/OmniDirectionMovement.cs b/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/Utils/OmniDirectionMovement.cs index 014855849..2ff26401a 100644 --- a/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/Utils/OmniDirectionMovement.cs +++ b/samples/Tutorials/CSharpIntermediate/CSharpIntermediate/CSharpIntermediate.Game/Utils/OmniDirectionMovement.cs @@ -55,7 +55,7 @@ namespace CSharpIntermediate.Code movement.X -= 1; } - var delta = (float)Game.UpdateTime.Elapsed.TotalSeconds; + var delta = (float)Game.UpdateTime.WarpElapsed.TotalSeconds; Entity.Transform.Position += movement * delta * MoveSpeed; } } diff --git a/samples/UI/UIParticles/UIParticles.Game/SplashScript.cs b/samples/UI/UIParticles/UIParticles.Game/SplashScript.cs index f7da3a989..9734128ac 100644 --- a/samples/UI/UIParticles/UIParticles.Game/SplashScript.cs +++ b/samples/UI/UIParticles/UIParticles.Game/SplashScript.cs @@ -211,7 +211,7 @@ namespace UIParticles while (secondsCountdown > 0f) { await Script.NextFrame(); - secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; } // Remove @@ -283,7 +283,7 @@ namespace UIParticles private void DrawFuse() { - fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.Elapsed.TotalSeconds * 0.04f); + fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.WarpElapsed.TotalSeconds * 0.04f); var gaugeCurrentRegion = lifeBarGaugeImage.Region; gaugeCurrentRegion.Width = Math.Max(1, fusePercentage * gaugeBarRegion.Width); @@ -323,7 +323,7 @@ namespace UIParticles private void NewGameState() { - fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.Elapsed.TotalSeconds * 0.03f); + fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.WarpElapsed.TotalSeconds * 0.03f); DrawFuse(); if (desiredState == GameState.EndGame || fusePercentage <= 0f) diff --git a/sources/assets/Stride.Core.Assets/AssetRegistry.cs b/sources/assets/Stride.Core.Assets/AssetRegistry.cs index 3793f2130..8b7e75c5d 100644 --- a/sources/assets/Stride.Core.Assets/AssetRegistry.cs +++ b/sources/assets/Stride.Core.Assets/AssetRegistry.cs @@ -791,54 +791,70 @@ public static class AssetRegistry RegisteredAssetAssemblies.Remove(assembly); - foreach (var typeToRemove in RegisteredDefaultAssetExtension.Keys.Where(type => type.Assembly == assembly).ToList()) + foreach (var typeToRemove in RegisteredDefaultAssetExtension.Keys.Where(type => type.Assembly == assembly)) { RegisteredDefaultAssetExtension.Remove(typeToRemove); } - foreach (var typeToRemove in AssetTypes.ToList().Where(type => type.Assembly == assembly)) + AssetTypes.RemoveWhere(type => type.Assembly == assembly); + + RegisteredPackageSessionAnalysisTypes.RemoveWhere(type => type.Assembly == assembly); + + for (var i = RegisteredImportersInternal.Count - 1; i >= 0; i--) { - AssetTypes.Remove(typeToRemove); + if (RegisteredImportersInternal[i].GetType().Assembly == assembly) + RegisteredImportersInternal.RemoveAt(i); } - foreach (var typeToRemove in RegisteredPackageSessionAnalysisTypes.Where(type => type.Assembly == assembly).ToList()) - { - RegisteredPackageSessionAnalysisTypes.Remove(typeToRemove); - } - - foreach (var instance in RegisteredImportersInternal.Where(instance => instance.GetType().Assembly == assembly).ToList()) - { - RegisteredImportersInternal.Remove(instance); - } - - foreach (var typeToRemove in RegisteredFormatVersions.Keys.Where(type => type.Assembly == assembly).ToList()) + foreach (var typeToRemove in RegisteredFormatVersions.Keys.Where(type => type.Assembly == assembly)) { RegisteredFormatVersions.Remove(typeToRemove); } - foreach (var typeToRemove in RegisteredAssetUpgraders.Keys.Where(type => type.Key.Assembly == assembly).ToList()) + foreach (var typeToRemove in RegisteredAssetUpgraders.Keys.Where(type => type.Key.Assembly == assembly)) { RegisteredAssetUpgraders.Remove(typeToRemove); } - foreach (var extensionToRemove in RegisteredAssetFileExtensions.Where(keyValue => keyValue.Value.Assembly == assembly).Select(keyValue => keyValue.Key).ToList()) + foreach (var extensionToRemove in RegisteredAssetFileExtensions.Where(keyValue => keyValue.Value.Assembly == assembly).Select(keyValue => keyValue.Key)) { RegisteredAssetFileExtensions.Remove(extensionToRemove); } - foreach (var upgraderToRemove in RegisteredPackageUpgraders.Where(keyValue => keyValue.Value.GetType().Assembly == assembly).Select(keyValue => keyValue.Key).ToList()) + lock (AlwaysMarkAsRootAssetTypes) + { + AlwaysMarkAsRootAssetTypes.RemoveWhere(type => type.Assembly == assembly); + } + + // An asset type of this assembly can target a content type of another one (an engine type, for example) + foreach (var (contentType, assetTypes) in ContentToAssetTypes) + { + assetTypes.RemoveAll(type => type.Assembly == assembly); + if (assetTypes.Count == 0 || contentType.Assembly == assembly) + ContentToAssetTypes.Remove(contentType); + } + + foreach (var typeToRemove in AssetToContentTypes.Keys.Where(type => type.Assembly == assembly)) + { + AssetToContentTypes.Remove(typeToRemove); + } + + foreach (var upgraderToRemove in RegisteredPackageUpgraders.Where(keyValue => keyValue.Value.GetType().Assembly == assembly).Select(keyValue => keyValue.Key)) { RegisteredPackageUpgraders.Remove(upgraderToRemove); } - foreach (var instance in RegisteredSerializerFactories.Where(instance => instance.GetType().Assembly == assembly).ToList()) + foreach (var pair in RegisteredAssetFactories.Where(pair => pair.Value.GetType().Assembly == assembly)) { - RegisteredSerializerFactories.Remove(instance); + RegisteredAssetFactories.Remove(pair.Key); } - foreach (var instance in RegisteredDataVisitNodes.Where(instance => instance.GetType().Assembly == assembly).ToList()) + RegisteredSerializerFactories.RemoveWhere(instance => instance.GetType().Assembly == assembly); + + for (var i = RegisteredDataVisitNodes.Count - 1; i >= 0; i--) { - RegisteredDataVisitNodes.Remove(instance); + if (RegisteredDataVisitNodes[i].GetType().Assembly == assembly) + RegisteredDataVisitNodes.RemoveAt(i); } } } diff --git a/sources/assets/Stride.Core.Assets/PackageSession.Dependencies.cs b/sources/assets/Stride.Core.Assets/PackageSession.Dependencies.cs index bf1435ce3..de304f7e3 100644 --- a/sources/assets/Stride.Core.Assets/PackageSession.Dependencies.cs +++ b/sources/assets/Stride.Core.Assets/PackageSession.Dependencies.cs @@ -80,8 +80,9 @@ partial class PackageSession /// Cheap scan for projects with a pending package upgrade that may carry source-code migrations. /// Reuses the cached MSBuild project (no restore) and the same upgrade-needed checks as /// : a direct PackageReference whose declared version - /// is below the upgrader's target. Returns one entry per (project, upgrader); the runner does the - /// version-gate and only opens a workspace when source rules actually apply. + /// is below the upgrader's target. Returns one entry per (project, upgrader) for the projects whose + /// upgrade was confirmed; the runner does the version-gate and only opens a workspace when source + /// rules actually apply. /// private List DetectPendingCodeUpgrades(ILogger log, PackageLoadParameters loadParameters) { @@ -105,6 +106,8 @@ partial class PackageSession continue; } + var projectCodeUpgrades = new List(); + var pendingPackageUpgrades = new List(); var seen = new HashSet(); foreach (var packageReference in msProject.GetItems("PackageReference")) { @@ -123,8 +126,13 @@ partial class PackageSession if (!seen.Add(upgrader)) continue; - result.Add(new PendingCodeUpgrade(upgrader, project.FullPath, range.MinVersion)); + projectCodeUpgrades.Add(new PendingCodeUpgrade(upgrader, project.FullPath, range.MinVersion)); + pendingPackageUpgrades.Add(new PendingPackageUpgrade(upgrader, new PackageDependency(packageReference.EvaluatedInclude, range), null)); } + + // The source migration writes user files, so it needs the same confirmation as the package upgrade. + if (projectCodeUpgrades.Count > 0 && PackageLoadParameters.ShouldUpgrade(RequestPackageUpgrade(project.Package, pendingPackageUpgrades, loadParameters))) + result.AddRange(projectCodeUpgrades); } return result; } @@ -275,17 +283,8 @@ partial class PackageSession if (pendingPackageUpgrades.Count > 0) { - var upgradeAllowed = packageUpgradeAllowed != false ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade; - // Need upgrades, let's ask user confirmation - if (loadParameters.PackageUpgradeRequested != null && !packageUpgradeAllowed.HasValue) - { - upgradeAllowed = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades); - if (upgradeAllowed == PackageUpgradeRequestedAnswer.UpgradeAll) - packageUpgradeAllowed = true; - if (upgradeAllowed == PackageUpgradeRequestedAnswer.DoNotUpgradeAny) - packageUpgradeAllowed = false; - } + var upgradeAllowed = RequestPackageUpgrade(package, pendingPackageUpgrades, loadParameters); if (!PackageLoadParameters.ShouldUpgrade(upgradeAllowed)) { diff --git a/sources/assets/Stride.Core.Assets/PackageSession.cs b/sources/assets/Stride.Core.Assets/PackageSession.cs index 72c5c402e..b86d32497 100644 --- a/sources/assets/Stride.Core.Assets/PackageSession.cs +++ b/sources/assets/Stride.Core.Assets/PackageSession.cs @@ -484,6 +484,7 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder private AssetDependencyManager dependencies; private AssetSourceTracker sourceTracker; private bool? packageUpgradeAllowed; + private readonly Dictionary packageUpgradeAnswers = []; public event DirtyFlagChangedDelegate AssetDirtyChanged; private TaskCompletionSource saveCompletion; @@ -996,6 +997,31 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder /// public void DisarmUpgradeBackup() => UpgradeBackup = null; + // Asks the front-end at most once per package whether to upgrade it. Every upgrade step (source migration, + // project references, assets) goes through here, so nothing is written before the user has answered. + private PackageUpgradeRequestedAnswer RequestPackageUpgrade(Package package, IList pendingPackageUpgrades, PackageLoadParameters loadParameters) + { + if (packageUpgradeAllowed is { } allowed) + return allowed ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade; + if (loadParameters.PackageUpgradeRequested is null) + return PackageUpgradeRequestedAnswer.Upgrade; + if (packageUpgradeAnswers.TryGetValue(package, out var answer)) + return answer; + + answer = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades); + packageUpgradeAnswers[package] = answer; + if (answer == PackageUpgradeRequestedAnswer.UpgradeAll) + packageUpgradeAllowed = true; + if (answer == PackageUpgradeRequestedAnswer.DoNotUpgradeAny) + packageUpgradeAllowed = false; + + // The callback may have opted out of the backup (e.g. the GameStudio dialog's checkbox). + if (!loadParameters.BackupBeforeUpgrade) + DisarmUpgradeBackup(); + + return answer; + } + /// /// Make sure packages have their dependencies loaded. /// @@ -1498,6 +1524,7 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder packagesCopy.Remove(package); pendingPackageUpgradesPerPackage.Remove(package); + packageUpgradeAnswers.Remove(package); IsDirty = true; } @@ -1596,22 +1623,8 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder if (pendingPackageUpgrades.Count > 0) { - var upgradeAllowed = packageUpgradeAllowed != false ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade; - // Need upgrades, let's ask user confirmation - if (loadParameters.PackageUpgradeRequested is not null && !packageUpgradeAllowed.HasValue) - { - upgradeAllowed = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades); - if (upgradeAllowed == PackageUpgradeRequestedAnswer.UpgradeAll) - packageUpgradeAllowed = true; - if (upgradeAllowed == PackageUpgradeRequestedAnswer.DoNotUpgradeAny) - packageUpgradeAllowed = false; - - // The confirmation callback may have opted out of the backup (e.g. the GameStudio dialog's - // checkbox); disarm so the upgrade writes that follow this point don't snapshot. - if (!loadParameters.BackupBeforeUpgrade) - DisarmUpgradeBackup(); - } + var upgradeAllowed = RequestPackageUpgrade(package, pendingPackageUpgrades, loadParameters); if (!PackageLoadParameters.ShouldUpgrade(upgradeAllowed)) { diff --git a/sources/assets/Stride.Core.Assets/PackageStore.cs b/sources/assets/Stride.Core.Assets/PackageStore.cs index b79d293bd..533322815 100644 --- a/sources/assets/Stride.Core.Assets/PackageStore.cs +++ b/sources/assets/Stride.Core.Assets/PackageStore.cs @@ -117,6 +117,22 @@ public class PackageStore return package is null ? null : (UDirectory)store.GetRealPath(package); } + /// The local-install directory of an installed package (see ). + public UDirectory GetPackageDirectory(NugetLocalPackage package) + { + ArgumentNullException.ThrowIfNull(package); + return (UDirectory)store.GetRealPath(package); + } + + /// Every installed copy of , for callers that pick a version themselves. + /// The package. + /// The versions to take from local package sources first, null for all. + public IEnumerable GetLocalPackages(string packageName, PackageVersionRange? localBuildRange = null) + { + ArgumentNullException.ThrowIfNull(packageName); + return store.GetLocalPackages(packageName, localBuildRange); + } + /// /// Fetches (if needed) and installs at /// into the local package store, from the configured NuGet sources. Returns the installed @@ -128,6 +144,19 @@ public class PackageStore return store.InstallPackage(packageName, version, [], progress); } + /// Raised as downloads, with the bytes downloaded so far (throttled to ~250ms, and once more when a download ends). + public event Action? DownloadProgress + { + add => store.NugetDownloadProgress += value; + remove => store.NugetDownloadProgress -= value; + } + + /// + /// The size of the downloads the current has started, in bytes. The final size once + /// the downloads have started, so only meaningful for an install of one package (and its small dependencies). + /// + public long StartedDownloadBytes => store.NugetStartedDownloadBytes; + /// /// Gets the default package manager. /// diff --git a/sources/assets/Stride.Core.Assets/Templates/TemplateDownloads.cs b/sources/assets/Stride.Core.Assets/Templates/TemplateDownloads.cs new file mode 100644 index 000000000..f3e31347f --- /dev/null +++ b/sources/assets/Stride.Core.Assets/Templates/TemplateDownloads.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Packages; + +namespace Stride.Core.Assets.Templates; + +/// A template package being downloaded, or one whose download failed. +/// The package. +/// Bytes downloaded so far. +/// Size of the download, 0 until it starts. +/// Position of this package in its batch, from 1. +/// Number of packages in the batch. +/// Whether the download failed (the package is not available). +public sealed record TemplateDownload(string PackageId, long DownloadedBytes, long TotalBytes = 0, int Index = 1, int Count = 1, bool Failed = false) +{ + /// Progress from 0 to 1, or null while the size is not known. + public double? Fraction => TotalBytes > 0 ? Math.Clamp((double)DownloadedBytes / TotalBytes, 0, 1) : null; +} + +/// +/// The template package downloads in progress, for the windows that list templates. The template sources report here +/// (); windows show and refresh on . +/// +public static class TemplateDownloads +{ + private static readonly object ThisLock = new(); + private static TemplateDownload? current; + + /// The download in progress (or the last failed one), null when there is none. + public static TemplateDownload? Current + { + get { lock (ThisLock) return current; } + } + + /// Raised when changes, on the reporting thread (possibly a background one). + public static event Action? Changed; + + /// Sets : a download's progress, its failure, or null once it is done. + public static void Report(TemplateDownload? download) + { + lock (ThisLock) + { + if (Equals(current, download)) + return; + current = download; + } + Changed?.Invoke(); + } + + /// A short status line for , e.g. "Downloading Stride.Templates.Samples (2/2): 45.2 / 131.9 MB". + public static string Describe(TemplateDownload download) + { + if (download.Failed) + return $"Could not download {download.PackageId}. Check the network connection, then restart Game Studio."; + var package = download.Count > 1 ? $"{download.PackageId} ({download.Index}/{download.Count})" : download.PackageId; + return download.DownloadedBytes > 0 + ? $"Downloading {package}: {DownloadSize.Format(download.DownloadedBytes, download.TotalBytes)}" + : $"Downloading {package}..."; + } +} diff --git a/sources/assets/Stride.Core.Assets/Templates/TemplateManager.cs b/sources/assets/Stride.Core.Assets/Templates/TemplateManager.cs index 447a38cb4..a80581a03 100644 --- a/sources/assets/Stride.Core.Assets/Templates/TemplateManager.cs +++ b/sources/assets/Stride.Core.Assets/Templates/TemplateManager.cs @@ -12,11 +12,27 @@ public class TemplateManager private static readonly List Generators = []; private static readonly PackageCollection ExtraPackages = []; + /// + /// Adds a package whose templates lists. May be called from any thread (template + /// packages downloaded in the background register when ready). Registering a package already registered, which + /// gained templates since, only signals the change. + /// public static void RegisterPackage(Package package) { - ExtraPackages.Add(package); + lock (ThisLock) + { + if (!ExtraPackages.Contains(package)) + ExtraPackages.Add(package); + } + PackagesChanged?.Invoke(); } + /// + /// Raised after added templates, on the calling thread (possibly a background one). + /// Lets an open template list show packages that arrive late. + /// + public static event Action? PackagesChanged; + /// /// Registers the specified factory. /// @@ -56,7 +72,12 @@ public class TemplateManager /// A sequence containing all registered template descriptions. public static IEnumerable FindTemplates(PackageSession? session = null) { - var packages = session?.Packages.Concat(ExtraPackages).Distinct(DistinctPackagePathComparer.Default) ?? ExtraPackages; + List extraPackages; + lock (ThisLock) + { + extraPackages = [.. ExtraPackages]; + } + var packages = session?.Packages.Concat(extraPackages).Distinct(DistinctPackagePathComparer.Default) ?? extraPackages; // TODO this will not work if the same package has different versions return [.. packages.SelectMany(package => package.Templates).OrderBy(tpl => tpl.Order).ThenBy(tpl => tpl.Name)]; } diff --git a/sources/assets/Stride.Core.Packages/DownloadSize.cs b/sources/assets/Stride.Core.Packages/DownloadSize.cs new file mode 100644 index 000000000..c95527bbc --- /dev/null +++ b/sources/assets/Stride.Core.Packages/DownloadSize.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Packages; + +/// Formats package download sizes the same way wherever they are shown (the stride CLI, Game Studio). +public static class DownloadSize +{ + /// The bytes downloaded so far, e.g. "45.2 MB". No total: it is not known before the downloads start. + public static string Format(long downloadedBytes) => $"{Megabytes(downloadedBytes)} MB"; + + /// + /// "45.2 / 131.9 MB", for a download whose size is known ( above 0), else as + /// . + /// + public static string Format(long downloadedBytes, long totalBytes) + => totalBytes > 0 ? $"{Megabytes(downloadedBytes)} / {Megabytes(totalBytes)} MB" : Format(downloadedBytes); + + private static string Megabytes(long bytes) => $"{bytes / 1048576.0:0.0}"; +} diff --git a/sources/assets/Stride.Core.Packages/INugetDownloadProgress.cs b/sources/assets/Stride.Core.Packages/INugetDownloadProgress.cs index 9525c560d..ab267705e 100644 --- a/sources/assets/Stride.Core.Packages/INugetDownloadProgress.cs +++ b/sources/assets/Stride.Core.Packages/INugetDownloadProgress.cs @@ -2,6 +2,12 @@ namespace Stride.Core.Packages; interface INugetDownloadProgress { + /// A package download of bytes started. + void DownloadStarted(long length); + /// Additional bytes were read from an in-flight package download. void DownloadAdvanced(long bytesRead); + + /// An in-flight package download reached its end. + void DownloadCompleted(); } diff --git a/sources/assets/Stride.Core.Packages/NugetDownloadProgressHandler.cs b/sources/assets/Stride.Core.Packages/NugetDownloadProgressHandler.cs index 6b53ec7d2..8632d3c21 100644 --- a/sources/assets/Stride.Core.Packages/NugetDownloadProgressHandler.cs +++ b/sources/assets/Stride.Core.Packages/NugetDownloadProgressHandler.cs @@ -51,6 +51,7 @@ internal sealed class DownloadProgressMessageHandler : DelegatingHandler && response.Content is { } content && content.Headers.ContentLength is > 0) { + progress.DownloadStarted(content.Headers.ContentLength.Value); response.Content = new ProgressHttpContent(content, content.Headers.ContentLength.Value, progress); } return response; @@ -86,6 +87,7 @@ internal sealed class ProgressHttpContent : HttpContent await stream.WriteAsync(buffer.AsMemory(0, read)); progress.DownloadAdvanced(read); } + progress.DownloadCompleted(); } protected override bool TryComputeLength(out long length) @@ -107,6 +109,7 @@ internal sealed class ProgressReadStream : Stream private readonly Stream inner; private readonly long length; private readonly INugetDownloadProgress progress; + private bool completed; public ProgressReadStream(Stream inner, long length, INugetDownloadProgress progress) { @@ -118,17 +121,27 @@ internal sealed class ProgressReadStream : Stream public override int Read(byte[] buffer, int offset, int count) { var read = inner.Read(buffer, offset, count); - if (read > 0) - progress.DownloadAdvanced(read); + Advance(read, count); return read; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { var read = await inner.ReadAsync(buffer, cancellationToken); + Advance(read, buffer.Length); + return read; + } + + // Reports the bytes read, and the end of the download once (a read of 0 bytes for a non-empty buffer). + private void Advance(int read, int requested) + { if (read > 0) progress.DownloadAdvanced(read); - return read; + else if (requested > 0 && !completed) + { + completed = true; + progress.DownloadCompleted(); + } } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/sources/assets/Stride.Core.Packages/NugetStore.cs b/sources/assets/Stride.Core.Packages/NugetStore.cs index a0c987c4f..b1549f20e 100644 --- a/sources/assets/Stride.Core.Packages/NugetStore.cs +++ b/sources/assets/Stride.Core.Packages/NugetStore.cs @@ -39,6 +39,7 @@ public partial class NugetStore : INugetDownloadProgress // Download byte counter (reset per InstallPackage); fed by DownloadProgressHandlerProvider. private long downloadedBytes; + private long startedDownloadBytes; private long lastReportTicks; private readonly string? oldRootDirectory; @@ -218,9 +219,15 @@ public partial class NugetStore : INugetDownloadProgress /// public event Action? NugetRestoreInstalling; - /// Event raised as packages download, with the total bytes downloaded so far (throttled to ~1s). + /// Event raised as packages download, with the total bytes downloaded so far (throttled to ~250ms, and once more when a download ends). public event Action? NugetDownloadProgress; + /// + /// The size of the downloads the current has started so far, in bytes. It grows as + /// downloads start, so it is the final size only when a single package is being downloaded. + /// + public long NugetStartedDownloadBytes => Interlocked.Read(ref startedDownloadBytes); + /// /// Event executed before a package's packageinstall.exe setup step runs. /// @@ -272,16 +279,20 @@ public partial class NugetStore : INugetDownloadProgress /// The list of packages sorted from the most recent to the oldest. public IList GetPackagesInstalled(IEnumerable packageIds) { - return [.. packageIds.SelectMany(GetLocalPackages).OrderByDescending(p => p.Version)]; + return [.. packageIds.SelectMany(packageId => GetLocalPackages(packageId)).OrderByDescending(p => p.Version)]; } /// /// List of all installed packages. /// /// A list of packages. - public IEnumerable GetLocalPackages(string packageId) + /// The package. + /// + /// The versions to take from local-folder sources first (see ), null for all. + /// + public IEnumerable GetLocalPackages(string packageId, PackageVersionRange? localBuildRange = null) { - SyncLocalFolderSources(packageId); + SyncLocalFolderSources(packageId, localBuildRange); var res = new List(); @@ -357,21 +368,26 @@ public partial class NugetStore : INugetDownloadProgress } /// - /// Mirrors any .nupkg of from configured local-folder NuGet sources + /// Mirrors the .nupkg files of from configured local-folder NuGet sources /// (e.g. a worktree's bin/packages) into the global packages folder when the source nupkg /// is newer than the extracted form. Lets dev-built packages flow into /// without a separate restore step. /// - private void SyncLocalFolderSources(string packageId) + /// + /// The versions to mirror, null for all: only what the caller's query could use. A lookup for one version does + /// not mirror a local copy of another (same id, maybe different content) over the installed one. + /// + private void SyncLocalFolderSources(string packageId, PackageVersionRange? versionRange = null) { if (InstallPath == null) return; lock (localFolderSyncLock) { - // Each id is synced at most once per process - if (!syncedPackageIds.Add(packageId)) + // Each id, for each range asked of it, is synced at most once per process. + if (!syncedPackageIds.Add($"{packageId}/{versionRange}")) return; + var wanted = versionRange?.ToVersionRange(); if (localFolderResources == null) { @@ -400,19 +416,22 @@ public partial class NugetStore : INugetDownloadProgress foreach (var (source, findPackageById) in localFolderResources) { - List versions; + List sourceVersions; try { // Versions come from file names; no nupkg is opened at this point - versions = (await findPackageById.GetAllVersionsAsync(packageId, localFolderCacheContext, NativeLogger, CancellationToken.None).ConfigureAwait(false)).ToList(); + sourceVersions = (await findPackageById.GetAllVersionsAsync(packageId, localFolderCacheContext, NativeLogger, CancellationToken.None).ConfigureAwait(false)).ToList(); } catch { continue; } - foreach (var version in versions) + foreach (var version in sourceVersions) { + // Satisfies, not a float-aware check: the content packs are prereleases (4.4.0-beta7-dev4). + if (wanted is not null && !wanted.Satisfies(version)) + continue; var identity = new PackageIdentity(packageId, version); // Try the conventional file name first to avoid opening the package @@ -494,6 +513,7 @@ public partial class NugetStore : INugetDownloadProgress { currentProgressReport = progress; Interlocked.Exchange(ref downloadedBytes, 0); + Interlocked.Exchange(ref startedDownloadBytes, 0); Interlocked.Exchange(ref lastReportTicks, 0); try { @@ -556,7 +576,8 @@ public partial class NugetStore : INugetDownloadProgress [ new() { - LibraryRange = new LibraryRange(packageId, new VersionRange(version.ToNuGetVersion()), LibraryDependencyTarget.Package), + // Exactly this version: a bare version is ">=" to NuGet, which would silently take a higher one. + LibraryRange = new LibraryRange(packageId, new VersionRange(version.ToNuGetVersion(), true, version.ToNuGetVersion(), true), LibraryDependencyTarget.Package), } ], }); @@ -659,7 +680,7 @@ public partial class NugetStore : INugetDownloadProgress /// /// It is safe to call it concurrently be cause we operations are done using the FileLock. /// Package to uninstall. - public async Task UninstallPackage(NugetPackage package, ProgressReport progress) + public async Task UninstallPackage(NugetPackage package, ProgressReport? progress) { #if DEBUG var installedPackages = GetPackagesInstalled([package.Id]); @@ -786,7 +807,8 @@ public partial class NugetStore : INugetDownloadProgress return GetPackagesInstalled([packageId]).FirstOrDefault(p => allowPrereleaseVersions || string.IsNullOrEmpty(p.Version.SpecialVersion)); } - var packages = GetLocalPackages(packageId); + // The range is the mirror filter too: local-folder sources give up only what this lookup could use. + var packages = GetLocalPackages(packageId, constraintProvider is null ? versionRange : null); if (!allowUnlisted) { @@ -1024,17 +1046,29 @@ public partial class NugetStore : INugetDownloadProgress return package.Version.SpecialVersion?.StartsWith("dev", StringComparison.Ordinal) == true && !package.Version.SpecialVersion.Contains('.'); } + void INugetDownloadProgress.DownloadStarted(long length) + { + Interlocked.Add(ref startedDownloadBytes, length); + } + void INugetDownloadProgress.DownloadAdvanced(long bytesRead) { Interlocked.Add(ref downloadedBytes, bytesRead); - // Throttle UI updates to ~1s; chunk reads fire far more often than that. + // Throttle UI updates to ~250ms; chunk reads fire far more often than that. var now = Environment.TickCount64; - if (now - Interlocked.Read(ref lastReportTicks) < 1000) + if (now - Interlocked.Read(ref lastReportTicks) < 250) return; Interlocked.Exchange(ref lastReportTicks, now); NugetDownloadProgress?.Invoke(Interlocked.Read(ref downloadedBytes)); } + void INugetDownloadProgress.DownloadCompleted() + { + // Not throttled: the final count stays shown while the package is extracted. + Interlocked.Exchange(ref lastReportTicks, Environment.TickCount64); + NugetDownloadProgress?.Invoke(Interlocked.Read(ref downloadedBytes)); + } + /// /// Deletes a package's cached download (nupkg_{id}.{version}.dat) from NuGet's HTTP cache. NuGet keeps /// these indefinitely with no size limit, so removing them on uninstall reclaims space (Stride packages are @@ -1056,7 +1090,7 @@ public partial class NugetStore : INugetDownloadProgress catch { /* best-effort */ } } - private static void RunPackageInstall(string packageInstall, string arguments, ProgressReport progress) + private static void RunPackageInstall(string packageInstall, string arguments, ProgressReport? progress) { // Run packageinstall.exe using var process = Process.Start(new ProcessStartInfo(packageInstall, arguments) diff --git a/sources/core/Stride.Core.Tests/TestPriorityLinkedQueue.cs b/sources/core/Stride.Core.Tests/TestPriorityLinkedQueue.cs index e3604f4bb..e9c2135f8 100644 --- a/sources/core/Stride.Core.Tests/TestPriorityLinkedQueue.cs +++ b/sources/core/Stride.Core.Tests/TestPriorityLinkedQueue.cs @@ -75,6 +75,117 @@ public class TestPriorityLinkedQueue CheckPriorityQueue(priorityQueue); } + /// + /// The node filling the hole is bigger than its new children, so it has to move down. + /// + [Fact] + public void TestRemovalMovingNodeDown() + { + var queue = new PriorityNodeQueue(); + var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]); + + queue.Remove(nodes[1]); + + Assert.Equal(-1, nodes[1].Index); + CheckPriorityQueue(queue, [0, 2, 3, 4, 5, 6, 7]); + } + + /// + /// The node filling the hole is smaller than its new parent, so it has to move up. + /// + [Fact] + public void TestRemovalMovingNodeUp() + { + var queue = new PriorityNodeQueue(); + var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]); + + queue.Remove(nodes[4]); + + Assert.Equal(-1, nodes[4].Index); + CheckPriorityQueue(queue, [0, 1, 2, 3, 4, 6, 7]); + } + + /// + /// The node sitting last leaves no hole behind, so nothing moves. + /// + [Fact] + public void TestRemovalOfTheLastNode() + { + var queue = new PriorityNodeQueue(); + var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]); + + queue.Remove(nodes[7]); + + Assert.Equal(-1, nodes[7].Index); + CheckPriorityQueue(queue, [0, 1, 2, 4, 5, 6, 7]); + } + + [Fact] + public void TestRemovalWithDescendingComparer() + { + var queue = new PriorityNodeQueue(Comparer.Create((a, b) => b.CompareTo(a))); + var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]); + + queue.Remove(nodes[4]); + + CheckPriorityQueue(queue, [7, 6, 4, 3, 2, 1, 0]); + } + + [Fact] + public void TestRandomRemoval() + { + var random = new Random(0); + var queue = new PriorityNodeQueue(); + List> nodes = []; + for (int i = 0; i < 1000; ++i) + { + nodes.Add(queue.Enqueue(random.Next(100))); + } + + var expected = nodes.Select(x => x.Value).ToList(); + for (int i = 0; i < 500; ++i) + { + var node = nodes[random.Next(nodes.Count)]; + queue.Remove(node); + nodes.Remove(node); + expected.Remove(node.Value); + + Assert.Equal(-1, node.Index); + Assert.Equal(expected.Count, queue.Count); + } + + expected.Sort(); + + CheckPriorityQueue(queue, expected); + } + + [Fact] + public void TestClearReleasesNodes() + { + var queue = new PriorityNodeQueue(); + var node = queue.Enqueue(5); + + queue.Clear(); + + Assert.Equal(-1, node.Index); + + // A released node belongs to no queue anymore, so it can be queued again + queue.Enqueue(node); + + Assert.Equal(5, queue.Peek()); + } + + private static List> Enqueue(PriorityNodeQueue priorityQueue, int[] values) + { + List> nodes = []; + foreach (var value in values) + { + nodes.Add(priorityQueue.Enqueue(value)); + } + + return nodes; + } + private static void CheckPriorityQueue(PriorityNodeQueue priorityQueue) { int lastItem = int.MinValue; @@ -85,4 +196,16 @@ public class TestPriorityLinkedQueue lastItem = value; } } + + /// + /// Drains the queue, which must return every expected item, in the order given. + /// + private static void CheckPriorityQueue(PriorityNodeQueue priorityQueue, IEnumerable expectedItems) + { + var items = new List(); + while (!priorityQueue.Empty) + items.Add(priorityQueue.Dequeue()); + + Assert.Equal(expectedItems, items); + } } diff --git a/sources/core/Stride.Core.Tests/TestPriorityQueue.cs b/sources/core/Stride.Core.Tests/TestPriorityQueue.cs index c4ba0bbbd..c42d0ccc8 100644 --- a/sources/core/Stride.Core.Tests/TestPriorityQueue.cs +++ b/sources/core/Stride.Core.Tests/TestPriorityQueue.cs @@ -75,6 +75,90 @@ public class TestPriorityQueue CheckPriorityQueue(priorityQueue); } + /// + /// The item filling the hole is bigger than its new children, so it has to move down. + /// + [Fact] + public void TestRemovalMovingItemDown() + { + var queue = new PriorityQueue(); + foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 }) + queue.Enqueue(value); + + queue.Remove(1); + + CheckPriorityQueue(queue, [0, 2, 3, 4, 5, 6, 7]); + } + + /// + /// The item filling the hole is smaller than its new parent, so it has to move up. + /// + [Fact] + public void TestRemovalMovingItemUp() + { + var queue = new PriorityQueue(); + foreach (var value in new[] { 26, 38, 33, 54, 69, 64, 21 }) + queue.Enqueue(value); + + queue.Remove(54); + + CheckPriorityQueue(queue, [21, 26, 33, 38, 64, 69]); + } + + /// + /// The item sitting last leaves no hole behind, so nothing moves. + /// + [Fact] + public void TestRemovalOfTheLastItem() + { + var queue = new PriorityQueue(); + foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 }) + queue.Enqueue(value); + + queue.Remove(3); + + CheckPriorityQueue(queue, [0, 1, 2, 4, 5, 6, 7]); + } + + [Fact] + public void TestRemovalWithDescendingComparer() + { + var queue = new PriorityQueue(Comparer.Create((a, b) => b.CompareTo(a))); + foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 }) + queue.Enqueue(value); + + queue.Remove(5); + + CheckPriorityQueue(queue, [7, 6, 4, 3, 2, 1, 0]); + } + + [Fact] + public void TestRandomRemoval() + { + var random = new Random(0); + var queue = new PriorityQueue(); + var expected = new List(); + for (int i = 0; i < 1000; ++i) + { + var value = random.Next(100); + queue.Enqueue(value); + expected.Add(value); + } + + for (int i = 0; i < 500; ++i) + { + var value = expected[random.Next(expected.Count)]; + queue.Remove(value); + expected.Remove(value); + + Assert.Equal(expected.Count, queue.Count); + } + + expected.Sort(); + + CheckPriorityQueue(queue, expected); + } + private static void CheckPriorityQueue(PriorityQueue priorityQueue) { int lastItem = int.MinValue; @@ -85,4 +169,16 @@ public class TestPriorityQueue lastItem = value; } } + + /// + /// Drains the queue, which must return every expected item, in the order given. + /// + private static void CheckPriorityQueue(PriorityQueue priorityQueue, IEnumerable expectedItems) + { + var items = new List(); + while (!priorityQueue.Empty) + items.Add(priorityQueue.Dequeue()); + + Assert.Equal(expectedItems, items); + } } diff --git a/sources/core/Stride.Core/Collections/PriorityNodeQueue.cs b/sources/core/Stride.Core/Collections/PriorityNodeQueue.cs index 2b786c784..4695acd8b 100644 --- a/sources/core/Stride.Core/Collections/PriorityNodeQueue.cs +++ b/sources/core/Stride.Core/Collections/PriorityNodeQueue.cs @@ -5,16 +5,16 @@ namespace Stride.Core.Collections; /// /// Implements a priority queue of type T. -/// +/// /// Elements may be added to the queue in any order, but when we pull /// elements out of the queue, they will be returned in 'ascending' order. /// Adding new elements into the queue may be done at any time, so this is /// useful to implement a dynamically growing and shrinking queue. Both adding -/// an element and removing the first element are log(N) operations. -/// -/// The queue is implemented using a priority-heap data structure. For more +/// an element and removing the first element are log(N) operations. +/// +/// The queue is implemented using a priority-heap data structure. For more /// details on this elegant and simple data structure see "Programming Pearls" -/// in our library. The tree is implemented atop a list, where 2N and 2N+1 are +/// in our library. The tree is implemented atop a list, where 2N+1 and 2N+2 are /// the child nodes of node N. The tree is balanced and left-aligned so there /// are no 'holes' in this list. /// @@ -40,6 +40,9 @@ public class PriorityNodeQueue /// Clear all the elements from the priority queue public void Clear() { + foreach (var item in items) + item.Index = -1; + items.Clear(); } @@ -53,48 +56,21 @@ public class PriorityNodeQueue if (index == -1) return; - // The element to return is of course the first element in the array, - // or the root of the tree. However, this will leave a 'hole' there. We - // fill up this hole with the last element from the array. This will - // break the heap property. So we bubble the element downwards by swapping - // it with it's lower child until it reaches it's correct level. The lower - // child (one of the orignal elements with index 1 or 2) will now be at the - // head of the queue (root of the tree). - var nMax = items.Count - 1; - var itemMax = items[nMax]; - itemMax.Index = index; - var itemToRemove = items[index]; - itemToRemove.Index = -1; - items[index] = itemMax; - items.RemoveAt(nMax); // Move the last element to the top - - var p = index; - while (true) - { - // c is the child we want to swap with. If there - // is no child at all, then the heap is balanced - var c = p * 2; - if (c >= nMax) break; - - // If the second child is smaller than the first, that's the one - // we want to swap with this parent. - if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) c++; - // If the parent is already smaller than this smaller child, then - // we are done - if (comparer.Compare(items[p].Value, items[c].Value) <= 0) - break; - - // Othewise, swap parent and child, and follow down the parent - var tmp = items[p]; - var tmp2 = items[c]; - tmp.Index = c; - tmp2.Index = p; - items[p] = tmp2; - items[c] = tmp; - p = c; - } - + // The item leaves a 'hole', which the last item of the list fills up + var lastIndex = items.Count - 1; + var lastItem = items[lastIndex]; + items.RemoveAt(lastIndex); item.Index = -1; + + if (index == lastIndex) + return; + + items[index] = lastItem; + lastItem.Index = index; + + // The item taken from the end can be smaller than its new parent, or bigger than its children + if (!MoveUp(index)) + MoveDown(index); } /// Add an element to the priority queue - O(log(n)) time operation. @@ -114,35 +90,11 @@ public class PriorityNodeQueue if (item.Index != -1) throw new InvalidOperationException("Item belongs to another PriorityNodeQueue."); - // We add the item to the end of the list (at the bottom of the - // tree). Then, the heap-property could be violated between this element - // and it's parent. If this is the case, we swap this element with the - // parent (a safe operation to do since the element is known to be less - // than it's parent). Now the element move one level up the tree. We repeat - // this test with the element and it's new parent. The element, if lesser - // than everybody else in the tree will eventually bubble all the way up - // to the root of the tree (or the head of the list). It is easy to see - // this will take log(N) time, since we are working with a balanced binary - // tree. - var n = items.Count; + // The item goes at the bottom of the tree, then moves up to its level + item.Index = items.Count; items.Add(item); - item.Index = n; - while (n != 0) - { - var p = n / 2; // This is the 'parent' of this item - if (comparer.Compare(items[n].Value, items[p].Value) >= 0) - break; // Item >= parent - // Swap item and parent - var tmp = items[n]; - var tmp2 = items[p]; - tmp2.Index = n; - tmp.Index = p; - items[n] = tmp2; - items[p] = tmp; - - n = p; // And continue - } + MoveUp(item.Index); } /// Returns the number of elements in the queue. @@ -164,49 +116,66 @@ public class PriorityNodeQueue /// The first element in the queue, in ascending order. public T Dequeue() { - // The element to return is of course the first element in the array, - // or the root of the tree. However, this will leave a 'hole' there. We - // fill up this hole with the last element from the array. This will - // break the heap property. So we bubble the element downwards by swapping - // it with it's lower child until it reaches it's correct level. The lower - // child (one of the orignal elements with index 1 or 2) will now be at the - // head of the queue (root of the tree). - var nMax = items.Count - 1; - var itemToRemove = items[0]; - var itemMax = items[nMax]; - itemMax.Index = 0; - var val = itemToRemove.Value; - itemToRemove.Index = -1; - items[0] = itemMax; - items.RemoveAt(nMax); // Move the last element to the top + var item = items[0]; + var value = item.Value; - var p = 0; - while (true) + Remove(item); + + return value; + } + + /// + /// Moves an item towards the head of the queue, while it is smaller than its parent. + /// + /// The index of the item to move. + /// true if the item moved; otherwise, false. + private bool MoveUp(int index) + { + var moved = false; + while (index > 0) { - // c is the child we want to swap with. If there - // is no child at all, then the heap is balanced - var c = p * 2; - if (c >= nMax) break; - - // If the second child is smaller than the first, that's the one - // we want to swap with this parent. - if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) c++; - // If the parent is already smaller than this smaller child, then - // we are done - if (comparer.Compare(items[p].Value, items[c].Value) <= 0) + var parent = (index - 1) / 2; + if (comparer.Compare(items[index].Value, items[parent].Value) >= 0) break; - // Othewise, swap parent and child, and follow down the parent - var tmp = items[p]; - var tmp2 = items[c]; - tmp.Index = c; - tmp2.Index = p; - items[p] = tmp2; - items[c] = tmp; - - p = c; + Swap(index, parent); + index = parent; + moved = true; } - return val; + return moved; + } + + /// + /// Moves an item towards the tail of the queue, while it is bigger than one of its children. + /// + /// The index of the item to move. + private void MoveDown(int index) + { + while (true) + { + // Stop when the item has no child left + var child = (index * 2) + 1; + if (child >= items.Count) + break; + + // Of the two children, the smaller one takes the place of the parent + var rightChild = child + 1; + if (rightChild < items.Count && comparer.Compare(items[rightChild].Value, items[child].Value) < 0) + child = rightChild; + + if (comparer.Compare(items[index].Value, items[child].Value) <= 0) + break; + + Swap(index, child); + index = child; + } + } + + private void Swap(int first, int second) + { + (items[first], items[second]) = (items[second], items[first]); + items[first].Index = first; + items[second].Index = second; } } diff --git a/sources/core/Stride.Core/Collections/PriorityQueue.cs b/sources/core/Stride.Core/Collections/PriorityQueue.cs index 0f6c225d1..a490cb6df 100644 --- a/sources/core/Stride.Core/Collections/PriorityQueue.cs +++ b/sources/core/Stride.Core/Collections/PriorityQueue.cs @@ -38,39 +38,53 @@ public class PriorityQueue /// public void Remove(T item) { - // Find index of item to remove. var index = items.IndexOf(item); if (index == -1) return; - // Remove requested element and place last one instead var maxCount = items.Count - 1; items[index] = items[maxCount]; items.RemoveAt(maxCount); - // Bubble elements so that order is respected again - var parentIndex = index; - while (true) + if (index == maxCount) + return; + + // Check if we need to bubble up or down + var parent = (index - 1) / 2; + if (index > 0 && comparer.Compare(items[index], items[parent]) < 0) { - var childIndex = parentIndex * 2; + var child = index; + while (child > 0) + { + parent = (child - 1) / 2; + if (comparer.Compare(items[child], items[parent]) >= 0) + break; - // Check if there is any child, otherwise we're done - if (childIndex >= maxCount) - break; + (items[parent], items[child]) = (items[child], items[parent]); + child = parent; + } + } + else + { + var parentIndex = index; + while (true) + { + var left = parentIndex * 2 + 1; + if (left >= items.Count) + break; - // Check which of the two child we need to swap - if (childIndex + 1 < maxCount && comparer.Compare(items[childIndex + 1], items[childIndex]) < 0) - childIndex++; + var right = left + 1; + var childIndex = left; - // Order might already be OK, if yes we're done - if (comparer.Compare(items[parentIndex], items[childIndex]) <= 0) - break; + if (right < items.Count && comparer.Compare(items[right], items[left]) < 0) + childIndex = right; - // Need swap - (items[parentIndex], items[childIndex]) = (items[childIndex], items[parentIndex]); + if (comparer.Compare(items[parentIndex], items[childIndex]) <= 0) + break; - // Continue with child - parentIndex = childIndex; + (items[parentIndex], items[childIndex]) = (items[childIndex], items[parentIndex]); + parentIndex = childIndex; + } } } @@ -87,7 +101,7 @@ public class PriorityQueue // Bubble elements so that order is respected again while (childIndex != 0) { - var parentIndex = childIndex / 2; + var parentIndex = (childIndex - 1) / 2; // Should we swap with parent? if (comparer.Compare(items[childIndex], items[parentIndex]) >= 0) @@ -133,7 +147,7 @@ public class PriorityQueue while (true) { // Check if there is any child, otherwise we're done - var childIndex = parentIndex * 2; + var childIndex = (parentIndex * 2) + 1; if (childIndex >= maxCount) break; diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/AssemblyReloading/ReloadAssembliesOperation.cs b/sources/editor/Stride.Assets.Presentation.Wpf/AssemblyReloading/ReloadAssembliesOperation.cs index 3a19de5c0..c576f7f22 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/AssemblyReloading/ReloadAssembliesOperation.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/AssemblyReloading/ReloadAssembliesOperation.cs @@ -78,7 +78,7 @@ namespace Stride.Assets.Presentation.AssemblyReloading { // If first time, load assembly if (firstTime) - loadedAssembly.NewAssembly = assemblyContainer.LoadAssemblyFromPath(loadedAssembly.PackageLoadedAssembly.Path); + loadedAssembly.NewAssembly = assemblyContainer.LoadAssemblyFromPath(loadedAssembly.PackageLoadedAssembly.Path, log); // Load assembly assembly = newVersion diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Stride.Assets.Presentation.Wpf.csproj b/sources/editor/Stride.Assets.Presentation.Wpf/Stride.Assets.Presentation.Wpf.csproj index 1e2ff0a21..e8cd21157 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Stride.Assets.Presentation.Wpf.csproj +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Stride.Assets.Presentation.Wpf.csproj @@ -69,8 +69,9 @@ diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Animation/AnimationBlend.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Animation/AnimationBlend.cs index ad13736e4..ce02fc4e9 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Animation/AnimationBlend.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Animation/AnimationBlend.cs @@ -58,8 +58,7 @@ public class ##Scriptname## : SyncScript, IBlendTreeBuilder public override void Update() { - // Use DrawTime rather than UpdateTime - var time = Game.DrawTime; + var time = Game.UpdateTime; // This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration long blendedMaxDuration = 0; @@ -67,7 +66,7 @@ public class ##Scriptname## : SyncScript, IBlendTreeBuilder var currentTicks = TimeSpan.FromTicks((long)(currentTime * blendedMaxDuration)); - currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % blendedMaxDuration); + currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) % blendedMaxDuration); currentTime = (currentTicks.Ticks/(double) blendedMaxDuration); } diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Events/EventBroadcaster.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Events/EventBroadcaster.cs index 8c65c5e4c..c3abc47de 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Events/EventBroadcaster.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Events/EventBroadcaster.cs @@ -44,7 +44,7 @@ public class ##Scriptname## : SyncScript // Condition 2 - the trigger can be a time interval if (TimeInterval > 0) { - timeIntervalCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + timeIntervalCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; if (timeIntervalCountdown <= 0f) { timeIntervalCountdown = TimeInterval; diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Physics/PlayerController.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Physics/PlayerController.cs index 248262dca..fbe502296 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Physics/PlayerController.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Physics/PlayerController.cs @@ -97,7 +97,7 @@ public class ##Scriptname## : SyncScript var rotationDelta = Input.MouseDelta; // Compute translation speed according to framerate and modifiers - var translationSpeed = Speed * (float)Game.UpdateTime.Elapsed.TotalSeconds; + var translationSpeed = Speed * (float)Game.UpdateTime.WarpElapsed.TotalSeconds; // Take shortest path var deltaPitch = desiredPitch - pitch; @@ -109,7 +109,7 @@ public class ##Scriptname## : SyncScript desiredYaw = yaw + deltaYaw; // Perform orientation transition - var rotationAdaptation = (float)Game.UpdateTime.Elapsed.TotalSeconds * RotationAdaptationSpeed; + var rotationAdaptation = (float)Game.UpdateTime.WarpElapsed.TotalSeconds * RotationAdaptationSpeed; yaw = Math.Abs(deltaYaw) < rotationAdaptation ? desiredYaw : yaw + rotationAdaptation * Math.Sign(deltaYaw); pitch = Math.Abs(deltaPitch) < rotationAdaptation ? desiredPitch : pitch + rotationAdaptation * Math.Sign(deltaPitch); diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Prefabs/PrefabInstance.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Prefabs/PrefabInstance.cs index e1ac8e0fb..6a921601a 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Prefabs/PrefabInstance.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/Assets/Scripts/Prefabs/PrefabInstance.cs @@ -69,7 +69,7 @@ public class ##Scriptname## : AsyncScript if (TimeInterval > 0) { - timeIntervalCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + timeIntervalCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; if (timeIntervalCountdown <= 0f) { timeIntervalCountdown = TimeInterval; @@ -115,7 +115,7 @@ public class ##Scriptname## : AsyncScript while (secondsCountdown > 0f) { await Script.NextFrame(); - secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds; + secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds; } // Remove diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateParametersWindow.xaml.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateParametersWindow.xaml.cs index 12cf6e457..5c42470d0 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateParametersWindow.xaml.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateParametersWindow.xaml.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using TextBox = System.Windows.Controls.TextBox; using Microsoft.TemplateEngine.Abstractions; using Stride.Assets.Templates; +using Stride.Core.Assets.Templates; using Stride.Core.Presentation.Controls; using SDDialogResult = Stride.Core.Presentation.Services.DialogResult; using Stride.Core; @@ -59,7 +61,12 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow public IReadOnlyList SelectedAssetPacks => assetPackChecks.Where(t => t.CheckBox.IsChecked == true).Select(t => t.Identity).ToList(); - public DotNetNewTemplateParametersWindow(ITemplateInfo template, IReadOnlyList? assetPacks = null) + /// The template whose parameters are asked. + /// + /// The asset packs to offer, null for none. While it is not complete (the AssetPacks package is downloading), + /// the packs row shows the download progress; it is filled in once the task completes. + /// + public DotNetNewTemplateParametersWindow(ITemplateInfo template, Task>? assetPacks = null) { ArgumentNullException.ThrowIfNull(template); TemplateName = template.Name ?? template.Identity; @@ -72,8 +79,71 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow DescriptionTextBlock.Visibility = Visibility.Collapsed; BuildControls(template); - if (assetPacks is { Count: > 0 }) - ParametersPanel.Children.Add(BuildAssetPacksRow(assetPacks)); + if (assetPacks is not null) + AddAssetPacksRow(assetPacks); + } + + private async void AddAssetPacksRow(Task> assetPacks) + { + var row = new StackPanel { Margin = new Thickness(0, 6, 0, 6) }; + row.Children.Add(new TextBlock + { + Text = "Asset packs — Ready-made asset collections added to your project", + Margin = new Thickness(0, 0, 0, 3), + TextWrapping = TextWrapping.Wrap, + }); + ParametersPanel.Children.Add(row); + + // Until the packs are ready (the AssetPacks package is downloading), show the download. + var status = new TextBlock { TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 3) }; + var progress = new ProgressBar { Height = 6, Minimum = 0, Maximum = 1, IsIndeterminate = true }; + void Show() + { + var download = TemplateDownloads.Current; + status.Text = download is null ? "Preparing the asset packs..." : TemplateDownloads.Describe(download); + // Indeterminate until the download's size is known. + progress.IsIndeterminate = download?.Fraction is null; + progress.Value = download?.Fraction ?? 0; + } + void OnChanged() => _ = Dispatcher.InvokeAsync(Show); + if (!assetPacks.IsCompleted) + { + row.Children.Add(status); + row.Children.Add(progress); + Show(); + TemplateDownloads.Changed += OnChanged; + } + + IReadOnlyList packs; + try + { + packs = await assetPacks; + } + catch (OperationCanceledException) + { + // Same as an empty list: no packs offered. Other failures are already reported as an empty list by + // GetAssetPackTemplatesAsync, so anything else escaping is a bug worth a crash report. + packs = []; + } + finally + { + TemplateDownloads.Changed -= OnChanged; + row.Children.Remove(status); + row.Children.Remove(progress); + } + + if (packs.Count == 0) + { + row.Children.Add(new TextBlock + { + Text = "No asset packs are available. The AssetPacks package could not be downloaded.", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 2), + }); + return; + } + foreach (var check in BuildAssetPackChecks(packs)) + row.Children.Add(check); } private void BuildControls(ITemplateInfo template) @@ -240,19 +310,12 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow } /// - /// Checkbox list for the optional asset packs (Building blocks, Materials, ...), one per + /// Checkboxes for the optional asset packs (Building blocks, Materials, ...), one per /// item template of the AssetPacks package. All unchecked by default; each selected pack is /// instantiated into the generated game library after the main template. /// - private UIElement BuildAssetPacksRow(IReadOnlyList assetPacks) + private IEnumerable BuildAssetPackChecks(IReadOnlyList assetPacks) { - var row = new StackPanel { Margin = new Thickness(0, 6, 0, 6) }; - row.Children.Add(new TextBlock - { - Text = "Asset packs — Ready-made asset collections added to your project", - Margin = new Thickness(0, 0, 0, 3), - TextWrapping = TextWrapping.Wrap, - }); foreach (var pack in assetPacks) { var label = string.IsNullOrEmpty(pack.Description) ? pack.Name : $"{pack.Name} — {pack.Description}"; @@ -262,10 +325,9 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow IsChecked = false, Margin = new Thickness(0, 2, 0, 2), }; - row.Children.Add(item); assetPackChecks.Add((pack.Identity, item)); + yield return item; } - return row; } private static bool ParseBool(string? s) => bool.TryParse(s, out var b) && b; diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/WpfDotNetNewParameterPrompt.cs b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/WpfDotNetNewParameterPrompt.cs index 7d3e40ab6..02eb3884b 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/Templates/WpfDotNetNewParameterPrompt.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/Templates/WpfDotNetNewParameterPrompt.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.TemplateEngine.Abstractions; using Stride.Assets.Templates; @@ -15,10 +14,8 @@ internal sealed class WpfDotNetNewParameterPrompt : IDotNetNewParameterPrompt public async Task PromptAsync(ITemplateInfo template, TemplateDotNetNewDescription description) { // Templates that opt into asset packs get the pack checkboxes; the bridge downloads the - // AssetPacks package on first use. Empty list (e.g. offline) = no packs section. - IReadOnlyList assetPacks = []; - if (description.OffersAssetPacks) - assetPacks = await DotNetNewTemplateBridge.GetAssetPackTemplatesAsync().ConfigureAwait(true); + // AssetPacks package on first use, while the window is already open. Empty list (e.g. offline) = no packs. + var assetPacks = description.OffersAssetPacks ? DotNetNewTemplateBridge.GetAssetPackTemplatesAsync() : null; var dialog = new DotNetNewTemplateParametersWindow(template, assetPacks); var result = await dialog.ShowModal(); diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/ViewModels/NewOrOpenSessionTemplateCollectionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/ViewModels/NewOrOpenSessionTemplateCollectionViewModel.cs index 90011a184..d4f8cd0ff 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/ViewModels/NewOrOpenSessionTemplateCollectionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/ViewModels/NewOrOpenSessionTemplateCollectionViewModel.cs @@ -26,6 +26,8 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels private readonly IModalDialog dialog; private readonly TemplateDescriptionGroupViewModel recentGroup; private readonly TemplateDescriptionGroupViewModel rootGroup; + private readonly TemplateDescriptionGroupViewModel defaultGroup; + private readonly HashSet listedTemplates = new(ReferenceEqualityComparer.Instance); private string solutionName; private UDirectory solutionLocation; private bool arePropertiesValid; @@ -37,16 +39,15 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels rootGroup = new TemplateDescriptionGroupViewModel(serviceProvider, "New project"); // Add a default General group - var defaultGroup = new TemplateDescriptionGroupViewModel(rootGroup, "General"); + defaultGroup = new TemplateDescriptionGroupViewModel(rootGroup, "General"); + AddNewTemplates(); - foreach (TemplateDescription template in TemplateManager.FindTemplates(TemplateScope.Session)) - { - if (template.Id == StrideLibraryTemplateId) - continue; - var viewModel = new PackageTemplateViewModel(serviceProvider, template); - var group = ProcessGroup(rootGroup, template.Group) ?? defaultGroup; - group.Templates.Add(viewModel); - } + // Template packages still downloading at startup: show their progress, list their templates once ready. + TemplateManager.PackagesChanged += OnTemplatePackagesChanged; + TemplateDownloads.Changed += OnTemplateDownloadsChanged; + UpdateDownloadStatus(); + DependentProperties.Add(nameof(SelectedGroup), [nameof(ShowDownloadStatus)]); + DependentProperties.Add(nameof(DownloadStatus), [nameof(ShowDownloadStatus)]); recentGroup = new TemplateDescriptionGroupViewModel(serviceProvider, "Recent projects"); foreach (var file in EditorViewModel.Instance.RecentFiles) @@ -75,6 +76,80 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels public ICommandBase BrowseForExistingProjectCommand { get; private set; } + /// The template download in progress (or failed), null when there is none. + public string DownloadStatus { get; private set { SetValue(ref field, value); } } + + /// Whether is shown: only while new project templates are listed, where the downloads add templates. + public bool ShowDownloadStatus + { + get + { + if (DownloadStatus is null) + return false; + for (var group = SelectedGroup; group is not null; group = group.Parent) + { + if (group == rootGroup) + return true; + } + return false; + } + } + + /// Whether a template download is in progress ( may also report a failure). + public bool IsDownloading { get; private set { SetValue(ref field, value); } } + + /// Progress of the download from 0 to 1 (0 while its size is not known). + public double DownloadProgress { get; private set { SetValue(ref field, value); } } + + /// Whether the size of the download is not known yet (no progress to show). + public bool IsDownloadSizeUnknown { get; private set { SetValue(ref field, value); } } = true; + + /// + public override void Destroy() + { + TemplateManager.PackagesChanged -= OnTemplatePackagesChanged; + TemplateDownloads.Changed -= OnTemplateDownloadsChanged; + base.Destroy(); + } + + private void AddNewTemplates() + { + if (IsDestroyed) + return; + var added = false; + foreach (TemplateDescription template in TemplateManager.FindTemplates(TemplateScope.Session)) + { + if (template.Id == StrideLibraryTemplateId || !listedTemplates.Add(template)) + continue; + var viewModel = new PackageTemplateViewModel(ServiceProvider, template); + var group = ProcessGroup(rootGroup, template.Group) ?? defaultGroup; + group.Templates.Add(viewModel); + added = true; + } + if (added && SelectedGroup is not null) + { + // Refreshing the list clears the selection in the view: keep the user's choice. + var selected = SelectedTemplate; + UpdateTemplateList(); + SelectedTemplate = selected; + } + } + + private void OnTemplatePackagesChanged() => Dispatcher.InvokeAsync(AddNewTemplates); + + private void OnTemplateDownloadsChanged() => Dispatcher.InvokeAsync(UpdateDownloadStatus); + + private void UpdateDownloadStatus() + { + if (IsDestroyed) + return; + var download = TemplateDownloads.Current; + DownloadStatus = download is null ? null : TemplateDownloads.Describe(download); + IsDownloading = download is { Failed: false }; + DownloadProgress = download?.Fraction ?? 0; + IsDownloadSizeUnknown = download?.Fraction is null; + } + public override bool ValidateProperties(out string error) { if (SelectedTemplate is TemplateDescriptionViewModel) diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/Views/ProjectSelectionWindow.xaml b/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/Views/ProjectSelectionWindow.xaml index 5e0266739..f27f4bfd3 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/Views/ProjectSelectionWindow.xaml +++ b/sources/editor/Stride.Core.Assets.Editor.Wpf/Components/TemplateDescriptions/Views/ProjectSelectionWindow.xaml @@ -40,6 +40,15 @@ + + + + + + diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/SessionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/SessionViewModel.cs index ffd3b3492..dfc148a0f 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/SessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/SessionViewModel.cs @@ -479,12 +479,14 @@ namespace Stride.Core.Assets.Editor.ViewModel { // Snapshot every file the upgrade overwrites into a timestamped backup folder under the solution. // Copy-on-write, so it costs nothing unless an upgrade actually runs and modifies files. The dialog - // checkbox below can opt out; the callback updates this flag, which the session honors. + // checkbox below can opt out; the callback updates this flag, which the session honors. The choice + // covers the whole upgrade: only the first dialog can change it, later ones show it greyed out. var loadParameters = new PackageLoadParameters { CancelToken = cancellationSource.Token, BackupBeforeUpgrade = true, }; + var backupChoiceMade = false; loadParameters.PackageUpgradeRequested = (package, pendingUpgrades) => { // Generate message (in markdown, so we need to double line feeds) @@ -507,10 +509,11 @@ namespace Stride.Core.Assets.Editor.ViewModel new DialogButtonInfo { Content = Tr._p("Button", "Skip"), Result = (int)PackageUpgradeRequestedAnswer.DoNotUpgrade }, }; var applyToAll = new DialogCheckBoxInfo { Content = Tr._p("Message", "Do this for every package in the solution"), IsChecked = false }; - var backup = new DialogCheckBoxInfo { Content = Tr._p("Message", "Back up each modified file to a timestamped folder under the solution"), IsChecked = true }; + var backup = new DialogCheckBoxInfo { Content = Tr._p("Message", "Back up each modified file to a timestamped folder under the solution"), IsChecked = loadParameters.BackupBeforeUpgrade, IsEnabled = !backupChoiceMade }; var buttonResult = workProgress.ServiceProvider.Get().CheckedMessageBoxAsync(message.ToString(), [applyToAll, backup], buttons).Result; loadParameters.BackupBeforeUpgrade = backup.IsChecked == true; + backupChoiceMade = true; var result = (PackageUpgradeRequestedAnswer)buttonResult; if (applyToAll.IsChecked == true) { diff --git a/sources/editor/Stride.GameStudio/Program.cs b/sources/editor/Stride.GameStudio/Program.cs index e427bb8ce..97ea77373 100644 --- a/sources/editor/Stride.GameStudio/Program.cs +++ b/sources/editor/Stride.GameStudio/Program.cs @@ -431,6 +431,7 @@ public static class Program var viewModel = new NewOrOpenSessionTemplateCollectionViewModel(serviceProvider, startupWindow); startupWindow.Templates = viewModel; startupWindow.ShowDialog(); + viewModel.Destroy(); // The user selected a template to instantiate if (startupWindow.NewSessionParameters != null) diff --git a/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj b/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj index 57b32c7da..40aa57ca1 100644 --- a/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj +++ b/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj @@ -97,15 +97,22 @@ + Stride.Templates.Common.targets. ReferenceOutputAssembly=false would suppress the dependency. + Games is engine-versioned and packed with every build. Starters and Samples are content-versioned + (sources/templates/StrideSamplesVersion.props): a normal build does not pack them and the dependency + names the exact published version (pinned below); with StridePackContentTemplates=true it follows the + produced -devN pack. A PackageReference would not do: restore would fail before the first publish of a new + version. --> false false + true false + true + + + <_StrideExactDependencyNames>;@(ProjectReference->WithMetadataValue('StrideExactDependency', 'true')->'%(Filename)'); + + + <_ProjectReferencesWithVersions Condition="$(_StrideExactDependencyNames.Contains(';%(_ProjectReferencesWithVersions.Filename);')) And !$([System.String]::Copy('%(_ProjectReferencesWithVersions.ProjectVersion)').StartsWith('['))"> + [%(_ProjectReferencesWithVersions.ProjectVersion)] + + + + PreserveNewest diff --git a/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj b/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj index 67059e7ba..eb8240cee 100644 --- a/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj +++ b/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj @@ -21,6 +21,7 @@ + diff --git a/sources/engine/Stride.Assets.Tests/TestCodeUpgrade.cs b/sources/engine/Stride.Assets.Tests/TestCodeUpgrade.cs index 66cd7ac8a..0c9981cb0 100644 --- a/sources/engine/Stride.Assets.Tests/TestCodeUpgrade.cs +++ b/sources/engine/Stride.Assets.Tests/TestCodeUpgrade.cs @@ -363,4 +363,110 @@ public class TestCodeUpgrade Assert.Contains("v.IsSRgb(2)", result); } + + // The #1715 shape: memory helpers left Utilities for MemoryUtilities (same namespace), some renamed. + private const string MovedMembersSource = """ + namespace TestNs + { + static class Old + { + public static nint AllocateMemory(int sizeInBytes, int align = 16) => 0; + public static void FreeMemory(nint buffer) { } + public static void Swap(ref T left, ref T right) { } + public static void Dispose(object o) { } + } + static class Other + { + public static void FreeMemory(nint buffer) { } + } + } + """; + + private static readonly CodeUpgrade MoveUpgrade = Rewrite( + StaticMemberMove("TestNs.Old", "AllocateMemory", "TestNs.New", "Allocate"), + StaticMemberMove("TestNs.Old", "FreeMemory", "TestNs.New", "Free"), + StaticMemberMove("TestNs.Old", "Swap", "TestNs.New"), + ParameterRename("TestNs.Old", "AllocateMemory", "align", "alignment")); + + [Fact] + public async Task StaticMemberMoveRetargetsOnlyTheMatchingSymbol() + { + var source = MovedMembersSource + """ + + namespace TestNs + { + class Usage + { + void A(nint p) => Old.FreeMemory(p); + void B(nint p) => Other.FreeMemory(p); + void C(object o) => Old.Dispose(o); + void D(nint p) => TestNs.Old.FreeMemory(p); + void E(nint p) => global::TestNs.Old.FreeMemory(p); + System.Action F() => Old.FreeMemory; + string G() => nameof(Old.FreeMemory); + void H(ref int a, ref int b) => Old.Swap(ref a, ref b); + } + } + """; + + var result = await ApplyAsync(source, MoveUpgrade); + + Assert.Contains("=> New.Free(p);", result); + // The qualified receiver keeps its shape. + Assert.Contains("=> TestNs.New.Free(p);", result); + Assert.Contains("=> global::TestNs.New.Free(p);", result); + // Method groups, nameof and generic names are references too. + Assert.Contains("=> New.Free;", result); + Assert.Contains("nameof(New.Free)", result); + Assert.Contains("New.Swap(ref a, ref b)", result); + // The unrelated same-named member and the members that stayed are untouched. + Assert.Contains("Other.FreeMemory(p)", result); + Assert.Contains("Old.Dispose(o)", result); + } + + [Fact] + public async Task StaticMemberMoveComposesWithParameterRename() + { + // Both rewrites hit the same reference (the method name) but edit disjoint nodes. + var source = MovedMembersSource + """ + + namespace TestNs + { + class Usage + { + nint A() => Old.AllocateMemory(64, align: 32); + } + } + """; + + var result = await ApplyAsync(source, MoveUpgrade); + + Assert.Contains("New.Allocate(64, alignment: 32)", result); + } + + [Fact] + public async Task StaticMemberMoveQualifiesAliasAndUsingStaticReferences() + { + // Neither form names the old type at the call site, so the new type can't be assumed in scope. + var source = """ + using static TestNs.Old; + using Alias = TestNs.Old; + + """ + MovedMembersSource + """ + + namespace UserNs + { + class Usage + { + void A(nint p) => FreeMemory(p); + void B(nint p) => Alias.FreeMemory(p); + } + } + """; + + var result = await ApplyAsync(source, MoveUpgrade); + + Assert.Contains("void A(nint p) => TestNs.New.Free(p);", result); + Assert.Contains("void B(nint p) => TestNs.New.Free(p);", result); + } } diff --git a/sources/engine/Stride.Assets.Tests/TestContentTemplateResolver.cs b/sources/engine/Stride.Assets.Tests/TestContentTemplateResolver.cs new file mode 100644 index 000000000..9e0e8fe22 --- /dev/null +++ b/sources/engine/Stride.Assets.Tests/TestContentTemplateResolver.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +#nullable enable +using Stride.Assets.Templates; +using Stride.Core; +using Xunit; + +namespace Stride.Assets.Tests +{ + /// + /// The exact-version rules shared by the Game Studio bridge and the stride CLI to pick a content template package. + /// + public class TestContentTemplateResolver + { + [Theory] + [InlineData("4.4.0-dev4", "dev4")] + [InlineData("4.4.0-dev", "dev")] + [InlineData("4.4.0-beta8-dev4", "dev4")] + [InlineData("4.4.0", null)] + [InlineData("4.4.0-beta8", null)] + [InlineData("4.4.0-devices1", null)] + public void DevSuffixOf(string host, string? expected) + => Assert.Equal(expected, ContentTemplateResolver.DevSuffixOf(new PackageVersion(host))); + + [Theory] + [InlineData("4.4.0-beta7", "4.4.0-dev4", "4.4.0-beta7-dev4")] + [InlineData("4.4.0", "4.4.0-beta8-dev2", "4.4.0-dev2")] + [InlineData("4.4.0", "4.4.0-beta8", null)] + public void DevPackVersion(string content, string host, string? expected) + => Assert.Equal(expected is null ? null : new PackageVersion(expected), ContentTemplateResolver.DevPackVersion(new PackageVersion(content), new PackageVersion(host))); + + [Theory] + [InlineData("4.4.0-beta7-dev4", "4.4.0-dev4", "4.4.0-beta7")] + [InlineData("4.4.0-dev4", "4.4.0-beta8-dev4", "4.4.0")] + [InlineData("4.4.0-beta7", "4.4.0-dev4", "4.4.0-beta7")] + [InlineData("4.4.0-beta7-dev2", "4.4.0-dev4", "4.4.0-beta7-dev2")] + [InlineData("4.4.1", "4.4.1", "4.4.1")] + public void ContentVersionFromPin(string pinned, string host, string expected) + => Assert.Equal(new PackageVersion(expected), ContentTemplateResolver.ContentVersionFromPin(new PackageVersion(pinned), new PackageVersion(host))); + + [Theory] + [InlineData("4.4.0-beta7", true)] + [InlineData("4.4.0-beta7-dev4", true)] + [InlineData("4.4.0-beta7-dev3", false)] + [InlineData("4.4.0-dev4", false)] + [InlineData("4.4.1-dev4", false)] + [InlineData("4.4.0", false)] + public void IsAcceptableForDevHost(string candidate, bool expected) + => Assert.Equal(expected, ContentTemplateResolver.IsAcceptable(new PackageVersion(candidate), new PackageVersion("4.4.0-beta7"), new PackageVersion("4.4.0-dev4"))); + + [Theory] + [InlineData("4.4.0", true)] + [InlineData("4.4.0-dev4", false)] + [InlineData("4.4.1", false)] + public void IsAcceptableForReleaseHost(string candidate, bool expected) + => Assert.Equal(expected, ContentTemplateResolver.IsAcceptable(new PackageVersion(candidate), new PackageVersion("4.4.0"), new PackageVersion("4.4.0"))); + + [Fact] + public void LocalBuildRange() + { + // A dev host takes its own pack from local feeds, a release host the plain version; both exactly. + AssertExactRange("4.4.0-beta7-dev4", ContentTemplateResolver.LocalBuildRange(new PackageVersion("4.4.0-beta7"), new PackageVersion("4.4.0-dev4"))); + AssertExactRange("4.4.0", ContentTemplateResolver.LocalBuildRange(new PackageVersion("4.4.0"), new PackageVersion("4.4.0"))); + } + + private static void AssertExactRange(string version, PackageVersionRange range) + { + Assert.Equal(new PackageVersion(version), range.MinVersion); + Assert.Equal(new PackageVersion(version), range.MaxVersion); + Assert.True(range.IsMinInclusive); + Assert.True(range.IsMaxInclusive); + } + } +} diff --git a/sources/engine/Stride.Assets/CodeUpgrade/CodeUpgrades.cs b/sources/engine/Stride.Assets/CodeUpgrade/CodeUpgrades.cs index 813b8bff2..c47c075b3 100644 --- a/sources/engine/Stride.Assets/CodeUpgrade/CodeUpgrades.cs +++ b/sources/engine/Stride.Assets/CodeUpgrade/CodeUpgrades.cs @@ -159,6 +159,69 @@ public static class CodeUpgrades }); } + /// + /// Migrates a static member that moved to another (non-nested) type, optionally renamed: + /// Utilities.FreeMemory(p) → MemoryUtilities.Free(p). Alias and using static + /// references become fully qualified, since the new type can't be assumed in scope. + /// + public static SymbolRewrite StaticMemberMove(string declaringType, string memberName, string newDeclaringType, string newMemberName = null) + { + ArgumentNullException.ThrowIfNull(declaringType); + ArgumentNullException.ThrowIfNull(memberName); + ArgumentNullException.ThrowIfNull(newDeclaringType); + newMemberName ??= memberName; + + var oldTypeName = declaringType[(declaringType.LastIndexOf('.') + 1)..]; + var newTypeName = newDeclaringType[(newDeclaringType.LastIndexOf('.') + 1)..]; + var sameNamespace = declaringType[..^oldTypeName.Length] == newDeclaringType[..^newTypeName.Length]; + + return new SymbolRewrite( + compilation => ResolveMembers(compilation, declaringType, memberName), + (editor, referenceNode, symbol) => + { + // Anything that isn't the member-name identifier (e.g. a reference inside a doc comment, + // which the engine doesn't descend into) is left alone. + if (referenceNode is not SimpleNameSyntax name || name.Identifier.ValueText != memberName) + return; + + // WithIdentifier keeps the type argument list of a generic name (Swap). + var newName = name.WithIdentifier(SyntaxFactory.Identifier(name.Identifier.LeadingTrivia, newMemberName, name.Identifier.TrailingTrivia)); + + if (name.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == name) + { + var receiver = memberAccess.Expression; + var newReceiver = receiver; + if (editor.SemanticModel.GetSymbolInfo(receiver).Symbol is ITypeSymbol) + { + var receiverTypeName = receiver switch + { + MemberAccessExpressionSyntax qualified => qualified.Name, + AliasQualifiedNameSyntax aliasQualified => aliasQualified.Name, + _ => receiver as SimpleNameSyntax, + }; + newReceiver = sameNamespace && receiverTypeName is not null && receiverTypeName.Identifier.ValueText == oldTypeName + ? receiver.ReplaceNode(receiverTypeName, SyntaxFactory.IdentifierName(newTypeName).WithTriviaFrom(receiverTypeName)) + : SyntaxFactory.ParseExpression(newDeclaringType).WithTriviaFrom(receiver); + } + editor.ReplaceNode(memberAccess, memberAccess.WithExpression(newReceiver).WithName(newName)); + } + else if (name.Parent is not MemberBindingExpressionSyntax) + { + // Bare reference through `using static OldType;`, which doesn't bring the new type + // (or even its namespace) in scope. + var qualified = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParseExpression(newDeclaringType), + newName.WithoutTrivia()); + editor.ReplaceNode(name, qualified.WithTriviaFrom(name)); + } + else + { + editor.ReplaceNode(name, newName); + } + }); + } + /// /// Finds the argument list the referenced member is called with, and yields the /// of each named argument in it. Reference forms without an argument diff --git a/sources/engine/Stride.Assets/CodeUpgrade/SymbolRewriteEngine.cs b/sources/engine/Stride.Assets/CodeUpgrade/SymbolRewriteEngine.cs index a433d4fbc..cf341b7e4 100644 --- a/sources/engine/Stride.Assets/CodeUpgrade/SymbolRewriteEngine.cs +++ b/sources/engine/Stride.Assets/CodeUpgrade/SymbolRewriteEngine.cs @@ -87,12 +87,13 @@ internal static class SymbolRewriteEngine continue; var editor = await DocumentEditor.CreateAsync(document, cancellationToken); - var appliedSpans = new HashSet(); + var appliedSpans = new HashSet<(TextSpan, SymbolRewrite)>(); foreach (var edit in edits) { // A node can be matched by multiple symbols (e.g. extension skeleton + implementation); - // apply each span once. - if (!appliedSpans.Add(edit.Span)) + // apply each rewrite once per span. Distinct rewrites may share a span (a moved method + // that also had a parameter renamed), as long as they edit disjoint nodes. + if (!appliedSpans.Add((edit.Span, edit.Rewrite))) continue; var node = root.FindNode(edit.Span, getInnermostNodeForTie: true); if (node is null) diff --git a/sources/engine/Stride.Assets/Stride.Assets.csproj b/sources/engine/Stride.Assets/Stride.Assets.csproj index 721afdf6b..a95027f49 100644 --- a/sources/engine/Stride.Assets/Stride.Assets.csproj +++ b/sources/engine/Stride.Assets/Stride.Assets.csproj @@ -10,6 +10,13 @@ STRIDE_VIDEO_FFMPEG;$(DefineConstants) + + + + diff --git a/sources/engine/Stride.Assets/StridePackageUpgrader.cs b/sources/engine/Stride.Assets/StridePackageUpgrader.cs index 2a8c37e72..203d31149 100644 --- a/sources/engine/Stride.Assets/StridePackageUpgrader.cs +++ b/sources/engine/Stride.Assets/StridePackageUpgrader.cs @@ -104,6 +104,21 @@ namespace Stride.Assets Rewrite( ParameterRename("Stride.Graphics.IndexBufferBinding", ".ctor", "count", "indexCount")), + // 4.4: the memory helpers of Stride.Core.Utilities moved to Stride.Core.MemoryUtilities, + // some renamed along with their parameters (#1715). + Rewrite( + StaticMemberMove("Stride.Core.Utilities", "CopyWithAlignmentFallback", "Stride.Core.MemoryUtilities"), + StaticMemberMove("Stride.Core.Utilities", "Clear", "Stride.Core.MemoryUtilities"), + StaticMemberMove("Stride.Core.Utilities", "Swap", "Stride.Core.MemoryUtilities"), + StaticMemberMove("Stride.Core.Utilities", "AllocateMemory", "Stride.Core.MemoryUtilities", "Allocate"), + StaticMemberMove("Stride.Core.Utilities", "AllocateClearedMemory", "Stride.Core.MemoryUtilities", "AllocateCleared"), + StaticMemberMove("Stride.Core.Utilities", "FreeMemory", "Stride.Core.MemoryUtilities", "Free"), + StaticMemberMove("Stride.Core.Utilities", "IsMemoryAligned", "Stride.Core.MemoryUtilities", "IsAligned"), + ParameterRename("Stride.Core.Utilities", "AllocateMemory", "align", "alignment"), + ParameterRename("Stride.Core.Utilities", "AllocateClearedMemory", "align", "alignment"), + ParameterRename("Stride.Core.Utilities", "IsMemoryAligned", "align", "alignment"), + ParameterRename("Stride.Core.Utilities", "IsMemoryAligned", "memoryPtr", "memoryAddress")), + // 4.4: SharpDX and SharpFont were dropped, Vortice no longer flows to game projects, // and Stride.Core.Shaders was retired by the sdsl rewrite; their leftover (unused) // using directives would no longer compile (#3249). diff --git a/sources/engine/Stride.Assets/Templates/ContentTemplateResolver.cs b/sources/engine/Stride.Assets/Templates/ContentTemplateResolver.cs new file mode 100644 index 000000000..5b6c0ae15 --- /dev/null +++ b/sources/engine/Stride.Assets/Templates/ContentTemplateResolver.cs @@ -0,0 +1,157 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Stride.Core; +using Stride.Core.Packages; + +namespace Stride.Assets.Templates; + +/// +/// Picks the installed copy of a content template package (Samples, Starters, AssetPacks) for an engine. Shared by +/// source between the Game Studio bridge and the stride CLI, so both hosts resolve the same package for the same +/// engine. +/// +/// +/// The content packages are versioned independently of the engine: an engine names the exact content version it was +/// released with (StrideSamplesVersion in sources/templates/StrideSamplesVersion.props, recorded as +/// Stride.GameStudio's pinned dependency), and that is what resolves. The one exception is a developer's own +/// checkout: with StridePackContentTemplates, a build with the checkout's -devN suffix packs the content as +/// exactly that version plus the suffix +/// (4.4.0-beta7-dev4), and that pack wins over the published one (it is what the samples being edited look +/// like; the build removes it when the flag is turned off). Only a dev engine looks for it, and only with its own +/// suffix, and then does not copy a local build of the published version (see ). +/// +public static class ContentTemplateResolver +{ + public const string SamplesPackageId = "Stride.Templates.Samples"; + public const string StartersPackageId = "Stride.Templates.Games.Starters"; + public const string AssetPacksPackageId = "Stride.Templates.AssetPacks"; + + /// The content-versioned template packages, all released together at one content version. + public static readonly IReadOnlyList PackageIds = [SamplesPackageId, StartersPackageId, AssetPacksPackageId]; + + /// + /// The package whose pinned dependency on in Stride.GameStudio names the content + /// version of an installed engine (the CLI reads it, since it targets engines other than its own build). + /// + public const string ContentVersionDependencyId = SamplesPackageId; + + // The checkout suffix ends the prerelease label: "dev4", or "beta8-dev4" for a dev build of a beta engine. + private static readonly Regex DevSuffix = new("(?:^|-)(dev[0-9]*)$", RegexOptions.CultureInvariant); + + public static bool IsContentPackage(string packageId) + => PackageIds.Contains(packageId, StringComparer.OrdinalIgnoreCase); + + /// + /// The checkout suffix (dev, dev3, ...) of a development engine build (4.4.0-dev3, + /// 4.4.0-beta8-dev3), null for a release or prerelease engine. Only a dev host may use a dev content pack. + /// + public static string? DevSuffixOf(PackageVersion hostVersion) + => DevSuffix.Match(hostVersion.SpecialVersion ?? string.Empty) is { Success: true } match ? match.Groups[1].Value : null; + + /// + /// The version of this checkout's own pack of (4.4.0-beta7-dev4), null + /// when is not a dev build. + /// + public static PackageVersion? DevPackVersion(PackageVersion contentVersion, PackageVersion hostVersion) + => DevSuffixOf(hostVersion) is { } devSuffix ? new PackageVersion($"{contentVersion}-{devSuffix}") : null; + + /// + /// The content version an installed engine names through its pinned dependency . A dev + /// engine built with StridePackContentTemplates pins its own dev pack (4.4.0-beta7-dev4); its content + /// version is that without the engine's suffix, as the running engine sees it. + /// + public static PackageVersion ContentVersionFromPin(PackageVersion pinned, PackageVersion hostVersion) + { + var devSuffix = DevSuffixOf(hostVersion); + var text = pinned.ToString(); + return devSuffix is not null && text.EndsWith($"-{devSuffix}", StringComparison.OrdinalIgnoreCase) + ? new PackageVersion(text[..^(devSuffix.Length + 1)]) + : pinned; + } + + /// + /// Whether an installed may serve as content version + /// for the engine : exactly that version, or exactly this checkout's pack of it. + /// + public static bool IsAcceptable(PackageVersion candidate, PackageVersion contentVersion, PackageVersion hostVersion) + => candidate.Equals(contentVersion) || candidate.Equals(DevPackVersion(contentVersion, hostVersion)); + + /// + /// The package among to use: this checkout's pack when there is one, else the exact + /// content version. Null when neither is installed. + /// + public static NugetLocalPackage? Pick(IEnumerable installed, PackageVersion contentVersion, PackageVersion hostVersion) + { + var devPack = DevPackVersion(contentVersion, hostVersion); + var candidates = installed.ToList(); + return candidates.FirstOrDefault(package => package.Version.Equals(devPack)) + ?? candidates.FirstOrDefault(package => package.Version.Equals(contentVersion)); + } + + /// + /// The one version a host takes from local package sources (a checkout's bin/packages, the nugetdev feed), + /// as an exact range. A dev host takes only this checkout's pack: a local file carrying the published version + /// (same number, maybe different content) is not taken over the published one. A host without a checkout suffix + /// (a CI build, or a checkout building the clean version) packs the content under the plain version, and takes + /// that local build. + /// + public static PackageVersionRange LocalBuildRange(PackageVersion contentVersion, PackageVersion hostVersion) + { + var version = DevPackVersion(contentVersion, hostVersion) ?? contentVersion; + return new PackageVersionRange(version, true, version, true); + } + + /// + /// Resolves for the engine: picks among the installed copies, and when none is + /// acceptable installs the exact content version from the configured sources (once; it then stays in the + /// store). Returns null when it is neither installed nor obtainable (offline, or not published yet). + /// + /// + /// Lists the installed copies of a package id, first taking the given range from local package sources + /// (). + /// + /// Installs exactly a version of a package from the sources; null when it could not be obtained. + /// Receives one line saying which copy won, or why none did. + public static async Task ResolveAsync( + string packageId, PackageVersion contentVersion, PackageVersion hostVersion, + Func> installed, + Func> install, + Action? log = null) + { + var localBuildRange = LocalBuildRange(contentVersion, hostVersion); + var picked = Pick(installed(packageId, localBuildRange), contentVersion, hostVersion); + if (picked is not null) + { + log?.Invoke(picked.Version.Equals(contentVersion) + ? $"{packageId}: using the content version {contentVersion} this engine names." + : $"{packageId}: using this checkout's pack {picked.Version} over the content version {contentVersion} this engine names."); + return picked; + } + + log?.Invoke($"{packageId} {contentVersion} is not installed; installing it from the package sources."); + NugetLocalPackage? fetched; + try + { + fetched = await install(packageId, contentVersion).ConfigureAwait(false); + } + catch (Exception e) + { + log?.Invoke($"{packageId} {contentVersion} could not be installed: {e.Message}"); + return null; + } + + // Re-pick rather than trust the returned object: only the exact version may be used, whatever the install + // reports. + picked = fetched is null ? null : Pick(installed(packageId, localBuildRange), contentVersion, hostVersion); + log?.Invoke(picked is null + ? $"{packageId} {contentVersion} could not be installed (not published, or no package source reachable)." + : $"{packageId}: installed the content version {contentVersion} this engine names."); + return picked; + } +} diff --git a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs index 6ed47c7ad..0b8184dcf 100644 --- a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs +++ b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Edge.Settings; @@ -33,20 +35,18 @@ namespace Stride.Assets.Templates; public static class DotNetNewTemplateBridge { /// - /// Package IDs the bridge tries to resolve via on startup. - /// Only Stride.Templates.Games (NewGame) ships in the GameStudio installer; the - /// others are dev-only here (present in %LocalAppData%\stride\nugetdev when the - /// solution has been built, absent in installer-only setups). End users reach Starters / - /// Samples via the editor's template store (future) or CLI dotnet new install, and - /// AssetPacks via the on-demand download in ; this - /// list just controls which packages the bridge proactively probes on startup. Missing - /// packages are tolerated (per-package warning, no error). + /// Package IDs the bridge resolves on startup. Stride.Templates.Games (NewGame) is engine-versioned and + /// installed with Game Studio. The others carry the content version this engine names + /// (, resolved by ): Starters and + /// Samples are Game Studio dependencies too, AssetPacks is fetched on demand (also by + /// ). A package that is neither installed nor obtainable is tolerated + /// (per-package warning, no error). /// private static readonly string[] BundledTemplatePackageIds = { "Stride.Templates.Games", - "Stride.Templates.Games.Starters", - "Stride.Templates.Samples", + ContentTemplateResolver.StartersPackageId, + ContentTemplateResolver.SamplesPackageId, AssetPacksPackageId, }; @@ -56,28 +56,45 @@ public static class DotNetNewTemplateBridge /// when a game template offering packs is /// instantiated on a machine that doesn't have it yet. /// - public const string AssetPacksPackageId = "Stride.Templates.AssetPacks"; + public const string AssetPacksPackageId = ContentTemplateResolver.AssetPacksPackageId; private static DotNetNewTemplateRegistry? registry; private static readonly object InitLock = new(); + /// Serializes content downloads (). + private static readonly SemaphoreSlim DownloadGate = new(1, 1); + /// /// In-process singleton; null until has run. /// Consumed by DotNetNewTemplateGenerator at instantiation time. /// public static DotNetNewTemplateRegistry? Registry => registry; + /// Identities of the templates already registered with (guarded by ). + private static readonly HashSet RegisteredTemplateIdentities = new(StringComparer.Ordinal); + /// - /// Resolves each entry via , - /// installs them into the shared registry, and wraps every loaded dotnet new template as a - /// registered with . - /// Tolerates missing packages — dev workflows that haven't built every template project yet - /// still load the editor. + /// The package holding the descriptions handed to . Never saved to disk and never + /// backed by a real .sdpkg: its is the only thing + /// reads. A later pass (the background content download) adds to this + /// same package, so the descriptions of one host live in one place. FullPath must be non-null for the + /// DistinctPackagePathComparer used in FindTemplates; the sentinel path collides with no real package. + /// + private static readonly Package SyntheticPackage = new() { FullPath = new UFile("Stride.DotNetNewTemplates.synthetic") }; + + /// + /// Resolves each entry installed on this machine, installs them into the + /// shared registry, and wraps every loaded dotnet new template as a + /// registered with . Never downloads: this runs during editor startup, often on the + /// UI thread. Starters and Samples missing locally are downloaded in the background and their templates are + /// registered when ready (AssetPacks are fetched on demand by ). + /// Tolerates missing packages — dev workflows that haven't built every template project yet still load the editor. /// public static void RegisterProjectTemplates() { var logger = GlobalLogger.GetLogger("DotNetNewTemplateBridge"); logger.ActivateLog(LogMessageType.Info); + var missingContent = new List(); lock (InitLock) { // Stride-owned settings tree ({profileDir}/.templateengine/...), kept out of the user's @@ -103,85 +120,135 @@ public static class DotNetNewTemplateBridge // can be resolved back to absolute on-disk locations at registration time. var sdtplsByIdentity = new Dictionary(); foreach (var packageId in BundledTemplatePackageIds) - InstallBundledPackage(packageId, sdtplsByIdentity, logger); - - var templatesTask = registry.GetTemplatesAsync(); - templatesTask.Wait(); - var templates = templatesTask.Result; - logger.Info($"Loaded {templates.Count} dotnet new template(s) total"); - - // Synthetic package: holds only the TemplateDescriptions we want TemplateManager to - // surface. Never saved to disk, never has a real .sdpkg — the descriptions are the - // only thing FindTemplates() reads (it does `packages.SelectMany(p => p.Templates)`). - // FullPath needs to be non-null so the DistinctPackagePathComparer (used to de-dup - // ExtraPackages in FindTemplates) doesn't NRE in GetHashCode; using a sentinel path - // ensures it doesn't collide with any real package. - var synthetic = new Package { FullPath = new UFile("Stride.DotNetNewTemplates.synthetic") }; - foreach (var template in templates) { - // Item templates (asset packs) are not stand-alone projects — they surface as - // checkboxes inside the parameter dialog of templates that offer them, not as - // entries in the New-Project list. - if (IsItemTemplate(template)) - continue; - - // Cross-ref by template.json identity → matches the sdtpl Id (we set - // `identity = sdtpl.Id` in the preprocessor) so the dict lookup hits. - sdtplsByIdentity.TryGetValue(template.Identity, out var source); - var sdtpl = source?.Metadata; - var shortName = template.ShortNameList.FirstOrDefault(); - // Per-template content dir inside the package, e.g. /content/stride-fps/. - // Icon/Screenshot relative paths in .sdtpl resolve against this dir at runtime. - var templateContentDir = source != null && shortName != null - ? Path.Combine(source.InstallSource, "content", shortName) - : null; - var description = new TemplateDotNetNewDescription + if (!InstallBundledPackage(packageId, sdtplsByIdentity, logger) + && ContentTemplateResolver.IsContentPackage(packageId) && packageId != AssetPacksPackageId) { - Id = sdtpl?.Id ?? TryParseGuid(template.Identity), - Name = sdtpl?.Name ?? template.Name ?? shortName ?? template.Identity, - Description = sdtpl?.Description ?? template.Description, - FullDescription = sdtpl?.FullDescription, - DefaultOutputName = sdtpl?.DefaultOutputName ?? template.DefaultName ?? "MyGame", - Group = sdtpl?.Group ?? template.GroupIdentity ?? "Stride", - Scope = (sdtpl?.Scope != null && Enum.TryParse(sdtpl.Scope, ignoreCase: true, out var parsedScope)) - ? parsedScope - : TemplateScope.Session, - TemplateIdentity = template.Identity, - TemplateShortName = shortName ?? string.Empty, - OffersAssetPacks = sdtpl?.HasParameter("assetPacks") == true, - // FullPath must be non-null: TemplateDescriptionViewModel calls - // Template.FullPath.GetFullDirectory() unconditionally when constructing - // image paths. We point it at a synthetic file inside the per-template - // content dir so the directory part is the natural relative-resolution root - // (matches what TemplateDescription.FullPath means for real .sdpkg-backed - // templates). Falls back to a per-identity sentinel if we couldn't resolve - // a content dir (e.g. template not in the aggregated metadata). - FullPath = templateContentDir != null - ? new UFile(Path.Combine(templateContentDir, ".synthetic.sdtpl")) - : new UFile($"{template.Identity}.synthetic"), - Icon = templateContentDir != null ? ResolveTemplateAsset(sdtpl?.Icon, templateContentDir) : null, - }; - if (templateContentDir != null && sdtpl != null) - { - foreach (var s in sdtpl.Screenshots) - { - var resolved = ResolveTemplateAsset(s, templateContentDir); - if (resolved != null) - description.Screenshots.Add(resolved); - } + missingContent.Add(packageId); } - synthetic.Templates.Add(description); } - TemplateManager.RegisterPackage(synthetic); + RegisterNewTemplates(sdtplsByIdentity, logger); } + + if (missingContent.Count > 0) + _ = Task.Run(() => DownloadMissingContentAsync(missingContent, logger)); + } + + /// + /// Downloads content packages () this machine doesn't have, then installs them into the + /// registry and registers their templates, so they show in the New-Project dialog once ready. Best effort. + /// + private static async Task DownloadMissingContentAsync(IReadOnlyList packageIds, Logger logger) + { + try + { + var fetched = new List(); + for (var i = 0; i < packageIds.Count; i++) + { + if (await InstallContentPackageAsync(packageIds[i], logger, i + 1, packageIds.Count).ConfigureAwait(false) is not null) + fetched.Add(packageIds[i]); + } + if (fetched.Count == 0) + return; + + lock (InitLock) + { + var sdtplsByIdentity = new Dictionary(); + foreach (var packageId in fetched) + InstallBundledPackage(packageId, sdtplsByIdentity, logger); + RegisterNewTemplates(sdtplsByIdentity, logger); + } + } + catch (Exception e) + { + logger.Warning($"Could not download the template content ({string.Join(", ", packageIds)}): {e.Message}"); + } + } + + /// + /// Wraps every registry template not registered yet as a , in one + /// synthetic package registered with . Caller holds . + /// + private static void RegisterNewTemplates(Dictionary sdtplsByIdentity, Logger logger) + { + var templatesTask = registry!.GetTemplatesAsync(); + templatesTask.Wait(); + var templates = templatesTask.Result; + logger.Info($"Loaded {templates.Count} dotnet new template(s) total"); + + var added = 0; + foreach (var template in templates) + { + // Item templates (asset packs) are not stand-alone projects — they surface as + // checkboxes inside the parameter dialog of templates that offer them, not as + // entries in the New-Project list. + if (IsItemTemplate(template)) + continue; + + // Registered by an earlier pass (startup, then the background download). + if (!RegisteredTemplateIdentities.Add(template.Identity)) + continue; + + // Cross-ref by template.json identity → matches the sdtpl Id (we set + // `identity = sdtpl.Id` in the preprocessor) so the dict lookup hits. + sdtplsByIdentity.TryGetValue(template.Identity, out var source); + var sdtpl = source?.Metadata; + var shortName = template.ShortNameList.FirstOrDefault(); + // Per-template content dir inside the package, e.g. /content/stride-fps/. + // Icon/Screenshot relative paths in .sdtpl resolve against this dir at runtime. + var templateContentDir = source != null && shortName != null + ? Path.Combine(source.InstallSource, "content", shortName) + : null; + var description = new TemplateDotNetNewDescription + { + Id = sdtpl?.Id ?? TryParseGuid(template.Identity), + Name = sdtpl?.Name ?? template.Name ?? shortName ?? template.Identity, + Description = sdtpl?.Description ?? template.Description, + FullDescription = sdtpl?.FullDescription, + DefaultOutputName = sdtpl?.DefaultOutputName ?? template.DefaultName ?? "MyGame", + Group = sdtpl?.Group ?? template.GroupIdentity ?? "Stride", + Scope = (sdtpl?.Scope != null && Enum.TryParse(sdtpl.Scope, ignoreCase: true, out var parsedScope)) + ? parsedScope + : TemplateScope.Session, + TemplateIdentity = template.Identity, + TemplateShortName = shortName ?? string.Empty, + OffersAssetPacks = sdtpl?.HasParameter("assetPacks") == true, + // FullPath must be non-null: TemplateDescriptionViewModel calls + // Template.FullPath.GetFullDirectory() unconditionally when constructing + // image paths. We point it at a synthetic file inside the per-template + // content dir so the directory part is the natural relative-resolution root + // (matches what TemplateDescription.FullPath means for real .sdpkg-backed + // templates). Falls back to a per-identity sentinel if we couldn't resolve + // a content dir (e.g. template not in the aggregated metadata). + FullPath = templateContentDir != null + ? new UFile(Path.Combine(templateContentDir, ".synthetic.sdtpl")) + : new UFile($"{template.Identity}.synthetic"), + Icon = templateContentDir != null ? ResolveTemplateAsset(sdtpl?.Icon, templateContentDir) : null, + }; + if (templateContentDir != null && sdtpl != null) + { + foreach (var s in sdtpl.Screenshots) + { + var resolved = ResolveTemplateAsset(s, templateContentDir); + if (resolved != null) + description.Screenshots.Add(resolved); + } + } + SyntheticPackage.Templates.Add(description); + added++; + } + + // Registering the same package again signals the new descriptions to an open template list. + if (added > 0) + TemplateManager.RegisterPackage(SyntheticPackage); } /// /// The asset-pack item templates available for the New Game flow, in registry order. When /// none are installed and is true, resolves the - /// package via — downloading it - /// from the configured NuGet sources if this machine doesn't have it — and installs it into + /// package at this engine's content version — downloading + /// it from the configured NuGet sources if this machine doesn't have it — and installs it into /// the registry. Returns an empty list when the package can't be obtained (e.g. offline); /// callers degrade by not offering packs. /// @@ -195,28 +262,21 @@ public static class DotNetNewTemplateBridge if (packs.Count > 0 || !allowDownload) return packs; - var version = new PackageVersion(DesiredVersionFor(AssetPacksPackageId)); - var versionRange = new PackageVersionRange(version, true, version, true); - if (PackageStore.Instance.GetPackageDirectory(AssetPacksPackageId, versionRange) is null) - { - logger.Info($"{AssetPacksPackageId} {version} not present locally; downloading..."); - try + // Download first, then the same path as startup probing: install the now-local package into the registry + // (no TemplateManager registration — item templates stay out of the New-Project list). InstallBundledPackage + // is synchronous and reindexes the package, so it goes to the thread pool rather than the UI thread. + var installed = await InstallContentPackageAsync(AssetPacksPackageId, logger).ConfigureAwait(false) is not null + && await Task.Run(() => { - await PackageStore.Instance.InstallPackage(AssetPacksPackageId, version).ConfigureAwait(false); - } - catch (Exception e) - { - logger.Warning($"Could not download {AssetPacksPackageId} {version}: {e.Message}. Asset packs will be unavailable."); - return []; - } - } - - lock (InitLock) + lock (InitLock) + { + return InstallBundledPackage(AssetPacksPackageId, new Dictionary(), logger); + } + }).ConfigureAwait(false); + if (!installed) { - // Same path as startup probing: resolves the package dir and installs it into the - // registry (no TemplateManager registration — item templates stay out of the - // New-Project list). - InstallBundledPackage(AssetPacksPackageId, new Dictionary(), logger); + logger.Warning($"{AssetPacksPackageId} is unavailable; asset packs will not be offered."); + return []; } return FilterAssetPacks(await registry.GetTemplatesAsync().ConfigureAwait(false)); } @@ -230,16 +290,95 @@ public static class DotNetNewTemplateBridge => template.TagsCollection.TryGetValue("type", out var type) && string.Equals(type, "item", StringComparison.OrdinalIgnoreCase); /// - /// The package version this build expects for : content packages - /// (Samples, Starters, AssetPacks) use + the engine NuGet suffix; - /// everything else tracks . + /// The content version this engine names (Samples, Starters, AssetPacks): the committed value of + /// sources/templates/StrideSamplesVersion.props, embedded as a resource. /// - private static string DesiredVersionFor(string packageId) + private static PackageVersion ContentVersion => LazyContentVersion.Value; + + private static readonly Lazy LazyContentVersion = new(() => { - var contentVersioned = packageId is "Stride.Templates.Samples" or "Stride.Templates.Games.Starters" or AssetPacksPackageId; - return contentVersioned - ? StrideVersion.SamplesVersion + StrideVersion.NuGetVersionSuffix - : StrideVersion.NuGetVersion; + using var stream = typeof(DotNetNewTemplateBridge).Assembly + .GetManifestResourceStream("StrideSamplesVersion.props") + ?? throw new InvalidOperationException("StrideSamplesVersion.props is not embedded in Stride.Assets."); + using var reader = new StreamReader(stream); + var match = Regex.Match(reader.ReadToEnd(), "<_StrideCommittedSamplesVersion>([^<]+)This build's own version, the engine the content is resolved for. + private static PackageVersion HostVersion => new(StrideVersion.NuGetVersion); + + /// + /// The installed directory of for this build, null when it is not installed. Never + /// downloads ( does): this also runs on the UI thread at startup. Content + /// packages resolve through (this checkout's own pack of the content + /// version, or exactly the content version); everything else is exactly . + /// + private static UDirectory? ResolvePackageDirectory(string packageId, Logger logger) + { + if (ContentTemplateResolver.IsContentPackage(packageId)) + { + var installed = PackageStore.Instance.GetLocalPackages(packageId, ContentTemplateResolver.LocalBuildRange(ContentVersion, HostVersion)); + var package = ContentTemplateResolver.Pick(installed, ContentVersion, HostVersion); + logger.Info(package is null + ? $"{packageId}: content version {ContentVersion} is not installed." + : $"{packageId}: using {package.Version} (content version {ContentVersion})."); + return package is not null ? PackageStore.Instance.GetPackageDirectory(package) : null; + } + + // Exact range, so release/prerelease ordering is moot. + var exact = new PackageVersionRange(HostVersion, true, HostVersion, true); + return PackageStore.Instance.GetPackageDirectory(packageId, exact); + } + + /// + /// Resolves content package , downloading it when missing, and reports the download to + /// so the template windows can show it. + /// + /// Position of in its batch, from 1 (shown with the progress). + /// Number of packages in the batch. + private static async Task InstallContentPackageAsync(string packageId, Logger logger, int index = 1, int count = 1) + { + // One content download at a time: the store reports progress for all of them together. + await DownloadGate.WaitAsync().ConfigureAwait(false); + try + { + var downloading = false; + // A content install downloads the package and at most its small engine marker dependency, both started + // together, so the size of the started downloads is the size to show. + void OnProgress(long downloadedBytes) + => TemplateDownloads.Report(new TemplateDownload(packageId, downloadedBytes, PackageStore.Instance.StartedDownloadBytes, index, count)); + Task Install(string id, PackageVersion version) + { + downloading = true; + TemplateDownloads.Report(new TemplateDownload(packageId, 0, 0, index, count)); + return PackageStore.Instance.InstallPackage(id, version); + } + + PackageStore.Instance.DownloadProgress += OnProgress; + NugetLocalPackage? package = null; + try + { + package = await ContentTemplateResolver.ResolveAsync( + packageId, ContentVersion, HostVersion, + PackageStore.Instance.GetLocalPackages, + Install, + message => logger.Info(message)).ConfigureAwait(false); + return package; + } + finally + { + PackageStore.Instance.DownloadProgress -= OnProgress; + if (downloading) + TemplateDownloads.Report(package is null ? new TemplateDownload(packageId, 0, 0, index, count, Failed: true) : null); + } + } + finally + { + DownloadGate.Release(); + } } /// @@ -291,8 +430,8 @@ public static class DotNetNewTemplateBridge } /// - /// True when exists AND — for packages we manage — its version - /// matches . Mount points use NuGet's global-folder layout + /// True when exists AND — for packages we manage — its version is one this + /// build may use (see ). Mount points use NuGet's global-folder layout /// (<root>\<id>\<version>), so id/version are the trailing two segments; /// unrecognized entries are left untouched. /// @@ -302,43 +441,36 @@ public static class DotNetNewTemplateBridge return false; var trimmed = mountPointUri.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - var version = Path.GetFileName(trimmed); + var versionText = Path.GetFileName(trimmed); var id = Path.GetFileName(Path.GetDirectoryName(trimmed) ?? string.Empty); var managedId = BundledTemplatePackageIds.FirstOrDefault(p => string.Equals(p, id, StringComparison.OrdinalIgnoreCase)); if (managedId is null) return true; - try - { - return new PackageVersion(version).Equals(new PackageVersion(DesiredVersionFor(managedId))); - } - catch - { - return string.Equals(version, DesiredVersionFor(managedId), StringComparison.OrdinalIgnoreCase); - } + if (!PackageVersion.TryParse(versionText, out var version)) + return false; + return ContentTemplateResolver.IsContentPackage(managedId) + ? ContentTemplateResolver.IsAcceptable(version, ContentVersion, HostVersion) + : version.Equals(HostVersion); } /// - /// Resolves via , installs it into the - /// registry (skipping if unchanged since last session), and merges its templates.sdtpls - /// entries into . Tolerates a missing package by logging. + /// Resolves for this build (), installs it + /// into the registry (skipping if unchanged since last session), and merges its templates.sdtpls + /// entries into . Tolerates a missing package by logging; returns whether + /// the package is installed in the registry. /// - private static void InstallBundledPackage(string packageId, Dictionary sdtplsByIdentity, Logger logger) + private static bool InstallBundledPackage(string packageId, Dictionary sdtplsByIdentity, Logger logger) { - // Resolve each package at the exact version it's packed at (exact range, so release/prerelease ordering is - // moot). Content-versioned (Samples, Starters) = StrideSamplesVersion + engine suffix, matching the pack in - // Stride.Templates.Common.targets from the same source; Games is engine-versioned. - var version = new PackageVersion(DesiredVersionFor(packageId)); - var versionRange = new PackageVersionRange(version, true, version, true); - var packageDir = PackageStore.Instance.GetPackageDirectory(packageId, versionRange); + var packageDir = ResolvePackageDirectory(packageId, logger); if (packageDir is null) { - // Not installed yet (e.g. fresh checkout before first build of that templates - // project). Not fatal — the editor still opens; this package's templates just - // won't appear in the New-Project dialog until built at least once. - logger.Warning($"{packageId} {version} not found via PackageStore; its templates will be unavailable."); - return; + // Not installed and not obtainable (a fresh checkout before the first build of Stride.Templates.Games, + // or content that is not published yet / no package source reachable). Not fatal — the editor still + // opens; this package's templates just won't appear in the New-Project dialog. + logger.Warning($"{packageId} is not available; its templates will be unavailable."); + return false; } // Install from the extracted package directory rather than the .nupkg file inside it. // Reasoning: pointing the bootstrapper at the .nupkg path inside NuGet's global cache @@ -371,7 +503,7 @@ public static class DotNetNewTemplateBridge { foreach (var d in diagnostics) logger.Error($"Template install ({packageId}): {d}"); - return; + return false; } } @@ -379,6 +511,7 @@ public static class DotNetNewTemplateBridge // the installSource it came from so Icon/Screenshot relative paths can be resolved later. foreach (var (id, meta) in LoadAggregatedSdtpls(installSource, logger)) sdtplsByIdentity[id] = new TemplateMetadataSource(meta, installSource); + return true; } /// diff --git a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateGenerator.cs b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateGenerator.cs index 3b1ec5f8e..4eddedb67 100644 --- a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateGenerator.cs +++ b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Edge.Template; @@ -110,7 +111,9 @@ public class DotNetNewTemplateGenerator : SessionTemplateGenerator if (sdpkg == null) return false; InstantiateAssetPacks(parameters, sdpkg); - if (!UpgradeGeneratedProjects(parameters)) + // Content packed for an older engine keeps its version here: IntegrateIntoSession runs the package + // upgraders on it, which migrate the assets and the code and stamp the version themselves. + if (!UpgradeGeneratedProjects(parameters, stampOlderContent: false)) return false; return IntegrateIntoSession(sdpkg, parameters); } @@ -244,16 +247,46 @@ public class DotNetNewTemplateGenerator : SessionTemplateGenerator /// them to . Runs without a session or /// AssemblyContainer — same standalone path the legacy session-load uses internally. /// - protected virtual bool UpgradeGeneratedProjects(SessionTemplateGeneratorParameters parameters) + /// + /// Whether content packed for an older engine is stamped too. False when a session load follows: the package + /// upgraders it runs need the version the content came from, and stamping would leave the content looking + /// current and never upgraded. + /// + protected virtual bool UpgradeGeneratedProjects(SessionTemplateGeneratorParameters parameters, bool stampOlderContent = true) { var log = parameters.Logger; - foreach (var csproj in Directory.EnumerateFiles(parameters.OutputDirectory, "*.csproj", SearchOption.AllDirectories)) + var csprojs = Directory.EnumerateFiles(parameters.OutputDirectory, "*.csproj", SearchOption.AllDirectories).ToList(); + + if (!stampOlderContent && GetContentEngineVersion(csprojs) is { } contentVersion + && contentVersion.Version < new PackageVersion(StridePackageUpgrader.CurrentVersion).Version) + { + log.Info($"Template content is written for Stride {contentVersion}; it is upgraded to {StridePackageUpgrader.CurrentVersion} as it is loaded."); + return true; + } + + foreach (var csproj in csprojs) { StridePackageUpgrader.UpgradeProjectVersions(csproj, log); } return true; } + /// + /// The Stride.Engine version the instantiated content names, null when none of its projects references it + /// (a template of assets alone). The prerelease suffix is part of it; upgrades compare the numbers only. + /// + private static PackageVersion GetContentEngineVersion(IEnumerable csprojPaths) + { + foreach (var csprojPath in csprojPaths) + { + var match = Regex.Match(File.ReadAllText(csprojPath), "Stride\\.Engine\"\\s+Version=\"([^\"]+)\""); + if (match.Success && PackageVersion.TryParse(match.Groups[1].Value, out var version)) + return version; + } + + return null; + } + /// /// Phase 3 — wires the generated package into : /// , per-platform exec registration, diff --git a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateRegistry.cs b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateRegistry.cs index 8ba0bbb6b..40830b100 100644 --- a/sources/engine/Stride.Assets/Templates/DotNetNewTemplateRegistry.cs +++ b/sources/engine/Stride.Assets/Templates/DotNetNewTemplateRegistry.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using Microsoft.TemplateEngine.Abstractions; @@ -47,6 +48,7 @@ public sealed class DotNetNewTemplateRegistry : IDisposable // IPathInfo directly (only EngineEnvironmentSettings does); redirecting via IEnvironment // is the cleanest hook into DefaultPathInfo's path derivation. // "dotnetcli" fallback so our host reads each template's dotnetcli.host.json (isVisible etc.). + RemoveDuplicateEntries(profileDir); var host = new DefaultTemplateEngineHost(HostIdentifier, hostVersion, fallbackHostTemplateConfigNames: new[] { "dotnetcli" }); var environment = new StrideTemplateEngineEnvironment(profileDir); bootstrapper = new Bootstrapper( @@ -56,6 +58,37 @@ public sealed class DotNetNewTemplateRegistry : IDisposable environment: environment); } + /// + /// Keeps one entry per mount point in the persisted packages.json (the last one, the latest install): a + /// forced reinstall of a .nupkg adds an entry each time. + /// + private static void RemoveDuplicateEntries(string profileDir) + { + var packagesJson = Path.Combine(profileDir, ".templateengine", "packages.json"); + try + { + if (!File.Exists(packagesJson) || JsonNode.Parse(File.ReadAllText(packagesJson))?["Packages"] is not JsonArray packages) + return; + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var removed = 0; + for (var i = packages.Count - 1; i >= 0; i--) + { + var uri = packages[i]?["MountPointUri"]?.GetValue(); + if (uri is null || seen.Add(uri)) + continue; + packages.RemoveAt(i); + removed++; + } + if (removed > 0) + File.WriteAllText(packagesJson, packages.Root.ToJsonString()); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or System.Text.Json.JsonException or InvalidOperationException) + { + // Best effort: the template engine reads the file as it is. + } + } + /// /// Installs (or reinstalls) the template package at . Accepts either /// a path to a .nupkg file (handled by the NuGet installer) or a path to an extracted @@ -88,6 +121,24 @@ public sealed class DotNetNewTemplateRegistry : IDisposable return (success, diagnostics); } + /// + /// Uninstalls the installed packages that none of (as given to + /// ) installed, so templates of a package version no longer resolved do not + /// linger next to the current ones. A .nupkg source is matched by file name, since the registry mounts + /// its own copy. + /// + public async Task RemovePackagesExceptAsync(IReadOnlyCollection sources, CancellationToken cancellationToken = default) + { + var directories = new HashSet(sources.Where(Directory.Exists).Select(s => Path.GetFullPath(s).TrimEnd('\\', '/')), StringComparer.OrdinalIgnoreCase); + var nupkgNames = new HashSet(sources.Where(File.Exists).Select(Path.GetFileName)!, StringComparer.OrdinalIgnoreCase); + var stale = (await bootstrapper.GetManagedTemplatePackagesAsync(cancellationToken).ConfigureAwait(false)) + .Where(package => !directories.Contains(package.MountPointUri.TrimEnd('\\', '/')) + && !nupkgNames.Contains(Path.GetFileName(package.MountPointUri))) + .ToList(); + if (stale.Count > 0) + await bootstrapper.UninstallTemplatePackagesAsync(stale, cancellationToken).ConfigureAwait(false); + } + /// Enumerates all available templates (across all installed packages). public async Task> GetTemplatesAsync(CancellationToken cancellationToken = default) { diff --git a/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierReadTestShader.sdsl b/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierReadTestShader.sdsl new file mode 100644 index 000000000..58a2c727d --- /dev/null +++ b/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierReadTestShader.sdsl @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Graphics.Tests +{ + /// + /// Reads a structured buffer written by a previous dispatch and increments each element. + /// + internal shader BufferBarrierReadTestShader : ComputeShaderBase + { + stage StructuredBuffer Input; + stage RWStructuredBuffer Result; + + override void Compute() + { + Result[streams.DispatchThreadId.x] = Input[streams.DispatchThreadId.x] + 1; + } + }; +} diff --git a/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierTestShader.sdsl b/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierTestShader.sdsl new file mode 100644 index 000000000..c5ebe7488 --- /dev/null +++ b/sources/engine/Stride.Graphics.Tests.11_0/Assets/BufferBarrierTestShader.sdsl @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Graphics.Tests +{ + /// + /// Writes a value derived from each thread's dispatch index into a structured buffer. + /// + internal shader BufferBarrierTestShader : ComputeShaderBase + { + stage RWStructuredBuffer Output; + + override void Compute() + { + Output[streams.DispatchThreadId.x] = streams.DispatchThreadId.x * 3; + } + }; +} diff --git a/sources/engine/Stride.Graphics.Tests.11_0/TestBufferBarrier.cs b/sources/engine/Stride.Graphics.Tests.11_0/TestBufferBarrier.cs new file mode 100644 index 000000000..4782df5d0 --- /dev/null +++ b/sources/engine/Stride.Graphics.Tests.11_0/TestBufferBarrier.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Threading.Tasks; + +using Xunit; + +using Stride.Core; +using Stride.Core.Mathematics; +using Stride.Rendering; +using Stride.Rendering.ComputeEffect; + +namespace Stride.Graphics.Tests; + +/// +/// Covers a compute write handed to a second dispatch that reads it as a shader resource. +/// +/// +/// The consumer is a dispatch rather than a readback: a readback copy emits its own barrier from +/// the buffer's access and stage masks, which would synchronise the write even with no transition +/// and leave the test unable to fail. Run with STRIDE_VULKAN_SYNC_VALIDATION=1 to have Vulkan +/// report the hazard when the transition is missing. +/// +public class TestBufferBarrier : GraphicTestGameBase +{ + private const int ElementCount = 64; + + private ComputeEffectShader writeEffect; + private ComputeEffectShader readEffect; + private Buffer sharedBuffer; + private Buffer resultBuffer; + + protected override void RegisterTests() + { + base.RegisterTests(); + + FrameGameSystem.Draw(ComputeWriteIsVisibleToASubsequentDispatch); + } + + protected override async Task LoadContent() + { + await base.LoadContent(); + + sharedBuffer = Buffer.Structured.New(GraphicsDevice, ElementCount, unorderedAccess: true).DisposeBy(this); + resultBuffer = Buffer.Structured.New(GraphicsDevice, ElementCount, unorderedAccess: true).DisposeBy(this); + + var renderContext = RenderContext.GetShared(Services); + writeEffect = new ComputeEffectShader(renderContext) + { + ShaderSourceName = "BufferBarrierTestShader", + ThreadNumbers = new Int3(ElementCount, 1, 1), + ThreadGroupCounts = new Int3(1, 1, 1), + }; + writeEffect.DisposeBy(this); + + readEffect = new ComputeEffectShader(renderContext) + { + ShaderSourceName = "BufferBarrierReadTestShader", + ThreadNumbers = new Int3(ElementCount, 1, 1), + ThreadGroupCounts = new Int3(1, 1, 1), + }; + readEffect.DisposeBy(this); + } + + private void ComputeWriteIsVisibleToASubsequentDispatch() + { + var commandList = GraphicsContext.CommandList; + var renderDrawContext = new RenderDrawContext(Services, RenderContext.GetShared(Services), GraphicsContext); + + commandList.ResourceBarrierTransition(sharedBuffer, BarrierLayout.UnorderedAccess); + writeEffect.Parameters.Set(BufferBarrierTestShaderKeys.Output, sharedBuffer); + ((RendererBase)writeEffect).Draw(renderDrawContext); + + commandList.ResourceBarrierTransition(sharedBuffer, BarrierLayout.ShaderResource); + commandList.ResourceBarrierTransition(resultBuffer, BarrierLayout.UnorderedAccess); + readEffect.Parameters.Set(BufferBarrierReadTestShaderKeys.Input, sharedBuffer); + readEffect.Parameters.Set(BufferBarrierReadTestShaderKeys.Result, resultBuffer); + ((RendererBase)readEffect).Draw(renderDrawContext); + + var values = resultBuffer.GetData(commandList); + + Assert.Equal(ElementCount, values.Length); + for (uint i = 0; i < ElementCount; i++) + { + Assert.Equal((i * 3) + 1, values[i]); + } + } + + [SkippableFact] + public void ComputeWriteIsVisibleToASubsequentRead() + { + RunGameTest(new TestBufferBarrier()); + } +} diff --git a/sources/engine/Stride.Graphics/BarrierLayout.cs b/sources/engine/Stride.Graphics/BarrierLayout.cs index d9351252e..b63977b98 100644 --- a/sources/engine/Stride.Graphics/BarrierLayout.cs +++ b/sources/engine/Stride.Graphics/BarrierLayout.cs @@ -20,12 +20,14 @@ public enum BarrierLayout Common, /// - /// The resource is used as a render target. + /// The resource is used as a render target. Covers reads as well as writes, because blending + /// and a load operation that preserves the existing contents both read the attachment. /// RenderTarget, /// - /// The resource is used as a writable depth-stencil buffer. + /// The resource is used as a writable depth-stencil buffer. Covers reads as well as writes, + /// because the depth and stencil tests read the attachment. /// DepthStencilWrite, diff --git a/sources/engine/Stride.Graphics/Vulkan/BarrierMapping.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/BarrierMapping.Vulkan.cs index ba982ff5b..1f6d33e41 100644 --- a/sources/engine/Stride.Graphics/Vulkan/BarrierMapping.Vulkan.cs +++ b/sources/engine/Stride.Graphics/Vulkan/BarrierMapping.Vulkan.cs @@ -54,8 +54,8 @@ internal static class BarrierMapping /// internal static VkAccessFlags ToVkAccessFlags(BarrierLayout layout) => layout switch { - BarrierLayout.RenderTarget => VkAccessFlags.ColorAttachmentWrite, - BarrierLayout.DepthStencilWrite => VkAccessFlags.DepthStencilAttachmentWrite, + BarrierLayout.RenderTarget => VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.ColorAttachmentRead, + BarrierLayout.DepthStencilWrite => VkAccessFlags.DepthStencilAttachmentWrite | VkAccessFlags.DepthStencilAttachmentRead, BarrierLayout.DepthStencilRead => VkAccessFlags.DepthStencilAttachmentRead, BarrierLayout.ShaderResource => VkAccessFlags.ShaderRead, BarrierLayout.UnorderedAccess => VkAccessFlags.ShaderRead | VkAccessFlags.ShaderWrite, @@ -75,7 +75,7 @@ internal static class BarrierMapping BarrierLayout.RenderTarget => VkPipelineStageFlags.ColorAttachmentOutput, BarrierLayout.DepthStencilWrite => VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests, BarrierLayout.DepthStencilRead => VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests, - BarrierLayout.ShaderResource => VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader, + BarrierLayout.ShaderResource => VkPipelineStageFlags.VertexShader | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader, BarrierLayout.UnorderedAccess => VkPipelineStageFlags.ComputeShader, BarrierLayout.CopySource => VkPipelineStageFlags.Transfer, BarrierLayout.CopyDest => VkPipelineStageFlags.Transfer, diff --git a/sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs index a464768d1..d6b8118a1 100644 --- a/sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs +++ b/sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs @@ -34,6 +34,11 @@ namespace Stride.Graphics // that's a known limitation to be addressed by adding a "last-submitted layout" tracker. private readonly Dictionary currentCbLayouts = new(); + /// + /// The last access this command buffer synchronised each buffer against. + /// + private readonly Dictionary currentCbBufferLayouts = new(); + // Dynamic rendering state: whether a render pass instance (vkCmdBeginRendering) is active, // whether the bound attachments changed since it began, and which depth layout it declared. private bool activeRendering; @@ -85,6 +90,7 @@ namespace Stride.Graphics CleanupRenderPass(); boundDescriptorSets.Clear(); currentCbLayouts.Clear(); + currentCbBufferLayouts.Clear(); renderingDirty = true; @@ -487,6 +493,11 @@ namespace Stride.Graphics /// /// Transitions a resource to a new layout using the cross-platform barrier abstraction. /// + /// + /// For a , and + /// are read as the source but never written. + /// The copy and upload paths rely on them staying the set of every legal usage. + /// // TODO: subresource parameter is currently ignored — Vulkan always transitions all // subresources. This is incorrect when different subresources need different layouts // (e.g. reading mip 0 as ShaderResource while writing mip 1 as RenderTarget). @@ -543,12 +554,64 @@ namespace Stride.Graphics var memoryBarrier = new VkImageMemoryBarrier(texture.NativeImage, new VkImageSubresourceRange(texture.NativeImageAspect, 0, uint.MaxValue, 0, uint.MaxValue), oldAccessMask, newAccessMask, oldLayout, newVkLayout); GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, sourceStages, newStages, VkDependencyFlags.None, 0, null, 0, null, 1, &memoryBarrier); } + else if (resource is Buffer buffer) + { + VkAccessFlags oldAccessMask; + VkPipelineStageFlags sourceStages; + if (currentCbBufferLayouts.TryGetValue(buffer, out var fromLayout)) + { + if (fromLayout == newLayout) + return; + + oldAccessMask = BarrierMapping.ToVkAccessFlags(fromLayout); + sourceStages = BarrierMapping.ToVkPipelineStageFlags(fromLayout); + } + else + { + oldAccessMask = buffer.NativeAccessMask; + sourceStages = buffer.NativePipelineStageMask; + } + + var newAccessMask = BarrierMapping.ToVkAccessFlags(newLayout); + var newStages = BarrierMapping.ToVkPipelineStageFlags(newLayout); + + sourceStages = FixStagesForAccess(sourceStages, oldAccessMask); + newStages = FixStagesForAccess(newStages, newAccessMask); + + if (sourceStages == VkPipelineStageFlags.None) + sourceStages = VkPipelineStageFlags.TopOfPipe; + if (newStages == VkPipelineStageFlags.None) + newStages = VkPipelineStageFlags.BottomOfPipe; + + currentCbBufferLayouts[buffer] = newLayout; + + CleanupRenderPass(); + + var bufferMemoryBarrier = new VkBufferMemoryBarrier(buffer.NativeBuffer, oldAccessMask, newAccessMask); + GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, sourceStages, newStages, VkDependencyFlags.None, 0, null, 1, &bufferMemoryBarrier, 0, null); + } else { throw new NotImplementedException(); } } + /// + /// Adopts the transfer layout as the tracked state of a texture that has no defined layout. + /// The restore barrier after a copy then has a legal target. + /// + private void AdoptTransferLayoutIfUndefined(Texture texture, BarrierLayout transferLayout) + { + if (texture.NativeLayout != VkImageLayout.Undefined) + return; + + texture.NativeLayout = BarrierMapping.ToVkImageLayout(transferLayout); + texture.NativeAccessMask = BarrierMapping.ToVkAccessFlags(transferLayout); + texture.NativePipelineStageMask = BarrierMapping.ToVkPipelineStageFlags(transferLayout); + texture.LayoutTracker.Set(uint.MaxValue, transferLayout); + currentCbLayouts[texture] = transferLayout; + } + [Obsolete("Use BarrierLayout overload instead.")] public void ResourceBarrierTransition(GraphicsResource resource, GraphicsResourceState newState) { @@ -1077,6 +1140,7 @@ namespace Stride.Graphics } else { + AdoptTransferLayoutIfUndefined(sourceParent, BarrierLayout.CopySource); imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferSrcOptimal; imageBarriers[imageBarrierCount].newLayout = sourceParent.NativeLayout; imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferRead; @@ -1092,6 +1156,7 @@ namespace Stride.Graphics } else { + AdoptTransferLayoutIfUndefined(destinationParent, BarrierLayout.CopyDest); imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferDstOptimal; imageBarriers[imageBarrierCount].newLayout = destinationParent.NativeLayout; imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferWrite; @@ -1281,6 +1346,7 @@ namespace Stride.Graphics } else { + AdoptTransferLayoutIfUndefined(sourceParent, BarrierLayout.CopySource); imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferSrcOptimal; imageBarriers[imageBarrierCount].newLayout = sourceParent.NativeLayout; imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferRead; @@ -1296,6 +1362,7 @@ namespace Stride.Graphics } else { + AdoptTransferLayoutIfUndefined(destinationParent, BarrierLayout.CopyDest); imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferDstOptimal; imageBarriers[imageBarrierCount].newLayout = destinationParent.NativeLayout; imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferWrite; @@ -1497,6 +1564,7 @@ namespace Stride.Graphics }; GraphicsDevice.NativeDeviceApi.vkCmdCopyBufferToImage(currentCommandList.NativeCommandBuffer, uploadResource, texture.NativeImage, VkImageLayout.TransferDstOptimal, 1, &bufferCopy); + AdoptTransferLayoutIfUndefined(texture, BarrierLayout.CopyDest); memoryBarrier = new VkImageMemoryBarrier(texture.NativeImage, subresourceRange, VkAccessFlags.TransferWrite, texture.NativeAccessMask, VkImageLayout.TransferDstOptimal, texture.NativeLayout); GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, VkPipelineStageFlags.Transfer, texture.NativePipelineStageMask, VkDependencyFlags.None, memoryBarrierCount: 0, memoryBarriers: null, bufferMemoryBarrierCount: 0, bufferMemoryBarriers: null, imageMemoryBarrierCount: 1, &memoryBarrier); } diff --git a/sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs index d968941f8..c9ef4adaf 100644 --- a/sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs +++ b/sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs @@ -157,6 +157,14 @@ namespace Stride.Graphics // We use GraphicsDevice (similar to OpenGL) private static readonly Logger Log = GlobalLogger.GetLogger(nameof(GraphicsDevice)); + /// + /// Creates the Vulkan instance and, when requested, its validation layer and debug messenger. + /// + /// Enable the Khronos validation layer. + /// + /// STRIDE_VULKAN_SYNC_VALIDATION=1 adds synchronization validation. The validation layer + /// provides its extension, so the ICD's instance extension list does not include it. + /// public unsafe GraphicsAdapterFactoryInstance(bool enableValidation) { var pEngineName = new VkUtf8ReadOnlyString("Stride"u8); @@ -198,7 +206,7 @@ namespace Stride.Graphics Log.Info($"Vulkan validation layer {(enableValidation ? "enabled" : "not found")} for debug instance"); } - var supportedExtensionNames = stackalloc VkUtf8String[] + Span supportedExtensionNames = stackalloc VkUtf8String[] { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME, @@ -209,8 +217,7 @@ namespace Stride.Graphics VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, VK_EXT_DEBUG_UTILS_EXTENSION_NAME }; - var supportedExtensions = new Span(supportedExtensionNames, 8); - var availableExtensionNames = GetAvailableExtensionNames(supportedExtensions); + var availableExtensionNames = GetAvailableExtensionNames(supportedExtensionNames); // Surface extensions are optional at instance creation (not available with headless ICDs). // They are validated later when a swapchain is actually created. var desiredExtensionNames = new HashSet(); @@ -236,6 +243,11 @@ namespace Stride.Graphics desiredExtensionNames.Add(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); HasDebugUtilsSupport = enableDebugUtils; + bool enableSyncValidation = enableValidation + && Environment.GetEnvironmentVariable("STRIDE_VULKAN_SYNC_VALIDATION") == "1"; + if (enableSyncValidation) + desiredExtensionNames.Add(VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME); + using VkStringArray ppEnabledLayerNames = new(enabledLayerNames); using VkStringArray ppEnabledExtensionNames = new(desiredExtensionNames); @@ -250,6 +262,16 @@ namespace Stride.Graphics ppEnabledExtensionNames = ppEnabledExtensionNames, }; + var syncValidationFeature = VkValidationFeatureEnableEXT.SynchronizationValidation; + var validationFeatures = new VkValidationFeaturesEXT + { + sType = VkStructureType.ValidationFeaturesEXT, + enabledValidationFeatureCount = 1, + pEnabledValidationFeatures = &syncValidationFeature, + }; + if (enableSyncValidation) + instanceCreateInfo.pNext = &validationFeatures; + // Silence MoltenVK's per-instance info dump (153-line extension list, device banner). // Set via env var instead of VkLayerSettingsCreateInfoEXT — the layer-settings struct // requires VK_EXT_layer_settings to be enabled, and MoltenVK acting as ICD without the @@ -270,6 +292,7 @@ namespace Stride.Graphics var layerSettings = new VkLayerSettingsCreateInfoEXT { sType = VkStructureType.LayerSettingsCreateInfoEXT, + pNext = instanceCreateInfo.pNext, settingCount = 1, pSettings = &mvkLogLevelSetting, }; diff --git a/sources/engine/Stride.Graphics/Vulkan/SwapChainGraphicsPresenter.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/SwapChainGraphicsPresenter.Vulkan.cs index ee7199bb6..cbcf44149 100644 --- a/sources/engine/Stride.Graphics/Vulkan/SwapChainGraphicsPresenter.Vulkan.cs +++ b/sources/engine/Stride.Graphics/Vulkan/SwapChainGraphicsPresenter.Vulkan.cs @@ -195,6 +195,14 @@ namespace Stride.Graphics AcquireNextImage(); } + /// + /// Acquires the next swapchain image and points the back buffer at it. + /// + /// + /// The tracked layout is reset to here rather than seeded to + /// Present at creation. A layout transition is a use of the image, and the specification permits + /// use only between acquisition and presentation. + /// private unsafe void AcquireNextImage() { // Get next image @@ -223,6 +231,11 @@ namespace Stride.Graphics // Flip render targets backBuffer.SetNativeHandles(swapchainImages[currentBufferIndex].NativeImage, swapchainImages[currentBufferIndex].NativeColorAttachmentView); + backBuffer.NativeLayout = VkImageLayout.Undefined; + backBuffer.NativeAccessMask = VkAccessFlags.None; + backBuffer.NativePipelineStageMask = VkPipelineStageFlags.TopOfPipe; + backBuffer.LayoutTracker.Set(uint.MaxValue, BarrierLayout.Undefined); + lock (GraphicsDevice.QueueLock) { // Signal vkAcquireNextImageKHR Fence => GraphicsDevice.CommandList (so that next command list will wait for this to complete) @@ -626,25 +639,6 @@ namespace Stride.Graphics viewType = VkImageViewType.Image2D, }; - // We initialize swapchain images to PresentSource, since we swap them out while in this layout. - backBuffer.NativeAccessMask = VkAccessFlags.MemoryRead; - backBuffer.NativeLayout = VkImageLayout.PresentSrcKHR; - - var imageMemoryBarrier = new VkImageMemoryBarrier - { - sType = VkStructureType.ImageMemoryBarrier, - subresourceRange = new VkImageSubresourceRange(VkImageAspectFlags.Color, 0, 1, 0, 1), - oldLayout = VkImageLayout.Undefined, - newLayout = VkImageLayout.PresentSrcKHR, - srcAccessMask = VkAccessFlags.None, - dstAccessMask = VkAccessFlags.MemoryRead - }; - - var commandBuffer = GraphicsDevice.NativeCopyCommandPools.Value.GetObject(0); - - var beginInfo = new VkCommandBufferBeginInfo { sType = VkStructureType.CommandBufferBeginInfo }; - GraphicsDevice.NativeDeviceApi.vkBeginCommandBuffer(commandBuffer, &beginInfo); - GraphicsDevice.NativeDeviceApi.vkGetSwapchainImagesKHR(GraphicsDevice.NativeDevice, swapChain, out uint swapchainImageCount); Span buffers = stackalloc VkImage[(int)swapchainImageCount]; GraphicsDevice.NativeDeviceApi.vkGetSwapchainImagesKHR(GraphicsDevice.NativeDevice, swapChain, buffers); @@ -655,30 +649,8 @@ namespace Stride.Graphics // Create image views swapchainImages[index].NativeImage = createInfo.image = buffers[index]; GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkCreateImageView(GraphicsDevice.NativeDevice, &createInfo, null, out swapchainImages[index].NativeColorAttachmentView)); - - // Transition to default layout - imageMemoryBarrier.image = buffers[index]; - GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(commandBuffer, VkPipelineStageFlags.AllCommands, VkPipelineStageFlags.AllCommands, VkDependencyFlags.None, 0, null, 0, null, 1, &imageMemoryBarrier); } - // Close and submit - GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkEndCommandBuffer(commandBuffer)); - - lock (GraphicsDevice.QueueLock) - { - var commandBufferInfo = GraphicsDevice.CommandBufferSubmit(commandBuffer); - var submitInfo = new VkSubmitInfo2 - { - sType = VkStructureType.SubmitInfo2, - commandBufferInfoCount = 1, - pCommandBufferInfos = &commandBufferInfo, - }; - GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkQueueSubmit2(GraphicsDevice.NativeCommandQueue, 1, &submitInfo, VkFence.Null)); - GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkQueueWaitIdle(GraphicsDevice.NativeCommandQueue)); - } - - GraphicsDevice.NativeCopyCommandPools.Value.RecycleObject(0, commandBuffer); - // Create submit semaphores submitSemaphores = new VkSemaphore[buffers.Length]; var semaphoreCreateInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo }; diff --git a/sources/engine/Stride.Graphics/Vulkan/Texture.Vulkan.cs b/sources/engine/Stride.Graphics/Vulkan/Texture.Vulkan.cs index cf5760609..63b55da45 100644 --- a/sources/engine/Stride.Graphics/Vulkan/Texture.Vulkan.cs +++ b/sources/engine/Stride.Graphics/Vulkan/Texture.Vulkan.cs @@ -173,10 +173,10 @@ namespace Stride.Graphics NativeAccessMask = VkAccessFlags.TransferRead; if (NativeLayout == VkImageLayout.ColorAttachmentOptimal) - NativeAccessMask = VkAccessFlags.ColorAttachmentWrite; + NativeAccessMask = VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.ColorAttachmentRead; if (NativeLayout == VkImageLayout.DepthStencilAttachmentOptimal) - NativeAccessMask = VkAccessFlags.DepthStencilAttachmentWrite; + NativeAccessMask = VkAccessFlags.DepthStencilAttachmentWrite | VkAccessFlags.DepthStencilAttachmentRead; if (NativeLayout == VkImageLayout.ShaderReadOnlyOptimal) NativeAccessMask = VkAccessFlags.ShaderRead | VkAccessFlags.InputAttachmentRead; @@ -184,7 +184,7 @@ namespace Stride.Graphics NativePipelineStageMask = IsRenderTarget ? VkPipelineStageFlags.ColorAttachmentOutput : IsDepthStencil ? VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests : - IsShaderResource || IsUnorderedAccess ? VkPipelineStageFlags.VertexInput | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader : + IsShaderResource || IsUnorderedAccess ? VkPipelineStageFlags.VertexShader | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader : VkPipelineStageFlags.None; if (ParentTexture != null) diff --git a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewEffect.sdsl b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewEffect.sdsl index f458e6f9a..dc0936ffd 100644 --- a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewEffect.sdsl +++ b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewEffect.sdsl @@ -5,19 +5,11 @@ namespace Stride.Rendering.Voxels.Debug effect VoxelVisualizationViewEffect { using params VoxelVisualizationViewShaderKeys; - using params MarchAttributesKeys; mixin VoxelVisualizationViewShader; if (VoxelVisualizationViewShaderKeys.marcher != null) { mixin compose marcher = VoxelVisualizationViewShaderKeys.marcher; } - if (MarchAttributesKeys.AttributeSamplers != null) - { - foreach (var attr in MarchAttributesKeys.AttributeSamplers) - { - mixin compose AttributeSamplers += (attr); - } - } } } diff --git a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewShader.sdsl b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewShader.sdsl index 09594aed1..04ead5f86 100644 --- a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewShader.sdsl +++ b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/Shaders/VoxelVisualizationViewShader.sdsl @@ -2,7 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. namespace Stride.Rendering.Voxels.Debug { - shader VoxelVisualizationViewShader : MarchAttributes, ImageEffectShader + shader VoxelVisualizationViewShader : ImageEffectShader { compose VoxelMarchMethod marcher; diff --git a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/VoxelVisualizationView.cs b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/VoxelVisualizationView.cs index 5c18fe6fe..5b7a6f175 100644 --- a/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/VoxelVisualizationView.cs +++ b/sources/engine/Stride.Voxels/Voxels/GraphicsCompositor/DebugVisualizations/VoxelVisualizationView.cs @@ -31,17 +31,15 @@ namespace Stride.Rendering.Voxels.Debug voxelDebugEffectShader.Parameters.Set(VoxelVisualizationViewShaderKeys.viewInv, Matrix.Invert(ViewProjection)); voxelDebugEffectShader.Parameters.Set(VoxelVisualizationViewShaderKeys.background, (Vector4)Background); - attr.UpdateSamplingLayout("AttributeSamplers[0]"); - attr.ApplySamplingParameters(viewContext, voxelDebugEffectShader.Parameters); MarchMethod.UpdateMarchingLayout("marcher"); MarchMethod.ApplyMarchingParameters(voxelDebugEffectShader.Parameters); - voxelDebugEffectShader.Parameters.Set(VoxelVisualizationViewShaderKeys.marcher, MarchMethod.GetMarchingShader(0)); + MarchMethod.ApplyAttributeSamplers(attr, 0, viewContext, voxelDebugEffectShader.Parameters); ShaderSourceCollection collection = new ShaderSourceCollection { attr.GetSamplingShader() }; - voxelDebugEffectShader.Parameters.Set(MarchAttributesKeys.AttributeSamplers, collection); + voxelDebugEffectShader.Parameters.Set(VoxelVisualizationViewShaderKeys.marcher, MarchMethod.GetMarchingShader(0, collection)); return voxelDebugEffectShader; } diff --git a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelEffect.sdsl b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelEffect.sdsl index 8009fe7e4..d3d321ae6 100644 --- a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelEffect.sdsl +++ b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelEffect.sdsl @@ -11,7 +11,6 @@ namespace Stride.Rendering.Voxels.VoxelGI effect LightVoxelEffect { using params LightVoxelShaderKeys; - using params MarchAttributesKeys; mixin LightVoxelShader; @@ -23,12 +22,5 @@ namespace Stride.Rendering.Voxels.VoxelGI { mixin compose specularMarcher = LightVoxelShaderKeys.specularMarcher; } - if (MarchAttributesKeys.AttributeSamplers!=null) - { - foreach (var attr in MarchAttributesKeys.AttributeSamplers) - { - mixin compose AttributeSamplers += (attr); - } - } }; } diff --git a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelRenderer.cs b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelRenderer.cs index c15f8a02c..05b3b2a05 100644 --- a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelRenderer.cs +++ b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelRenderer.cs @@ -86,7 +86,6 @@ namespace Stride.Rendering.Voxels.VoxelGI private PermutationParameterKey diffuseMarcherKey; private PermutationParameterKey specularMarcherKey; - private PermutationParameterKey attributeSamplersKey; public RenderLight Light { get; set; } @@ -141,7 +140,6 @@ namespace Stride.Rendering.Voxels.VoxelGI diffuseMarcherKey = LightVoxelShaderKeys.diffuseMarcher.ComposeWith(compositionName); specularMarcherKey = LightVoxelShaderKeys.specularMarcher.ComposeWith(compositionName); - attributeSamplersKey = MarchAttributesKeys.AttributeSamplers.ComposeWith(compositionName); if (traceAttribute != null) { @@ -149,7 +147,6 @@ namespace Stride.Rendering.Voxels.VoxelGI ((LightVoxel)Light.Type).DiffuseMarcher.UpdateMarchingLayout("diffuseMarcher." + compositionName); if (((LightVoxel)Light.Type).SpecularMarcher != null) ((LightVoxel)Light.Type).SpecularMarcher.UpdateMarchingLayout("specularMarcher." + compositionName); - traceAttribute.UpdateSamplingLayout("AttributeSamplers[0]." + compositionName); } } @@ -161,12 +158,11 @@ namespace Stride.Rendering.Voxels.VoxelGI { traceAttribute.GetSamplingShader() }; - renderEffect.EffectValidator.ValidateParameter(attributeSamplersKey, collection); if (((LightVoxel)Light.Type).DiffuseMarcher != null) - renderEffect.EffectValidator.ValidateParameter(diffuseMarcherKey, ((LightVoxel)Light.Type).DiffuseMarcher.GetMarchingShader(0)); + renderEffect.EffectValidator.ValidateParameter(diffuseMarcherKey, ((LightVoxel)Light.Type).DiffuseMarcher.GetMarchingShader(0, collection)); if (((LightVoxel)Light.Type).SpecularMarcher != null) - renderEffect.EffectValidator.ValidateParameter(specularMarcherKey, ((LightVoxel)Light.Type).SpecularMarcher.GetMarchingShader(0)); + renderEffect.EffectValidator.ValidateParameter(specularMarcherKey, ((LightVoxel)Light.Type).SpecularMarcher.GetMarchingShader(0, collection)); } } @@ -200,7 +196,8 @@ namespace Stride.Rendering.Voxels.VoxelGI { lightVoxel.DiffuseMarcher?.ApplyMarchingParameters(parameters); lightVoxel.SpecularMarcher?.ApplyMarchingParameters(parameters); - traceAttribute.ApplySamplingParameters(viewContext, parameters); + lightVoxel.DiffuseMarcher?.ApplyAttributeSamplers(traceAttribute, 0, viewContext, parameters); + lightVoxel.SpecularMarcher?.ApplyAttributeSamplers(traceAttribute, 0, viewContext, parameters); } } } diff --git a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelShader.sdsl b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelShader.sdsl index 0d679a9ec..4589c1bde 100644 --- a/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelShader.sdsl +++ b/sources/engine/Stride.Voxels/Voxels/Light/LightVoxelShader.sdsl @@ -6,7 +6,7 @@ namespace Stride.Rendering.Voxels.VoxelGI /// /// Defines a Voxel environment light /// - shader LightVoxelShader : MarchAttributes, Camera, Texturing, EnvironmentLight, MaterialPixelShadingStream, NormalStream, PositionStream4, Transformation + shader LightVoxelShader : Camera, Texturing, EnvironmentLight, MaterialPixelShadingStream, NormalStream, PositionStream4, Transformation { cbuffer PerView.Lighting { diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/IVoxelMarchMethod.cs b/sources/engine/Stride.Voxels/Voxels/Marching/IVoxelMarchMethod.cs index 7cebccc1b..08baf573f 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/IVoxelMarchMethod.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/IVoxelMarchMethod.cs @@ -9,8 +9,12 @@ namespace Stride.Rendering.Voxels { public interface IVoxelMarchMethod { - ShaderSource GetMarchingShader(int attrID); + ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers); void UpdateMarchingLayout(string compositionName); void ApplyMarchingParameters(ParameterCollection parameters); + /// + /// Binds as this marcher's AttributeSamplers[attrID], at the path given to . + /// + void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters); } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/IVoxelMarchSet.cs b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/IVoxelMarchSet.cs index b113eba9f..d443c33e4 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/IVoxelMarchSet.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/IVoxelMarchSet.cs @@ -9,8 +9,12 @@ namespace Stride.Rendering.Voxels { public interface IVoxelMarchSet { - ShaderSource GetMarchingShader(int attrID); + ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers); void UpdateMarchingLayout(string compositionName); void ApplyMarchingParameters(ParameterCollection parameters); + /// + /// Binds as the AttributeSamplers[attrID] of the marcher this set runs, at the path given to . + /// + void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters); } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetBase.cs b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetBase.cs index 9b6b0565b..8c4625c63 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetBase.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetBase.cs @@ -32,5 +32,9 @@ namespace Stride.Rendering.Voxels Marcher.ApplyMarchingParameters(parameters); parameters.Set(OffsetKey, Offset); } + public virtual void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters) + { + Marcher.ApplyAttributeSamplers(attribute, attrID, viewContext, parameters); + } } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere12.cs b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere12.cs index ad19f64a7..ba9efb427 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere12.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere12.cs @@ -21,11 +21,11 @@ namespace Stride.Rendering.Voxels Marcher = marcher; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); mixin.Mixins.Add(new ShaderClassSource("VoxelMarchSetHemisphere12")); - mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID)); + mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID, attributeSamplers)); return mixin; } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere6.cs b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere6.cs index f76fccb81..380497f14 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere6.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetHemisphere6.cs @@ -21,11 +21,11 @@ namespace Stride.Rendering.Voxels Marcher = marcher; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); mixin.Mixins.Add(new ShaderClassSource("VoxelMarchSetHemisphere6")); - mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID)); + mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID, attributeSamplers)); return mixin; } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetRandomHemisphere.cs b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetRandomHemisphere.cs index 925197f97..211788334 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetRandomHemisphere.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/MarchSets/VoxelMarchSetRandomHemisphere.cs @@ -26,11 +26,11 @@ namespace Stride.Rendering.Voxels { Marcher = marcher; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); mixin.Mixins.Add(new ShaderClassSource("VoxelMarchSetRandomHemisphere")); - mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID)); + mixin.AddComposition("Marcher", Marcher.GetMarchingShader(attrID, attributeSamplers)); return mixin; } @@ -51,5 +51,9 @@ namespace Stride.Rendering.Voxels parameters.Set(CountKey, Count); parameters.Set(TimeKey, AnimateNoise ? time : 0f); } + public void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters) + { + Marcher.ApplyAttributeSamplers(attribute, attrID, viewContext, parameters); + } } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributes.sdsl b/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributes.sdsl index d4d195721..c81fe4764 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributes.sdsl +++ b/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributes.sdsl @@ -2,5 +2,5 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. shader MarchAttributes { - stage compose IVoxelSampler AttributeSamplers[]; + compose IVoxelSampler AttributeSamplers[]; }; \ No newline at end of file diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesEffect.sdsl b/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesEffect.sdsl deleted file mode 100644 index 47a20343f..000000000 --- a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesEffect.sdsl +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Sean Boettger -// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -namespace Stride.Rendering.Voxels -{ - partial effect MarchAttributesEffect - { - using params MarchAttributesKeys; - - mixin MarchAttributes; - if (MarchAttributesKeys.AttributeSamplers!=null) - { - foreach (var attr in MarchAttributesKeys.AttributeSamplers) - { - mixin compose AttributeSamplers += (attr); - } - } - }; -} \ No newline at end of file diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesKeys.cs b/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesKeys.cs deleted file mode 100644 index 85a7d5381..000000000 --- a/sources/engine/Stride.Voxels/Voxels/Marching/Shaders/MarchAttributesKeys.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Sean Boettger -// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. - -using Stride.Shaders; - -namespace Stride.Rendering.Voxels -{ - public partial class MarchAttributesKeys - { - public static readonly PermutationParameterKey AttributeSamplers = ParameterKeys.NewPermutation(); - } -} diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchBeam.cs b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchBeam.cs index dd00b1319..760000b58 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchBeam.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchBeam.cs @@ -25,19 +25,28 @@ namespace Stride.Rendering.Voxels StepScale = stepScale; BeamDiameter = diameter; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); mixin.Mixins.Add(new ShaderClassSource("VoxelMarchBeam", Steps, StepScale, BeamDiameter)); mixin.Macros.Add(new ShaderMacro("AttributeID", attrID)); + foreach (var sampler in attributeSamplers) + mixin.AddCompositionToArray("AttributeSamplers", sampler); return mixin; } + string compositionName; public void UpdateMarchingLayout(string compositionName) { + this.compositionName = compositionName; } public void ApplyMarchingParameters(ParameterCollection parameters) { } + public void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters) + { + attribute.UpdateSamplingLayout($"AttributeSamplers[{attrID}].{compositionName}"); + attribute.ApplySamplingParameters(viewContext, parameters); + } } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchCone.cs b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchCone.cs index e68c0c1ba..7639342dd 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchCone.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchCone.cs @@ -36,7 +36,7 @@ namespace Stride.Rendering.Voxels ConeRatio = ratio; EditMode = false; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); if (EditMode) @@ -49,6 +49,8 @@ namespace Stride.Rendering.Voxels mixin.Macros.Add(new ShaderMacro("sampleFunction", Fast ? "SampleNearestMip" : "Sample")); } mixin.Macros.Add(new ShaderMacro("AttributeID", attrID)); + foreach (var sampler in attributeSamplers) + mixin.AddCompositionToArray("AttributeSamplers", sampler); return mixin; } @@ -58,8 +60,10 @@ namespace Stride.Rendering.Voxels ValueParameterKey ConeRatioKey; ValueParameterKey FastKey; ValueParameterKey OffsetKey; + string compositionName; public void UpdateMarchingLayout(string compositionName) { + this.compositionName = compositionName; if (EditMode) { StepsKey = VoxelMarchConeEditModeKeys.steps.ComposeWith(compositionName); @@ -80,5 +84,10 @@ namespace Stride.Rendering.Voxels parameters.Set(OffsetKey, StartOffset); } } + public void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters) + { + attribute.UpdateSamplingLayout($"AttributeSamplers[{attrID}].{compositionName}"); + attribute.ApplySamplingParameters(viewContext, parameters); + } } } diff --git a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchConePerMipmap.cs b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchConePerMipmap.cs index e782786f8..aec8c2631 100644 --- a/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchConePerMipmap.cs +++ b/sources/engine/Stride.Voxels/Voxels/Marching/VoxelMarchConePerMipmap.cs @@ -30,17 +30,21 @@ namespace Stride.Rendering.Voxels ConeRatio = ratio; Steps = steps; } - public ShaderSource GetMarchingShader(int attrID) + public ShaderSource GetMarchingShader(int attrID, ShaderSourceCollection attributeSamplers) { var mixin = new ShaderMixinSource(); mixin.Mixins.Add(new ShaderClassSource("VoxelMarchConePerMipmap", Steps)); mixin.Macros.Add(new ShaderMacro("AttributeID", attrID)); + foreach (var sampler in attributeSamplers) + mixin.AddCompositionToArray("AttributeSamplers", sampler); return mixin; } ValueParameterKey OffsetKey; ValueParameterKey ConeRatioInvKey; + string compositionName; public void UpdateMarchingLayout(string compositionName) { + this.compositionName = compositionName; OffsetKey = VoxelMarchConePerMipmapKeys.offset.ComposeWith(compositionName); ConeRatioInvKey = VoxelMarchConePerMipmapKeys.coneRatioInv.ComposeWith(compositionName); } @@ -49,5 +53,10 @@ namespace Stride.Rendering.Voxels parameters.Set(OffsetKey, StartOffset); parameters.Set(ConeRatioInvKey, 1.0f/ConeRatio); } + public void ApplyAttributeSamplers(VoxelAttribute attribute, int attrID, VoxelViewContext viewContext, ParameterCollection parameters) + { + attribute.UpdateSamplingLayout($"AttributeSamplers[{attrID}].{compositionName}"); + attribute.ApplySamplingParameters(viewContext, parameters); + } } } diff --git a/sources/launcher/Stride.Cli/Core/StrideVersionManager.cs b/sources/launcher/Stride.Cli/Core/StrideVersionManager.cs index 590310e70..c92a61b2f 100644 --- a/sources/launcher/Stride.Cli/Core/StrideVersionManager.cs +++ b/sources/launcher/Stride.Cli/Core/StrideVersionManager.cs @@ -304,13 +304,17 @@ public sealed class StrideVersionManager // The asset packs are the one template package nothing depends on: Stride.GameStudio deliberately declares no // dependency on it, since the packs are optional content nobody should pay for at install time. Installing a // Stride version therefore never puts it in the store, which is why discovery alone never finds it. - private const string AssetPacksPackageId = "Stride.Templates.AssetPacks"; + private const string AssetPacksPackageId = ContentTemplateResolver.AssetPacksPackageId; + + // Template packages versioned with the engine, which Stride.GameStudio depends on at its own version. + private static readonly string[] EngineTemplatePackageIds = ["Stride.Templates.Games"]; /// /// Opens a template registry over the installed template packages compatible with the given version, /// plus any explicitly requested (a package id or a local .nupkg path). - /// The asset packs are fetched first when the store has no compatible copy, since nothing installs them. - /// Returns null if none could be installed. The caller owns the returned registry and must dispose it. + /// Content packages (Samples, Starters, AssetPacks) resolve at the content version the engine names, and + /// are fetched when the store has no copy. Returns null if none could be installed. The caller owns the + /// returned registry and must dispose it. /// public async Task OpenTemplateRegistry( PackageVersion version, IEnumerable? extraPackages = null, CancellationToken cancellationToken = default) @@ -322,11 +326,13 @@ public sealed class StrideVersionManager Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "stride", "cli", "templates", version.ToString()); - await EnsureAssetPacksInstalled(version, cancellationToken); - var registry = new DotNetNewTemplateRegistry(version.ToString(), profileDir); + var packageDirs = await ResolveTemplatePackages(version, extraPackages, cancellationToken); + // The profile holds exactly this run's packages: a version resolved by an earlier run would offer the same + // templates with other content. + await registry.RemovePackagesExceptAsync(packageDirs, cancellationToken); var installedAny = false; - foreach (var packageDir in ResolveTemplatePackages(version, extraPackages)) + foreach (var packageDir in packageDirs) { var (success, _) = await registry.InstallPackageAsync(packageDir); installedAny |= success; @@ -341,10 +347,71 @@ public sealed class StrideVersionManager return registry; } + /// + /// The content version an installed engine names: Stride.GameStudio pins its Stride.Templates.Samples + /// dependency to it, and all content packages share that one number. Null for an engine from before the + /// pinning, whose content packages are then found by their engine marker like any other template package. + /// + private PackageVersion? ContentVersionOf(NugetLocalPackage? gameStudio, PackageVersion version) + => gameStudio?.GetDependencyFloor(ContentTemplateResolver.ContentVersionDependencyId) is { } pinned + ? ContentTemplateResolver.ContentVersionFromPin(pinned, version) + : null; + + /// + /// The installed engine-versioned template packages () that + /// depends on, at exactly the version it names (installed from the sources when + /// missing). Their content names the engine packages, so another version would not match the engine. A + /// package it does not name (an engine from before the dependency) is left to discovery. + /// + private async Task> ResolveEngineTemplatePackages(NugetLocalPackage? gameStudio) + { + var packages = new List(); + foreach (var packageId in EngineTemplatePackageIds) + { + if (gameStudio?.GetDependencyFloor(packageId) is not { } named) + continue; + var package = store.FindLocalPackage(packageId, new PackageVersionRange(named, true, named, true)); + if (package is null) + { + try + { + package = await store.InstallPackage(packageId, named, [], progress: null); + } + catch (Exception) + { + // Offline or not obtainable: `new` goes on with the templates that are installed. + } + } + if (package is not null && package.Version.Equals(named)) + packages.Add(package); + } + return packages; + } + + /// + /// Resolves the content packages for an engine that names a content version: the exact version (or a + /// local dev pack of it for a dev engine), installed from the sources when missing. Best effort: without a + /// reachable source a missing package is simply left out. Same rule as the Game Studio bridge. + /// + private async Task> ResolveContentPackages(PackageVersion contentVersion, PackageVersion version) + { + var packages = new List(); + foreach (var packageId in ContentTemplateResolver.PackageIds) + { + var package = await ContentTemplateResolver.ResolveAsync(packageId, contentVersion, version, + store.GetLocalPackages, + (id, packageVersion) => store.InstallPackage(id, packageVersion, [], progress: null)); + if (package is not null) + packages.Add(package); + } + return packages; + } + /// /// Downloads the asset packs when the store holds no copy compatible with , so - /// that discovery can find them. Game Studio fetches the same package the same way when its New Game dialog - /// first offers the packs. Best effort: without a reachable source the packs are simply left out. + /// that discovery can find them. Only for an engine that names no content version (see + /// ); a newer engine's packs resolve with the other content packages. + /// Best effort: without a reachable source the packs are simply left out. /// private async Task EnsureAssetPacksInstalled(PackageVersion version, CancellationToken cancellationToken) { @@ -385,16 +452,34 @@ public sealed class StrideVersionManager .FirstOrDefault(dependency => string.Equals(dependency.Item1, TemplateMarkerDependencyId, StringComparison.OrdinalIgnoreCase)) ?.Item2.MinVersion; - // The directories to install into the registry: one per discovered template package, plus any explicit - // extra packages. - private IEnumerable ResolveTemplatePackages(PackageVersion version, IEnumerable? extraPackages) + // The directories to install into the registry: the content packages at the engine's content version, one per + // discovered engine-versioned template package, plus any explicit extra packages. + private async Task> ResolveTemplatePackages(PackageVersion version, IEnumerable? extraPackages, CancellationToken cancellationToken) { - // Discover template packages by package type + tag, then per id pick the newest version whose + var paths = new List(); + + // The template packages the engine names are used at exactly that version, never discovered: "newest + // compatible" is the drift the pinning exists to prevent. Content packages (Samples, Starters, AssetPacks) + // follow the content version, the engine-versioned ones (Games) the version Game Studio depends on. An + // engine from before the pinning names none of them, so they go through discovery like the rest. + var gameStudio = store.GetLocalPackages(MainPackageId).FirstOrDefault(package => package.Version.Equals(version)); + var pinned = await ResolveEngineTemplatePackages(gameStudio); + var contentVersion = ContentVersionOf(gameStudio, version); + if (contentVersion is not null) + pinned.AddRange(await ResolveContentPackages(contentVersion, version)); + else + await EnsureAssetPacksInstalled(version, cancellationToken); + var pinnedIds = new HashSet(pinned.Select(package => package.Id), StringComparer.OrdinalIgnoreCase); + if (contentVersion is not null) + pinnedIds.UnionWith(ContentTemplateResolver.PackageIds); + + // Discover the other template packages by package type + tag, then per id pick the newest version whose // Stride.Core marker floor is <= the requested version. Unmarked (legacy) packages have unknown // engine compatibility, so they are a last resort: any marked match wins over a newer unmarked one // (otherwise a co-installed newer dev package without the marker silently hijacks the request). var discovered = store.GetAllPackagesInstalled() .Where(package => package.IsTemplatePackage && package.HasTag(TemplateTag)) + .Where(package => !pinnedIds.Contains(package.Id)) .GroupBy(package => package.Id, StringComparer.OrdinalIgnoreCase) .Select(group => group.Where(package => IsCompatible(package.GetDependencyFloor(TemplateMarkerDependencyId), version)) @@ -405,12 +490,14 @@ public sealed class StrideVersionManager .FirstOrDefault()) .OfType(); - foreach (var package in discovered) + foreach (var package in pinned.Concat(discovered)) { - // Prefer the extracted directory; fall back to the loose .nupkg of a not-yet-mirrored local source. - var path = package.NupkgPath ?? package.Path; + // Prefer the extracted directory (the registry replaces its entry on reinstall); fall back to the loose + // .nupkg of a not-yet-mirrored local source. An extracted package reports the .nupkg inside its folder + // too, so tell them apart by NuGet's extraction marker. + var path = File.Exists(Path.Combine(package.Path, ".nupkg.metadata")) ? package.Path : package.NupkgPath ?? package.Path; if (!string.IsNullOrEmpty(path)) - yield return path; + paths.Add(path); } foreach (var extra in extraPackages ?? []) @@ -419,15 +506,17 @@ public sealed class StrideVersionManager // package id and resolved to its newest installed copy. if (File.Exists(extra) || Directory.Exists(extra)) { - yield return extra; + paths.Add(extra); continue; } var newest = store.GetPackagesInstalled([extra]).FirstOrDefault(); var resolved = newest is null ? null : store.GetInstalledPath(newest.Id, newest.Version); if (!string.IsNullOrEmpty(resolved)) - yield return resolved; + paths.Add(resolved); } + + return paths; } /// Locates the Game Studio executable for the given version, or null if not found. diff --git a/sources/launcher/Stride.Cli/InstallProgressConsole.cs b/sources/launcher/Stride.Cli/InstallProgressConsole.cs index a7928f1e7..60e0ac03f 100644 --- a/sources/launcher/Stride.Cli/InstallProgressConsole.cs +++ b/sources/launcher/Stride.Cli/InstallProgressConsole.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System.Threading; using Stride.Cli.Core; +using Stride.Core.Packages; // Renders install/update/uninstall progress on a single console line: a spinner with the downloaded size // during download, then "installing X/Y (package)" (or "removing X/Y" on uninstall) as packages are processed. @@ -69,7 +70,7 @@ internal sealed class InstallProgressConsole : IProgress, IDisp InstallStage.Installing => $"Stride {current.Version}: installing {current.Completed}/{current.Total} ({current.Package})", InstallStage.SettingUp => $"Stride {current.Version}: setting up {current.Package}", InstallStage.Removing => $"Stride {current.Version}: removing {current.Completed}/{current.Total} ({current.Package})", - _ when current.DownloadedBytes > 0 => $"Stride {current.Version}: downloading {Mb(current.DownloadedBytes)} MB", + _ when current.DownloadedBytes > 0 => $"Stride {current.Version}: downloading {DownloadSize.Format(current.DownloadedBytes)}", // No bytes flowing (resolving, or restoring from the local cache). _ => $"Stride {current.Version}: restoring", }; @@ -79,8 +80,6 @@ internal sealed class InstallProgressConsole : IProgress, IDisp wrote = true; } - private static string Mb(long bytes) => (bytes / 1048576.0).ToString("0.0"); - public void Dispose() { lock (gate) diff --git a/sources/launcher/Stride.Cli/NewCommand.cs b/sources/launcher/Stride.Cli/NewCommand.cs index 8c67c9e85..56b6d3cbe 100644 --- a/sources/launcher/Stride.Cli/NewCommand.cs +++ b/sources/launcher/Stride.Cli/NewCommand.cs @@ -137,7 +137,12 @@ internal static class NewCommand else { Console.WriteLine($"Upgrading the project from Stride {templateVersion} to {version}..."); - Tools.Run(compiler, $"the Asset Compiler for Stride {version}", ["upgrade", solutionFile], wait: true, sessionPath: solutionFile, toolReadsHostArgs: false); + var exitCode = Tools.Run(compiler, $"the Asset Compiler for Stride {version}", ["upgrade", solutionFile], wait: true, sessionPath: solutionFile, toolReadsHostArgs: false); + if (exitCode != 0) + { + Console.Error.WriteLine($"The upgrade to Stride {version} failed (exit code {exitCode}). The project was created, but it may still be on Stride {templateVersion}."); + return 1; + } } } diff --git a/sources/launcher/Stride.Cli/Stride.Cli.csproj b/sources/launcher/Stride.Cli/Stride.Cli.csproj index bbffbc040..397f8bf77 100644 --- a/sources/launcher/Stride.Cli/Stride.Cli.csproj +++ b/sources/launcher/Stride.Cli/Stride.Cli.csproj @@ -55,6 +55,7 @@ + diff --git a/sources/launcher/Stride.Launcher/App.axaml b/sources/launcher/Stride.Launcher/App.axaml index 30e536301..442e5f163 100644 --- a/sources/launcher/Stride.Launcher/App.axaml +++ b/sources/launcher/Stride.Launcher/App.axaml @@ -1,38 +1,35 @@ - - - - + + + + + + - - - + + + + 0 1 2 #17000000 + + - - - - - - + + + 0 1 2 #17FFFFFF + + - 12 - 0,0,0,20 + 12 + 4 @@ -45,17 +42,31 @@ - + + + - - - diff --git a/sources/launcher/Stride.Launcher/Assets/Images/EditorIcon.png b/sources/launcher/Stride.Launcher/Assets/Images/EditorIcon.png deleted file mode 100644 index 14a0a4b8a..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/EditorIcon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1b8b34e7027bfce596b902a7a0452890abcc6d7a3c512f4d0489dbf25eb553d -size 468 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/chat.png b/sources/launcher/Stride.Launcher/Assets/Images/chat.png deleted file mode 100644 index 079b6d101..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/chat.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c07739923bded8e272078aab77453e494b9ea29bbbb76510a20319009d0c4df -size 477 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/delete.png b/sources/launcher/Stride.Launcher/Assets/Images/delete.png deleted file mode 100644 index f3ec5cd83..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/delete.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a271f900243478c6c08cfc96e69d518387dd5ff7b13cdc87033d9c080cd63774 -size 361 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/discord.png b/sources/launcher/Stride.Launcher/Assets/Images/discord.png deleted file mode 100644 index 31fe710a2..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/discord.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:97b3bbbf2c7c3e08c2c7428f912cb378156725c3fd376a993f258eef11bc9a21 -size 408 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/download.png b/sources/launcher/Stride.Launcher/Assets/Images/download.png deleted file mode 100644 index a1fbbaebd..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/download.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb985f0835af217388f760dd502bcf4e3b010e72bebbbfbcb270ba30c55ae5c1 -size 398 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/facebook.png b/sources/launcher/Stride.Launcher/Assets/Images/facebook.png deleted file mode 100644 index 578490164..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/facebook.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e2d1b330cd48fe16e39cc2ce5fc67c5193bf0b323a1ebe5502c845ce8ff575f8 -size 837 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/getting-started.png b/sources/launcher/Stride.Launcher/Assets/Images/getting-started.png deleted file mode 100644 index bc652d0e1..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/getting-started.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96439d163eb1ddb80fefb65c3e6d5899ca52c75b428487af5f56f3dfe1c0354f -size 368 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/github.png b/sources/launcher/Stride.Launcher/Assets/Images/github.png deleted file mode 100644 index f52bd0da9..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/github.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb6a4121d0b7481546c8ee6a7283f084f85d7cca624ce5bac0169780351fb870 -size 433 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/issues.png b/sources/launcher/Stride.Launcher/Assets/Images/issues.png deleted file mode 100644 index b925bfb44..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/issues.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8034e16df3b3973d7ea672b0849887184734ff87a3110d771338907cea5e24ff -size 451 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/list.png b/sources/launcher/Stride.Launcher/Assets/Images/list.png deleted file mode 100644 index 015045238..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/list.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff001a3184d887221f1b393d929b8f5a3444786e98edbfd821c67d33ee426574 -size 242 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/news.png b/sources/launcher/Stride.Launcher/Assets/Images/news.png deleted file mode 100644 index cae641730..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/news.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be9116fe7faeda2a2bc66bb1af16f3fde91099a6ac8df0a5229f3f13c87de689 -size 779 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/note.png b/sources/launcher/Stride.Launcher/Assets/Images/note.png deleted file mode 100644 index fb1754358..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/note.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0dd607303a37a7b840c6e31e6c12cc22af6236f632497e961f745390e6b3a4c7 -size 379 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/opencollective.png b/sources/launcher/Stride.Launcher/Assets/Images/opencollective.png deleted file mode 100644 index 8331b94ad..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/opencollective.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:443e8913f8c96ef92dd3a3adc7d2f0fc5e1faa27477fe68fd24fbbfa0b2c930d -size 905 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/recent-projects.png b/sources/launcher/Stride.Launcher/Assets/Images/recent-projects.png deleted file mode 100644 index 364e2977b..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/recent-projects.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03d7b568bc1754b6ba5654a05efe512eff6aeafa5842cfdc4a2700631317c533 -size 537 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/reddit.png b/sources/launcher/Stride.Launcher/Assets/Images/reddit.png deleted file mode 100644 index 765d19906..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/reddit.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:782876732b8839df14a581e2ad4017f9945bbbabd0c54e6945341f234855a59e -size 1073 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/roadmap.png b/sources/launcher/Stride.Launcher/Assets/Images/roadmap.png deleted file mode 100644 index 096380c95..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/roadmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f18477e35a05941eab8a71a99de80566547e25b5a9202cc45eb4a26ba3bbf3d8 -size 277 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/robot-sunset.png b/sources/launcher/Stride.Launcher/Assets/Images/robot-sunset.png new file mode 100644 index 000000000..803384218 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Assets/Images/robot-sunset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da3d9caa26ce094e2b9036cd662903750bb639ca398162e3d0341e8b2b8131f6 +size 1807754 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/showcase.png b/sources/launcher/Stride.Launcher/Assets/Images/showcase.png deleted file mode 100644 index 7a130880e..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/showcase.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:941a600729cd4790a0dd3ca87dc760d61fc6c13298608693a596bb322f72c734 -size 459 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/survey.png b/sources/launcher/Stride.Launcher/Assets/Images/survey.png deleted file mode 100644 index 30209d54a..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/survey.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5cc466a22dcea703a000a38076d82758f6ec40f60329d08c80700ebf56beddac -size 392 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/switch-version.png b/sources/launcher/Stride.Launcher/Assets/Images/switch-version.png deleted file mode 100644 index 8640cbe20..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/switch-version.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00cdfce728f8f150deb8ad59be77cd166369b18c075994c19a2be615302e51cb -size 629 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/twitch.png b/sources/launcher/Stride.Launcher/Assets/Images/twitch.png deleted file mode 100644 index 6f6690cd6..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/twitch.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:19938bf7ebeac736554b6c660ec1790bab5a5e8ffa764ce5ceb3355d6fea6058 -size 573 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/update.png b/sources/launcher/Stride.Launcher/Assets/Images/update.png deleted file mode 100644 index 0586eb84f..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/update.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f03c534a2a53815f646b4ade22008123221a2fcea1dbeb1249c789c887112e33 -size 570 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/upgrade.png b/sources/launcher/Stride.Launcher/Assets/Images/upgrade.png deleted file mode 100644 index de6e2c1d7..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/upgrade.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a128a6c8e2e03efdfd9a9cd85a7a943afb75876ab83248b4652ce5e81db579a -size 435 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/visual-studio.png b/sources/launcher/Stride.Launcher/Assets/Images/visual-studio.png deleted file mode 100644 index 5c13e8c14..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/visual-studio.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c267b423e001eebe0d210891eb62e63a562c02127ac53239fc5d4586abcae7d -size 587 diff --git a/sources/launcher/Stride.Launcher/Assets/Images/xtwitter.png b/sources/launcher/Stride.Launcher/Assets/Images/xtwitter.png deleted file mode 100644 index 10f4b74fb..000000000 --- a/sources/launcher/Stride.Launcher/Assets/Images/xtwitter.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01cd3a3045cdbee8ad98b786366b384024a897186b2f548c20d39feec5249a8b -size 714 diff --git a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs index 9365fa22e..9437a2bf6 100644 --- a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs @@ -59,6 +59,15 @@ namespace Stride.Launcher.Assets.Localization { resourceCulture = value; } } + + /// + /// Looks up a localized string similar Fetching NuGet packages... + /// + public static string Synchronizing { + get { + return ResourceManager.GetString("Synchronizing", resourceCulture); + } + } /// /// Looks up a localized string similar to It seems you do not have any version of Stride currently installed. Would you like to install the latest version?. diff --git a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx index 105930b7b..90e3c00b1 100644 --- a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx @@ -117,6 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Fetching NuGet packages... + /!\ Text must be short + It seems you do not have any version of Stride currently installed. Would you like to install the latest version? Displayed when there is no installed version of Stride @@ -322,7 +326,7 @@ Title of this category - An installed Visual Studio is required, which can be found here + Visual Studio is required, click here to download Subtitle of VisualStudioExtension category diff --git a/sources/launcher/Stride.Launcher/Launcher.cs b/sources/launcher/Stride.Launcher/Launcher.cs index 08da128c4..5cc1b1409 100644 --- a/sources/launcher/Stride.Launcher/Launcher.cs +++ b/sources/launcher/Stride.Launcher/Launcher.cs @@ -157,7 +157,7 @@ internal static class Launcher // Uninstall packages (they might have uninstall actions) var store = new NugetStore(path); - foreach (var package in store.MainPackageIds.SelectMany(store.GetLocalPackages).FilterStrideMainPackages().ToList()) + foreach (var package in store.MainPackageIds.SelectMany(packageId => store.GetLocalPackages(packageId)).FilterStrideMainPackages().ToList()) { await store.UninstallPackage(package, null); } diff --git a/sources/launcher/Stride.Launcher/Views/Icons.axaml b/sources/launcher/Stride.Launcher/Views/Icons.axaml new file mode 100644 index 000000000..18b6e7976 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Views/Icons.axaml @@ -0,0 +1,93 @@ + + + + M9.055 2.672C9.019 2.622 8.999 2.561 8.999 2.498C8.999 2.435 9.019 2.374 9.055 2.323C9.091 2.272 9.142 2.233 9.201 2.212L9.373 2.157H9.375L9.97 1.964C10.167 1.896 10.348 1.79 10.503 1.652C10.529 1.629 10.554 1.605 10.579 1.58C10.75 1.407 10.878 1.197 10.954 0.966L11.203 0.201C11.223 0.142 11.262 0.091 11.313 0.055C11.364 0.019 11.425 0 11.488 0H11.503C11.56 0.003 11.615 0.022 11.662 0.055C11.714 0.091 11.752 0.142 11.773 0.201L12.022 0.966C12.099 1.198 12.23 1.41 12.403 1.583C12.458 1.638 12.517 1.689 12.579 1.735C12.713 1.834 12.862 1.911 13.021 1.964L13.786 2.213L13.801 2.217C13.86 2.237 13.911 2.275 13.947 2.326C13.984 2.378 14.003 2.438 14.003 2.501C14.003 2.564 13.983 2.625 13.947 2.676C13.911 2.727 13.86 2.765 13.801 2.786L13.036 3.035C12.803 3.112 12.591 3.243 12.418 3.416C12.245 3.589 12.114 3.8 12.037 4.033L11.788 4.798C11.781 4.817 11.772 4.836 11.761 4.854C11.739 4.889 11.711 4.92 11.677 4.944C11.627 4.98 11.567 5 11.506 5H11.502C11.439 5 11.378 4.98 11.327 4.944C11.275 4.908 11.237 4.857 11.216 4.798L10.967 4.033C10.958 4.006 10.948 3.979 10.938 3.952C10.86 3.75 10.74 3.566 10.586 3.413C10.483 3.311 10.368 3.223 10.242 3.152C10.183 3.119 10.121 3.089 10.058 3.064C10.056 3.064 10.051 3.061 10.051 3.061L9.968 3.031L9.201 2.782C9.142 2.761 9.091 2.723 9.055 2.672ZM15.958 5.862C15.929 5.822 15.888 5.791 15.841 5.774L15.829 5.771L15.217 5.572V5.574C15.031 5.512 14.862 5.408 14.723 5.269C14.584 5.13 14.48 4.961 14.418 4.775L14.219 4.163C14.203 4.116 14.172 4.075 14.131 4.046C14.09 4.018 14.041 4.002 13.991 4.002C13.941 4.002 13.892 4.017 13.851 4.046C13.811 4.075 13.78 4.116 13.763 4.163L13.564 4.775C13.503 4.959 13.401 5.127 13.264 5.266C13.128 5.404 12.961 5.51 12.777 5.573L12.165 5.772C12.118 5.788 12.076 5.819 12.048 5.86C12.02 5.901 12.004 5.95 12.004 6C12.004 6.05 12.019 6.099 12.048 6.14C12.077 6.18 12.118 6.211 12.165 6.228L12.748 6.417V6.42L12.772 6.428C12.959 6.49 13.128 6.594 13.267 6.733C13.405 6.872 13.51 7.042 13.572 7.228L13.772 7.84C13.788 7.887 13.819 7.928 13.86 7.957C13.9 7.985 13.947 8.001 13.996 8.001H14.003C14.053 8.001 14.102 7.986 14.143 7.957C14.184 7.928 14.214 7.887 14.231 7.84L14.43 7.228C14.492 7.042 14.596 6.873 14.735 6.734C14.874 6.595 15.043 6.491 15.229 6.429L15.841 6.23C15.888 6.214 15.93 6.183 15.958 6.142C15.986 6.101 16.002 6.052 16.002 6.002C16.002 5.952 15.987 5.903 15.958 5.862ZM13 9.5C13 10.328 12.328 11 11.5 11H7.312L4 13.898V11H2.5C1.672 11 1 10.328 1 9.5V4.5C1 3.672 1.672 3 2.5 3H8V2H2.5C1.119 2 0 3.119 0 4.5V9.5C0 10.881 1.119 12 2.5 12H3V13.942C3 14.784 3.992 15.234 4.625 14.679L7.688 11.999H11.5C12.881 11.999 14 10.88 14 9.499V8.999H13V9.5Z + + + M12 13.5C12 13.776 11.776 14 11.5 14H3.5C3.224 14 3 13.776 3 13.5C3 13.224 3.224 13 3.5 13H11.5C11.776 13 12 13.224 12 13.5ZM7.146 11.854C7.341 12.049 7.658 12.049 7.853 11.854L11.853 7.854C12.048 7.659 12.048 7.342 11.853 7.147C11.658 6.952 11.341 6.952 11.146 7.147L8 10.294V1.5C8 1.224 7.776 1 7.5 1C7.224 1 7 1.224 7 1.5V10.293L3.854 7.146C3.659 6.951 3.342 6.951 3.147 7.146C2.952 7.341 2.952 7.658 3.147 7.853L7.147 11.853L7.146 11.854Z + + + M7.97553 0C3.57186 0 0 3.57186 0 7.97553C0 11.4985 2.29969 14.4832 5.43119 15.5596C5.82263 15.6086 5.96942 15.3639 5.96942 15.1682C5.96942 14.9725 5.96942 14.4832 5.96942 13.7982C3.76758 14.2875 3.27829 12.7217 3.27829 12.7217C2.93578 11.792 2.39755 11.5474 2.39755 11.5474C1.66361 11.0581 2.44648 11.0581 2.44648 11.0581C3.22936 11.107 3.66972 11.8899 3.66972 11.8899C4.40367 13.1131 5.52905 12.7706 5.96942 12.5749C6.01835 12.0367 6.263 11.6942 6.45872 11.4985C4.69725 11.3028 2.83792 10.6177 2.83792 7.53517C2.83792 6.65443 3.1315 5.96942 3.66972 5.38226C3.6208 5.23547 3.32722 4.40367 3.76758 3.32722C3.76758 3.32722 4.4526 3.1315 5.96942 4.15902C6.6055 3.9633 7.29052 3.91437 7.97553 3.91437C8.66055 3.91437 9.34556 4.01223 9.98165 4.15902C11.4985 3.1315 12.1835 3.32722 12.1835 3.32722C12.6239 4.40367 12.3303 5.23547 12.2813 5.43119C12.7706 5.96942 13.1131 6.70336 13.1131 7.5841C13.1131 10.6667 11.2538 11.3028 9.49235 11.4985C9.78593 11.7431 10.0306 12.2324 10.0306 12.9664C10.0306 14.0428 10.0306 14.8746 10.0306 15.1682C10.0306 15.3639 10.1774 15.6086 10.5688 15.5596C13.7492 14.4832 16 11.4985 16 7.97553C15.9511 3.57186 12.3792 0 7.97553 0Z + + + M9 8C9 8.55228 8.55228 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55228 7 9 7.44772 9 8Z + M1 8C1 11.859 4.14 15 8 15C11.86 15 15 11.859 15 8C15 4.141 11.86 1 8 1C4.14 1 1 4.141 1 8ZM2 8C2 4.691 4.691 2 8 2C11.309 2 14 4.691 14 8C14 11.309 11.309 14 8 14C4.691 14 2 11.309 2 8Z + + + M2 4.5C2.55228 4.5 3 4.05228 3 3.5C3 2.94772 2.55228 2.5 2 2.5C1.44772 2.5 1 2.94772 1 3.5C1 4.05228 1.44772 4.5 2 4.5ZM2 9C2.55228 9 3 8.55228 3 8C3 7.44772 2.55228 7 2 7C1.44772 7 1 7.44772 1 8C1 8.55228 1.44772 9 2 9ZM3 12.5C3 13.0523 2.55228 13.5 2 13.5C1.44772 13.5 1 13.0523 1 12.5C1 11.9477 1.44772 11.5 2 11.5C2.55228 11.5 3 11.9477 3 12.5ZM5.5 3C5.22386 3 5 3.22386 5 3.5C5 3.77614 5.22386 4 5.5 4H14.5C14.7761 4 15 3.77614 15 3.5C15 3.22386 14.7761 3 14.5 3H5.5ZM5 8C5 7.72386 5.22386 7.5 5.5 7.5H14.5C14.7761 7.5 15 7.72386 15 8C15 8.27614 14.7761 8.5 14.5 8.5H5.5C5.22386 8.5 5 8.27614 5 8ZM5.5 12C5.22386 12 5 12.2239 5 12.5C5 12.7761 5.22386 13 5.5 13H14.5C14.7761 13 15 12.7761 15 12.5C15 12.2239 14.7761 12 14.5 12H5.5Z + + + M11.5 2H4.5C3.119 2 2 3.119 2 4.5V11.5C2 12.881 3.119 14 4.5 14H11.5C12.881 14 14 12.881 14 11.5V4.5C14 3.119 12.881 2 11.5 2ZM3 4.5C3 3.672 3.672 3 4.5 3H6V6H3V4.5ZM4.5 13C3.672 13 3 12.328 3 11.5V7H6V13H4.5ZM13 11.5C13 12.328 12.328 13 11.5 13H7V7H13V11.5ZM13 6H7V3H11.5C12.328 3 13 3.672 13 4.5V6Z + + + M14 2H10C10 0.897 9.103 0 8 0C6.897 0 6 0.897 6 2H2C1.724 2 1.5 2.224 1.5 2.5C1.5 2.776 1.724 3 2 3H2.54L3.349 12.708C3.456 13.994 4.55 15 5.84 15H10.159C11.449 15 12.543 13.993 12.65 12.708L13.459 3H13.999C14.275 3 14.499 2.776 14.499 2.5C14.499 2.224 14.275 2 13.999 2H14ZM8 1C8.551 1 9 1.449 9 2H7C7 1.449 7.449 1 8 1ZM11.655 12.625C11.591 13.396 10.934 14 10.16 14H5.841C5.067 14 4.41 13.396 4.346 12.625L3.544 3H12.458L11.656 12.625H11.655ZM7 5.5V11.5C7 11.776 6.776 12 6.5 12C6.224 12 6 11.776 6 11.5V5.5C6 5.224 6.224 5 6.5 5C6.776 5 7 5.224 7 5.5ZM10 5.5V11.5C10 11.776 9.776 12 9.5 12C9.224 12 9 11.776 9 11.5V5.5C9 5.224 9.224 5 9.5 5C9.776 5 10 5.224 10 5.5Z + + + + + M492.5 69.8c-.2-.3-.4-.6-.8-.7-38.1-17.5-78.4-30-119.7-37.1-.4-.1-.8 0-1.1 .1s-.6 .4-.8 .8c-5.5 9.9-10.5 20.2-14.9 30.6-44.6-6.8-89.9-6.8-134.4 0-4.5-10.5-9.5-20.7-15.1-30.6-.2-.3-.5-.6-.8-.8s-.7-.2-1.1-.2c-41.3 7.1-81.6 19.6-119.7 37.1-.3 .1-.6 .4-.8 .7-76.2 113.8-97.1 224.9-86.9 334.5 0 .3 .1 .5 .2 .8s.3 .4 .5 .6c44.4 32.9 94 58 146.8 74.2 .4 .1 .8 .1 1.1 0s.7-.4 .9-.7c11.3-15.4 21.4-31.8 30-48.8 .1-.2 .2-.5 .2-.8s0-.5-.1-.8-.2-.5-.4-.6-.4-.3-.7-.4c-15.8-6.1-31.2-13.4-45.9-21.9-.3-.2-.5-.4-.7-.6s-.3-.6-.3-.9 0-.6 .2-.9 .3-.5 .6-.7c3.1-2.3 6.2-4.7 9.1-7.1 .3-.2 .6-.4 .9-.4s.7 0 1 .1c96.2 43.9 200.4 43.9 295.5 0 .3-.1 .7-.2 1-.2s.7 .2 .9 .4c2.9 2.4 6 4.9 9.1 7.2 .2 .2 .4 .4 .6 .7s.2 .6 .2 .9-.1 .6-.3 .9-.4 .5-.6 .6c-14.7 8.6-30 15.9-45.9 21.8-.2 .1-.5 .2-.7 .4s-.3 .4-.4 .7-.1 .5-.1 .8 .1 .5 .2 .8c8.8 17 18.8 33.3 30 48.8 .2 .3 .6 .6 .9 .7s.8 .1 1.1 0c52.9-16.2 102.6-41.3 147.1-74.2 .2-.2 .4-.4 .5-.6s.2-.5 .2-.8c12.3-126.8-20.5-236.9-86.9-334.5zm-302 267.7c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.4 59.2-52.8 59.2zm195.4 0c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.2 59.2-52.8 59.2z + + + M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5l0-170.3-52.8 0 0-78.2 52.8 0 0-33.7c0-87.1 39.4-127.5 125-127.5 16.2 0 44.2 3.2 55.7 6.4l0 70.8c-6-.6-16.5-1-29.6-1-42 0-58.2 15.9-58.2 57.2l0 27.8 83.6 0-14.4 78.2-69.3 0 0 175.9C413.8 494.8 512 386.9 512 256z + + + M52.402 31.916c0 4.03-1.17 7.895-3.178 11.087l8.196 8.23c4.014-5.375 6.523-12.094 6.523-19.318s-2.51-13.942-6.523-19.318l-8.196 8.23c2.007 3.192 3.178 6.887 3.178 11.087z + M32.004 52.41c-11.207 0-20.406-9.24-20.406-20.493s9.2-20.493 20.406-20.493c4.182 0 7.86 1.176 11.04 3.36l8.196-8.23C45.887 2.52 39.197 0 32.004 0 14.44 0 .057 14.278.057 32.084S14.44 64 32.004 64c7.36 0 14.05-2.52 19.403-6.55l-8.196-8.23c-3.178 2.016-7.025 3.192-11.207 3.192z + + + M64 32l320 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32zM305.9 166.4c20.6 0 37.3-16.7 37.3-37.3s-16.7-37.3-37.3-37.3c-18 0-33.1 12.8-36.6 29.8-30.2 3.2-53.8 28.8-53.8 59.9l0 .2c-32.8 1.4-62.8 10.7-86.6 25.5-8.8-6.8-19.9-10.9-32-10.9-28.9 0-52.3 23.4-52.3 52.3 0 21 12.3 39 30.1 47.4 1.7 60.7 67.9 109.6 149.3 109.6s147.6-48.9 149.3-109.7c17.7-8.4 29.9-26.4 29.9-47.3 0-28.9-23.4-52.3-52.3-52.3-12 0-23 4-31.9 10.8-24-14.9-54.3-24.2-87.5-25.4l0-.1c0-22.2 16.5-40.7 37.9-43.7 3.9 16.5 18.7 28.7 36.3 28.7l.2-.2zM155 248.1c14.6 0 25.8 15.4 25 34.4s-11.8 25.9-26.5 25.9-27.5-7.7-26.6-26.7 13.5-33.5 28.1-33.5l0-.1zm166.4 33.5c.9 19-12 26.7-26.6 26.7s-25.6-6.9-26.5-25.9 10.3-34.4 25-34.4 27.3 14.6 28.1 33.5l0 .1zm-42.1 49.6c-9 21.5-30.3 36.7-55.1 36.7s-46.1-15.1-55.1-36.7c-1.1-2.6 .7-5.4 3.4-5.7 16.1-1.6 33.5-2.5 51.7-2.5s35.6 .9 51.7 2.5c2.7 .3 4.5 3.1 3.4 5.7z + + + M317.8 18.7c13.2 0-1.4-2.2 162.8 57 .7 .4 1.1 .4 1.8 .8 17.6 7.3 29.6 24.5 29.6 44.2l0 267c0 19.7-12.1 36.9-29.6 44.2-.4 0-1.1 .4-2.9 1.1L335 484.6c-5.5 2.2-11.3 3.3-17.2 3.3-16.8 0-31.8-8.8-40.2-22-.5-.5-6.2-10.3-10.2-16.1-1.1-1.5-1.8-2.9-2.9-4.4L92.5 155.5C81.9 137.2 64 125.8 44.2 122.9l92.2-45 .4-.4c6.2-2.9 13.5-4.8 20.8-4.8 16.1 0 30.3 8 39.1 20.1l0 .4 169 238.1 0-166-36.6 51.6-84.9-119.6 31.8-54.9c.7-.7 1.1-1.4 1.4-1.8 8.4-13.2 23.4-21.9 40.2-21.9zm-117 390.9l-4 5.5 0 .3c-8.8 12.1-23 20.1-39.1 20.1-7.3 0-14.6-1.8-20.8-4.7l-.4-.4-92.9-45c20.1-2.9 38-13.9 49-32.6l17.2-28.5 20.5-34.4 70.6 119.6zM24.1 159.1c11.7-2.9 27.8-.7 36.9 15l47.9 80.1-47.9 80.1c-6.9 12.1-17.5 16.5-27.4 16.5-3.3 0-6.6-.7-9.5-1.5-11.7-3.3-24.1-13.2-24.1-31.8L0 190.9c0-18.6 12.4-28.5 24.1-31.8z + + + M357.2 48L427.8 48 273.6 224.2 455 464 313 464 201.7 318.6 74.5 464 3.8 464 168.7 275.5-5.2 48 140.4 48 240.9 180.9 357.2 48zM332.4 421.8l39.1 0-252.4-333.8-42 0 255.3 333.8z + + + + + M292.41,236.617l-42.814-27.769c5.495-15.665,4.255-33.162-3.707-48.011l35.117-31.373 c19.292,12.035,45.001,9.686,61.771-7.085c19.521-19.52,19.521-51.171,0-70.692c-19.522-19.521-51.175-19.521-70.694,0 c-15.378,15.378-18.632,38.274-9.788,56.848l-35.121,31.378c-16.812-11.635-38.258-13.669-56.688-6.078l-40.5-55.733 c14.528-19.074,13.095-46.421-4.331-63.849c-19.004-19.004-49.816-19.004-68.821,0c-19.005,19.005-19.005,49.818,0,68.822 c13.646,13.646,33.374,17.491,50.451,11.545l40.505,55.738c-20.002,23.461-18.936,58.729,3.242,80.906 c0.426,0.426,0.864,0.825,1.303,1.237l-39.242,68.874c-16.31-3.857-34.179,0.564-46.899,13.286 c-19.521,19.522-19.521,51.175,0,70.694c19.521,19.521,51.173,19.521,70.693,0c19.317-19.315,19.508-50.503,0.593-70.069 l39.239-68.867c19.705,5.658,41.737,0.978,57.573-14.033l42.855,27.79c-2.736,12.706,0.821,26.498,10.696,36.372 c15.469,15.469,40.544,15.469,56.012,0c15.468-15.466,15.468-40.543,0-56.011C329.831,226.518,307.908,225.209,292.41,236.617z M83.129,338.906c-0.951,1.078-1.846,2.096-2.724,2.973c-1.094,1.093-2.589,2.425-4.444,2.998 c-2.33,0.719-4.711,0.086-6.536-1.739c-4.772-4.771-2.947-13.799,4.246-20.989c7.195-7.195,16.219-9.021,20.993-4.247 c1.824,1.822,2.457,4.205,1.737,6.536c-0.572,1.855-1.904,3.354-2.997,4.444c-0.878,0.876-1.896,1.771-2.975,2.722 c-1.245,1.096-2.535,2.229-3.805,3.497C85.355,336.37,84.224,337.66,83.129,338.906z M279.56,59.17 c7.193-7.193,16.219-9.02,20.991-4.247c1.823,1.823,2.458,4.205,1.737,6.537c-0.572,1.856-1.905,3.354-2.997,4.446 c-0.876,0.875-1.894,1.77-2.974,2.72c-1.246,1.097-2.534,2.229-3.805,3.498c-1.271,1.271-2.403,2.562-3.5,3.808 c-0.948,1.076-1.846,2.097-2.72,2.973c-1.093,1.093-2.591,2.425-4.446,2.998c-2.332,0.719-4.712,0.086-6.536-1.739 C270.541,75.391,272.366,66.362,279.56,59.17z M73.322,37.854c-0.928,1.05-1.799,2.042-2.648,2.895 c-1.063,1.063-2.521,2.358-4.329,2.919c-2.269,0.698-4.587,0.083-6.364-1.691c-4.646-4.647-2.866-13.436,4.138-20.438 c7.003-7.004,15.788-8.782,20.436-4.135c1.776,1.776,2.395,4.095,1.692,6.363c-0.561,1.807-1.854,3.265-2.918,4.326 c-0.854,0.854-1.846,1.727-2.896,2.648c-1.213,1.066-2.469,2.17-3.704,3.406C75.492,35.384,74.387,36.642,73.322,37.854z M159.967,155.76c8.593-8.594,19.371-10.774,25.073-5.073c2.18,2.181,2.937,5.024,2.078,7.81 c-0.688,2.218-2.277,4.005-3.583,5.312c-1.047,1.047-2.265,2.112-3.553,3.248c-1.486,1.311-3.026,2.662-4.544,4.179 c-1.518,1.519-2.87,3.058-4.178,4.547c-1.136,1.287-2.205,2.505-3.251,3.55c-1.306,1.31-3.093,2.896-5.311,3.582 c-2.784,0.859-5.628,0.104-7.811-2.077C149.189,175.132,151.374,164.354,159.967,155.76z M299.11,262.103 c-0.868,0.866-2.056,1.923-3.524,2.376c-1.846,0.569-3.729,0.068-5.178-1.377c-3.783-3.781-2.338-10.933,3.365-16.633 c5.697-5.7,12.849-7.146,16.632-3.362c1.443,1.443,1.945,3.33,1.376,5.179c-0.453,1.471-1.51,2.656-2.375,3.521 c-0.694,0.692-1.5,1.402-2.355,2.155c-0.984,0.866-2.008,1.766-3.013,2.771c-1.007,1.006-1.907,2.026-2.771,3.016 C300.512,260.604,299.802,261.409,299.11,262.103z + + + M20,0H4C1.794,0,0,1.794,0,4v16c0,2.206,1.794,4,4,4h16c2.206,0,4-1.794,4-4V4C24,1.794,22.206,0,20,0z M22,20 c0,1.104-0.896,2-2,2H4c-1.103,0-2-0.896-2-2V4c0-1.103,0.897-2,2-2h16c1.104,0,2,0.897,2,2V20z + M18.5,7h-13c-0.552,0-1,0.448-1,1s0.448,1,1,1h13c0.552,0,1-0.448,1-1S19.052,7,18.5,7z + M18.5,11h-13c-0.552,0-1,0.448-1,1s0.448,1,1,1h13c0.552,0,1-0.448,1-1S19.052,11,18.5,11z + M12,15H5.5c-0.552,0-1,0.448-1,1s0.448,1,1,1H12c0.552,0,1-0.448,1-1S12.552,15,12,15z + + + M229.65723,141.65723l-96,96a8.003,8.003,0,0,1-11.31446,0l-96-96A8.00065,8.00065,0,0,1,32,128H72V48A16.01833,16.01833,0,0,1,88,32h80a16.01833,16.01833,0,0,1,16,16v80h40a8.00066,8.00066,0,0,1,5.65723,13.65723Z + + + M49.462,12.579V1.48c0-0.544-0.301-1.042-0.795-1.304c-0.488-0.26-1.069-0.231-1.521,0.073L16.483,20.888 c-0.411,0.282-0.652,0.739-0.652,1.229c0,0.491,0.238,0.957,0.652,1.23L47.132,43.98c0.459,0.305,1.033,0.335,1.528,0.087 c0.479-0.263,0.788-0.768,0.788-1.314V30.822c4.827,0.034,8.804,0.241,12.137,0.64c15.947,1.899,20.212,11.466,20.387,11.873 c0.222,0.553,0.776,0.9,1.365,0.9c0.085,0,0.191-0.003,0.287-0.027c0.688-0.137,1.191-0.743,1.191-1.448 C84.815,18.591,56.116,13.444,49.462,12.579z + M81.239,74.371L50.583,53.742c-0.445-0.308-1.026-0.343-1.521-0.093c-0.479,0.268-0.78,0.767-0.78,1.314v11.933 c-4.833-0.037-8.81-0.243-12.136-0.644c-15.954-1.898-20.22-11.464-20.388-11.871c-0.231-0.555-0.782-0.9-1.364-0.9 c-0.094,0-0.198,0.004-0.294,0.028c-0.69,0.137-1.191,0.742-1.191,1.45c0,24.164,28.696,29.318,35.358,30.188v11.098 c0,0.537,0.294,1.041,0.788,1.304c0.488,0.257,1.069,0.229,1.521-0.075l30.663-20.636c0.41-0.284,0.653-0.739,0.653-1.231 C81.893,75.11,81.653,74.642,81.239,74.371z + + + + + M8 12h4V6h3l-5-5-5 5h3v6zm11.338 1.532c-.21-.224-1.611-1.723-2.011-2.114A1.503 1.503 0 0 0 16.285 11h-1.757l3.064 2.994h-3.544a.274.274 0 0 0-.24.133L12.992 16H7.008l-.816-1.873a.276.276 0 0 0-.24-.133H2.408L5.471 11H3.715c-.397 0-.776.159-1.042.418-.4.392-1.801 1.891-2.011 2.114-.489.521-.758.936-.63 1.449l.561 3.074c.128.514.691.936 1.252.936h16.312c.561 0 1.124-.422 1.252-.936l.561-3.074c.126-.513-.142-.928-.632-1.449z + + + + + M 57,25L 57,51L 45.5,51C 42.8572,51 41,54 41,54L 35,54C 35,54 33.1428,51 30.5,51L 19,51L 19,25L 34.5,25C 34.5,25 35.9205,25.7102 37.4765,27.3448C 39.3408,25 41.25,25 41.25,25L 57,25 Z M 35,29.652C 34.3526,28.8549 33.5338,28 33,28L 22,28L 22,48L 31,48C 31,48 33,47.75 35,50.75L 35,29.652 Z M 41,50.75C 43,47.75 45,48 45,48L 54,48L 54,28L 43,28C 42.4662,28 41.6474,28.8549 41,29.652L 41,50.75 Z + + + M 27,48C 27,50 26.5,53 25.5,53L 52,53C 54.5,53 55,50.5 55,48L 55,25L 27,25L 27,48 Z M 59,48C 59,52.3723 56,57 52.5,57L 21,57C 18.3766,57 16,52.8723 16,48.5L 16,27L 23,27L 23,21L 59,21L 59,48 Z M 20,31L 20,49C 20,51.6234 20.6255,53.25 21.5,53.25C 22.3744,53.25 23,51.6234 23,49L 23,31L 20,31 Z M 41,34L 53,34L 53,48L 41,48L 41,34 Z M 29,28L 53,28L 53,31L 29,31L 29,28 Z M 29,34L 39,34L 39,36L 29,36L 29,34 Z M 29,38L 39,38L 39,40L 29,40L 29,38 Z M 29,42L 39,42L 39,44L 29,44L 29,42 Z M 29,46L 39,46L 39,48L 29,48L 29,46 Z + + + M 38,23.75C 46.7445,23.75 53.8333,30.8388 53.8333,39.5833C 53.8333,48.3278 46.7445,55.4167 38,55.4167C 29.2555,55.4167 22.1667,48.3278 22.1667,39.5833C 22.1667,30.8388 29.2555,23.75 38,23.75 Z M 38,27.7083C 31.4416,27.7083 26.125,33.025 26.125,39.5833C 26.125,46.1417 31.4416,51.4583 38,51.4583C 44.5584,51.4583 49.875,46.1417 49.875,39.5833C 49.875,33.025 44.5584,27.7083 38,27.7083 Z M 37.6042,30.875C 38.4786,30.875 39.1875,31.5839 39.1875,32.4583L 39.1875,38.3959L 45.125,38.3959C 45.9994,38.3959 46.7083,39.1047 46.7083,39.9792L 46.7083,40.7708C 46.7083,41.6453 45.9994,42.3542 45.125,42.3542L 38,42.3542C 37.2083,42.2222 36.4167,42.0903 35.9548,41.6285C 35.493,41.1667 35.3611,40.375 35.2292,39.5833L 35.2292,32.4583C 35.2292,31.5839 35.938,30.875 36.8125,30.875L 37.6042,30.875 Z M 49.0833,20.5833C 52.5811,20.5833 55.4167,23.4189 55.4167,26.9167C 55.4167,28.4256 54.889,29.8113 54.008,30.8993C 51.9429,27.1006 48.5651,24.1189 44.4835,22.5632C 45.6379,21.3439 47.2718,20.5833 49.0833,20.5833 Z M 26.9167,20.5834C 28.7282,20.5834 30.3621,21.3439 31.5165,22.5632C 27.4349,24.1189 24.0571,27.1006 21.992,30.8993C 21.111,29.8113 20.5833,28.4256 20.5833,26.9167C 20.5833,23.4189 23.4189,20.5834 26.9167,20.5834 Z + + \ No newline at end of file diff --git a/sources/launcher/Stride.Launcher/Views/MainView.axaml b/sources/launcher/Stride.Launcher/Views/MainView.axaml index b18b1a50f..7288b9944 100644 --- a/sources/launcher/Stride.Launcher/Views/MainView.axaml +++ b/sources/launcher/Stride.Launcher/Views/MainView.axaml @@ -13,41 +13,20 @@ mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="768" x:Class="Stride.Launcher.Views.MainView" x:DataType="vm:MainViewModel"> - - - - - - - - - - - - - - - - - - - - + Background="{DynamicResource AccentBrush}" + Padding="10,2"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + @@ -280,7 +222,9 @@ VerticalAlignment="Stretch" IsVisible="{Binding IsProcessing}" /> + HorizontalAlignment="Center" + VerticalAlignment="Center" + IsVisible="{Binding IsProcessing}" /> - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - private void SimplifyNotSupportedConstantsInShader(SpirvContext context, SpirvBuffer temp) + /// False if such a constant could not be resolved: the module would not be a valid shader. + private bool SimplifyNotSupportedConstantsInShader(SpirvContext context, ILogger log) { // Collect instructions to simplify first to avoid modifying the buffer during iteration var toSimplify = new List<(int Index, object Value, int TypeId, int ResultId)>(); + var success = true; foreach (var i in context) { if (i.Op == Op.OpSpecConstantOp && (OpSpecConstantOp)i is { } specConstantOp) @@ -1153,45 +1156,30 @@ public partial class ShaderMixer(IExternalShaderLoader shaderLoader) if (!ExpressionExtensions.ShaderSpecConstantOpSupportedOps.Contains((Op)specConstantOp.Opcode)) { var resultType = i.Data.Memory.Span[1]; + var resultId = i.Data.IdResult!.Value; if (context.TryGetConstantValue(i, out var value, out _) && value != null) - toSimplify.Add((i.Index, value, resultType, i.Data.IdResult!.Value)); + { + toSimplify.Add((i.Index, value, resultType, resultId)); + } + else + { + var name = context.Names.TryGetValue(resultId, out var constantName) ? $"'{constantName}'" : $"%{resultId}"; + log.Error($"Constant {name} uses {(Op)specConstantOp.Opcode}, which is not allowed in a constant of a shader module, and its value could not be computed"); + success = false; + } } } } - // Replace each OpSpecConstantOp with a resolved OpConstant - var buffer = context.GetBuffer(); - foreach (var (index, value, typeId, resultId) in toSimplify) + // Replace each OpSpecConstantOp with a resolved constant + // Note: from the last one, since a vector inserts the constants of its components before itself + for (var i = toSimplify.Count - 1; i >= 0; i--) { - // Build replacement OpConstant instruction manually - var wordCount = value is long or ulong or double ? 5 : 4; - var mem = CommunityToolkit.HighPerformance.Buffers.MemoryOwner.Allocate(wordCount); - mem.Span[0] = (wordCount << 16) | (int)Op.OpConstant; - mem.Span[1] = typeId; - mem.Span[2] = resultId; - switch (value) - { - case int v: mem.Span[3] = v; break; - case uint v: mem.Span[3] = unchecked((int)v); break; - case float v: mem.Span[3] = BitConverter.SingleToInt32Bits(v); break; - case long v: - mem.Span[3] = (int)(v & 0xFFFFFFFF); - mem.Span[4] = (int)(v >> 32); - break; - case ulong v: - mem.Span[3] = (int)(v & 0xFFFFFFFF); - mem.Span[4] = (int)(v >> 32); - break; - case double v: - var bits = BitConverter.DoubleToInt64Bits(v); - mem.Span[3] = (int)(bits & 0xFFFFFFFF); - mem.Span[4] = (int)(bits >> 32); - break; - default: - throw new NotSupportedException($"Cannot simplify constant of type {value.GetType()}"); - } - buffer.Replace(index, new OpData(mem)); + var (index, value, typeId, resultId) = toSimplify[i]; + context.ReplaceByConstant(index, typeId, resultId, value); } + + return success; } private static void RemoveInstructionWhere(SpirvBuffer buffer, Func match) diff --git a/sources/shaders/Stride.Shaders.Effects/ShaderCompilerVersion.cs b/sources/shaders/Stride.Shaders.Effects/ShaderCompilerVersion.cs index 83b76fdc4..12acfb6dc 100644 --- a/sources/shaders/Stride.Shaders.Effects/ShaderCompilerVersion.cs +++ b/sources/shaders/Stride.Shaders.Effects/ShaderCompilerVersion.cs @@ -37,5 +37,5 @@ public static class ShaderCompilerVersion /// Bump when what is derived from the SPIR-V changes while the SPIR-V does not: reflection, or the /// bytecode compiled from it for each graphics API. Invalidates the Effect caches only. /// - public const int Effect = 0; + public const int Effect = 1; } diff --git a/sources/shaders/Stride.Shaders.Parsers/Core/ConstantEvaluator.cs b/sources/shaders/Stride.Shaders.Parsers/Core/ConstantEvaluator.cs new file mode 100644 index 000000000..5a7de8ed2 --- /dev/null +++ b/sources/shaders/Stride.Shaders.Parsers/Core/ConstantEvaluator.cs @@ -0,0 +1,329 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Stride.Shaders.Spirv.Building; +using static Stride.Shaders.Spirv.Specification; + +namespace Stride.Shaders.Core; + +/// +/// Evaluates the operations a constant can be built from (the inner operations of OpSpecConstantOp) on known values. +/// +/// +/// A value is a boxed CLR scalar matching its SPIR-V type: bool, int, uint, long, ulong, float or double. +/// A vector is a of those, on which an operation applies per component. +/// An operation that is not supported, or not defined for its operands (e.g. a division by zero), is not evaluated. +/// Note: the switch expressions cast their first case to object, otherwise the cases are converted to a common type. +/// +public static class ConstantEvaluator +{ + public static bool TryEvaluateUnary(Op op, object operand, SymbolType resultType, [NotNullWhen(true)] out object? result) + { + // A vector is evaluated per component + if (operand is ConstantVector vector) + { + result = null; + if (resultType is not VectorType vectorResultType || vectorResultType.Size != vector.Values.Length) + return false; + + var values = new object[vector.Values.Length]; + for (var i = 0; i < values.Length; i++) + { + if (vector.Values[i] is ConstantVector || !TryEvaluateUnary(op, vector.Values[i], vectorResultType.BaseType, out values[i]!)) + return false; + } + + result = new ConstantVector { Values = values }; + return true; + } + + result = (op, operand) switch + { + (Op.OpSNegate or Op.OpNot, int v) => EvaluateIntegerUnary(op, v), + (Op.OpSNegate or Op.OpNot, uint v) => EvaluateIntegerUnary(op, v), + (Op.OpSNegate or Op.OpNot, long v) => EvaluateIntegerUnary(op, v), + (Op.OpSNegate or Op.OpNot, ulong v) => EvaluateIntegerUnary(op, v), + (Op.OpFNegate, float v) => -v, + (Op.OpFNegate, double v) => -v, + (Op.OpLogicalNot, bool v) => !v, + _ when resultType is ScalarType scalarResultType => EvaluateConversion(op, operand, scalarResultType), + _ => null, + }; + return result is not null; + } + + public static bool TryEvaluateBinary(Op op, object left, object right, [NotNullWhen(true)] out object? result) + { + // Vectors are evaluated per component + if (left is ConstantVector leftVector) + { + result = null; + if (right is not ConstantVector rightVector || rightVector.Values.Length != leftVector.Values.Length) + return false; + + var values = new object[leftVector.Values.Length]; + for (var i = 0; i < values.Length; i++) + { + if (leftVector.Values[i] is ConstantVector || !TryEvaluateBinary(op, leftVector.Values[i], rightVector.Values[i], out values[i]!)) + return false; + } + + result = new ConstantVector { Values = values }; + return true; + } + + if (op is Op.OpShiftLeftLogical or Op.OpShiftRightLogical or Op.OpShiftRightArithmetic) + { + // The shift amount does not need to have the type of the shifted value + result = TryGetInteger(right, out var shift, out _) + ? left switch + { + int l => EvaluateShift(op, l, (int)shift, signed: true), + uint l => EvaluateShift(op, l, (int)shift, signed: false), + long l => EvaluateShift(op, l, (int)shift, signed: true), + ulong l => EvaluateShift(op, l, (int)shift, signed: false), + _ => null, + } + : null; + } + else + { + result = (left, right) switch + { + (int l, int r) => EvaluateIntegerBinary(op, l, r, signed: true), + (uint l, uint r) => EvaluateIntegerBinary(op, l, r, signed: false), + (long l, long r) => EvaluateIntegerBinary(op, l, r, signed: true), + (ulong l, ulong r) => EvaluateIntegerBinary(op, l, r, signed: false), + (float l, float r) => EvaluateFloatBinary(op, l, r), + (double l, double r) => EvaluateFloatBinary(op, l, r), + (bool l, bool r) => EvaluateLogicalBinary(op, l, r), + _ => null, + }; + } + + return result is not null; + } + + /// + /// Number of operands of an operation that can be evaluated, all of them ids: 1 for , + /// 2 for and 3 for OpSelect, which only needs the operand its condition selects. + /// 0 for any other operation, whose operands might not be ids (e.g. the literal indices of OpCompositeExtract). + /// + public static int GetEvaluatedOperandCount(Op op) + { + return op switch + { + Op.OpSNegate or Op.OpNot or Op.OpFNegate or Op.OpLogicalNot + or Op.OpConvertFToS or Op.OpConvertFToU or Op.OpConvertSToF or Op.OpConvertUToF + or Op.OpSConvert or Op.OpUConvert or Op.OpFConvert or Op.OpBitcast => 1, + Op.OpIAdd or Op.OpISub or Op.OpIMul or Op.OpUDiv or Op.OpSDiv or Op.OpUMod or Op.OpSRem or Op.OpSMod + or Op.OpShiftRightLogical or Op.OpShiftRightArithmetic or Op.OpShiftLeftLogical + or Op.OpBitwiseOr or Op.OpBitwiseXor or Op.OpBitwiseAnd + or Op.OpFAdd or Op.OpFSub or Op.OpFMul or Op.OpFDiv or Op.OpFRem or Op.OpFMod + or Op.OpLogicalOr or Op.OpLogicalAnd or Op.OpLogicalEqual or Op.OpLogicalNotEqual + or Op.OpIEqual or Op.OpINotEqual + or Op.OpULessThan or Op.OpSLessThan or Op.OpUGreaterThan or Op.OpSGreaterThan + or Op.OpULessThanEqual or Op.OpSLessThanEqual or Op.OpUGreaterThanEqual or Op.OpSGreaterThanEqual => 2, + Op.OpSelect => 3, + _ => 0, + }; + } + + /// + /// Number of leading operands that are ids, for any operation that can be part of a constant: the ones of + /// , and the operations on composites, whose other operands are literal indices. + /// 0 for any other operation. + /// + public static int GetIdOperandCount(Op op) + { + return op switch + { + Op.OpCompositeExtract => 1, + Op.OpCompositeInsert or Op.OpVectorShuffle => 2, + _ => GetEvaluatedOperandCount(op), + }; + } + + private static object? EvaluateIntegerUnary(Op op, T value) where T : IBinaryInteger + { + return op switch + { + Op.OpSNegate => (object)(T.Zero - value), + Op.OpNot => ~value, + _ => null, + }; + } + + private static object? EvaluateShift(Op op, T value, int shift, bool signed) where T : IBinaryInteger + { + // Not defined in SPIR-V (C# would only keep the low bits of the shift amount) + if (shift < 0 || shift >= value.GetByteCount() * 8) + return null; + + return op switch + { + Op.OpShiftLeftLogical => (object)(value << shift), + Op.OpShiftRightLogical => value >>> shift, + // On an unsigned type, SPIR-V still extends the sign bit where C# does not + Op.OpShiftRightArithmetic when signed => value >> shift, + _ => null, + }; + } + + private static object? EvaluateIntegerBinary(Op op, T left, T right, bool signed) where T : IBinaryInteger, IMinMaxValue + { + // Not defined in SPIR-V: a division by zero, and the one signed division that overflows (which throws in C#) + if (op is Op.OpSDiv or Op.OpSRem or Op.OpSMod or Op.OpUDiv or Op.OpUMod + && (right == T.Zero || (signed && left == T.MinValue && right == T.Zero - T.One))) + return null; + + return op switch + { + Op.OpIAdd => (object)(left + right), + Op.OpISub => left - right, + Op.OpIMul => left * right, + Op.OpBitwiseAnd => left & right, + Op.OpBitwiseOr => left | right, + Op.OpBitwiseXor => left ^ right, + Op.OpIEqual => left == right, + Op.OpINotEqual => left != right, + Op.OpSDiv when signed => left / right, + Op.OpSRem when signed => left % right, + // Remainder with the sign of the divisor + Op.OpSMod when signed => (left % right + right) % right, + Op.OpSLessThan when signed => left < right, + Op.OpSGreaterThan when signed => left > right, + Op.OpSLessThanEqual when signed => left <= right, + Op.OpSGreaterThanEqual when signed => left >= right, + Op.OpUDiv when !signed => left / right, + Op.OpUMod when !signed => left % right, + Op.OpULessThan when !signed => left < right, + Op.OpUGreaterThan when !signed => left > right, + Op.OpULessThanEqual when !signed => left <= right, + Op.OpUGreaterThanEqual when !signed => left >= right, + _ => null, + }; + } + + private static object? EvaluateFloatBinary(Op op, T left, T right) where T : IFloatingPointIeee754 + { + return op switch + { + Op.OpFAdd => (object)(left + right), + Op.OpFSub => left - right, + Op.OpFMul => left * right, + Op.OpFDiv => left / right, + Op.OpFRem => left % right, + // Remainder with the sign of the divisor + Op.OpFMod => (left % right + right) % right, + _ => null, + }; + } + + private static object? EvaluateLogicalBinary(Op op, bool left, bool right) + { + return op switch + { + Op.OpLogicalAnd => (object)(left && right), + Op.OpLogicalOr => left || right, + Op.OpLogicalEqual => left == right, + Op.OpLogicalNotEqual => left != right, + _ => null, + }; + } + + private static object? EvaluateConversion(Op op, object operand, ScalarType resultType) + { + switch (op) + { + case Op.OpConvertFToS or Op.OpConvertFToU or Op.OpFConvert when operand is float or double: + { + var value = Convert.ToDouble(operand); + return (op, resultType.Type) switch + { + (Op.OpConvertFToS, Scalar.Int) => (object)(int)value, + (Op.OpConvertFToS, Scalar.Int64) => (long)value, + (Op.OpConvertFToU, Scalar.UInt) => (uint)value, + (Op.OpConvertFToU, Scalar.UInt64) => (ulong)value, + (Op.OpFConvert, Scalar.Float) => (float)value, + (Op.OpFConvert, Scalar.Double) => value, + _ => null, + }; + } + // The operand is read as signed or unsigned depending on the operation, whatever its type + case Op.OpConvertSToF or Op.OpConvertUToF when TryGetInteger(operand, out var signExtended, out var zeroExtended): + { + var value = op == Op.OpConvertSToF ? (double)signExtended : (double)zeroExtended; + return resultType.Type switch + { + Scalar.Float => (object)(float)value, + Scalar.Double => value, + _ => null, + }; + } + case Op.OpSConvert when TryGetInteger(operand, out var signExtended, out _): + return FromBits(resultType, unchecked((ulong)signExtended)); + case Op.OpUConvert when TryGetInteger(operand, out _, out var zeroExtended): + return FromBits(resultType, zeroExtended); + case Op.OpBitcast when TryGetBits(operand, out var bits, out var width) && width == GetWidth(resultType): + return FromBits(resultType, bits); + default: + return null; + } + } + + private static bool TryGetInteger(object value, out long signExtended, out ulong zeroExtended) + { + (signExtended, zeroExtended) = value switch + { + int v => ((long)v, (ulong)(uint)v), + uint v => ((long)(int)v, (ulong)v), + long v => (v, (ulong)v), + ulong v => ((long)v, v), + _ => (0L, 0UL), + }; + return value is int or uint or long or ulong; + } + + private static bool TryGetBits(object value, out ulong bits, out int width) + { + (bits, width) = value switch + { + int v => ((ulong)(uint)v, 32), + uint v => ((ulong)v, 32), + float v => ((ulong)BitConverter.SingleToUInt32Bits(v), 32), + long v => ((ulong)v, 64), + ulong v => (v, 64), + double v => (BitConverter.DoubleToUInt64Bits(v), 64), + _ => (0UL, 0), + }; + return width != 0; + } + + private static int GetWidth(ScalarType type) + { + return type.Type switch + { + Scalar.Int or Scalar.UInt or Scalar.Float => 32, + Scalar.Int64 or Scalar.UInt64 or Scalar.Double => 64, + _ => 0, + }; + } + + // Keeps the low bits of the value that fit the type + private static object? FromBits(ScalarType type, ulong bits) + { + return type.Type switch + { + Scalar.Int => (object)(int)bits, + Scalar.UInt => (uint)bits, + Scalar.Int64 => (long)bits, + Scalar.UInt64 => bits, + Scalar.Float => BitConverter.UInt32BitsToSingle((uint)bits), + Scalar.Double => BitConverter.UInt64BitsToDouble(bits), + _ => null, + }; + } +} diff --git a/sources/shaders/Stride.Shaders.Parsers/Core/ConstantExpression.cs b/sources/shaders/Stride.Shaders.Parsers/Core/ConstantExpression.cs index 6bdb8db99..87bf931cc 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Core/ConstantExpression.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Core/ConstantExpression.cs @@ -58,12 +58,13 @@ public abstract record ConstantExpression /// public static ConstantExpression FromValue(object value) => value switch { - int i => new IntConstExpr(i), - uint u => new IntConstExpr(u), - long l => new IntConstExpr(l), - ulong u => new IntConstExpr((long)u), - float f => new FloatConstExpr(f), - double d => new FloatConstExpr(d), + int i => new IntConstExpr(i, ScalarType.Int), + uint u => new IntConstExpr(u, ScalarType.UInt), + long l => new IntConstExpr(l, ScalarType.Int64), + ulong u => new IntConstExpr((long)u, ScalarType.UInt64), + Half h => new FloatConstExpr((double)h, ScalarType.Half), + float f => new FloatConstExpr(f, ScalarType.Float), + double d => new FloatConstExpr(d, ScalarType.Double), bool b => new BoolConstExpr(b), string s => new StringConstExpr(s), _ => throw new NotSupportedException($"Unsupported constant type: {value.GetType()}") @@ -101,27 +102,25 @@ public abstract record ConstantExpression if (typeInst.Op == Op.OpTypeInt) { var type = (OpTypeInt)typeInst; - long val = type switch + return type switch { - { Width: <= 32, Signedness: 0 } => (long)operand.ToLiteral(), - { Width: <= 32, Signedness: 1 } => operand.ToLiteral(), - { Width: 64, Signedness: 0 } => (long)operand.ToLiteral(), - { Width: 64, Signedness: 1 } => operand.ToLiteral(), + { Width: <= 32, Signedness: 0 } => new IntConstExpr(operand.ToLiteral(), ScalarType.UInt), + { Width: <= 32, Signedness: 1 } => new IntConstExpr(operand.ToLiteral(), ScalarType.Int), + { Width: 64, Signedness: 0 } => new IntConstExpr((long)operand.ToLiteral(), ScalarType.UInt64), + { Width: 64, Signedness: 1 } => new IntConstExpr(operand.ToLiteral(), ScalarType.Int64), _ => throw new NotImplementedException($"Unsupported int width {type.Width}"), }; - return new IntConstExpr(val); } else if (typeInst.Op == Op.OpTypeFloat) { var type = new OpTypeFloat(typeInst); - double val = type switch + return type switch { - { Width: 16 } => (double)operand.ToLiteral(), - { Width: 32 } => operand.ToLiteral(), - { Width: 64 } => operand.ToLiteral(), + { Width: 16 } => new FloatConstExpr((double)operand.ToLiteral(), ScalarType.Half), + { Width: 32 } => new FloatConstExpr(operand.ToLiteral(), ScalarType.Float), + { Width: 64 } => new FloatConstExpr(operand.ToLiteral(), ScalarType.Double), _ => throw new NotImplementedException($"Unsupported float width {type.Width}"), }; - return new FloatConstExpr(val); } else throw new NotImplementedException($"Unsupported constant type {typeInst.Op}"); @@ -135,6 +134,14 @@ public abstract record ConstantExpression return new StringConstExpr(operand.ToLiteral()); } + case Op.OpConstantNull: + { + var typeId = inst.Data.Memory.Span[1]; + if (!context.ReverseTypes.TryGetValue(typeId, out var nullType)) + throw new InvalidOperationException($"Cannot find type for null constant type id {typeId}"); + return new NullConstExpr(nullType); + } + case Op.OpGenericParameterSDSL: case Op.OpGenericReferenceSDSL: { @@ -158,57 +165,33 @@ public abstract record ConstantExpression case Op.OpSpecConstantOp: { var op = (Op)inst.Data.Memory.Span[3]; - switch (op) + if (!context.ReverseTypes.TryGetValue(inst.Data.Memory.Span[1], out var resultType)) + throw new InvalidOperationException($"Cannot find result type of constant {inst.Data.Memory.Span[2]}"); + // Operations on composites: their last operands are literal indices + if (op is Op.OpCompositeExtract or Op.OpCompositeInsert or Op.OpVectorShuffle) { - // Conversions (unary) - case Op.OpConvertFToS: - case Op.OpConvertFToU: - case Op.OpConvertSToF: - case Op.OpConvertUToF: - case Op.OpSNegate: - case Op.OpFNegate: - case Op.OpNot: - case Op.OpLogicalNot: + var idOperandCount = ConstantEvaluator.GetIdOperandCount(op); + var operands = new ConstantExpression[idOperandCount]; + for (int i = 0; i < idOperandCount; i++) + operands[i] = ParseFromBuffer(inst.Data.Memory.Span[4 + i], buffer, context); + return CompositeOpExpr.Create(op, resultType, operands, inst.Data.Memory.Span[(4 + idOperandCount)..].ToArray()); + } + + // Note: the operation decides how many operands are ids, the length of the instruction does not + switch (ConstantEvaluator.GetEvaluatedOperandCount(op)) + { + case 1: { var operandExpr = ParseFromBuffer(inst.Data.Memory.Span[4], buffer, context); - return new UnaryOpExpr(op, operandExpr); + return new UnaryOpExpr(op, resultType, operandExpr); } - // Binary operations - case Op.OpIAdd: - case Op.OpISub: - case Op.OpIMul: - case Op.OpUDiv: - case Op.OpSDiv: - case Op.OpFAdd: - case Op.OpFSub: - case Op.OpFMul: - case Op.OpFDiv: - case Op.OpShiftRightLogical: - case Op.OpShiftRightArithmetic: - case Op.OpShiftLeftLogical: - case Op.OpBitwiseOr: - case Op.OpBitwiseXor: - case Op.OpBitwiseAnd: - case Op.OpLogicalOr: - case Op.OpLogicalAnd: - case Op.OpLogicalEqual: - case Op.OpLogicalNotEqual: - case Op.OpIEqual: - case Op.OpINotEqual: - case Op.OpULessThan: - case Op.OpSLessThan: - case Op.OpUGreaterThan: - case Op.OpSGreaterThan: - case Op.OpULessThanEqual: - case Op.OpSLessThanEqual: - case Op.OpUGreaterThanEqual: - case Op.OpSGreaterThanEqual: + case 2: { var left = ParseFromBuffer(inst.Data.Memory.Span[4], buffer, context); var right = ParseFromBuffer(inst.Data.Memory.Span[5], buffer, context); - return new BinaryOpExpr(op, left, right); + return new BinaryOpExpr(op, resultType, left, right); } - case Op.OpSelect: + case 3: { var cond = ParseFromBuffer(inst.Data.Memory.Span[4], buffer, context); var trueVal = ParseFromBuffer(inst.Data.Memory.Span[5], buffer, context); @@ -227,51 +210,54 @@ public abstract record ConstantExpression } /// -/// Integer constant. Covers int, uint, long, ulong — signedness determined at emission by SPIR-V type context. +/// Integer constant of type int, uint, long or ulong (whose value is stored as its bits). /// -public sealed record IntConstExpr(long Value) : ConstantExpression +public sealed record IntConstExpr(long Value, ScalarType Type) : ConstantExpression { public override int Emit(SpirvContext context) { - // For values that fit in int, use int (signed) — matches the common case for array sizes - if (Value is >= int.MinValue and <= int.MaxValue) - return context.CompileConstant((int)Value).Id; - return context.CompileConstant(Value).Id; + TryEvaluate(out var value); + return context.CompileConstant(Type, value!).Id; } public override bool TryEvaluate(out object? value) { - if (Value is >= int.MinValue and <= int.MaxValue) - value = (int)Value; - else - value = Value; + // Note: first cast to object is important, otherwise all the cases are converted to a common type + value = Type.Type switch + { + Scalar.Int => (object)(int)Value, + Scalar.UInt => (uint)Value, + Scalar.Int64 => Value, + Scalar.UInt64 => (ulong)Value, + _ => throw new NotSupportedException($"Unsupported integer constant type {Type}"), + }; return true; } - public override string ToString() => Value.ToString(); + public override string ToString() => Type.Type == Scalar.UInt64 ? ((ulong)Value).ToString() : Value.ToString(); } /// -/// Float constant. Covers float, double — precision determined at emission. +/// Float constant of type half, float or double. /// -public sealed record FloatConstExpr(double Value) : ConstantExpression +public sealed record FloatConstExpr(double Value, ScalarType Type) : ConstantExpression { public override int Emit(SpirvContext context) { - // Use float precision if the value fits without loss - var asFloat = (float)Value; - if ((double)asFloat == Value) - return context.CompileConstant(asFloat).Id; - return context.CompileConstant(Value).Id; + TryEvaluate(out var value); + return context.CompileConstant(Type, value!).Id; } public override bool TryEvaluate(out object? value) { - var asFloat = (float)Value; - if ((double)asFloat == Value) - value = asFloat; - else - value = Value; + // Note: first cast to object is important, otherwise all the cases are converted to a common type + value = Type.Type switch + { + Scalar.Half => (object)(Half)Value, + Scalar.Float => (float)Value, + Scalar.Double => Value, + _ => throw new NotSupportedException($"Unsupported float constant type {Type}"), + }; return true; } @@ -351,64 +337,34 @@ public sealed record GenericParamExpr(int Index, string DeclaringClass) : Consta /// /// Unary spec-constant operation (SNegate, FNegate, Not, LogicalNot, conversions). /// -public sealed record UnaryOpExpr(Op Op, ConstantExpression Operand) : ConstantExpression +public sealed record UnaryOpExpr(Op Op, SymbolType ResultType, ConstantExpression Operand) : ConstantExpression { public override int Emit(SpirvContext context) { // Try constant folding first — avoids OpSpecConstantOp which some backends don't support. if (TryEvaluate(out var folded) && folded is not null) - return FromValue(folded).Emit(context); + return context.CompileConstant(ResultType, folded).Id; var operandId = Operand.Emit(context); - var operandTypeId = GetEmittedTypeId(context, operandId); - var resultTypeId = ResolveResultType(context, operandTypeId); var resultId = context.Bound++; - Span instruction = [(int)Specification.Op.OpSpecConstantOp, resultTypeId, resultId, (int)Op, operandId]; + Span instruction = [(int)Specification.Op.OpSpecConstantOp, context.GetOrRegister(ResultType), resultId, (int)Op, operandId]; instruction[0] |= instruction.Length << 16; context.Add(new OpData(instruction)); return resultId; } - private int ResolveResultType(SpirvContext context, int operandTypeId) - { - return Op switch - { - // Conversions change the result type - Specification.Op.OpConvertFToS => context.GetOrRegister(ScalarType.Int), - Specification.Op.OpConvertFToU => context.GetOrRegister(ScalarType.UInt), - Specification.Op.OpConvertSToF => context.GetOrRegister(ScalarType.Float), - Specification.Op.OpConvertUToF => context.GetOrRegister(ScalarType.Float), - // Unary ops keep the same type - _ => operandTypeId, - }; - } - public override bool TryEvaluate(out object? value) { value = null; - if (!Operand.TryEvaluate(out var operandValue) || operandValue is null) - return false; - - value = Op switch - { - Specification.Op.OpSNegate => (object)(-(int)operandValue), - Specification.Op.OpFNegate => -(float)operandValue, - Specification.Op.OpNot when operandValue is int i => ~i, - Specification.Op.OpLogicalNot when operandValue is bool b => !b, - Specification.Op.OpConvertFToS => (int)(float)operandValue, - Specification.Op.OpConvertFToU => (uint)(float)operandValue, - Specification.Op.OpConvertSToF => (float)(int)operandValue, - Specification.Op.OpConvertUToF => (float)(uint)operandValue, - _ => null, - }; - return value is not null; + return Operand.TryEvaluate(out var operandValue) && operandValue is not null + && ConstantEvaluator.TryEvaluateUnary(Op, operandValue, ResultType, out value); } public override ConstantExpression Substitute(string declaringClass, ConstantExpression[] args) { var newOperand = Operand.Substitute(declaringClass, args); if (ReferenceEquals(newOperand, Operand)) return this; - var result = new UnaryOpExpr(Op, newOperand); + var result = new UnaryOpExpr(Op, ResultType, newOperand); return result.TryEvaluate(out var val) && val is not null ? FromValue(val) : result; } @@ -418,19 +374,18 @@ public sealed record UnaryOpExpr(Op Op, ConstantExpression Operand) : ConstantEx /// /// Binary spec-constant operation (IAdd, IMul, ISub, etc.) /// -public sealed record BinaryOpExpr(Op Op, ConstantExpression Left, ConstantExpression Right) : ConstantExpression +public sealed record BinaryOpExpr(Op Op, SymbolType ResultType, ConstantExpression Left, ConstantExpression Right) : ConstantExpression { public override int Emit(SpirvContext context) { // Try constant folding first — avoids OpSpecConstantOp which some backends don't support. if (TryEvaluate(out var folded) && folded is not null) - return FromValue(folded).Emit(context); + return context.CompileConstant(ResultType, folded).Id; var leftId = Left.Emit(context); var rightId = Right.Emit(context); - var resultTypeId = GetEmittedTypeId(context, leftId); var resultId = context.Bound++; - Span instruction = [(int)Specification.Op.OpSpecConstantOp, resultTypeId, resultId, (int)Op, leftId, rightId]; + Span instruction = [(int)Specification.Op.OpSpecConstantOp, context.GetOrRegister(ResultType), resultId, (int)Op, leftId, rightId]; instruction[0] |= instruction.Length << 16; context.Add(new OpData(instruction)); return resultId; @@ -439,40 +394,9 @@ public sealed record BinaryOpExpr(Op Op, ConstantExpression Left, ConstantExpres public override bool TryEvaluate(out object? value) { value = null; - if (!Left.TryEvaluate(out var leftVal) || leftVal is null) - return false; - if (!Right.TryEvaluate(out var rightVal) || rightVal is null) - return false; - - value = Op switch - { - Specification.Op.OpIAdd when leftVal is int l && rightVal is int r => (object)(l + r), - Specification.Op.OpISub when leftVal is int l && rightVal is int r => l - r, - Specification.Op.OpIMul when leftVal is int l && rightVal is int r => l * r, - Specification.Op.OpSDiv when leftVal is int l && rightVal is int r => l / r, - Specification.Op.OpUDiv when leftVal is uint l && rightVal is uint r => l / r, - Specification.Op.OpFAdd when leftVal is float l && rightVal is float r => l + r, - Specification.Op.OpFSub when leftVal is float l && rightVal is float r => l - r, - Specification.Op.OpFMul when leftVal is float l && rightVal is float r => l * r, - Specification.Op.OpFDiv when leftVal is float l && rightVal is float r => l / r, - Specification.Op.OpBitwiseAnd when leftVal is int l && rightVal is int r => l & r, - Specification.Op.OpBitwiseOr when leftVal is int l && rightVal is int r => l | r, - Specification.Op.OpBitwiseXor when leftVal is int l && rightVal is int r => l ^ r, - Specification.Op.OpShiftLeftLogical when leftVal is int l && rightVal is int r => l << r, - Specification.Op.OpShiftRightArithmetic when leftVal is int l && rightVal is int r => l >> r, - Specification.Op.OpLogicalAnd when leftVal is bool l && rightVal is bool r => l && r, - Specification.Op.OpLogicalOr when leftVal is bool l && rightVal is bool r => l || r, - Specification.Op.OpLogicalEqual when leftVal is bool l && rightVal is bool r => l == r, - Specification.Op.OpLogicalNotEqual when leftVal is bool l && rightVal is bool r => l != r, - Specification.Op.OpIEqual when leftVal is int l && rightVal is int r => l == r, - Specification.Op.OpINotEqual when leftVal is int l && rightVal is int r => l != r, - Specification.Op.OpSLessThan when leftVal is int l && rightVal is int r => l < r, - Specification.Op.OpSGreaterThan when leftVal is int l && rightVal is int r => l > r, - Specification.Op.OpSLessThanEqual when leftVal is int l && rightVal is int r => l <= r, - Specification.Op.OpSGreaterThanEqual when leftVal is int l && rightVal is int r => l >= r, - _ => null, - }; - return value is not null; + return Left.TryEvaluate(out var leftValue) && leftValue is not null + && Right.TryEvaluate(out var rightValue) && rightValue is not null + && ConstantEvaluator.TryEvaluateBinary(Op, leftValue, rightValue, out value); } public override ConstantExpression Substitute(string declaringClass, ConstantExpression[] args) @@ -480,7 +404,7 @@ public sealed record BinaryOpExpr(Op Op, ConstantExpression Left, ConstantExpres var newLeft = Left.Substitute(declaringClass, args); var newRight = Right.Substitute(declaringClass, args); if (ReferenceEquals(newLeft, Left) && ReferenceEquals(newRight, Right)) return this; - var result = new BinaryOpExpr(Op, newLeft, newRight); + var result = new BinaryOpExpr(Op, ResultType, newLeft, newRight); return result.TryEvaluate(out var val) && val is not null ? FromValue(val) : result; } @@ -581,3 +505,132 @@ public sealed record CompositeConstExpr(SymbolType Type, ConstantExpression[] Co public override string ToString() => $"composite({Type}, [{string.Join(", ", (object[])Components)}])"; } +/// +/// Null constant (OpConstantNull), e.g. the `(StructType)0` zero-initialization. +/// +public sealed record NullConstExpr(SymbolType Type) : ConstantExpression +{ + public override int Emit(SpirvContext context) => context.CreateDefaultConstantComposite(Type).Id; + + public override bool TryEvaluate(out object? value) + { + value = null; + return false; + } + + public override string ToString() => $"null({Type})"; +} + +/// +/// Spec-constant operation on composites, whose last operands are literal indices: OpCompositeExtract (`v.x`), +/// OpVectorShuffle (`v.xy`) and OpCompositeInsert. +/// +public sealed record CompositeOpExpr : ConstantExpression +{ + public Op Op { get; } + public SymbolType ResultType { get; } + public ConstantExpression[] Operands { get; } + public int[] Literals { get; } + + private CompositeOpExpr(Op op, SymbolType resultType, ConstantExpression[] operands, int[] literals) + { + Op = op; + ResultType = resultType; + Operands = operands; + Literals = literals; + } + + /// + /// Creates the operation, or directly the expression it selects when its operands are composites with known components. + /// + public static ConstantExpression Create(Op op, SymbolType resultType, ConstantExpression[] operands, int[] literals) + { + switch (op) + { + case Specification.Op.OpCompositeExtract: + { + var current = operands[0]; + foreach (var index in literals) + { + if (!TryGetComponents(current, out var components) || index >= components.Length) + return new CompositeOpExpr(op, resultType, operands, literals); + current = components[index]; + } + return current; + } + case Specification.Op.OpVectorShuffle when TryGetComponents(operands[0], out var left) && TryGetComponents(operands[1], out var right): + { + var selected = new ConstantExpression[literals.Length]; + for (int i = 0; i < literals.Length; i++) + { + // Note: 0xFFFFFFFF selects an undefined component + if ((uint)literals[i] >= left.Length + right.Length) + return new CompositeOpExpr(op, resultType, operands, literals); + selected[i] = literals[i] < left.Length ? left[literals[i]] : right[literals[i] - left.Length]; + } + return new CompositeConstExpr(resultType, selected); + } + default: + return new CompositeOpExpr(op, resultType, operands, literals); + } + } + + // A vector can be constructed from other vectors, in which case its constituents are not its components + private static bool TryGetComponents(ConstantExpression expression, out ConstantExpression[] components) + { + components = expression is CompositeConstExpr composite ? composite.Components : []; + return expression is CompositeConstExpr { Type: var type } && (type is not VectorType vectorType || vectorType.Size == components.Length); + } + + public override int Emit(SpirvContext context) + { + Span operandIds = stackalloc int[Operands.Length]; + for (int i = 0; i < Operands.Length; i++) + operandIds[i] = Operands[i].Emit(context); + + var resultId = context.Bound++; + Span instruction = [(int)Specification.Op.OpSpecConstantOp, context.GetOrRegister(ResultType), resultId, (int)Op, .. operandIds, .. Literals]; + instruction[0] |= instruction.Length << 16; + context.Add(new OpData(instruction)); + return resultId; + } + + public override bool TryEvaluate(out object? value) + { + // Note: Create() already returned the selected expression when it could + value = null; + return false; + } + + public override ConstantExpression Substitute(string declaringClass, ConstantExpression[] args) + { + var newOperands = new ConstantExpression[Operands.Length]; + bool changed = false; + for (int i = 0; i < Operands.Length; i++) + { + newOperands[i] = Operands[i].Substitute(declaringClass, args); + if (!ReferenceEquals(newOperands[i], Operands[i])) + changed = true; + } + return changed ? Create(Op, ResultType, newOperands, Literals) : this; + } + + public bool Equals(CompositeOpExpr? other) => + other is not null && Op == other.Op && ResultType == other.ResultType + && Operands.AsSpan().SequenceEqual(other.Operands) && Literals.AsSpan().SequenceEqual(other.Literals); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Op); + hash.Add(ResultType); + foreach (var operand in Operands) + hash.Add(operand); + foreach (var literal in Literals) + hash.Add(literal); + return hash.ToHashCode(); + } + + public override string ToString() => $"{Op}({string.Join(", ", (object[])Operands)}; {string.Join(", ", Literals)})"; +} + diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs index 6b7bb2e60..18532d0d9 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs @@ -425,7 +425,7 @@ internal class IntrinsicImplementations : IntrinsicsDeclarations var float2Type = context.GetOrRegister(new VectorType(ScalarType.Float, 2)); var floatType = context.GetOrRegister(ScalarType.Float); var uintType = context.GetOrRegister(ScalarType.UInt); - var zero = context.AddConstant(0.0f); + var zero = context.CompileConstant(0.0f).Id; if (inputType is ScalarType) { diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs index e6cb5fbe0..2927f189b 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs @@ -377,7 +377,7 @@ public abstract partial class IdentifierBase(string name, TextLocation info) : L if (symbol.Id.Kind == SymbolKind.Shader) { if (constantOnly) - throw new NotImplementedException(); + throw new NotConstantExpressionException($"'{symbol.Id.Name}' is not a compile-time constant"); if (instance == null) instance = builder.Insert(new OpThisSDSL(context.Bound++)).ResultId; @@ -392,7 +392,7 @@ public abstract partial class IdentifierBase(string name, TextLocation info) : L else if (symbol.MemberAccessWithImplicitThis is { } thisType) { if (constantOnly) - throw new NotImplementedException(); + throw new NotConstantExpressionException($"'{symbol.Id.Name}' is not a compile-time constant"); instance ??= builder.Insert(new OpThisSDSL(context.Bound++)).ResultId; result.Id = builder.Insert(new OpMemberAccessSDSL(context.GetOrRegister(thisType), context.Bound++, instance.Value, result.Id)); @@ -400,7 +400,7 @@ public abstract partial class IdentifierBase(string name, TextLocation info) : L if (symbol.AccessChain is int accessChainIndex) { if (constantOnly) - throw new NotImplementedException(); + throw new NotConstantExpressionException($"'{symbol.Id.Name}' is not a compile-time constant"); var index = context.CompileConstant(accessChainIndex).Id; result.Id = builder.Insert(new OpAccessChain(resultType, context.Bound++, result.Id, [index])); @@ -806,9 +806,6 @@ public partial class TypeName(string name, TextLocation info) : Literal(info) else { var arrayComputedSize = -1; - if (arraySize is IntegerLiteral i) - arrayComputedSize = (int)i.Value; - var constantArraySize = arraySize.CompileConstantValue(table, context); var sizeExpr = ConstantExpression.ParseFromBuffer(constantArraySize.Id, context.GetBuffer(), context); if (sizeExpr.TryEvaluate(out var value) && value is IConvertible) diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/ShaderElements.MethodOrMember.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/ShaderElements.MethodOrMember.cs index df1c7222d..15c113ff0 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/ShaderElements.MethodOrMember.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/ShaderElements.MethodOrMember.cs @@ -509,6 +509,15 @@ public partial class ShaderMethod( { var (builder, context) = compiler; + // Note: these values end up as literals of OpExecutionMode + int EvaluateAttributeParameter(AnyShaderAttribute attribute, int index) + { + var parameter = attribute.Parameters[index]; + if (!parameter.TryEvaluateConstantInteger(table, context, out var value)) + table.AddError(new(parameter.Info, $"[{attribute.Name}] parameter must be a constant integer expression")); + return value; + } + if (Attributes != null) { Span attrParamBuffer = stackalloc int[8]; // max attribute parameters @@ -527,32 +536,18 @@ public partial class ShaderMethod( { var parameters = attrParamBuffer[..anyAttribute.Parameters.Count]; for (var index = 0; index < anyAttribute.Parameters.Count; index++) - { - var compiled = anyAttribute.Parameters[index].CompileConstantValue(table, context); - var expr = ConstantExpression.ParseFromBuffer(compiled.Id, context.GetBuffer(), context); - if (!expr.TryEvaluate(out var value) || value is null) - throw new InvalidOperationException(); - parameters[index] = Convert.ToInt32(value); - } + parameters[index] = EvaluateAttributeParameter(anyAttribute, index); context.Add(new OpExecutionMode(function.Id, Specification.ExecutionMode.LocalSize, new(parameters))); } } else if (anyAttribute.Name == "maxvertexcount") { - var compiled = anyAttribute.Parameters[0].CompileConstantValue(table, context); - var expr = ConstantExpression.ParseFromBuffer(compiled.Id, context.GetBuffer(), context); - if (!expr.TryEvaluate(out var value) || value is null) - throw new InvalidOperationException(); - context.Add(new OpExecutionMode(function.Id, Specification.ExecutionMode.OutputVertices, new(Convert.ToInt32(value)))); + context.Add(new OpExecutionMode(function.Id, Specification.ExecutionMode.OutputVertices, new(EvaluateAttributeParameter(anyAttribute, 0)))); } else if (anyAttribute.Name == "outputcontrolpoints") { - var compiled = anyAttribute.Parameters[0].CompileConstantValue(table, context); - var expr = ConstantExpression.ParseFromBuffer(compiled.Id, context.GetBuffer(), context); - if (!expr.TryEvaluate(out var value) || value is null) - throw new InvalidOperationException(); - context.Add(new OpExecutionMode(function.Id, Specification.ExecutionMode.OutputVertices, new(Convert.ToInt32(value)))); + context.Add(new OpExecutionMode(function.Id, Specification.ExecutionMode.OutputVertices, new(EvaluateAttributeParameter(anyAttribute, 0)))); } else if (anyAttribute.Name == "patchconstantfunc") { diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Statements.Control.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Statements.Control.cs index 5a6d1d71b..ae08374dc 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Statements.Control.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Statements.Control.cs @@ -176,10 +176,10 @@ public partial class SwitchStatement(Expression selector, TextLocation info) : F { var (builder, context) = compiler; - // Compile selector (must be integer scalar) + // Compile selector (must be a 32-bit integer scalar: the case labels are written as 32-bit literals) var selectorValue = Selector.CompileAsValue(table, compiler); - if (Selector.ValueType is not ScalarType st || !st.IsInteger()) - table.AddError(new(Selector.Info, "switch selector must evaluate to an integer scalar")); + if (Selector.ValueType is not ScalarType { Type: Scalar.Int or Scalar.UInt }) + table.AddError(new(Selector.Info, "switch selector must evaluate to an int or uint scalar")); // Pre-allocate block IDs: one per section + merge block var mergeBlock = context.Bound++; @@ -191,26 +191,39 @@ public partial class SwitchStatement(Expression selector, TextLocation info) : F var previousEscapeBlocks = builder.CurrentEscapeBlocks; builder.CurrentEscapeBlocks = new SpirvBuilder.EscapeBlocks(mergeBlock, mergeBlock); - // Build (literal, blockId) pairs and find default block + // Build the labels and find default block int defaultBlockId = mergeBlock; - var casePairs = new List<(int, int)>(); + var labels = new List<(int ConstantId, int? Value, int BlockId)>(); for (int i = 0; i < Sections.Count; i++) { foreach (var label in Sections[i].Labels) { if (label is DefaultLabel) defaultBlockId = sectionBlockIds[i]; - else if (label is CaseLabel caseLabel && caseLabel.Value is IntegerLiteral intLit) - casePairs.Add(((int)intLit.Value, sectionBlockIds[i])); + else if (label is not CaseLabel caseLabel || !caseLabel.Value.TryCompileConstantInteger(table, context, out var caseConstant, out var caseValue)) + table.AddError(new(label.Info, "case label must be a constant integer expression")); + // Two constants with different names can have the same value + else if (caseValue != null && labels.Exists(x => x.Value == caseValue)) + table.AddError(new(label.Info, $"case label has the same value ({caseValue}) as a previous label of this switch")); else - table.AddError(new(label.Info, "case label must be an integer literal")); + labels.Add((caseConstant.Id, caseValue, sectionBlockIds[i])); } } // Emit selection merge + switch builder.Insert(new OpSelectionMerge(mergeBlock, Specification.SelectionControlMask.None)); - Span<(int, int)> pairsSpan = casePairs.ToArray(); - builder.Insert(new OpSwitch(selectorValue.Id, defaultBlockId, new LiteralArray<(int, int)>(pairsSpan))); + // OpSwitch takes the value of its labels. A label that depends on a generic of the shader has none yet: + // OpSwitchIdSDSL takes constants, and becomes an OpSwitch once they are all resolved. + if (labels.TrueForAll(x => x.Value != null)) + { + Span<(int, int)> targets = labels.ConvertAll(x => (x.Value!.Value, x.BlockId)).ToArray(); + builder.Insert(new OpSwitch(selectorValue.Id, defaultBlockId, new LiteralArray<(int, int)>(targets))); + } + else + { + Span<(int, int)> targets = labels.ConvertAll(x => (x.ConstantId, x.BlockId)).ToArray(); + builder.Insert(new OpSwitchIdSDSL(selectorValue.Id, defaultBlockId, new LiteralArray<(int, int)>(targets))); + } // Compile each section's block for (int i = 0; i < Sections.Count; i++) diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/TextureMethodsImplementations.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/TextureMethodsImplementations.cs index 73eb973f3..9c9e15b17 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/TextureMethodsImplementations.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/TextureMethodsImplementations.cs @@ -465,18 +465,14 @@ internal class TextureMethodsImplementations : TextureMethodsDeclarations var componentConstant = context.CompileConstant(component); var resultTypeId = context.GetOrRegister(functionType.ReturnType); - // Try to promote all 4 offsets to constants (handles inline int2(x,y) constructors) - var co1 = TryPromoteToConstant(context, builder, o1.Id); - var co2 = TryPromoteToConstant(context, builder, o2.Id); - var co3 = TryPromoteToConstant(context, builder, o3.Id); - var co4 = TryPromoteToConstant(context, builder, o4.Id); - if (co1 >= 0 && co2 >= 0 && co3 >= 0 && co4 >= 0) + // Try to turn all 4 offsets into constants (handles inline constant expressions such as int2(x,y) constructors) + if (TryHoistOffsetsAsConstants(context, builder, o1, o2, o3, o4)) { var sampledImage = builder.Insert(new OpSampledImage(typeSampledImage, context.Bound++, texture.Id, s.Id)); var int2Type = new VectorType(ScalarType.Int, 2); var arrayType = context.GetOrRegister(new ArrayType(int2Type, 4)); var constOffsetsId = context.Bound++; - context.AddData(new OpConstantComposite(arrayType, constOffsetsId, [co1, co2, co3, co4])); + context.AddData(new OpConstantComposite(arrayType, constOffsetsId, [o1.Id, o2.Id, o3.Id, o4.Id])); Span operands = [constOffsetsId]; var gather = builder.Insert(new OpImageGather(resultTypeId, context.Bound++, sampledImage.ResultId, x.Id, componentConstant.Id, ImageOperandsMask.ConstOffsets, new EnumerantParameters(operands))); @@ -517,18 +513,14 @@ internal class TextureMethodsImplementations : TextureMethodsDeclarations var typeSampledImage = context.GetOrRegister(new SampledImage(textureType)); var resultTypeId = context.GetOrRegister(functionType.ReturnType); - // Try to promote all 4 offsets to constants (handles inline int2(x,y) constructors) - var co1 = TryPromoteToConstant(context, builder, o1.Id); - var co2 = TryPromoteToConstant(context, builder, o2.Id); - var co3 = TryPromoteToConstant(context, builder, o3.Id); - var co4 = TryPromoteToConstant(context, builder, o4.Id); - if (co1 >= 0 && co2 >= 0 && co3 >= 0 && co4 >= 0) + // Try to turn all 4 offsets into constants (handles inline constant expressions such as int2(x,y) constructors) + if (TryHoistOffsetsAsConstants(context, builder, o1, o2, o3, o4)) { var sampledImage = builder.Insert(new OpSampledImage(typeSampledImage, context.Bound++, texture.Id, s.Id)); var int2Type = new VectorType(ScalarType.Int, 2); var arrayType = context.GetOrRegister(new ArrayType(int2Type, 4)); var constOffsetsId = context.Bound++; - context.AddData(new OpConstantComposite(arrayType, constOffsetsId, [co1, co2, co3, co4])); + context.AddData(new OpConstantComposite(arrayType, constOffsetsId, [o1.Id, o2.Id, o3.Id, o4.Id])); Span operands = [constOffsetsId]; var gather = builder.Insert(new OpImageDrefGather(resultTypeId, context.Bound++, sampledImage.ResultId, x.Id, compareValue.Id, ImageOperandsMask.ConstOffsets, new EnumerantParameters(operands))); @@ -550,55 +542,16 @@ internal class TextureMethodsImplementations : TextureMethodsDeclarations return new(result.ResultId, result.ResultType); } - /// - /// Returns true if the given ID refers to a constant instruction (OpConstant, OpConstantComposite, etc.) in context. - /// - private static bool IsConstantInContext(SpirvContext context, int id) + // ConstOffsets takes the 4 offsets as one constant, so they all have to be constants + private static bool TryHoistOffsetsAsConstants(SpirvContext context, SpirvBuilder builder, params ReadOnlySpan offsets) { - if (!context.GetBuffer().TryGetInstructionById(id, out var inst)) - return false; - return inst.Op is Op.OpConstant or Op.OpConstantTrue or Op.OpConstantFalse - or Op.OpConstantComposite or Op.OpConstantNull - or Op.OpSpecConstantComposite; - } - - /// - /// Tries to promote a value to a constant in context for use as ConstOffset. - /// Handles the simple case: OpCompositeConstruct in builder where all constituents are already constants in context. - /// Also handles scalar constants already in context. - /// Returns the constant ID if successful, or -1 if the value cannot be promoted. - /// - private static int TryPromoteToConstant(SpirvContext context, SpirvBuilder builder, int id) - { - // Already a constant in context - if (IsConstantInContext(context, id)) - return id; - - var buf = builder.GetBuffer(); - if (!buf.TryGetInstructionById(id, out var inst)) - return -1; - - // OpCompositeConstruct with all-constant constituents - if (inst.Op == Op.OpCompositeConstruct) + foreach (var offset in offsets) { - var cspan = inst.Data.Memory.Span; - Span constituents = stackalloc int[cspan.Length - 3]; - for (int j = 3; j < cspan.Length; j++) - { - var promoted = TryPromoteToConstant(context, builder, cspan[j]); - if (promoted < 0) - return -1; - constituents[j - 3] = promoted; - } - - // All constituents are constants — emit OpConstantComposite in context - var cResultType = cspan[1]; - var cConstId = context.Bound++; - context.AddData(new OpConstantComposite(cResultType, cConstId, new(constituents))); - return cConstId; + if (!ExpressionExtensions.TryHoistAsConstant(context, builder.GetBuffer(), offset.Id)) + return false; } - return -1; + return true; } private static void StoreQueryComponent(SpirvContext context, SpirvBuilder builder, int uintTypeId, int sizeResultId, int sizeComponents, int componentIndex, SpirvValue outParam) @@ -660,12 +613,11 @@ internal class TextureMethodsImplementations : TextureMethodsDeclarations } if (offset != null) { - // Try to promote to constant (handles int2(1,3) style inline constructors) - var constId = TryPromoteToConstant(context, builder, offset.Value.Id); - if (constId >= 0) + // Turn it into a constant (handles inline constant expressions such as int2(1, Step * 2)) + if (ExpressionExtensions.TryHoistAsConstant(context, builder.GetBuffer(), offset.Value.Id)) { imask |= ImageOperandsMask.ConstOffset; - operands[operandCount++] = constId; + operands[operandCount++] = offset.Value.Id; } else { diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Class.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Class.cs index d5b70e707..ca239dc82 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Class.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Class.cs @@ -484,14 +484,7 @@ public partial class SpirvBuilder { var expr = ConstantExpression.ParseFromBuffer(typeArray.Length, shaderBuffers.Context.GetBuffer(), shaderBuffers.Context); if (expr.TryEvaluate(out var value) && value != null) - { - var resultType = lengthInstruction.Data.IdResultType!.Value; - var resultId = lengthInstruction.Data.IdResult!.Value; - if (value is int or long) - shaderBuffers.Context.Replace(lengthInstruction.Index, new OpConstant(resultType, resultId, System.Convert.ToInt32(value))); - else if (value is float or double) - shaderBuffers.Context.Replace(lengthInstruction.Index, new OpConstant(resultType, resultId, System.Convert.ToSingle(value))); - } + shaderBuffers.Context.Replace(lengthInstruction.Index, SpirvContext.CreateConstantInstruction(lengthInstruction.Data.IdResultType!.Value, lengthInstruction.Data.IdResult!.Value, value)); } } } diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Flow.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Flow.cs index c9143a8b2..c886dc3ca 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Flow.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Flow.cs @@ -46,6 +46,7 @@ public partial class SpirvBuilder case Op.OpBranch: case Op.OpBranchConditional: case Op.OpSwitch: + case Op.OpSwitchIdSDSL: return true; default: return false; diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.cs index b1688bedd..9e94e61a9 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.cs @@ -62,6 +62,7 @@ public partial class SpirvBuilder() (int)Op.OpBranch, (int)Op.OpBranchConditional, (int)Op.OpSwitch, + (int)Op.OpSwitchIdSDSL, (int)Op.OpReturn, (int)Op.OpReturnValue, (int)Op.OpKill, diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs index 85ec322de..bce2b31d5 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Numerics; using Stride.Shaders.Core; using Stride.Shaders.Parsing; using Stride.Shaders.Parsing.SDSL.AST; @@ -13,25 +12,6 @@ public partial class SpirvContext { public Dictionary<(SymbolType Type, object Value), SpirvValue> LiteralConstants { get; } = []; - public int AddConstant(TScalar value) - where TScalar : INumber - { - var data = value switch - { - uint v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.UInt), Bound++, v)), - int v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.Int), Bound++, v)), - ulong v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.UInt64), Bound++, v)), - long v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.Int64), Bound++, v)), - Half v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.Half), Bound++, v)), - float v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.Float), Bound++, v)), - double v => Buffer.AddData(new OpConstant(GetOrRegister(ScalarType.Double), Bound++, v)), - _ => throw new NotImplementedException() - }; - if (InstructionInfo.GetInfo(data).GetResultIndex(out var index)) - return data.Memory.Span[index + 1]; - throw new Exception("Constant has no result id"); - } - public object GetConstantValue(int constantId) { if (Buffer.TryGetInstructionById(constantId, out var constant)) @@ -83,96 +63,37 @@ public partial class SpirvContext if (i.Op == Specification.Op.OpSpecConstantOp) { - var resultType = i.Data.Memory.Span[1]; - var resultId = i.Data.Memory.Span[2]; - var op = (Specification.Op)i.Data.Memory.Span[3]; - switch (op) + // Layout: [header, resultType, resultId, opcode, operands...] + var span = i.Data.Memory.Span; + var op = (Specification.Op)span[3]; + typeId = span[1]; + + // Component of a constant vector (e.g. `v.x`) + if (op == Specification.Op.OpCompositeExtract) { - // Conversions - case Specification.Op.OpConvertFToS: - case Specification.Op.OpConvertFToU: - case Specification.Op.OpConvertSToF: - case Specification.Op.OpConvertUToF: - if (!TryGetConstantValue(i.Data.Memory.Span[4], out var convertOperand, out var convertOperandTypeId)) - return false; - value = op switch - { - // Note: first cast to object is important, otherwise int/float will be cast as float - Specification.Op.OpConvertFToS => (object)(int)(float)convertOperand, - Specification.Op.OpConvertFToU => (uint)(float)convertOperand, - Specification.Op.OpConvertSToF => (float)(int)convertOperand, - Specification.Op.OpConvertUToF => (float)(uint)convertOperand, - _ => throw new NotSupportedException($"Unsupported conversion op: {op}"), - }; - break; - // Bitcast: reinterpret the operand's bits as the target scalar type. - case Specification.Op.OpBitcast: - if (!TryGetConstantValue(i.Data.Memory.Span[4], out var bitcastOperand, out _)) - return false; - if (ReverseTypes[resultType] is not ScalarType { Type: var bitcastTargetScalar }) - throw new NotSupportedException($"OpBitcast result type {ReverseTypes[resultType]} is not a scalar"); - var bitcastBits = bitcastOperand switch - { - int v => (uint)v, - uint v => v, - float v => BitConverter.SingleToUInt32Bits(v), - _ => throw new NotSupportedException($"OpBitcast operand type {bitcastOperand.GetType()} is not supported"), - }; - value = bitcastTargetScalar switch - { - Scalar.Int => (object)(int)bitcastBits, - Scalar.UInt => bitcastBits, - Scalar.Float => BitConverter.UInt32BitsToSingle(bitcastBits), - _ => throw new NotSupportedException($"OpBitcast target scalar {bitcastTargetScalar} is not supported"), - }; - break; - // Unary operations - case Specification.Op.OpSNegate: - case Specification.Op.OpFNegate: - if (!TryGetConstantValue(i.Data.Memory.Span[4], out var unaryOperand, out var unaryOperandTypeId)) - return false; - if (unaryOperandTypeId != resultType) - return false; - value = op switch - { - // Note: first cast to object is important, otherwise int/float will be cast as float - Specification.Op.OpSNegate => (object)(-(int)unaryOperand), - Specification.Op.OpFNegate => -(float)unaryOperand, - _ => throw new NotSupportedException($"Unsupported unary op: {op}"), - }; - break; - // Binary operations - case Specification.Op.OpIAdd: - case Specification.Op.OpISub: - case Specification.Op.OpIMul: - case Specification.Op.OpFAdd: - case Specification.Op.OpFSub: - case Specification.Op.OpFMul: - case Specification.Op.OpFDiv: - if (!TryGetConstantValue(i.Data.Memory.Span[4], out var left, out var leftTypeId)) - return false; - if (!TryGetConstantValue(i.Data.Memory.Span[5], out var right, out var rightTypeId)) - return false; - if (leftTypeId != resultType || rightTypeId != resultType) - return false; - value = op switch - { - // Note: first cast to object is important, otherwise int/float will be cast as float - Specification.Op.OpIAdd => (object)((int)left + (int)right), - Specification.Op.OpISub => (int)left - (int)right, - Specification.Op.OpIMul => (int)left * (int)right, - Specification.Op.OpFAdd => (float)left + (float)right, - Specification.Op.OpFSub => (float)left - (float)right, - Specification.Op.OpFMul => (float)left * (float)right, - Specification.Op.OpFDiv => (float)left / (float)right, - _ => throw new NotSupportedException($"Unsupported binary op: {op}"), - }; - break; - default: - throw new NotImplementedException(); + if (span.Length != 6 || !TryGetConstantValue(span[4], out var composite, out _) + || composite is not ConstantVector vector || (uint)span[5] >= vector.Values.Length) + return false; + value = vector.Values[span[5]]; + return true; } - return true; + // Note: the operation decides how many operands are ids, the length of the instruction does not + switch (ConstantEvaluator.GetEvaluatedOperandCount(op)) + { + case 1: + return TryGetConstantValue(span[4], out var unaryOperand, out _) + && ConstantEvaluator.TryEvaluateUnary(op, unaryOperand, ReverseTypes[typeId], out value); + case 2: + return TryGetConstantValue(span[4], out var left, out _) + && TryGetConstantValue(span[5], out var right, out _) + && ConstantEvaluator.TryEvaluateBinary(op, left, right, out value); + case 3: + return TryGetConstantValue(span[4], out var condition, out _) && condition is bool conditionValue + && TryGetConstantValue(conditionValue ? span[5] : span[6], out value, out _); + default: + return false; + } } if ((i.Op == Specification.Op.OpConstantComposite || i.Op == Specification.Op.OpSpecConstantComposite) && @@ -243,6 +164,69 @@ public partial class SpirvContext throw new Exception("Cannot find type instruction for id " + typeId); } + /// + /// Adds the constant holding a value, scalar or vector, as returned by . + /// + public void AddConstant(int resultType, int resultId, object value) + { + foreach (var instruction in CreateConstantInstructions(resultType, resultId, value)) + Add(instruction); + } + + /// + /// Replaces the instruction defining a constant by the value it was computed to. A vector needs the constants of its components, + /// which are inserted before it. + /// + /// The number of instructions inserted before the replaced one. + public int ReplaceByConstant(int index, int resultType, int resultId, object value) + { + var instructions = CreateConstantInstructions(resultType, resultId, value); + for (var i = 0; i < instructions.Count - 1; i++) + Insert(index + i, instructions[i]); + Replace(index + instructions.Count - 1, instructions[^1]); + return instructions.Count - 1; + } + + // The constant itself comes last: a constant is defined before it is used + private List CreateConstantInstructions(int resultType, int resultId, object value) + { + if (value is not ConstantVector vector) + return [CreateConstantInstruction(resultType, resultId, value)]; + + // Note: the components are not shared with other constants, which could be defined after this one + var componentType = GetOrRegister(((VectorType)ReverseTypes[resultType]).BaseType); + var instructions = new List(); + Span componentIds = stackalloc int[vector.Values.Length]; + for (var i = 0; i < componentIds.Length; i++) + { + componentIds[i] = Bound++; + instructions.Add(CreateConstantInstruction(componentType, componentIds[i], vector.Values[i])); + } + + instructions.Add(new OpData(new OpConstantComposite(resultType, resultId, new(componentIds)).InstructionMemory)); + return instructions; + } + + /// + /// Creates the constant instruction holding a scalar value, as returned by . + /// + public static OpData CreateConstantInstruction(int resultType, int resultId, object value) + { + return value switch + { + true => new OpData(new OpConstantTrue(resultType, resultId).InstructionMemory), + false => new OpData(new OpConstantFalse(resultType, resultId).InstructionMemory), + int v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + uint v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + long v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + ulong v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + Half v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + float v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + double v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), + _ => throw new NotSupportedException($"Cannot create a constant of type {value.GetType()}"), + }; + } + public SpirvValue CreateDefaultConstantComposite(SymbolType type) { // TODO: cache results (either here or even more generally for any composite constant even if non-zero) @@ -268,45 +252,42 @@ public partial class SpirvContext return new(Buffer.AddData(new OpConstantComposite(GetOrRegister(type), Bound++, new(values)))); } - public Literal CreateLiteral(object value, TextLocation location = default) + /// + /// Gets the constant of a scalar value (bool, int, uint, long, ulong, half, float or double), which has the type of the value. + /// + public SpirvValue CompileConstant(object value) { - return value switch + var type = value switch { - bool i => new BoolLiteral(i, location), - sbyte i => new IntegerLiteral(new(8, false, true), i, location), - byte i => new IntegerLiteral(new(8, false, false), i, location), - short i => new IntegerLiteral(new(16, false, true), i, location), - ushort i => new IntegerLiteral(new(16, false, false), i, location), - int i => new IntegerLiteral(new(32, false, true), i, location), - uint i => new IntegerLiteral(new(32, false, false), i, location), - long i => new IntegerLiteral(new(64, false, true), i, location), - ulong i => new IntegerLiteral(new(64, false, false), (long)i, location), - float i => new FloatLiteral(new(32, true, true), i, location), - double i => new FloatLiteral(new(64, true, true), i, location), - _ => throw new NotSupportedException($"Unsupported literal type: {value.GetType()}"), + bool => ScalarType.Boolean, + int => ScalarType.Int, + uint => ScalarType.UInt, + long => ScalarType.Int64, + ulong => ScalarType.UInt64, + Half => ScalarType.Half, + float => ScalarType.Float, + double => ScalarType.Double, + _ => throw new NotSupportedException($"Unsupported constant type: {value.GetType()}"), }; - } - - public SpirvValue CompileConstant(object value, TextLocation location = default) - { - return CompileConstantLiteral(CreateLiteral(value, location)); + return CompileConstant(type, value); } public SpirvValue CompileConstantLiteral(Literal literal) { + // Note: first cast to object is important, otherwise all the cases are converted to a common type (a float to a double) object literalValue = literal switch { BoolLiteral lit => lit.Value, IntegerLiteral lit => lit.Suffix switch { - { Size: > 32, Signed: false } => lit.ULongValue, + { Size: > 32, Signed: false } => (object)lit.ULongValue, { Size: > 32, Signed: true } => lit.LongValue, { Signed: false } => lit.UIntValue, _ => lit.IntValue, }, FloatLiteral lit => lit.Suffix.Size switch { - > 32 => lit.DoubleValue, + > 32 => (object)lit.DoubleValue, _ => (float)lit.DoubleValue, }, _ => throw new NotImplementedException() @@ -314,42 +295,20 @@ public partial class SpirvContext literal.Type ??= ComputeLiteralType(literal); - if (LiteralConstants.TryGetValue((literal.Type, literalValue), out var result)) + return CompileConstant(literal.Type, literalValue); + } + + /// + /// Gets the constant of a scalar value with a given type, shared by all the uses of this value with this type. + /// + public SpirvValue CompileConstant(SymbolType type, object value) + { + if (LiteralConstants.TryGetValue((type, value), out var result)) return result; - var instruction = literal switch - { - BoolLiteral { Value: true } lit => Buffer.AddData(new OpConstantTrue(GetOrRegister(lit.Type), Bound++)), - BoolLiteral { Value: false } lit => Buffer.AddData(new OpConstantFalse(GetOrRegister(lit.Type), Bound++)), - IntegerLiteral lit => lit.Suffix switch - { - { Size: <= 8, Signed: false } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, (byte)lit.IntValue)), - { Size: <= 8, Signed: true } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, (sbyte)lit.IntValue)), - { Size: <= 16, Signed: false } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, (ushort)lit.IntValue)), - { Size: <= 16, Signed: true } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, (short)lit.IntValue)), - { Size: <= 32, Signed: false } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, lit.UIntValue)), - { Size: <= 32, Signed: true } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, lit.IntValue)), - { Size: <= 64, Signed: false } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, lit.ULongValue)), - { Size: <= 64, Signed: true } => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, lit.LongValue)), - _ => throw new NotImplementedException() - }, - FloatLiteral lit => lit.Suffix.Size switch - { - > 32 => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, lit.DoubleValue)), - _ => Buffer.AddData(new OpConstant(GetOrRegister(lit.Type), Bound++, (float)lit.DoubleValue)), - }, - _ => throw new NotImplementedException() - }; - - result = new(instruction); - LiteralConstants.Add((literal.Type, literalValue), result); - AddName(result.Id, literal switch - { - BoolLiteral lit => $"{lit.Type}_{lit.Value}", - IntegerLiteral lit => $"{lit.Type}_{lit.Value}", - FloatLiteral lit => $"{lit.Type}_{lit.Value.ToString(CultureInfo.InvariantCulture)}", - _ => throw new NotImplementedException() - }); + result = new(Buffer.Add(CreateConstantInstruction(GetOrRegister(type), Bound++, value)).Data); + LiteralConstants.Add((type, value), result); + AddName(result.Id, $"{type}_{Convert.ToString(value, CultureInfo.InvariantCulture)}"); return result; } diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.cs index 44086fba3..3c6de0e4d 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.cs @@ -303,6 +303,9 @@ public partial class SpirvContext public OpData Replace(int index, in T instruction) where T : struct, IMemoryInstruction, allows ref struct { ThrowIfFrozen(); return Buffer.Replace(index, instruction); } + public OpData Replace(int index, OpData data) + { ThrowIfFrozen(); return Buffer.Replace(index, data); } + public SpirvContext FluentAdd(in T value, out T result) where T : struct, IMemoryInstruction, allows ref struct { diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/ExpressionExtensions.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/ExpressionExtensions.cs index 4b64f98d6..55acfe697 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/ExpressionExtensions.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/ExpressionExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; using CommunityToolkit.HighPerformance; using Stride.Shaders.Core; using Stride.Shaders.Parsing.Analysis; @@ -10,7 +12,10 @@ namespace Stride.Shaders.Spirv.Building; public static class ExpressionExtensions { - public static HashSet ShaderSpecConstantOpSupportedOps = new() + /// + /// Operations that OpSpecConstantOp accepts in a shader module (SPIR-V accepts more with the Kernel capability). + /// + public static readonly FrozenSet ShaderSpecConstantOpSupportedOps = new HashSet { Op.OpSConvert, Op.OpUConvert, @@ -50,109 +55,173 @@ public static class ExpressionExtensions Op.OpSLessThanEqual, Op.OpUGreaterThanEqual, Op.OpSGreaterThanEqual, - }; + }.ToFrozenSet(); - public static HashSet KernelSpecConstantOpSupportedOps = new() + /// + /// Same as , for an expression that the shader code does not guarantee to be a constant. + /// + public static bool TryCompileConstantValue(this Expression expression, SymbolTable table, SpirvContext context, out SpirvValue result, SymbolType? expectedType = null) { - // Note: those are not supported in shaders - // but we'll make sure to simplify them once they can be resolved. - // They are needed for more complex constants. - Op.OpConvertFToS, - Op.OpConvertFToU, - Op.OpConvertSToF, - Op.OpConvertUToF, - Op.OpUConvert, - Op.OpConvertPtrToU, - Op.OpConvertUToPtr, - Op.OpGenericCastToPtr, - Op.OpPtrCastToGeneric, - Op.OpBitcast, - Op.OpFNegate, - Op.OpFAdd, - Op.OpFSub, - Op.OpFMul, - Op.OpFDiv, - Op.OpFRem, - Op.OpFMod, - Op.OpAccessChain, - Op.OpInBoundsAccessChain, - Op.OpPtrAccessChain, - Op.OpInBoundsPtrAccessChain, - }; + try + { + result = expression.CompileConstantValue(table, context, expectedType); + return true; + } + catch (NotConstantExpressionException) + { + result = default; + return false; + } + } + /// + /// Evaluates an int or uint expression whose value must be known when compiling, because it ends up as a literal + /// (e.g. a switch case label or a [numthreads] parameter). It can't depend on a generic that is not resolved yet. + /// + public static bool TryEvaluateConstantInteger(this Expression expression, SymbolTable table, SpirvContext context, out int value) + { + var result = expression.TryCompileConstantInteger(table, context, out _, out var evaluated) && evaluated != null; + value = evaluated ?? 0; + return result; + } + + /// + /// Compiles an int or uint constant expression. Its value is null when it is not known yet, which is when it depends + /// on a generic that is not resolved. + /// + public static bool TryCompileConstantInteger(this Expression expression, SymbolTable table, SpirvContext context, out SpirvValue constant, out int? value) + { + value = null; + if (!expression.TryCompileConstantValue(table, context, out constant) + || context.ReverseTypes[constant.TypeId] is not ScalarType { Type: Scalar.Int or Scalar.UInt }) + return false; + + if (context.TryGetConstantValue(constant.Id, out var evaluated, out _)) + value = evaluated is uint u ? unchecked((int)u) : (int)evaluated; + return true; + } + + /// + /// Compiles an expression as a constant of the context. + /// + /// The expression is not a compile-time constant. public static SpirvValue CompileConstantValue(this Expression expression, SymbolTable table, SpirvContext context, SymbolType? expectedType = null) { var compiler = new CompilerUnit(context, new()); - expression.ProcessSymbol(table, expectedType); - var result = expression.CompileAsValue(table, compiler, expectedType); - - if (expectedType != null) - compiler.Builder.Convert(context, result, expectedType); - var buffer = compiler.Builder.GetBuffer(); - - // Process each instruction and check if it can be converted to constant version. - // When all operands are known OpConstant values, fold at compile time to avoid - // OpSpecConstantOp which some SPIR-V backends (e.g. SPIRV-Cross) don't fully support. - for (int index = 0; index < buffer.Count; ++index) + try { - var i = buffer[index]; + expression.ProcessSymbol(table, expectedType); + var result = expression.CompileAsValue(table, compiler, expectedType); - if (i.Op == Op.OpCompositeConstruct) + if (expectedType != null) + result = compiler.Builder.Convert(context, result, expectedType); + + // Every instruction of the expression becomes a constant of the context + for (int index = 0; index < buffer.Count; ++index) { - // Check if all constituents are plain OpConstant — if so, use OpConstantComposite instead of OpSpecConstantComposite. - var span = i.Data.Memory.Span; - bool allConstant = true; - for (int j = 3; j < span.Length; j++) - { - if (!IsPlainConstant(context, span[j])) - { - allConstant = false; - break; - } - } + var i = buffer[index]; + if (GetConstantIdOperandCount(i) < 0) + throw new NotConstantExpressionException($"'{expression}' is not a compile-time constant ({i.Op} can't be part of one)"); - if (allConstant) - { - i.Data.Memory.Span[0] = (int)Op.OpConstantComposite | (i.Data.Memory.Length << 16); - } - else - { - i.Data.Memory.Span[0] = (int)Op.OpSpecConstantComposite | (i.Data.Memory.Length << 16); - } + result = AddAsConstant(context, i); + } - var instruction = context.Add(new(i.Data.Memory.Span)); - result = new(instruction.Data); - } - // Rewrite using OpSpecConstantOp when possible - else if (ShaderSpecConstantOpSupportedOps.Contains(i.Op) || KernelSpecConstantOpSupportedOps.Contains(i.Op)) - { - var resultType = i.Data.Memory.Span[1]; - var resultId = i.Data.Memory.Span[2]; + return result; + } + finally + { + buffer.Dispose(); + } + } - // Try to fold: if all operands are known constants, compute the result at compile time. - if (TryFoldConstantOp(context, i, out var foldedInstruction)) - { - context.Add(foldedInstruction); - result = new(resultId, resultType); - } - else - { - Span instruction = [(int)Op.OpSpecConstantOp, resultType, resultId, (int)i.Op, .. i.Data.Memory.Span[3..]]; - instruction[0] |= instruction.Length << 16; - context.Add(new OpData(instruction)); - result = new(resultId, resultType); - } - } - else - { - throw new InvalidOperationException($"OpCode {i.Op} not supported when compiling constant {expression}"); - } + /// + /// Turns a value computed in a function into a constant of the context, when the instructions computing it can all be + /// part of a constant (e.g. the `int2(Step * 2, 1)` of a texture offset). They move out of the function and keep their ids. + /// + public static bool TryHoistAsConstant(SpirvContext context, SpirvBuffer buffer, int id) + { + if (IsConstant(context, id)) + return true; + + if (!buffer.TryGetInstructionById(id, out var i)) + return false; + var idOperandCount = GetConstantIdOperandCount(i); + if (idOperandCount < 0) + return false; + + // Operands first: a constant is defined before it is used + for (var index = 0; index < idOperandCount; index++) + { + if (!TryHoistAsConstant(context, buffer, i.Data.Memory.Span[3 + index])) + return false; } - buffer.Dispose(); + AddAsConstant(context, i); + SpirvBuilder.SetOpNop(i.Data.Memory.Span); + return true; + } - return result; + /// + /// Returns true if the given ID refers to a constant of the context (which might not be resolved yet). + /// + public static bool IsConstant(SpirvContext context, int id) + { + return context.GetBuffer().TryGetInstructionById(id, out var inst) + && inst.Op is Op.OpConstant or Op.OpConstantTrue or Op.OpConstantFalse or Op.OpConstantComposite or Op.OpConstantNull + or Op.OpSpecConstant or Op.OpSpecConstantTrue or Op.OpSpecConstantFalse or Op.OpSpecConstantComposite or Op.OpSpecConstantOp + or Op.OpGenericParameterSDSL or Op.OpGenericReferenceSDSL; + } + + /// + /// Number of leading operands of an instruction that are ids (the others are literals), or -1 if it can't be part of a constant. + /// + /// + /// An operation that SPIR-V does not allow in a constant of a shader module (e.g. OpBitcast, OpFMul) is needed for more complex + /// constants: the mixer simplifies it once it can be resolved, so it has to be one that ConstantEvaluator knows. + /// + private static int GetConstantIdOperandCount(OpDataIndex i) + { + return i.Op switch + { + Op.OpCompositeConstruct => i.Data.Memory.Length - 3, + _ when ConstantEvaluator.GetIdOperandCount(i.Op) is > 0 and var count => count, + _ => -1, + }; + } + + // Adds the constant form of an instruction to the context, with the same result id + private static SpirvValue AddAsConstant(SpirvContext context, OpDataIndex i) + { + var span = i.Data.Memory.Span; + var resultType = span[1]; + var resultId = span[2]; + + if (i.Op == Op.OpCompositeConstruct) + { + // OpConstantComposite if all constituents are plain constants, OpSpecConstantComposite otherwise + var allConstant = true; + for (int j = 3; j < span.Length; j++) + allConstant &= IsPlainConstant(context, span[j]); + + Span instruction = [.. span]; + instruction[0] = (int)(allConstant ? Op.OpConstantComposite : Op.OpSpecConstantComposite) | (instruction.Length << 16); + context.Add(new OpData(instruction)); + } + // When all operands are known constant values, fold at compile time to avoid + // OpSpecConstantOp which some SPIR-V backends (e.g. SPIRV-Cross) don't fully support. + else if (TryFoldConstantOp(context, i, out var foldedValue)) + { + context.AddConstant(resultType, resultId, foldedValue); + } + else + { + Span instruction = [(int)Op.OpSpecConstantOp, resultType, resultId, (int)i.Op, .. span[3..]]; + instruction[0] |= instruction.Length << 16; + context.Add(new OpData(instruction)); + } + + return new(resultId, resultType); } /// @@ -167,85 +236,37 @@ public static class ExpressionExtensions } /// - /// Try to fold a binary/unary operation at compile time when all operands are known OpConstant values. - /// Returns true and the folded OpConstant instruction if successful. + /// Try to fold a unary/binary operation at compile time when all operands are known constant values. + /// Returns true and the value (scalar or vector) if successful. /// - private static bool TryFoldConstantOp(SpirvContext context, OpDataIndex instruction, out OpData foldedInstruction) + private static bool TryFoldConstantOp(SpirvContext context, OpDataIndex instruction, [NotNullWhen(true)] out object? result) { - foldedInstruction = default; + result = null; var span = instruction.Data.Memory.Span; - var resultType = span[1]; - var resultId = span[2]; - var op = instruction.Op; - // Get the type info for the result - if (!context.GetBuffer().TryGetInstructionById(resultType, out var typeInst)) + if (!context.ReverseTypes.TryGetValue(span[1], out var resultSymbolType)) return false; - // Handle unary operations (operand at index 3) - if (span.Length == 4) + // Note: the operation decides how many operands are ids, the length of the instruction does not + switch (ConstantEvaluator.GetEvaluatedOperandCount(instruction.Op)) { - if (!context.TryGetConstantValue(span[3], out var operandVal, out _) || operandVal is null) + // Unary operation (operand at index 3) + case 1: + if (!context.TryGetConstantValue(span[3], out var operandVal, out _) + || !ConstantEvaluator.TryEvaluateUnary(instruction.Op, operandVal, resultSymbolType, out result)) + return false; + break; + // Binary operation (operands at index 3, 4) + case 2: + if (!context.TryGetConstantValue(span[3], out var leftVal, out _) + || !context.TryGetConstantValue(span[4], out var rightVal, out _) + || !ConstantEvaluator.TryEvaluateBinary(instruction.Op, leftVal, rightVal, out result)) + return false; + break; + default: return false; - - object? result = op switch - { - Op.OpSNegate when operandVal is int v => (object)(-(int)v), - Op.OpFNegate when operandVal is float v => -v, - Op.OpConvertFToS when operandVal is float v => (int)v, - Op.OpConvertFToU when operandVal is float v => (uint)v, - Op.OpConvertSToF when operandVal is int v => (float)v, - Op.OpConvertUToF when operandVal is uint v => (float)v, - _ => null, - }; - if (result is null) return false; - foldedInstruction = EmitFoldedConstant(resultType, resultId, result, typeInst); - return true; } - // Handle binary operations (operands at index 3, 4) - if (span.Length == 5) - { - if (!context.TryGetConstantValue(span[3], out var leftVal, out _) || leftVal is null) - return false; - if (!context.TryGetConstantValue(span[4], out var rightVal, out _) || rightVal is null) - return false; - - object? result = op switch - { - Op.OpIAdd when leftVal is int l && rightVal is int r => (object)(l + r), - Op.OpISub when leftVal is int l && rightVal is int r => l - r, - Op.OpIMul when leftVal is int l && rightVal is int r => l * r, - Op.OpSDiv when leftVal is int l && rightVal is int r => l / r, - Op.OpFAdd when leftVal is float l && rightVal is float r => l + r, - Op.OpFSub when leftVal is float l && rightVal is float r => l - r, - Op.OpFMul when leftVal is float l && rightVal is float r => l * r, - Op.OpFDiv when leftVal is float l && rightVal is float r => l / r, - Op.OpIAdd when leftVal is uint l && rightVal is uint r => l + r, - Op.OpISub when leftVal is uint l && rightVal is uint r => l - r, - Op.OpIMul when leftVal is uint l && rightVal is uint r => l * r, - Op.OpUDiv when leftVal is uint l && rightVal is uint r => l / r, - _ => null, - }; - if (result is null) return false; - foldedInstruction = EmitFoldedConstant(resultType, resultId, result, typeInst); - return true; - } - - return false; - } - - private static OpData EmitFoldedConstant(int resultType, int resultId, object value, OpDataIndex typeInst) - { - return value switch - { - int v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - uint v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - float v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - double v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - long v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - ulong v => new OpData(new OpConstant(resultType, resultId, v).InstructionMemory), - _ => throw new NotSupportedException($"Cannot fold constant of type {value.GetType()}"), - }; + return true; } } diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/NotConstantExpressionException.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/NotConstantExpressionException.cs new file mode 100644 index 000000000..0b467e6c7 --- /dev/null +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/NotConstantExpressionException.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Shaders.Spirv.Building; + +/// +/// Thrown when an expression compiled as a constant (see ) is not one. +/// +public class NotConstantExpressionException(string message) : InvalidOperationException(message); diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/BuiltinProcessor.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/BuiltinProcessor.cs index 04f86dd1d..a26d3c2c4 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/BuiltinProcessor.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/BuiltinProcessor.cs @@ -100,6 +100,8 @@ internal static class BuiltinProcessor (ExecutionModel.TessellationControl, StreamVariableType.Output, "SV_INSIDETESSFACTOR") => new ArrayType(ScalarType.Float, 2), // DX might use float2 or float3 but Vulkan expects float3 in all cases (ExecutionModel.TessellationControl, StreamVariableType.Output, "SV_DOMAINLOCATION") => new VectorType(ScalarType.Float, 3), + // DX uses a uint but Vulkan expects an array, one element per 32 samples + (ExecutionModel.Fragment, _, "SV_COVERAGE") => new ArrayType(symbolType, 1), _ => symbolType, }; @@ -122,6 +124,7 @@ internal static class BuiltinProcessor ( >= ExecutionModel.Vertex, _, "SV_INSTANCEID" or "SV_VERTEXID") => false, // forward from VS to the next stages // Pixel shader inputs (SV_IsFrontFace) (ExecutionModel.Fragment, StreamVariableType.Input, "SV_ISFRONTFACE") => AddBuiltin(context, variable, BuiltIn.FrontFacing), + (ExecutionModel.Fragment, _, "SV_COVERAGE") => AddBuiltin(context, variable, BuiltIn.SampleMask), // SV_PrimitiveID (ExecutionModel.Geometry, StreamVariableType.Output, "SV_PRIMITIVEID") => AddBuiltin(context, variable, BuiltIn.PrimitiveId), (not ExecutionModel.Vertex, StreamVariableType.Input, "SV_PRIMITIVEID") => AddBuiltin(context, variable, BuiltIn.PrimitiveId), diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs index 301fd2990..b3fd35e29 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs @@ -127,7 +127,7 @@ internal static class EntryPointWrapperGenerator for (var inputIndex = 0; inputIndex < streamLayout.InputStreams.Count; inputIndex++) { var stream = streamLayout.InputStreams[inputIndex]; - var loadedValue = buffer.Add(new OpLoad(context.GetOrRegister(new ArrayType(stream.Info.Type, streamLayout.ArrayInputSize.Value)), context.Bound++, stream.Id, null, [])); + var loadedValue = buffer.Add(new OpLoad(context.GetOrRegister(new ArrayType(stream.InterfaceType, streamLayout.ArrayInputSize.Value)), context.Bound++, stream.Id, null, [])); inputLoadValues[inputIndex] = loadedValue.ResultId; } @@ -138,7 +138,7 @@ internal static class EntryPointWrapperGenerator for (var inputIndex = 0; inputIndex < streamLayout.InputStreams.Count; inputIndex++) { var stream = streamLayout.InputStreams[inputIndex]; - inputFieldValues[inputIndex] = buffer.Add(new OpCompositeExtract(context.Types[stream.Info.Type], context.Bound++, inputLoadValues[inputIndex], [arrayIndex])).ResultId; + inputFieldValues[inputIndex] = buffer.Add(new OpCompositeExtract(context.GetOrRegister(stream.InterfaceType), context.Bound++, inputLoadValues[inputIndex], [arrayIndex])).ResultId; inputFieldValues[inputIndex] = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.InterfaceType, stream.Info.Type, inputFieldValues[inputIndex]); } @@ -221,7 +221,7 @@ internal static class EntryPointWrapperGenerator foreach (var stream in streamLayout.PatchInputStreams) { var inputPtr = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Function)), context.Bound++, constantVariable, [context.CompileConstant(stream.Info.StreamStructFieldIndex).Id])).ResultId; - var inputResult = buffer.Add(new OpLoad(context.GetOrRegister(stream.Info.Type), context.Bound++, stream.Id, null, [])).ResultId; + var inputResult = buffer.Add(new OpLoad(context.GetOrRegister(stream.InterfaceType), context.Bound++, stream.Id, null, [])).ResultId; inputResult = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.InterfaceType, stream.Info.Type, inputResult); buffer.Add(new OpStore(inputPtr, inputResult, null, [])); } @@ -393,7 +393,7 @@ internal static class EntryPointWrapperGenerator foreach (var stream in streamLayout.InputStreams) { var streamPointer = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Private)), context.Bound++, streamLayout.StreamsVariableId, [context.CompileConstant(stream.Info.StreamStructFieldIndex).Id])).ResultId; - var inputResult = buffer.Add(new OpLoad(context.Types[stream.Info.Type], context.Bound++, stream.Id, null, [])).ResultId; + var inputResult = buffer.Add(new OpLoad(context.GetOrRegister(stream.InterfaceType), context.Bound++, stream.Id, null, [])).ResultId; inputResult = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.InterfaceType, stream.Info.Type, inputResult); buffer.Add(new OpStore(streamPointer, inputResult, null, [])); } diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/InterfaceProcessor.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/InterfaceProcessor.cs index 75b2b18c9..201ae6476 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/InterfaceProcessor.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/InterfaceProcessor.cs @@ -177,7 +177,8 @@ namespace Stride.Shaders.Spirv.Processing.Interfaces { if (stream.Value.Semantic is { } semantic) { - if ((semantic.ToUpperInvariant().StartsWith("SV_TARGET") || semantic.ToUpperInvariant() == "SV_DEPTH") && stream.Value.Write) + var upperSemantic = semantic.ToUpperInvariant(); + if ((upperSemantic.StartsWith("SV_TARGET") || upperSemantic == "SV_DEPTH" || upperSemantic == "SV_COVERAGE") && stream.Value.Write) stream.Value.Output = true; } } @@ -499,8 +500,10 @@ namespace Stride.Shaders.Spirv.Processing.Interfaces // Generate streams struct types (i.e. VS_STREAMS VS_INPUT and VS_OUTPUT) GenerateStreamStructTypes(context, executionModel, streams, inputStreams, outputStreams, out var inputType, out var outputType, out var streamsType, out var constantsType); - // Create a static global streams variable - var streamsVariable = context.Add(new OpVariable(context.GetOrRegister(new PointerType(streamsType, Specification.StorageClass.Private)), context.Bound++, Specification.StorageClass.Private, null)); + // Create a static global streams variable, zero-initialized so that reading a member + // before it is written is defined + var streamsInitializer = context.CreateDefaultConstantComposite(streamsType).Id; + var streamsVariable = context.Add(new OpVariable(context.GetOrRegister(new PointerType(streamsType, Specification.StorageClass.Private)), context.Bound++, Specification.StorageClass.Private, streamsInitializer)); context.AddName(streamsVariable.ResultId, $"streams{stage}"); var streamLayout = new StageStreamLayout(inputStreams, outputStreams, patchInputStreams, patchOutputStreams, inputType, outputType, streamsType, constantsType, arrayInputSize, arrayOutputSize, streamsVariable.ResultId); diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Tools/Dis.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Tools/Dis.cs index 36a86f299..157b5b32d 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Tools/Dis.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Tools/Dis.cs @@ -314,6 +314,24 @@ public static partial class Spv return this; } + private readonly DisWriter AppendPairIdRefIdRefs(SpvOperand operand) + { + var count = operand.Quantifier switch + { + OperandQuantifier.One => 1, + OperandQuantifier.ZeroOrMore => operand.Words.Length / 2, + OperandQuantifier.ZeroOrOne => operand.Words.Length == 0 ? 0 : 1, + _ => throw new NotSupportedException($"Unsupported operand quantifier: {operand.Quantifier}"), + }; + + for (int i = 0; i < count; ++i) + { + AppendIdRef(operand.Words[i * 2]); + AppendIdRef(operand.Words[i * 2 + 1]); + } + return this; + } + public readonly void Disassemble() { DisHeader(); @@ -409,6 +427,7 @@ public static partial class Spv }, OperandKind.PairIdRefLiteralInteger => AppendPairIdRefLiteralIntegers(operand), OperandKind.PairLiteralIntegerIdRef => AppendPairLiteralIntegerIdRefs(operand), + OperandKind.PairIdRefIdRef => AppendPairIdRefIdRefs(operand), _ => throw new NotImplementedException("Unsupported operand kind " + operand.Kind), }; } diff --git a/sources/shaders/Stride.Shaders.Spirv/Extensions/spirv.sdsl.grammar-ext.json b/sources/shaders/Stride.Shaders.Spirv/Extensions/spirv.sdsl.grammar-ext.json index c4881cb80..57cf293a4 100644 --- a/sources/shaders/Stride.Shaders.Spirv/Extensions/spirv.sdsl.grammar-ext.json +++ b/sources/shaders/Stride.Shaders.Spirv/Extensions/spirv.sdsl.grammar-ext.json @@ -382,6 +382,16 @@ { "kind" : "LiteralInteger", "name" : "Hash4" } ] }, + { + "opname": "OpSwitchIdSDSL", + "opcode": 8032, + "class": "Control-Flow", + "operands": [ + { "kind" : "IdRef", "name" : "'Selector'" }, + { "kind" : "IdRef", "name" : "'Default'" }, + { "kind" : "PairIdRefIdRef", "quantifier" : "*", "name" : "'Target'" } + ] + }, { "opname": "OpEffectSDFX", "opcode": 9000, diff --git a/sources/shaders/Stride.Shaders.Spirv/Generated/InstructionInfo.gen.cs b/sources/shaders/Stride.Shaders.Spirv/Generated/InstructionInfo.gen.cs index 29ef1fac5..9982bba63 100644 --- a/sources/shaders/Stride.Shaders.Spirv/Generated/InstructionInfo.gen.cs +++ b/sources/shaders/Stride.Shaders.Spirv/Generated/InstructionInfo.gen.cs @@ -3313,6 +3313,9 @@ public partial class InstructionInfo Instance.Register(Op.OpSourceHashSDSL, OperandKind.LiteralInteger, OperandQuantifier.One, "hash2", "Miscellaneous", []); Instance.Register(Op.OpSourceHashSDSL, OperandKind.LiteralInteger, OperandQuantifier.One, "hash3", "Miscellaneous", []); Instance.Register(Op.OpSourceHashSDSL, OperandKind.LiteralInteger, OperandQuantifier.One, "hash4", "Miscellaneous", []); + Instance.Register(Op.OpSwitchIdSDSL, OperandKind.IdRef, OperandQuantifier.One, "selector", "Control-Flow", []); + Instance.Register(Op.OpSwitchIdSDSL, OperandKind.IdRef, OperandQuantifier.One, "defaultId", "Control-Flow", []); + Instance.Register(Op.OpSwitchIdSDSL, OperandKind.PairIdRefIdRef, OperandQuantifier.ZeroOrMore, "targets", "Control-Flow", []); Instance.Register(Op.OpEffectSDFX, OperandKind.LiteralString, OperandQuantifier.One, "effectName", "Miscellaneous", []); Instance.Register(Op.OpParamsUseSDFX, OperandKind.IdRef, OperandQuantifier.One, "paramsName", "Miscellaneous", []); Instance.Register(Op.OpParamsSDFX, OperandKind.LiteralString, OperandQuantifier.One, "name", "Miscellaneous", []); diff --git a/sources/shaders/Stride.Shaders.Spirv/Generated/Instructions.gen.cs b/sources/shaders/Stride.Shaders.Spirv/Generated/Instructions.gen.cs index 74f3f202e..377acc5e0 100644 --- a/sources/shaders/Stride.Shaders.Spirv/Generated/Instructions.gen.cs +++ b/sources/shaders/Stride.Shaders.Spirv/Generated/Instructions.gen.cs @@ -132742,6 +132742,146 @@ public ref partial struct OpSourceHashSDSL : IMemoryInstruction public static implicit operator OpSourceHashSDSL(OpDataIndex odi) => new(odi); } +public ref partial struct OpSwitchIdSDSL : IMemoryInstruction +{ + private ref OpData opData; + public ref OpData OpData => ref opData; + + public MemoryOwner InstructionMemory + { + get + { + if (!Unsafe.IsNullRef(ref OpData)) + return OpData.Memory; + else + return field; + } + + private set + { + if (!Unsafe.IsNullRef(ref OpData)) + { + OpData.Memory.Dispose(); + OpData.Memory = value; + } + else + field = value; + } + } + + public OpSwitchIdSDSL() + { + InstructionMemory = MemoryOwner.Allocate(1); + InstructionMemory.Span[0] = (int)Op.OpSwitchIdSDSL | (1 << 16); + } + + public OpSwitchIdSDSL(OpDataIndex index) + { + InitializeProperties(ref index.Data); + opData = ref index.Data; + } + + public OpSwitchIdSDSL(ref OpData data) + { + InitializeProperties(ref data); + opData = ref data; + } + + public int Selector + { + get; + set + { + field = value; + if (InstructionMemory is not null) + UpdateInstructionMemory(); + } + } + + public int DefaultId + { + get; + set + { + field = value; + if (InstructionMemory is not null) + UpdateInstructionMemory(); + } + } + + public LiteralArray<(int, int)> Targets + { + get; + set + { + field.Assign(value); + if (InstructionMemory is not null) + UpdateInstructionMemory(); + } + } + + public OpSwitchIdSDSL(int selector, int defaultId, LiteralArray<(int, int)> targets) + { + Selector = selector; + DefaultId = defaultId; + Targets = targets; + UpdateInstructionMemory(); + opData = ref Unsafe.NullRef(); + } + + public void Attach(OpDataIndex index) + { + opData = ref index.Data; + } + + public void UpdateInstructionMemory() + { + InstructionMemory ??= MemoryOwner.Empty; + Span instruction = [(int)Op.OpSwitchIdSDSL, Selector, DefaultId, ..Targets.Words]; + instruction[0] |= instruction.Length << 16; + if (instruction.Length == InstructionMemory.Length) + instruction.CopyTo(InstructionMemory.Span); + else + { + var tmp = MemoryOwner.Allocate(instruction.Length); + instruction.CopyTo(tmp.Span); + InstructionMemory?.Dispose(); + InstructionMemory = tmp; + } + } + + private void InitializeProperties(ref OpData data) + { + foreach (var o in data) + { + switch (o.Name) + { + case "selector": + Selector = o.ToLiteral(); + break; + case "defaultId": + DefaultId = o.ToLiteral(); + break; + case "targets": + Targets = o.ToLiteralArray<(int, int)>(); + break; + // We ignore unrecognized operands + default: + break; + } + } + + if (Targets.WordCount == -1) + Targets = new(); + } + + public static implicit operator OpSwitchIdSDSL(OpDataIndex odi) => new(odi); + public void Dispose() + { + Targets.Dispose(); + } +} + public ref partial struct OpEffectSDFX : IMemoryInstruction { private ref OpData opData; diff --git a/sources/shaders/Stride.Shaders.Spirv/Generated/SpecificationOp.gen.cs b/sources/shaders/Stride.Shaders.Spirv/Generated/SpecificationOp.gen.cs index 3d45d1d9c..78b140223 100644 --- a/sources/shaders/Stride.Shaders.Spirv/Generated/SpecificationOp.gen.cs +++ b/sources/shaders/Stride.Shaders.Spirv/Generated/SpecificationOp.gen.cs @@ -792,6 +792,7 @@ public static partial class Specification OpEmitVertexSDSL = 8029, OpBinaryOperationSDSL = 8030, OpSourceHashSDSL = 8031, + OpSwitchIdSDSL = 8032, OpEffectSDFX = 9000, OpParamsUseSDFX = 9001, OpParamsSDFX = 9002, diff --git a/sources/shaders/Stride.Shaders.Tests/ConstantEvaluatorTests.cs b/sources/shaders/Stride.Shaders.Tests/ConstantEvaluatorTests.cs new file mode 100644 index 000000000..2a15c731e --- /dev/null +++ b/sources/shaders/Stride.Shaders.Tests/ConstantEvaluatorTests.cs @@ -0,0 +1,141 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Shaders.Core; +using Stride.Shaders.Spirv.Building; +using static Stride.Shaders.Spirv.Specification; + +namespace Stride.Shaders.Parsers.Tests; + +public class ConstantEvaluatorTests +{ + public static TheoryData UnaryCases => new() + { + { Op.OpSNegate, 3, ScalarType.Int, -3 }, + { Op.OpSNegate, 1u, ScalarType.UInt, uint.MaxValue }, + { Op.OpNot, 0u, ScalarType.UInt, uint.MaxValue }, + { Op.OpFNegate, 1.5f, ScalarType.Float, -1.5f }, + { Op.OpFNegate, 1.5, ScalarType.Double, -1.5 }, + { Op.OpLogicalNot, true, ScalarType.Boolean, false }, + // `static const uint X = 1` converts its int literal this way + { Op.OpBitcast, 1, ScalarType.UInt, 1u }, + { Op.OpBitcast, -1, ScalarType.UInt, uint.MaxValue }, + { Op.OpBitcast, uint.MaxValue, ScalarType.Int, -1 }, + { Op.OpBitcast, 1.0f, ScalarType.UInt, 0x3F800000u }, + { Op.OpBitcast, 0x3F800000u, ScalarType.Float, 1.0f }, + { Op.OpBitcast, -1L, ScalarType.UInt64, ulong.MaxValue }, + { Op.OpConvertFToS, -2.5f, ScalarType.Int, -2 }, + { Op.OpConvertFToU, 2.5f, ScalarType.UInt, 2u }, + { Op.OpConvertSToF, -2, ScalarType.Float, -2.0f }, + { Op.OpConvertUToF, uint.MaxValue, ScalarType.Double, 4294967295.0 }, + // The operation decides how the operand is read, not its type + { Op.OpConvertSToF, uint.MaxValue, ScalarType.Float, -1.0f }, + { Op.OpSConvert, -1, ScalarType.Int64, -1L }, + { Op.OpUConvert, -1, ScalarType.UInt64, (ulong)uint.MaxValue }, + { Op.OpUConvert, ulong.MaxValue, ScalarType.UInt, uint.MaxValue }, + { Op.OpFConvert, 1.5f, ScalarType.Double, 1.5 }, + }; + + [Theory] + [MemberData(nameof(UnaryCases))] + public void EvaluatesUnary(Op op, object operand, ScalarType resultType, object expected) + { + Assert.Equal(1, ConstantEvaluator.GetEvaluatedOperandCount(op)); + Assert.True(ConstantEvaluator.TryEvaluateUnary(op, operand, resultType, out var result)); + // Assert.Equal on object compares the type too: a uint result must not come back as an int + Assert.Equal(expected, result); + } + + public static TheoryData BinaryCases => new() + { + { Op.OpIAdd, 2, 3, 5 }, + { Op.OpIAdd, 2u, 3u, 5u }, + { Op.OpISub, 2u, 3u, uint.MaxValue }, + { Op.OpIMul, 3L, 4L, 12L }, + { Op.OpSDiv, -7, 2, -3 }, + { Op.OpUDiv, 7u, 2u, 3u }, + { Op.OpSRem, -7, 2, -1 }, + { Op.OpSMod, -7, 2, 1 }, + { Op.OpUMod, 7u, 2u, 1u }, + { Op.OpBitwiseAnd, 0xFFu, 0x0Fu, 0x0Fu }, + { Op.OpBitwiseOr, 0xF0, 0x0F, 0xFF }, + { Op.OpShiftLeftLogical, 1u, 4, 16u }, + { Op.OpShiftRightLogical, -1, 28, 15 }, + { Op.OpShiftRightArithmetic, -16, 2, -4 }, + { Op.OpFAdd, 1.5f, 2.0f, 3.5f }, + { Op.OpFDiv, 1.0, 4.0, 0.25 }, + { Op.OpFMod, -1.0f, 3.0f, 2.0f }, + { Op.OpFRem, -1.0f, 3.0f, -1.0f }, + { Op.OpIEqual, 2u, 2u, true }, + { Op.OpSLessThan, -1, 1, true }, + { Op.OpULessThan, uint.MaxValue, 1u, false }, + { Op.OpLogicalAnd, true, false, false }, + }; + + [Theory] + [MemberData(nameof(BinaryCases))] + public void EvaluatesBinary(Op op, object left, object right, object expected) + { + Assert.Equal(2, ConstantEvaluator.GetEvaluatedOperandCount(op)); + Assert.True(ConstantEvaluator.TryEvaluateBinary(op, left, right, out var result)); + Assert.Equal(expected, result); + } + + // Their operands are not all ids (literal indices), so a caller must not read them as such + [Theory] + [InlineData(Op.OpCompositeExtract)] + [InlineData(Op.OpCompositeInsert)] + [InlineData(Op.OpVectorShuffle)] + [InlineData(Op.OpAccessChain)] + public void OperationWithLiteralOperandsHasNoOperandCount(Op op) + { + Assert.Equal(0, ConstantEvaluator.GetEvaluatedOperandCount(op)); + } + + [Fact] + public void EvaluatesVectorsPerComponent() + { + static ConstantVector Vector(params object[] values) => new() { Values = values }; + var float3Type = new VectorType(ScalarType.Float, 3); + + Assert.True(ConstantEvaluator.TryEvaluateBinary(Op.OpFMul, Vector(1.0f, 2.0f, 3.0f), Vector(0.5f, 0.5f, 2.0f), out var product)); + Assert.Equal(new object[] { 0.5f, 1.0f, 6.0f }, Assert.IsType(product).Values); + + Assert.True(ConstantEvaluator.TryEvaluateUnary(Op.OpFNegate, Vector(1.0f, -2.0f, 0.5f), float3Type, out var negated)); + Assert.Equal(new object[] { -1.0f, 2.0f, -0.5f }, Assert.IsType(negated).Values); + + // The type of the components changes with the operation + Assert.True(ConstantEvaluator.TryEvaluateUnary(Op.OpConvertSToF, Vector(1, 2, 3), float3Type, out var converted)); + Assert.Equal(new object[] { 1.0f, 2.0f, 3.0f }, Assert.IsType(converted).Values); + Assert.True(ConstantEvaluator.TryEvaluateBinary(Op.OpSLessThan, Vector(1, 5), Vector(2, 2), out var compared)); + Assert.Equal(new object[] { true, false }, Assert.IsType(compared).Values); + + // Vectors of different sizes, a vector with a scalar, and a result type that is not the vector + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpFAdd, Vector(1.0f, 2.0f), Vector(1.0f, 2.0f, 3.0f), out _)); + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpFAdd, Vector(1.0f, 2.0f), 1.0f, out _)); + Assert.False(ConstantEvaluator.TryEvaluateUnary(Op.OpFNegate, Vector(1.0f, 2.0f), float3Type, out _)); + // One component that can't be evaluated + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpSDiv, Vector(1, 2), Vector(1, 0), out _)); + } + + [Fact] + public void DoesNotEvaluateWhatIsNotDefined() + { + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpSDiv, 1, 0, out _)); + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpUMod, 1u, 0u, out _)); + // The signed division that overflows, which throws in C# + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpSDiv, int.MinValue, -1, out _)); + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpSRem, long.MinValue, -1L, out _)); + // A shift by the width of the value or more, of which C# would only keep the low bits + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpShiftLeftLogical, 1, 32, out _)); + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpShiftRightLogical, 1u, -1, out _)); + // Operands of different types + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpIAdd, 1, 1u, out _)); + // A signed operation on an unsigned value, and the other way around + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpSLessThan, 1u, 2u, out _)); + Assert.False(ConstantEvaluator.TryEvaluateBinary(Op.OpUDiv, 4, 2, out _)); + // A bitcast keeps the width + Assert.False(ConstantEvaluator.TryEvaluateUnary(Op.OpBitcast, 1, ScalarType.UInt64, out _)); + Assert.False(ConstantEvaluator.TryEvaluateUnary(Op.OpFNegate, 1, ScalarType.Int, out _)); + } +} diff --git a/sources/shaders/Stride.Shaders.Tests/FrameRenderer.cs b/sources/shaders/Stride.Shaders.Tests/FrameRenderer.cs index 53731e4ff..ba61c84f4 100644 --- a/sources/shaders/Stride.Shaders.Tests/FrameRenderer.cs +++ b/sources/shaders/Stride.Shaders.Tests/FrameRenderer.cs @@ -101,6 +101,9 @@ public abstract class FrameRenderer(uint width = 800, uint height = 600) case { Type: EffectParameterType.Int }: *((int*)&cbufferDataPtr[offset]) = int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); break; + case { Type: EffectParameterType.UInt }: + *((uint*)&cbufferDataPtr[offset]) = uint.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); + break; case { Type: EffectParameterType.Float }: *((float*)&cbufferDataPtr[offset]) = float.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); break; diff --git a/sources/shaders/Stride.Shaders.Tests/FxcSupport.cs b/sources/shaders/Stride.Shaders.Tests/FxcSupport.cs new file mode 100644 index 000000000..6479d4d47 --- /dev/null +++ b/sources/shaders/Stride.Shaders.Tests/FxcSupport.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Text; +using Silk.NET.Core.Native; +using Silk.NET.Direct3D.Compilers; + +namespace Stride.Shaders.Parsers.Tests; + +/// +/// Compiles HLSL with fxc (d3dcompiler_47), what the Direct3D effect compiler runs on the shaders +/// SPIRV-Cross translates. +/// +internal static unsafe class FxcSupport +{ + public static void SkipUnlessAvailable() + { + if (!OperatingSystem.IsWindows()) + Assert.Skip("fxc (d3dcompiler_47) is only available on Windows"); + } + + /// + /// Compiles and returns fxc's diagnostics, or null when it succeeded. + /// + public static string? Compile(string source, string shaderModel, string entryPoint = "main") + { + using var compiler = D3DCompiler.GetApi(); + + ComPtr code = default; + ComPtr errors = default; + var sourceBytes = Encoding.ASCII.GetBytes(source); + + HResult hr = compiler.Compile( + in sourceBytes[0], + (nuint)sourceBytes.Length, + nameof(source), + null, + ref Unsafe.NullRef(), + entryPoint, + shaderModel, + 0, + 0, + ref code, + ref errors); + + var diagnostics = errors.Handle is not null ? SilkMarshal.PtrToString((nint)errors.GetBufferPointer()) : null; + + code.Dispose(); + errors.Dispose(); + + return hr.IsFailure ? diagnostics ?? $"fxc failed with 0x{hr.Value:X8}" : null; + } +} diff --git a/sources/shaders/Stride.Shaders.Tests/StrideShaderTests.cs b/sources/shaders/Stride.Shaders.Tests/StrideShaderTests.cs index d5e407163..0cad1b85a 100644 --- a/sources/shaders/Stride.Shaders.Tests/StrideShaderTests.cs +++ b/sources/shaders/Stride.Shaders.Tests/StrideShaderTests.cs @@ -128,6 +128,182 @@ public class StrideShaderTests && m.Text.Contains("[numthreads]") && m.Text.Contains("Compute")); } + // A texture offset has to be a constant of the module. It is written inline, so the instructions + // computing it (constructors, arithmetic on constants) are turned into constants. + [Fact] + public void TextureOffsetCanBeAConstantExpression() + { + SpirvCrossSupport.SkipUnlessAvailable(); + FxcSupport.SkipUnlessAvailable(); + + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.True(shaderMixer.MergeSDSL(new ShaderClassSource("TextureConstantOffset"), new ShaderMixer.Options(true), log, out var bytecode, out _, out _, out _), + string.Join(Environment.NewLine, log.Messages.Select(m => m.Text))); + + var validation = Spv.ValidateBinary(bytecode); + Assert.True(validation.IsValid, validation.Output); + + var translator = new SpirvTranslator(bytecode.ToArray().AsMemory().Cast()); + var fragment = translator.GetEntryPoints().First(x => x.ExecutionModel == ExecutionModel.Fragment); + var hlsl = translator.Translate(Backend.Hlsl, fragment); + Assert.Contains("int2(4, 2)", hlsl); + var errors = FxcSupport.Compile(hlsl, "ps_5_0"); + Assert.True(errors is null, errors + Environment.NewLine + hlsl); + } + + // The thread group size is a literal of the module, so it has to be known when compiling + [Fact] + public void NumThreadsVariableParameterIsReported() + { + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.False(shaderMixer.MergeSDSL(new ShaderClassSource("NumThreadsVariable"), new ShaderMixer.Options(true), log, out _, out _, out _, out _)); + + Assert.Contains(log.Messages, m => m.Type == Stride.Core.Diagnostics.LogMessageType.Error + && m.Text.Contains("[numthreads] parameter must be a constant integer expression")); + } + + // A `stage compose` is one slot for the whole effect: its declaring shader is promoted to the root, + // so a value supplied at a nested composition would have nothing to attach to. + [Fact] + public void StageCompositionSuppliedFromNestedIsReported() + { + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var shaderSource = new ShaderMixinSource + { + Mixins = { new ShaderClassSource("StageComposePathRoot") }, + Compositions = + { + ["nested"] = new ShaderMixinSource + { + Mixins = { new ShaderClassSource("StageComposePathSupplier") }, + Compositions = { ["Samplers"] = new ShaderArraySource { new ShaderClassSource("StageComposePathImpl") } }, + }, + }, + }; + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.False(shaderMixer.MergeSDSL(shaderSource, new ShaderMixer.Options(true), log, out _, out _, out _, out _)); + + Assert.Contains(log.Messages, m => m.Type == Stride.Core.Diagnostics.LogMessageType.Error + && m.Text.Contains("'Samplers'") && m.Text.Contains("StageComposePathDeclarer") + && m.Text.Contains("supplied at the root") && m.Text.Contains("'nested'")); + } + + // The other side of the rule: the value comes from the root even when only a nested composition + // inherits the shader declaring the slot. + [Fact] + public void StageCompositionDeclaredFromNestedIsSuppliedAtRoot() + { + AssertStageSamplerTextureIsBoundAtRoot(MergeStageComposePath("StageComposePathRoot", "nested")); + } + + // Two nested compositions inheriting the same declarer share the one slot. + [Fact] + public void StageCompositionDeclaredFromTwoNestedIsSuppliedAtRoot() + { + AssertStageSamplerTextureIsBoundAtRoot(MergeStageComposePath("StageComposePathRoot2", "nestedA", "nestedB")); + } + + private static EffectReflection MergeStageComposePath(string root, params string[] nestedSlots) + { + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var shaderSource = new ShaderMixinSource { Mixins = { new ShaderClassSource(root) } }; + foreach (var nestedSlot in nestedSlots) + shaderSource.Compositions[nestedSlot] = new ShaderMixinSource { Mixins = { new ShaderClassSource("StageComposePathSupplier") } }; + shaderSource.Compositions["Samplers"] = new ShaderArraySource { new ShaderClassSource("StageComposePathImpl") }; + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.True(shaderMixer.MergeSDSL(shaderSource, new ShaderMixer.Options(true), log, out _, out var reflection, out _, out _), + string.Join(Environment.NewLine, log.Messages.Select(m => m.Text))); + return reflection; + } + + // The supplied shader's texture gets the key of a root composition: no nested path after the slot + private static void AssertStageSamplerTextureIsBoundAtRoot(EffectReflection reflection) + { + var textureKeys = reflection.ResourceBindings.Select(b => b.KeyInfo.KeyName).Where(k => k.Contains("StageComposePathImpl.Tex")).Distinct().ToList(); + Assert.Equal(["StageComposePathImpl.Tex.Samplers[0]"], textureKeys); + } + + // SV_Coverage is a uint in HLSL and a one-element array decorated SampleMask in SPIR-V, on both sides. + [Fact] + public void CoverageIsSampleMaskInFragmentStage() + { + SpirvCrossSupport.SkipUnlessAvailable(); + FxcSupport.SkipUnlessAvailable(); + + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.True(shaderMixer.MergeSDSL(new ShaderClassSource("PSCoverage"), new ShaderMixer.Options(true), log, out var bytecode, out _, out _, out _), + string.Join(Environment.NewLine, log.Messages.Select(m => m.Text))); + + var validation = Spv.ValidateBinary(bytecode); + Assert.True(validation.IsValid, validation.Output); + + var translator = new SpirvTranslator(bytecode.ToArray().AsMemory().Cast()); + var fragment = translator.GetEntryPoints().First(x => x.ExecutionModel == ExecutionModel.Fragment); + var hlsl = translator.Translate(Backend.Hlsl, fragment); + // Read as an input and, since the shader writes it, declared as an output too + Assert.Contains("gl_SampleMaskIn : SV_Coverage", hlsl); + Assert.Contains("gl_SampleMask : SV_Coverage", hlsl); + var errors = FxcSupport.Compile(hlsl, "ps_5_0"); + Assert.True(errors is null, errors + Environment.NewLine + hlsl); + } + + // `static const uint X = 1` converts its int literal. The constant must stay usable, and keep its type, + // as an array size, once inherited, and when its value comes from a generic of the base shader. + // ConstInherited: every constant of a base shader is imported, whatever it is made of (vector, vector + // components, null struct...), even when only known once a generic of the base shader is. + [Theory] + [InlineData("ConstUIntArraySize")] + [InlineData("ConstUIntInherited")] + [InlineData("ConstUIntGeneric")] + [InlineData("ConstInherited")] + public void ConstantIsUsable(string shaderName) + { + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.True(shaderMixer.MergeSDSL(new ShaderClassSource(shaderName), new ShaderMixer.Options(true), log, out var bytecode, out _, out _, out _), + string.Join(Environment.NewLine, log.Messages.Select(m => m.Text))); + + var validation = Spv.ValidateBinary(bytecode); + Assert.True(validation.IsValid, validation.Output); + } + + // OpSwitch takes literal values: a label that cannot be evaluated when compiling, and two labels with + // the same value (easy with named constants), are errors. + // Constant labels (`static const`) are covered by the SwitchConstLabels render test. + [Theory] + [InlineData("SwitchVariableLabel", "case label must be a constant integer expression")] + [InlineData("SwitchDuplicateLabel", "case label has the same value (1)")] + // Only known once the generic of the base shader is: reported when mixing, with the location of the switch + [InlineData("SwitchGenericDuplicateLabel", "SwitchGenericDuplicateLabel.sdsl(8,")] + [InlineData("SwitchGenericDuplicateLabel", "two case labels with the same value (3)")] + public void InvalidSwitchIsReported(string shaderName, string expectedError) + { + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.False(shaderMixer.MergeSDSL(new ShaderClassSource(shaderName), new ShaderMixer.Options(true), log, out _, out _, out _, out _)); + + Assert.Contains(log.Messages, m => m.Type == Stride.Core.Diagnostics.LogMessageType.Error && m.Text.Contains(expectedError)); + } + // fxc rejects the same shader with X4532, so this reports rather than emitting a module that only // fails later, deep inside the HLSL legalizer. [Fact] @@ -1194,4 +1370,29 @@ new ShaderMacro("class", "shader"), [Stride.Shaders.Spirv.Specification.LoopControlMask.DontUnroll, Stride.Shaders.Spirv.Specification.LoopControlMask.Unroll, Stride.Shaders.Spirv.Specification.LoopControlMask.DontUnroll], controls); } + + // Writing only part of a stream the stage input does not carry must leave the rest defined. + // Checked with fxc, which rejects an undefined read with X4000; the HLSL text alone looks fine. + [Fact] + public void GeometryStreamsAssignThenPartialWriteCompilesWithFxc() + { + SpirvCrossSupport.SkipUnlessAvailable(); + FxcSupport.SkipUnlessAvailable(); + + var loader = new ShaderLoader("./assets/SDSL/CompilerTests"); + var shaderMixer = new ShaderMixer(loader); + shaderMixer.ShaderLoader.LoadExternalBuffer("GeometryStreamsAssignPartialWrite", [], out _, out _, out _); + + var log = new Stride.Core.Diagnostics.LoggerResult(); + Assert.True(shaderMixer.MergeSDSL(new ShaderClassSource("GeometryStreamsAssignPartialWrite"), new ShaderMixer.Options(true), log, out var bytecode, out _, out _, out _), + string.Join(Environment.NewLine, log.Messages.Select(m => m.Text))); + + var legalized = SpirvTools.LegalizeForHlsl(System.Runtime.InteropServices.MemoryMarshal.Cast(bytecode.ToArray())); + var translator = new SpirvTranslator(legalized.AsMemory()); + var geometry = translator.GetEntryPoints().First(x => x.ExecutionModel == ExecutionModel.Geometry); + var hlsl = translator.Translate(Backend.Hlsl, geometry); + + var errors = FxcSupport.Compile(hlsl, "gs_5_0"); + Assert.True(errors is null, errors + Environment.NewLine + hlsl); + } } diff --git a/sources/shaders/assets/SDSL/CompilerTests/ConstInherited.sdsl b/sources/shaders/assets/SDSL/CompilerTests/ConstInherited.sdsl new file mode 100644 index 000000000..0f224cb78 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/ConstInherited.sdsl @@ -0,0 +1,39 @@ +namespace Stride.Shaders.Tests; + +shader ConstInheritedBase +{ + struct ConstInheritedData + { + float3 Color; + float Alpha; + }; + + static const float3 Vec = float3(1.0, 2.0, 3.0); + static const bool Flag = true; + static const float Third = 1.0 / 3.0; + static const int Negative = -(2 + 3); + + // Components of a constant vector + static const float VecX = Vec.x; + static const float2 VecZY = Vec.zy; + + // Same, with components that are only known once the generic is + static const int3 Scaled = int3(TScale, TScale * 2, 7); + static const int ScaledY = Scaled.y; + + static const ConstInheritedData Empty = (ConstInheritedData)0; +} + +shader ConstInherited : ConstInheritedBase<3> +{ + stream float4 ColorTarget : SV_Target0; + + void PSMain() + { + ConstInheritedData data = Empty; + float3 v = Vec * Third + float3(VecZY, VecX) + data.Color; + int n = Negative + ScaledY + Scaled.z; + float x = Flag ? VecX : float(n); + streams.ColorTarget = float4(v, x + n); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/ConstUIntArraySize.sdsl b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntArraySize.sdsl new file mode 100644 index 000000000..eb816a1ff --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntArraySize.sdsl @@ -0,0 +1,19 @@ +namespace Stride.Shaders.Tests; + +shader ConstUIntArraySize +{ + stream float4 ColorTarget : SV_Target0; + + static const uint Count = 4; + + cbuffer Test + { + float Values[Count]; + } + + void PSMain() + { + float v = Values[3]; + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/ConstUIntBase.sdsl b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntBase.sdsl new file mode 100644 index 000000000..b7b5a60be --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntBase.sdsl @@ -0,0 +1,7 @@ +namespace Stride.Shaders.Tests; + +shader ConstUIntBase +{ + static const uint Mode_A = 1; + static const uint Count = 4; +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/ConstUIntGeneric.sdsl b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntGeneric.sdsl new file mode 100644 index 000000000..d5e2158c6 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntGeneric.sdsl @@ -0,0 +1,25 @@ +namespace Stride.Shaders.Tests; + +shader ConstUIntGenericBase +{ + static const uint Count = TCount; + static const uint CountPlusOne = TCount + 1; +} + +shader ConstUIntGeneric : ConstUIntGenericBase<3> +{ + stream float4 ColorTarget : SV_Target0; + + cbuffer Test + { + uint Selector; + float Values[CountPlusOne]; + } + + void PSMain() + { + uint count = Count; + float v = Selector == count ? Values[3] : 0.0; + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/ConstUIntInherited.sdsl b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntInherited.sdsl new file mode 100644 index 000000000..244ca2008 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/ConstUIntInherited.sdsl @@ -0,0 +1,19 @@ +namespace Stride.Shaders.Tests; + +shader ConstUIntInherited : ConstUIntBase +{ + stream float4 ColorTarget : SV_Target0; + + cbuffer Test + { + uint Selector; + } + + void PSMain() + { + uint mode = Mode_A; + uint total = Count / mode; + float v = Selector == total ? 1.0 : 0.0; + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/GeometryStreamsAssignPartialWrite.sdsl b/sources/shaders/assets/SDSL/CompilerTests/GeometryStreamsAssignPartialWrite.sdsl new file mode 100644 index 000000000..e5cebb382 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/GeometryStreamsAssignPartialWrite.sdsl @@ -0,0 +1,41 @@ +// A geometry shader that assigns the stage input over `streams` and then writes only EdgeA.xyz, +// never EdgeA.w - the shape of Stride.BepuPhysics.Debug's SinglePassWireframeShader. + +namespace Stride.Shaders.Tests; + +shader GeometryStreamsAssignPartialWrite +{ + stream float4 Position : POSITION; + stream float4 ShadingPosition : SV_Position; + stream float4 ColorTarget : SV_Target0; + + // Written in the geometry stage only, so the stage input does not carry it. + stream float4 EdgeA; + + void VSMain() + { + streams.ShadingPosition = streams.Position; + } + + [maxvertexcount(3)] + void GSMain(triangle Input input[3], inout TriangleStream outStream) + { + for (int i = 0; i < 3; ++i) + { + streams = input[i]; + + streams.EdgeA[0] = input[i].ShadingPosition.x; + streams.EdgeA[1] = input[i].ShadingPosition.y; + streams.EdgeA[2] = input[i].ShadingPosition.z; + + outStream.Append(streams); + } + + outStream.RestartStrip(); + } + + void PSMain() + { + streams.ColorTarget = float4(streams.EdgeA.xyz, 1); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/NumThreadsVariable.sdsl b/sources/shaders/assets/SDSL/CompilerTests/NumThreadsVariable.sdsl new file mode 100644 index 000000000..e0ec074f0 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/NumThreadsVariable.sdsl @@ -0,0 +1,19 @@ +namespace Stride.Shaders.Tests; + +shader NumThreadsVariable +{ + stage stream uint3 DispatchThreadId : SV_DispatchThreadID; + + RWBuffer Output; + + cbuffer Test + { + int ThreadCount; + } + + [numthreads(ThreadCount, 1, 1)] + void CSMain() + { + Output[streams.DispatchThreadId.x] = 1; + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/PSCoverage.sdsl b/sources/shaders/assets/SDSL/CompilerTests/PSCoverage.sdsl new file mode 100644 index 000000000..eedb09687 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/PSCoverage.sdsl @@ -0,0 +1,20 @@ +namespace Stride.Shaders.Tests; + +shader PSCoverage +{ + stream float4 ColorTarget : SV_Target0; + stream float4 Position : POSITION; + stream float4 ShadingPosition : SV_Position; + stream uint Coverage : SV_Coverage; + + void VSMain() + { + streams.ShadingPosition = streams.Position; + } + + stage void PSMain() + { + streams.ColorTarget = float4(countbits(streams.Coverage) / 4.0, 0, 0, 1); + streams.Coverage = streams.Coverage & 1; + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathBase.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathBase.sdsl new file mode 100644 index 000000000..417ceb496 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathBase.sdsl @@ -0,0 +1,10 @@ +// Base of the `stage compose` slot used by StageComposePathRoot. +namespace Stride.Shaders.Tests; + +shader StageComposePathBase +{ + float4 Sample() + { + return float4(0, 0, 0, 0); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathDeclarer.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathDeclarer.sdsl new file mode 100644 index 000000000..051bd645e --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathDeclarer.sdsl @@ -0,0 +1,13 @@ +// Declares the stage slot. Having a stage member is what gets this shader promoted to the root, +// away from the composition that supplies its value. +namespace Stride.Shaders.Tests; + +shader StageComposePathDeclarer +{ + stage compose StageComposePathBase Samplers[]; + + float4 ConsumeIndexed() + { + return Samplers[0].Sample(); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathImpl.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathImpl.sdsl new file mode 100644 index 000000000..f8e2cddf7 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathImpl.sdsl @@ -0,0 +1,12 @@ +// The value supplied for the stage slot. +namespace Stride.Shaders.Tests; + +shader StageComposePathImpl : StageComposePathBase +{ + Texture2D Tex; + + override float4 Sample() + { + return Tex.Load(int3(0, 0, 0)); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot.sdsl new file mode 100644 index 000000000..cf76fcaec --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot.sdsl @@ -0,0 +1,13 @@ +namespace Stride.Shaders.Tests; + +shader StageComposePathRoot +{ + stream float4 ColorTarget : SV_Target0; + + compose StageComposePathSupplier nested; + + void PSMain() + { + streams.ColorTarget = nested.Get(); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot2.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot2.sdsl new file mode 100644 index 000000000..e51cffa22 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathRoot2.sdsl @@ -0,0 +1,15 @@ +// Two nested compositions inherit the same stage slot declarer, so the root supplies it once for both. +namespace Stride.Shaders.Tests; + +shader StageComposePathRoot2 +{ + stream float4 ColorTarget : SV_Target0; + + compose StageComposePathSupplier nestedA; + compose StageComposePathSupplier nestedB; + + void PSMain() + { + streams.ColorTarget = nestedA.Get() + nestedB.Get(); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/StageComposePathSupplier.sdsl b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathSupplier.sdsl new file mode 100644 index 000000000..6c6b7fb9f --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/StageComposePathSupplier.sdsl @@ -0,0 +1,10 @@ +// Sits one level below the root, and is where the stage slot's value is supplied. +namespace Stride.Shaders.Tests; + +shader StageComposePathSupplier : StageComposePathDeclarer +{ + float4 Get() + { + return ConsumeIndexed(); + } +}; diff --git a/sources/shaders/assets/SDSL/CompilerTests/SwitchDuplicateLabel.sdsl b/sources/shaders/assets/SDSL/CompilerTests/SwitchDuplicateLabel.sdsl new file mode 100644 index 000000000..596695410 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/SwitchDuplicateLabel.sdsl @@ -0,0 +1,29 @@ +namespace Stride.Shaders.Tests; + +shader SwitchDuplicateLabel +{ + stream float4 ColorTarget : SV_Target0; + + static const int Mode_A = 1; + static const int Mode_B = 1; + + cbuffer Test + { + int Selector; + } + + void PSMain() + { + float v = 0.0; + switch (Selector) + { + case Mode_A: + v = 1.0; + break; + case Mode_B: + v = 0.5; + break; + } + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/SwitchGenericDuplicateLabel.sdsl b/sources/shaders/assets/SDSL/CompilerTests/SwitchGenericDuplicateLabel.sdsl new file mode 100644 index 000000000..323cc4fe7 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/SwitchGenericDuplicateLabel.sdsl @@ -0,0 +1,35 @@ +namespace Stride.Shaders.Tests; + +shader SwitchGenericDuplicateLabelBase +{ + float Pick(int selector) + { + float v = 0.0; + switch (selector) + { + case TMode: + v = 1.0; + break; + case 3: + v = 0.5; + break; + } + return v; + } +} + +shader SwitchGenericDuplicateLabel : SwitchGenericDuplicateLabelBase<3> +{ + stream float4 ColorTarget : SV_Target0; + + cbuffer Test + { + int Selector; + } + + void PSMain() + { + float v = Pick(Selector); + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/SwitchVariableLabel.sdsl b/sources/shaders/assets/SDSL/CompilerTests/SwitchVariableLabel.sdsl new file mode 100644 index 000000000..44a2e5288 --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/SwitchVariableLabel.sdsl @@ -0,0 +1,24 @@ +namespace Stride.Shaders.Tests; + +shader SwitchVariableLabel +{ + stream float4 ColorTarget : SV_Target0; + + cbuffer Test + { + int Selector; + int Label; + } + + void PSMain() + { + float v = 0.0; + switch (Selector) + { + case Label: + v = 1.0; + break; + } + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/CompilerTests/TextureConstantOffset.sdsl b/sources/shaders/assets/SDSL/CompilerTests/TextureConstantOffset.sdsl new file mode 100644 index 000000000..c3fb95dba --- /dev/null +++ b/sources/shaders/assets/SDSL/CompilerTests/TextureConstantOffset.sdsl @@ -0,0 +1,21 @@ +namespace Stride.Shaders.Tests; + +shader TextureConstantOffset +{ + stream float4 ColorTarget : SV_Target0; + stream float2 TexCoord : TEXCOORD0; + + static const int Step = 2; + + Texture2D Tex; + SamplerState Samp; + + void PSMain() + { + float4 a = Tex.Sample(Samp, streams.TexCoord, int2(-1, 0)); + float4 b = Tex.Sample(Samp, streams.TexCoord, int2(Step, 0)); + float4 c = Tex.Sample(Samp, streams.TexCoord, int2(Step * 2, 1 + 1)); + float4 d = Tex.Sample(Samp, streams.TexCoord, int2(Step, 1) * 2 - int2(1, 1)); + streams.ColorTarget = a + b + c + d; + } +} diff --git a/sources/shaders/assets/SDSL/RenderTests/ConstVectorMath.sdsl b/sources/shaders/assets/SDSL/RenderTests/ConstVectorMath.sdsl new file mode 100644 index 000000000..34e091df4 --- /dev/null +++ b/sources/shaders/assets/SDSL/RenderTests/ConstVectorMath.sdsl @@ -0,0 +1,31 @@ +// PSMain(ExpectedResult=#7F3F1FFF, cbuffer.Test=(Test1=0)) +// PSMain(ExpectedResult=#7F3F3FFF, cbuffer.Test=(Test1=1)) + +namespace Stride.Shaders.Tests; + +shader ConstVectorMathBase +{ + // Only known once the generic is: computed when mixing + static const float3 FromGeneric = float3(TValue, 63.0, 63.0) * (1.0 / 255.0); +} + +shader ConstVectorMath : ConstVectorMathBase<127> +{ + stream float4 ColorTarget : SV_Target0; + + // Vector with vector, vector with scalar, and a negated vector + static const float3 Scaled = float3(127.0, 63.0, 31.0) * float3(1.0, 1.0, 1.0); + static const float3 Normalized = Scaled * (1.0 / 255.0); + static const float2 Negated = -float2(-1.0, 0.0); + + cbuffer Test + { + int Test1; + } + + void PSMain() + { + float3 color = Test1 == 0 ? Normalized : FromGeneric; + streams.ColorTarget = float4(color, Negated.x); + } +} diff --git a/sources/shaders/assets/SDSL/RenderTests/SwitchConstLabels.sdsl b/sources/shaders/assets/SDSL/RenderTests/SwitchConstLabels.sdsl new file mode 100644 index 000000000..e30ba5a5c --- /dev/null +++ b/sources/shaders/assets/SDSL/RenderTests/SwitchConstLabels.sdsl @@ -0,0 +1,45 @@ +// PSMain(ExpectedResult=#FFFFFFFF, cbuffer.Test=(Test1=1)) +// PSMain(ExpectedResult=#7F7F7F7F, cbuffer.Test=(Test1=2)) +// PSMain(ExpectedResult=#3F3F3F3F, cbuffer.Test=(Test1=3)) +// PSMain(ExpectedResult=#1F1F1F1F, cbuffer.Test=(Test1=4)) +// PSMain(ExpectedResult=#00000000, cbuffer.Test=(Test1=99)) + +namespace Stride.Shaders.Tests; + +shader SwitchConstLabels +{ + stream float4 ColorTarget : SV_Target0; + + static const uint Mode_Full = 1; + static const uint Mode_Half = 2; + static const int Mode_Quarter = 3; + + cbuffer Test + { + uint Test1; + } + + void PSMain() + { + float v = 0.0; + switch (Test1) + { + case Mode_Full: + v = 1.0; + break; + case Mode_Half: + v = 127.0 / 255.0; + break; + case Mode_Quarter: + v = 63.0 / 255.0; + break; + case Mode_Half + 2: + v = 31.0 / 255.0; + break; + default: + v = 0.0; + break; + } + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shaders/assets/SDSL/RenderTests/SwitchGenericLabels.sdsl b/sources/shaders/assets/SDSL/RenderTests/SwitchGenericLabels.sdsl new file mode 100644 index 000000000..9f8e0ac8b --- /dev/null +++ b/sources/shaders/assets/SDSL/RenderTests/SwitchGenericLabels.sdsl @@ -0,0 +1,48 @@ +// PSMain(ExpectedResult=#FFFFFFFF, cbuffer.Test=(Test1=3)) +// PSMain(ExpectedResult=#7F7F7F7F, cbuffer.Test=(Test1=4)) +// PSMain(ExpectedResult=#3F3F3F3F, cbuffer.Test=(Test1=7)) +// PSMain(ExpectedResult=#00000000, cbuffer.Test=(Test1=99)) + +namespace Stride.Shaders.Tests; + +shader SwitchGenericLabelsBase +{ + static const int NextMode = TMode + 1; + + float Pick(int selector) + { + float v = 0.0; + switch (selector) + { + case TMode: + v = 1.0; + break; + case NextMode: + v = 127.0 / 255.0; + break; + case 7: + v = 63.0 / 255.0; + break; + default: + v = 0.0; + break; + } + return v; + } +} + +shader SwitchGenericLabels : SwitchGenericLabelsBase<3> +{ + stream float4 ColorTarget : SV_Target0; + + cbuffer Test + { + int Test1; + } + + void PSMain() + { + float v = Pick(Test1); + streams.ColorTarget = float4(v, v, v, v); + } +} diff --git a/sources/shared/SharedAssemblyInfo.cs b/sources/shared/SharedAssemblyInfo.cs index 226a0c1cf..87ea0c9a2 100644 --- a/sources/shared/SharedAssemblyInfo.cs +++ b/sources/shared/SharedAssemblyInfo.cs @@ -21,17 +21,17 @@ namespace Stride; /// This file is the source of truth for the version: it is the committed value, bumped per release (not derived /// from git tags). The generators (StrideVersionTasks.cs) read + + /// and overlay them into a single generated copy, SharedAssemblyInfo.Generated.cs, -/// which the Stride SDK swaps in at build time (adding the -devN worktree suffix on dev builds, +g<sha> on -/// package builds). Keep the shape of the MajorMinor/Patch/NuGetVersionSuffix/PublicVersion/BuildMetadata lines -/// (name = "value";) so the regexes match. Its is a sentinel (see below). +/// which the Stride SDK swaps in at build time (the suffix on package builds only, with +g<sha>; dev builds use +/// the -devN worktree suffix instead). Keep the shape of the MajorMinor/Patch/NuGetVersionSuffix/PublicVersion/ +/// BuildMetadata lines (name = "value";) so the regexes match. Its is a sentinel (see below). /// internal class StrideVersion { // ── Editable inputs ────────────────────────────────────────────────────────────────────────────────── // The version is MajorMinor.Patch + NuGetVersionSuffix. Edit these and bump per release — usually that's // release.yml bumping Patch automatically after a stable release, but you edit by hand to start a new - // major/minor cycle or a beta. (Patch is a string, not an int, only because a const string can't concatenate - // an int.) Don't edit the derived consts below. + // major/minor cycle, a beta, or the next beta. (Patch is a string, not an int, only because a const string + // can't concatenate an int.) Don't edit the derived consts below. /// /// Release line. The single source for major.minor; pins . Bump when starting a new @@ -47,16 +47,14 @@ internal class StrideVersion /// /// The prerelease suffix (e.g. -beta1), a cosmetic, NuGet-ordered label — asset upgraders ignore it (they gate on - /// the numeric version). Empty for a stable release. The generators overlay it with -devN on dev builds. + /// the numeric version). Empty for a stable release. Commit it before releasing a prerelease: release.yml and + /// release-samples.yml read it from here. Only package builds apply it; dev and CI builds stay at + /// MajorMinor.Patch (+ -devN), so the prereleases of one version share one dev version. /// - public const string NuGetVersionSuffix = ""; + public const string NuGetVersionSuffix = "-beta8"; - /// - /// Base version of the content-versioned template packages (Starters, Samples), independent of the engine - /// version. The bridge query and Stride.Templates.Common.targets pack both append - /// to it (one source, so they can't drift); StrideSamplesVersion.props reads it for the build. - /// - public const string SamplesVersion = "4.4.0"; + // The content template version (samples) is not here: it lives in sources/templates/StrideSamplesVersion.props, + // so a samples release rebuilds only Stride.Assets, not every assembly. // ── Derived / overlaid ─────────────────────────────────────────────────────────────────────────────── diff --git a/sources/targets/Stride.NuGetVersion.props b/sources/targets/Stride.NuGetVersion.props index 53d500d6c..8659af0da 100644 --- a/sources/targets/Stride.NuGetVersion.props +++ b/sources/targets/Stride.NuGetVersion.props @@ -14,7 +14,9 @@ <_StridePackageMajorMinor>$([System.Text.RegularExpressions.Regex]::Match($(_StrideSharedAssemblyInfoLines), `.*MajorMinor = \"(.*)\";.*`).Groups[1].Value) <_StridePackagePatch>$([System.Text.RegularExpressions.Regex]::Match($(_StrideSharedAssemblyInfoLines), `.*[^a-zA-Z]Patch = \"(.*)\";.*`).Groups[1].Value) <_StridePackagePublicVersion>$(_StridePackageMajorMinor).$(_StridePackagePatch) - $([System.Text.RegularExpressions.Regex]::Match($(_StrideSharedAssemblyInfoLines), `.*NuGetVersionSuffix = \"(.*)\";.*`).Groups[1].Value) + + $([System.Text.RegularExpressions.Regex]::Match($(_StrideSharedAssemblyInfoLines), `.*NuGetVersionSuffix = \"(.*)\";.*`).Groups[1].Value) $([System.Text.RegularExpressions.Regex]::Match($(_StrideSharedAssemblyInfoLines), `.*BuildMetadata = \"(.*)\";.*`).Groups[1].Value) diff --git a/sources/targets/StrideVersionTasks.cs b/sources/targets/StrideVersionTasks.cs index f9cb200cd..c466b21b8 100644 --- a/sources/targets/StrideVersionTasks.cs +++ b/sources/targets/StrideVersionTasks.cs @@ -7,8 +7,9 @@ // The version is the committed value in SharedAssemblyInfo.cs (MajorMinor.Patch + NuGetVersionSuffix); it is // bumped per release rather than derived from git tags. Both generators // overlay that value into a generated copy; they differ only in their wrapper concerns: -// ResolveStrideWorktreeVersion - dev builds: per-checkout ledger -> -devN suffix, cached, no metadata. -// StrideGitVersion - release/package builds: release suffix + "+g" build metadata, no ledger. +// ResolveStrideWorktreeVersion - dev and CI builds: MajorMinor.Patch without the committed suffix (so the betas of +// one version share one dev version), per-checkout ledger -> -devN, cached, no metadata. +// StrideGitVersion - release/package builds: committed suffix + "+g" build metadata, no ledger. using System; using System.Collections.Generic; using System.Diagnostics; @@ -108,9 +109,9 @@ public class ResolveStrideWorktreeVersion : Task string ledgerPath = !string.IsNullOrWhiteSpace(LedgerPath) ? LedgerPath : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "stride", "worktree-ids.txt"); - // Stamp = ledger mtime + committed version+suffix, so a manual worktree-ID remap or a committed - // version/suffix bump (edit to SharedAssemblyInfo.cs) invalidates the generated overlay. - string stamp = LedgerStamp(ledgerPath) + "|" + ReadCommittedVersion(); + // Stamp = ledger mtime + source file hash, so a manual worktree-ID remap or any edit to + // SharedAssemblyInfo.cs invalidates the generated overlay. + string stamp = LedgerStamp(ledgerPath) + "|" + SourceVersionFileHash(); if (string.IsNullOrWhiteSpace(OverrideId) && TryFastPath(stamp)) return true; Directory.CreateDirectory(Path.GetDirectoryName(ledgerPath)); @@ -217,15 +218,14 @@ public class ResolveStrideWorktreeVersion : Task private void WriteGeneratedFile(string suffix, string stamp) { - string majorMinor, version, baseSuffix; - StrideVersionUtil.ReadInputs(SourceVersionFile, out majorMinor, out version, out baseSuffix); + string majorMinor, version; + StrideVersionUtil.ReadInputs(SourceVersionFile, out majorMinor, out version, out _); - // Compose the committed suffix with the worktree suffix so the const matches the package: the consumer - // overlay replaces the whole NuGetVersionSuffix, so it must carry both (e.g. "-beta1" + "-dev3"), whereas the - // producer reads the committed "-beta1" from the file and appends only the worktree "-dev3" (WorktreeSuffix - // output stays worktree-only for that append). Always emit the overlay (even when it equals the committed - // version): the base file's PublicVersion is a sentinel, so leaving the base to compile would ship it. - string patched = StrideVersionUtil.Overlay(File.ReadAllText(SourceVersionFile), majorMinor, version, baseSuffix + suffix, null); + // The committed suffix (-beta1) is for release builds only: here the suffix is the worktree one alone, as on + // the producer side (Stride.NuGetVersion.props), so the const matches the package. Always emit the overlay + // (even when it equals the committed version): the base file's PublicVersion is a sentinel, so leaving the + // base to compile would ship it. + string patched = StrideVersionUtil.Overlay(File.ReadAllText(SourceVersionFile), majorMinor, version, suffix, null); // Cache key = stamp + resolved worktree suffix; TryFastPath reuses the file while the stamp matches. patched += "\n// version-stamp: " + stamp + "|" + suffix + "\n"; if (!File.Exists(GeneratedVersionFile) || File.ReadAllText(GeneratedVersionFile) != patched) @@ -233,22 +233,25 @@ public class ResolveStrideWorktreeVersion : Task Generated = true; } - // The committed version + suffix from the source file; "" if unreadable. Part of the cache stamp so a committed - // version or suffix bump invalidates the generated overlay. - private string ReadCommittedVersion() + // Hash of the whole source file; "" if unreadable. Part of the cache stamp so any edit to it (version, suffix) + // invalidates the generated overlay. + private string SourceVersionFileHash() { try { - string mm, mv, sfx; - StrideVersionUtil.ReadInputs(SourceVersionFile, out mm, out mv, out sfx); - return mv + sfx; + // FNV-1a: a change detector, not a signature. The task is compiled by RoslynCodeTaskFactory, so it can + // only use what the MSBuild host ships (no System.IO.Hashing). + ulong hash = 14695981039346656037; + foreach (var b in File.ReadAllBytes(SourceVersionFile)) + hash = (hash ^ b) * 1099511628211; + return hash.ToString("x16"); } catch { return string.Empty; } } private bool TryFastPath(string stamp) { - // Reuse the generated file when the stamp (override, ledger mtime, committed version) is unchanged; the file + // Reuse the generated file when the stamp (ledger mtime, source file hash) is unchanged; the file // marker is "stamp|suffix", so it must start with stamp + "|". if (!File.Exists(GeneratedVersionFile)) return false; @@ -319,7 +322,6 @@ public class StrideGitVersion : Task [Required] public string RootDirectory { get; set; } [Required] public string VersionFile { get; set; } [Required] public string GeneratedVersionFile { get; set; } - public string SuffixOverride { get; set; } [Output] public string NuGetVersion { get; set; } @@ -333,9 +335,6 @@ public class StrideGitVersion : Task string majorMinor, version, suffix; StrideVersionUtil.ReadInputs(sourcePath, out majorMinor, out version, out suffix); - // SuffixOverride (from -p:StrideVersionSuffix) overrides the committed suffix; it's the bare word. - if (!string.IsNullOrEmpty(SuffixOverride)) - suffix = "-" + SuffixOverride.TrimStart('-'); string sha = StrideVersionUtil.Git(RootDirectory, "rev-parse HEAD").Substring(0, 8); diff --git a/sources/templates/README.md b/sources/templates/README.md index e6eaf779d..f8b56901c 100644 --- a/sources/templates/README.md +++ b/sources/templates/README.md @@ -65,12 +65,19 @@ Common parameters (template-dependent): ## Developing locally -Building any of the three template projects produces a `.nupkg` in `bin/packages/` and auto-deploys it to `%LocalAppData%\stride\nugetdev` so the GameStudio bridge picks it up on next editor launch: +Building `Stride.Templates.Games` produces its `.nupkg` in `bin/packages/` and auto-deploys it to `%LocalAppData%\stride\nugetdev` so the GameStudio bridge picks it up on next editor launch: ```bash dotnet build sources/templates/Stride.Templates.Games/Stride.Templates.Games.csproj ``` +The three content packages (Starters, Samples, AssetPacks) are **not packed by a normal build**: GameStudio and the CLI use the published content version the engine names (see [Sample versioning](#sample-versioning)). While editing samples, opt the pack in; it is worktree-suffixed (`4.4.0-beta7-dev2`) and wins over the published content on this checkout. A build with the flag off removes it again: + +```bash +dotnet build sources/templates/Stride.Templates.Samples -p:StridePackContentTemplates=true # or set StridePackContentTemplates in build/Stride.Local.props +dotnet msbuild build/Stride.Samples.build -t:CleanContentTemplates # remove the dev packs without a build +``` + Opt in to register the freshly-built `.nupkg` with your global `dotnet new` registry on every build — handy when iterating on template content and testing via CLI: ```bash @@ -101,17 +108,19 @@ dotnet pack sources/templates/Stride.Templates.Games -p:StridePackageBuild=true ## Sample versioning -In-repo samples are committed at a **clean release version**, but locally only the `-devN` dev packages exist — so switch them to the local dev version to build/edit (e.g. in GameStudio), and back before committing. `SamplesToDevVersion` rewrites every `Stride.*` reference in the sample csprojs to this checkout's dev build (real edits); `SamplesToReleaseVersion` rewrites them back to the clean version. `Stride.Templates.Games` is engine-versioned; `Stride.Templates.Samples` + `.Games.Starters` + `.AssetPacks` are content-versioned at `StrideSamplesVersion`. Full details (engine version, `-devN`, release flow, the `StrideSamplesVersion` authority) — including why — are in **[docs/build/versioning.md](../../docs/build/versioning.md)**. +In-repo samples are committed at a **clean release version**, but locally only the `-devN` dev packages exist — so switch them to the local dev version to build/edit (e.g. in GameStudio), and back before committing. `SamplesToDevEngine` rewrites every `Stride.*` reference in the sample csprojs to this checkout's dev build (real edits); `SamplesToReleaseEngine` rewrites them back to the clean version. + +`Stride.Templates.Games` is engine-versioned. `Stride.Templates.Samples` + `.Games.Starters` + `.AssetPacks` are content-versioned: `StrideSamplesVersion` ([`StrideSamplesVersion.props`](StrideSamplesVersion.props)) is the exact published content version the engine uses. Sample changes are merged like any other change; `release-samples.yml` then publishes them under the next content version and commits that version. Full details (engine version, `-devN`, the samples release, the release order, the resolver) — including why — are in **[docs/build/versioning.md](../../docs/build/versioning.md)**. ```bash -dotnet msbuild build/Stride.Samples.build -t:SamplesToDevVersion # before editing/building (e.g. GameStudio) -dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseVersion # before committing -dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion # full release bump +dotnet msbuild build/Stride.Samples.build -t:SamplesToDevEngine # before editing/building (e.g. GameStudio) +dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseEngine # before committing +dotnet msbuild build/Stride.Samples.build -t:PrepareSamplesForRelease # upgrade the samples to this engine and pack them ``` ## Adding a new template sample -1. **Author the sample** at `samples////`. Standard Stride sample layout: `.Game/.Game.csproj` + `.Game/.Game.sdpkg`, optional `.Windows/.Windows.csproj` (and other per-platform exec dirs), `Assets/`, `Resources/`. The sample must `dotnet build` cleanly on its own (after `SamplesToDevVersion` — see [Sample versioning](#sample-versioning)) — the preprocessor only stages and transforms, it doesn't fix broken inputs. +1. **Author the sample** at `samples////`. Standard Stride sample layout: `.Game/.Game.csproj` + `.Game/.Game.sdpkg`, optional `.Windows/.Windows.csproj` (and other per-platform exec dirs), `Assets/`, `Resources/`. The sample must `dotnet build` cleanly on its own (after `SamplesToDevEngine` — see [Sample versioning](#sample-versioning)) — the preprocessor only stages and transforms, it doesn't fix broken inputs. 2. **Drop a `.sdtpl`** at `samples////.sdtpl`: @@ -171,8 +180,8 @@ and instantiates each selected pack into the generated game library. - **[`sources/tools/Stride.TemplateGenerator/TemplatePreprocessor.cs`](../tools/Stride.TemplateGenerator/TemplatePreprocessor.cs)** — the preprocess pipeline (sample → dotnet new template content). Pipeline steps inline-documented at the top of `Run`. - **[`sources/tools/Stride.TemplateGenerator/Program.cs`](../tools/Stride.TemplateGenerator/Program.cs)** — `preprocess-template` and `aggregate-sdtpls` subcommand dispatch. - **[`sources/templates/Stride.Templates.Common.targets`](Stride.Templates.Common.targets)** — shared MSBuild logic across the three packages (version derivation, content-versioning via `StrideSamplesVersion`, preprocess+aggregate Exec, auto-pack-deploy, CI safeguards, `StrideInstallTemplate` opt-in target). -- **[`../../build/Stride.Samples.build`](../../build/Stride.Samples.build)** — standalone sample version management: `SamplesToDevVersion` / `SamplesToReleaseVersion` (flip in-repo samples between local-dev and clean) and `UpgradeSamplesVersion` (full release bump: dev → asset migration → clean). Uses [`StrideSamplesVersion.props`](StrideSamplesVersion.props) (the content-version authority) and [`../targets/Stride.GitVersion.targets`](../targets/Stride.GitVersion.targets) (the `StrideGitVersion` task, shared with `Stride.build`). -- **[`sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateBridge.cs`](../editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateBridge.cs)** — GameStudio side: probes the three packages via `PackageStore` (content-versioned ones resolved by range `<=` engine version), installs into the editor's isolated TemplateEngine profile, registers each template as a `TemplateDotNetNewDescription` with `TemplateManager`. +- **[`../../build/Stride.Samples.build`](../../build/Stride.Samples.build)** — standalone sample version management: `SamplesToDevEngine` / `SamplesToReleaseEngine` (flip in-repo samples between local-dev and clean) and `UpgradeSamples` (full release bump: dev → asset migration → clean). Uses [`StrideSamplesVersion.props`](StrideSamplesVersion.props) (the content-version authority) and [`../targets/Stride.GitVersion.targets`](../targets/Stride.GitVersion.targets) (the `StrideGitVersion` task, shared with `Stride.build`). +- **[`sources/engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs`](../engine/Stride.Assets/Templates/DotNetNewTemplateBridge.cs)** — GameStudio side: resolves the four packages (Games at the engine version, the content packages through [`ContentTemplateResolver`](../engine/Stride.Assets/Templates/ContentTemplateResolver.cs), shared by source with the CLI), installs into the editor's isolated TemplateEngine profile, registers each template as a `TemplateDotNetNewDescription` with `TemplateManager`. - **[`sources/editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateGenerator.cs`](../editor/Stride.Assets.Presentation.Wpf/Templates/DotNetNewTemplateGenerator.cs)** — GameStudio session integration: dispatches instantiation through the registry, registers per-platform exec projects with the session post-load. ## Future work diff --git a/sources/templates/Stride.Templates.Common.targets b/sources/templates/Stride.Templates.Common.targets index 2d362f062..4f4037472 100644 --- a/sources/templates/Stride.Templates.Common.targets +++ b/sources/templates/Stride.Templates.Common.targets @@ -15,6 +15,11 @@ Imported here (vanilla Microsoft.NET.Sdk, outside the Stride SDK chain). -p:PackageVersion=X pins explicitly. --> + + + net10.0 true @@ -28,11 +33,27 @@ https://github.com/stride3d/stride git - - $(StrideSamplesVersion)$(StrideNuGetVersionSuffix) + + $(StrideSamplesVersion) $(StrideNuGetVersion) + + true + false + <_StrideContentPackEnabled Condition="'$(StrideTemplateContentVersioned)' != 'true' Or '$(StridePackContentTemplates)' == 'true'">true + false + + true + false true + $(PackageVersion)$(StrideWorktreeSuffix) + + <_StrideContentPackFeedGlobs>$(StrideRoot)bin\packages\$(PackageId).*.nupkg;$(LocalAppData)\stride\nugetdev\$(PackageId).*.nupkg + + + + + + <_StrideFeedContentPack Include="$(_StrideContentPackFeedGlobs)" /> + <_StrideLocalContentPack Include="@(_StrideFeedContentPack)" Condition="'$(StrideWorktreeSuffix)' != '' And $([System.String]::Copy('%(Filename)').EndsWith('$(StrideWorktreeSuffix)'))" /> + <_StridePublishedLikeContentPack Include="@(_StrideFeedContentPack)" Condition="!$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '-dev[0-9]*$'))" /> + <_StrideLocalContentPackCacheDir Include="$([System.IO.Directory]::GetDirectories('$(NuGetPackageRoot)$(PackageId.ToLowerInvariant())', '*$(StrideWorktreeSuffix)'))" + Condition="'$(NuGetPackageRoot)' != '' And '$(StrideWorktreeSuffix)' != '' And Exists('$(NuGetPackageRoot)$(PackageId.ToLowerInvariant())')" /> + + + + + + + + + + + - + static pre-expanded inputs, and MSBuild only globs wildcards written literally (can't be batched here). + A content pack that is not produced declares no output (nothing to be out of date against). --> + + + + <_StrideLocalContentPackGlobs>$(StrideRoot)bin\packages\$(PackageId).*$(_StrideEvalWorktreeSuffix).nupkg;$(LocalAppData)\stride\nugetdev\$(PackageId).*$(_StrideEvalWorktreeSuffix).nupkg + + + <_StrideLeftoverContentPack Include="$(_StrideLocalContentPackGlobs)" /> + + - + - + + - $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\shared\SharedAssemblyInfo.cs')), `.*SamplesVersion = \"(.*)\";.*`).Groups[1].Value) + <_StrideCommittedSamplesVersion>4.4.0 + $(_StrideCommittedSamplesVersion) diff --git a/sources/tests/tools/Stride.TextureConverter.Tests/TextureToolTest.cs b/sources/tests/tools/Stride.TextureConverter.Tests/TextureToolTest.cs index 69f2878eb..45b4bd084 100644 --- a/sources/tests/tools/Stride.TextureConverter.Tests/TextureToolTest.cs +++ b/sources/tests/tools/Stride.TextureConverter.Tests/TextureToolTest.cs @@ -757,6 +757,28 @@ namespace Stride.TextureConverter.Tests } } + // Lanczos3 (the asset compiler's filter) only exists in ImageSharp, which handles 8-bit BGRA/RGBA; + // other formats must still resize (issue #3441). + [Theory] + [InlineData(PixelFormat.B8G8R8A8_UNorm)] + [InlineData(PixelFormat.R16G16B16A16_Float)] + [InlineData(PixelFormat.R16_Float)] + [InlineData(PixelFormat.R32G32B32A32_Float)] + public void ResizeLanczos3AnyFormatTest(PixelFormat format) + { + using var image = texTool.Load(Module.PathToInputImages + "BgraSheet.dds"); + if (image.Format != format) + texTool.Convert(image, format); + int width = image.Width; + int height = image.Height; + + texTool.Resize(image, width / 2, height / 2, Filter.Rescaling.Lanczos3); + + Assert.Equal(width / 2, image.Width); + Assert.Equal(height / 2, image.Height); + Assert.Equal(format, image.Format); + } + private class AlphaLevelTest { public Rectangle Region; diff --git a/sources/tools/Stride.Templates.Tests/StrideGameTemplateSmokeTests.cs b/sources/tools/Stride.Templates.Tests/StrideGameTemplateSmokeTests.cs index 7e914374e..1fd4285a8 100644 --- a/sources/tools/Stride.Templates.Tests/StrideGameTemplateSmokeTests.cs +++ b/sources/tools/Stride.Templates.Tests/StrideGameTemplateSmokeTests.cs @@ -64,9 +64,11 @@ public class StrideGameTemplateSmokeTests string? nupkg = FindNupkg(enginePackagesDir, packageId); if (nupkg == null) { - output.WriteLine($"No pre-built nupkg for {packageId}; running dotnet pack."); - var packResult = RunDotnet(repoRoot, "pack", csproj, "-c", "Debug", "--nologo", "-v", "minimal"); - Assert.True(packResult.exitCode == 0, $"dotnet pack {packageId} failed:\n{packResult.output}"); + output.WriteLine($"No pre-built nupkg for {packageId}; building it."); + // These projects pack on build ('dotnet pack' would skip building the preprocessor they use). The + // content packs (Starters, Samples, AssetPacks) only pack when opted in; Games ignores the flag. + var packResult = RunDotnet(repoRoot, "build", csproj, "-c", "Debug", "--nologo", "-v", "minimal", "-p:StridePackContentTemplates=true"); + Assert.True(packResult.exitCode == 0, $"dotnet build {packageId} failed:\n{packResult.output}"); nupkg = FindNupkg(enginePackagesDir, packageId); } Assert.NotNull(nupkg); diff --git a/sources/tools/Stride.TextureConverter/Backend/TexLibraries/ImageSharpTexLib.cs b/sources/tools/Stride.TextureConverter/Backend/TexLibraries/ImageSharpTexLib.cs index 513c18e4d..d749627f0 100644 --- a/sources/tools/Stride.TextureConverter/Backend/TexLibraries/ImageSharpTexLib.cs +++ b/sources/tools/Stride.TextureConverter/Backend/TexLibraries/ImageSharpTexLib.cs @@ -65,7 +65,7 @@ namespace Stride.TextureConverter.TexLibraries return IsSupportedFile(((ExportRequest)request).FilePath); case RequestType.Rescaling: - return ((RescalingRequest)request).Filter != Filter.Rescaling.Nearest; + return ((RescalingRequest)request).Filter != Filter.Rescaling.Nearest && IsSupportedPixelFormat(imageFormat, out _); case RequestType.SwitchingChannels: case RequestType.GammaCorrection: diff --git a/sources/tools/Stride.TextureConverter/Frontend/TextureTool.cs b/sources/tools/Stride.TextureConverter/Frontend/TextureTool.cs index 0d1c2d759..5e5586bd7 100644 --- a/sources/tools/Stride.TextureConverter/Frontend/TextureTool.cs +++ b/sources/tools/Stride.TextureConverter/Frontend/TextureTool.cs @@ -1458,6 +1458,13 @@ namespace Stride.TextureConverter image.CurrentLibrary = library; } + else if (request is RescalingRequest rescaling && rescaling.Filter != Filter.Rescaling.Bicubic) + { + // Only ImageSharp implements this filter, and it handles 8-bit BGRA/RGBA only. + // DxtTexLib rescales every uncompressed format, so retry with the best filter it offers. + Log.Warning($"No library can rescale a {image.Format} image with the {rescaling.Filter} filter, falling back to {Filter.Rescaling.Bicubic}."); + ExecuteRequest(image, new FixedRescalingRequest(rescaling.ComputeWidth(image), rescaling.ComputeHeight(image), Filter.Rescaling.Bicubic)); + } else // If no library could be found, an exception is thrown { // No library was found, attempt to execute the request using a 2-step cast