diff --git a/.github/workflows/Check_commit_message.yml b/.github/workflows/Check_commit_message.yml deleted file mode 100644 index f84d60828..000000000 --- a/.github/workflows/Check_commit_message.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Commit Message Check - -on: - workflow_call: - -jobs: - commit-message: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Validate commit messages - run: | - BASE="${{ github.event.pull_request.base.sha }}" - HEAD="${{ github.event.pull_request.head.sha }}" - - COMMITS=$(git log --format="%H %s" "${BASE}..${HEAD}" --) - - if [ -z "${COMMITS}" ]; then - echo "No commits found in this PR." - exit 0 - fi - - FAILED=0 - while IFS= read -r LINE; do - SHA="${LINE%% *}" - MSG="${LINE#* }" - SHORT="${SHA::7}" - - if echo "${MSG}" | grep -qP '^\[.+?\]'; then - echo "✅ ${SHORT}: ${MSG}" - else - echo "❌ ${SHORT}: ${MSG}" - echo "::error::Commit ${SHORT} is missing a [Tag] prefix. Expected format: [Tag] Description (e.g. [CPU] Fix overflow in JIT)" - FAILED=1 - fi - done <<< "${COMMITS}" - - echo "" - - if [ "${FAILED}" -eq 1 ]; then - echo "::error::One or more commits are missing a [Tag] prefix." - echo "" - echo "Expected format: [Tag] Description" - echo "Examples: [CPU] " - echo " [GPU] " - echo " [UI] " - echo " [CI] " - exit 1 - fi - - echo "All commit messages have a valid [Tag] prefix." diff --git a/.github/workflows/Lint.yml b/.github/workflows/Lint.yml deleted file mode 100644 index 65b59c294..000000000 --- a/.github/workflows/Lint.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Lint - -on: - workflow_call: - -jobs: - lint: - name: Lint - runs-on: ubuntu-24.04 - outputs: - #LLVM_VERSION: ${{ steps.setup.outputs.LLVM_VERSION }} - UBUNTU_BASE: ${{ steps.setup.outputs.UBUNTU_BASE }} - steps: - - uses: actions/checkout@v6 - - name: Setup - id: setup - env: - LLVM_VERSION: 20 - run: | - UBUNTU_BASE=$(lsb_release -cs) - #echo "LLVM_VERSION=$LLVM_VERSION" >> "$GITHUB_OUTPUT" - echo "UBUNTU_BASE=$UBUNTU_BASE" >> "$GITHUB_OUTPUT" - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-$LLVM_VERSION main" - sudo apt-get -y update - sudo apt-get -y install clang-format-$LLVM_VERSION - - name: Lint - run: ./xenia-build.py lint --all - diff --git a/.github/workflows/Linux_x86.yml b/.github/workflows/Linux_x86.yml deleted file mode 100644 index 4e3b38838..000000000 --- a/.github/workflows/Linux_x86.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: Linux Build - -on: - workflow_call: - inputs: - config: - description: 'Build configuration (Release)' - required: false - default: 'release' - type: string - -jobs: - build: - name: Build - runs-on: ubuntu-24.04 - env: - LLVM_VERSION: 20 - UBUNTU_BASE: noble # We're running on ubuntu-24.04. Remember to change it after changing deploy env. - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Cache Vulkan SDK - id: cache-vulkan-sdk-linux - uses: actions/cache@v5 - with: - path: ~/vulkan-sdk - key: ${{ runner.os }}-vulkan-sdk-latest - - - name: Cache linuxdeploy tools - id: cache-linuxdeploy - uses: actions/cache@v5 - with: - path: ~/linuxdeploy - key: ${{ runner.os }}-linuxdeploy-tools - - - name: Setup build environment - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - sudo apt-add-repository "deb http://apt.llvm.org/${{ env.UBUNTU_BASE }}/ llvm-toolchain-${{ env.UBUNTU_BASE }}-${{ env.LLVM_VERSION }} main" - sudo apt-get -y update - sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-${{ env.LLVM_VERSION }} lld-${{ env.LLVM_VERSION }} ninja-build cmake spirv-tools libfuse2 - - # Pin LLVM tools to the correct version so system ar/ranlib/lld - # don't use the older LLVM 17 gold plugin with clang-20 LTO bitcode - sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/ld.lld ld.lld /usr/bin/ld.lld-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/llvm-ar llvm-ar /usr/bin/llvm-ar-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/llvm-ranlib llvm-ranlib /usr/bin/llvm-ranlib-${{ env.LLVM_VERSION }} 200 - sudo update-alternatives --install /usr/bin/llvm-nm llvm-nm /usr/bin/llvm-nm-${{ env.LLVM_VERSION }} 200 - - # Download linuxdeploy tools if not cached - if [ '${{ steps.cache-linuxdeploy.outputs.cache-hit }}' != 'true' ]; then - mkdir -p ~/linuxdeploy - wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O ~/linuxdeploy/linuxdeploy - chmod +x ~/linuxdeploy/linuxdeploy - fi - echo "$HOME/linuxdeploy" >> $GITHUB_PATH - - - name: Install Vulkan SDK - run: | - # Install Vulkan SDK - if [ '${{ steps.cache-vulkan-sdk-linux.outputs.cache-hit }}' != 'true' ]; then - wget -qO vulkan-sdk.tar.xz https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz - mkdir -p ~/vulkan-sdk - tar -xf vulkan-sdk.tar.xz -C ~/vulkan-sdk - fi - - VULKAN_SDK_VERSION=$(ls ~/vulkan-sdk) - echo "VULKAN_SDK=$HOME/vulkan-sdk/$VULKAN_SDK_VERSION/x86_64" >> $GITHUB_ENV - echo "$HOME/vulkan-sdk/$VULKAN_SDK_VERSION/x86_64/bin" >> $GITHUB_PATH - - # Verify shader tools are available - for tool in glslangValidator spirv-opt spirv-dis; do - which "$tool" && "$tool" --version 2>/dev/null || echo "Warning: $tool not found" - done - - - name: Download submodules - run: | - # Exclude not needed 3pp modules - EXCLUDE="DirectXShaderCompiler" - SUBMODULES=$(grep -oP '(?<=path = ).+' .gitmodules | grep -vE "$EXCLUDE") - git submodule update --init --depth=1 -j$(nproc) $SUBMODULES - - - name: Build Xenia - env: - CC: clang-${{ env.LLVM_VERSION }} - CXX: clang++-${{ env.LLVM_VERSION }} - run: ./xenia-build.py build --config=Release - - - name: Prepare AppImage - id: prepare_artifacts - run: | - binary=build/bin/Linux/Release/xenia_canary - if [ $(stat -c%s $binary) -le 100000 ]; then - echo "::error::Binary is too small." - exit 1 - fi - chmod +x $binary - - # Set up AppDir structure - APPDIR=artifacts/xenia_canary - mkdir -p $APPDIR/usr/bin - mkdir -p $APPDIR/usr/share/icons/hicolor/16x16/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/32x32/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/48x48/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/64x64/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/128x128/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/512x512/apps - mkdir -p $APPDIR/usr/share/icons/hicolor/1024x1024/apps - - # Install icons - cp assets/icon/16.png $APPDIR/usr/share/icons/hicolor/16x16/apps/xenia_canary.png - cp assets/icon/32.png $APPDIR/usr/share/icons/hicolor/32x32/apps/xenia_canary.png - cp assets/icon/48.png $APPDIR/usr/share/icons/hicolor/48x48/apps/xenia_canary.png - cp assets/icon/64.png $APPDIR/usr/share/icons/hicolor/64x64/apps/xenia_canary.png - cp assets/icon/128.png $APPDIR/usr/share/icons/hicolor/128x128/apps/xenia_canary.png - cp assets/icon/256.png $APPDIR/usr/share/icons/hicolor/256x256/apps/xenia_canary.png - cp assets/icon/512.png $APPDIR/usr/share/icons/hicolor/512x512/apps/xenia_canary.png - cp assets/icon/1024.png $APPDIR/usr/share/icons/hicolor/1024x1024/apps/xenia_canary.png - - # Copy any extra runtime data directories alongside the binary - find build/bin/Linux/Release -maxdepth 1 -type d ! -name Release -exec cp -r {} $APPDIR/usr/bin/ \; - - # Build AppImage with linuxdeploy (requires absolute paths) - linuxdeploy --appdir $APPDIR \ - --executable $binary \ - --desktop-file $GITHUB_WORKSPACE/assets/xenia_canary.desktop \ - --icon-file $GITHUB_WORKSPACE/assets/icon/256.png \ - --output appimage - - # Find the generated AppImage and move to release directory - appimage_file=$(ls -1 *.AppImage | head -n1) - if [ -z "$appimage_file" ]; then - echo "::error::AppImage file not found after linuxdeploy" - exit 1 - fi - - mkdir -p artifacts/release - mv "$appimage_file" artifacts/release/xenia_canary_linux.AppImage - chmod +x artifacts/release/xenia_canary_linux.AppImage - - - name: Upload Xenia Canary artifact - if: steps.prepare_artifacts.outcome == 'success' - uses: actions/upload-artifact@v7 - with: - name: xenia_canary_linux - path: artifacts/release - if-no-files-found: error - retention-days: 7 diff --git a/.github/workflows/Orchestrator.yml b/.github/workflows/Orchestrator.yml deleted file mode 100644 index b144c01be..000000000 --- a/.github/workflows/Orchestrator.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Orchestrator - -on: - push: - paths-ignore: - - '*.md' - - 'docs/**' - - 'LICENSE' - pull_request: - paths-ignore: - - '*.md' - - 'docs/**' - - 'LICENSE' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # =========================================================================== - # Pre-requirements - # =========================================================================== - commit-message: - name: Commit Message Validation - if: github.event_name == 'pull_request' - uses: ./.github/workflows/Check_commit_message.yml - - lint: - name: Lint - uses: ./.github/workflows/Lint.yml - - # Add optional steps here - - # =========================================================================== - # Stage 2: Platform builds - # =========================================================================== - build-windows: - name: Windows (x86-64) - needs: [lint, commit-message] - if: ${{ !failure() && !cancelled() }} - uses: ./.github/workflows/Windows_x86.yml - - build-linux: - name: Linux (x86-64) - needs: [lint, commit-message] - if: ${{ !failure() && !cancelled() }} - uses: ./.github/workflows/Linux_x86.yml - - # Uncomment when platform support is ready: - # build-windows-arm64: - # name: Windows-ARM64 - # needs: [lint] - # uses: ./.github/workflows/build-win_arm64.yml - - # build-linux-arm64: - # name: Linux-ARM64 - # needs: [lint] - # uses: ./.github/workflows/build-linux_arm64.yml - - # build-macos: - # name: macOS - # needs: [lint] - # uses: ./.github/workflows/build-macos.yml - - # build-android: - # name: Android - # needs: [lint] - # uses: ./.github/workflows/build-android.yml - - # =========================================================================== - # Stage 3: Release - # =========================================================================== - release: - name: Create Release - needs: [build-windows, build-linux] - if: | - always() && - github.repository == 'xenia-canary/xenia-canary' && - github.event_name == 'push' && - github.ref == 'refs/heads/canary_experimental' && - (needs.build-windows.result == 'success' && needs.build-linux.result == 'success') - uses: ./.github/workflows/Create_release.yml - permissions: - contents: write - secrets: inherit - with: - tag: ${{ github.sha }} - branch: ${{ github.ref_name }} diff --git a/.github/workflows/Windows_x86.yml b/.github/workflows/Windows_x86.yml deleted file mode 100644 index 82dbdddd8..000000000 --- a/.github/workflows/Windows_x86.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Windows (x86-64) - -on: - workflow_call: - inputs: - config: - description: 'Build configuration (Release)' - required: false - default: 'release' - type: string - -jobs: - build: - runs-on: windows-2025 - - env: - POWERSHELL_TELEMETRY_OPTOUT: 1 - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Cache Vulkan SDK - id: cache-vulkan-sdk - uses: actions/cache@v5 - with: - path: C:\VulkanSDK - key: ${{ runner.os }}-vulkan-sdk-${{ hashFiles('**/vulkan-sdk.exe') }} - restore-keys: | - ${{ runner.os }}-vulkan-sdk- - - - name: Install Vulkan SDK - run: | - # Install Vulkan SDK with spirv-tools - if (Test-Path -Path "C:\VulkanSDK") { - echo "Vulkan SDK found in cache." - } else { - Invoke-WebRequest -Uri "https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe" -OutFile "vulkan-sdk.exe" - Start-Process -FilePath "vulkan-sdk.exe" -ArgumentList "--accept-licenses", "--default-answer", "--confirm-command", "install" -Wait - } - $env:VULKAN_SDK = "C:\VulkanSDK\$(Get-ChildItem -Path 'C:\VulkanSDK' -Directory | Select-Object -First 1 -ExpandProperty Name)" - $env:PATH = "$env:VULKAN_SDK\Bin;$env:PATH" - echo "VULKAN_SDK=$env:VULKAN_SDK" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "$env:VULKAN_SDK\Bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - # Verify shader tools are available - foreach ($tool in @("glslangValidator", "spirv-opt", "spirv-dis")) { - $toolPath = "$env:VULKAN_SDK\Bin\$tool.exe" - if (Test-Path $toolPath) { - echo "$tool found at: $toolPath" - & $toolPath --version 2>$null - } else { - echo "Warning: $tool.exe not found at expected location" - } - } - - # Verify FXC is available (from Windows SDK) - $fxcPaths = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\fxc.exe" -ErrorAction SilentlyContinue | Sort-Object FullName - if ($fxcPaths) { - $fxcPath = $fxcPaths[-1].FullName - echo "FXC found at: $fxcPath" - } else { - echo "Warning: fxc.exe not found in Windows SDK" - } - - - name: Download submodules - run: git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS - - - name: Build Xenia - run: python xenia-build.py build --config=Release --target=xenia-app - - - name: Prepare artifacts - id: prepare_artifacts - run: | - if ((Get-Item 'build\bin\Windows\Release\xenia_canary.exe').Length -le 100000) { - echo "::error:: Executable is too small." - exit 1 - } - robocopy . build\bin\Windows\Release LICENSE /r:0 /w:0 - robocopy build\bin\Windows\Release artifacts\xenia_canary xenia_canary.exe LICENSE /r:0 /w:0 - If ($LastExitCode -le 7) { echo "LastExitCode = $LastExitCode";$LastExitCode = 0 } - - - name: Upload Xenia Canary artifact - if: steps.prepare_artifacts.outcome == 'success' - uses: actions/upload-artifact@v7 - with: - name: xenia_canary_windows - path: artifacts\xenia_canary - if-no-files-found: error - retention-days: 7 diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index fc585904c..f145270e5 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -604,7 +604,7 @@ class IConfigVarUpdate { // If you're reviewing a pull request with a change here, check if 1) has been // done by the submitter before merging. static constexpr uint32_t kLastCommittedUpdateDate = - MakeConfigVarUpdateDate(2026, 2, 16, 12); + MakeConfigVarUpdateDate(2026, 4, 9, 12); virtual ~IConfigVarUpdate() = default; diff --git a/src/xenia/base/testing/heap_test.cc b/src/xenia/base/testing/heap_test.cc index f83a435b6..b039cda46 100644 --- a/src/xenia/base/testing/heap_test.cc +++ b/src/xenia/base/testing/heap_test.cc @@ -85,14 +85,15 @@ TEST_CASE("heap_alloc_basic", "[heap]") { TEST_CASE("heap_alloc_top_down", "[heap]") { TestHeap h(0x80000000, 0x100000, 0x1000); + // Top-down treats high_page_number as exclusive, so the top page is + // never handed out. uint32_t addr = 0; REQUIRE(h.Alloc(0x1000, 0x1000, true, &addr)); - // Top-down: should be at the highest aligned address. - REQUIRE(addr == 0x800FF000); + REQUIRE(addr == 0x800FE000); REQUIRE(h.unreserved_page_count() == 255); REQUIRE(h.Alloc(0x2000, 0x1000, true, &addr)); - REQUIRE(addr == 0x800FD000); + REQUIRE(addr == 0x800FC000); REQUIRE(h.unreserved_page_count() == 253); } @@ -237,16 +238,18 @@ TEST_CASE("heap_alloc_alignment_top_down", "[heap]") { // 1MB heap, 4KB pages TestHeap h(0x80000000, 0x100000, 0x1000); - // Allocate 1 page at the top. + // Top-down skips the top page (0x800FF000), so a 1-page allocation + // lands on page 0xFE. uint32_t first = 0; REQUIRE(h.Alloc(0x1000, 0x1000, true, &first)); - REQUIRE(first == 0x800FF000); + REQUIRE(first == 0x800FE000); - // Allocate with 64KB alignment top-down — should align down. + // 64KB-aligned top-down: stride 16, exclusive high at page 0xFF, so + // the highest aligned base is page 0xE0. uint32_t aligned = 0; REQUIRE(h.Alloc(0x1000, 0x10000, true, &aligned)); REQUIRE((aligned % 0x10000) == 0); - REQUIRE(aligned == 0x800F0000); + REQUIRE(aligned == 0x800E0000); } // ============================================================================ diff --git a/src/xenia/base/testing/physical_heap_test.cc b/src/xenia/base/testing/physical_heap_test.cc new file mode 100644 index 000000000..7e83420f9 --- /dev/null +++ b/src/xenia/base/testing/physical_heap_test.cc @@ -0,0 +1,213 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/memory.h" + +#include "third_party/catch/include/catch.hpp" + +#include "xenia/base/memory.h" + +namespace xe { +namespace test { + +// All tests use kMemoryAllocationReserve which only touches the page table, +// not host memory. This lets us pass nullptr for membase and Memory*. + +TEST_CASE("PhysicalHeap::GetPhysicalAddress", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("heap with no offset returns heap-relative address") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + REQUIRE(heap.host_address_offset() == 0); + REQUIRE(heap.GetPhysicalAddress(0xA0000000) == 0); + REQUIRE(heap.GetPhysicalAddress(0xA0010000) == 0x10000); + REQUIRE(heap.GetPhysicalAddress(0xA1000000) == 0x1000000); + } + + SECTION("0xE0000000 heap always has 0x1000 physical offset") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + // The 0x1000 physical offset is baked into the view mapping + // (map_info target_address), not derived from host_address_offset. + REQUIRE(heap.GetPhysicalAddress(0xE0000000) == 0x1000); + REQUIRE(heap.GetPhysicalAddress(0xE0001000) == 0x2000); + REQUIRE(heap.GetPhysicalAddress(0xE0010000) == 0x11000); + } +} + +TEST_CASE("PhysicalHeap::Alloc alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("returned address is page-aligned") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.Alloc(0x10000, 0x10000, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr != 0); + REQUIRE(addr % 0x10000 == 0); + REQUIRE(addr >= 0xA0000000); + REQUIRE(addr < 0xC0000000); + } + + SECTION("multiple allocations with different alignments") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + for (uint32_t alignment : {0x10000u, 0x20000u, 0x40000u, 0x100000u}) { + uint32_t addr = 0; + bool ok = heap.Alloc(alignment, alignment, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr % alignment == 0); + } + } +} + +TEST_CASE("PhysicalHeap::AllocRange alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + SECTION("returned address respects alignment within range") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.AllocRange(0xA0000000, 0xBFFFFFFF, 0x10000, 0x10000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x10000 == 0); + REQUIRE(addr >= 0xA0000000); + REQUIRE(addr <= 0xBFFFFFFF); + } + + SECTION("large alignment preserved through translation") { + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xC0000000, + 0x20000000, 16 * 1024 * 1024, &parent); + + uint32_t addr = 0; + bool ok = heap.AllocRange(0xC0000000, 0xDFFFFFFF, 0x1000000, 0x1000000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000000 == 0); + } +} + +TEST_CASE("PhysicalHeap::AllocFixed alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xA0000000, + 0x20000000, 64 * 1024, &parent); + + // AllocFixed at a specific aligned address must succeed + bool ok = heap.AllocFixed(0xA0100000, 0x10000, 0x10000, + kMemoryAllocationReserve, kMemoryProtectRead); + REQUIRE(ok); +} + +TEST_CASE("PhysicalHeap vE0000000 alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + // The 0xE0000000 heap always has a 0x1000 physical offset, so the + // translation offset is 0xE0000000 - 0x1000 = 0xDFFFF000, which is + // 4KB-aligned but not 64KB-aligned. This is true on all platforms. + uint32_t physical_base = heap.GetPhysicalAddress(heap.heap_base()); + REQUIRE(physical_base == 0x1000); + + SECTION("page-size allocation preserves alignment") { + uint32_t addr = 0; + bool ok = heap.Alloc(0x1000, 0x1000, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000 == 0); + REQUIRE(addr >= 0xE0000000); + } + + SECTION("translation offset is 4KB-aligned") { + uint32_t translation_offset = heap.heap_base() - physical_base; + REQUIRE(translation_offset % heap.page_size() == 0); + } + + SECTION("alloc with alignment larger than page_size is rejected") { + // vE0000000 has a 0x1000 physical translation offset, so a 64KB + // alignment request can't produce a 64KB-aligned guest address. + // PhysicalHeap::Alloc forces top-down, which here lands one stride + // past the end of the child heap and BaseHeap::AllocFixed rejects + // it as out of range. + uint32_t alignment = 0x10000; // 64KB + uint32_t addr = 0; + bool ok = heap.Alloc(0x10000, alignment, kMemoryAllocationReserve, + kMemoryProtectRead, false, &addr); + REQUIRE_FALSE(ok); + } +} + +TEST_CASE("PhysicalHeap vE0000000 AllocRange alignment", "[memory]") { + VirtualHeap parent; + parent.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0x00000000, + 0x20000000, 4096); + + PhysicalHeap heap; + heap.Initialize(nullptr, nullptr, HeapType::kGuestPhysical, 0xE0000000, + 0x1FD00000, 4096, &parent); + + SECTION("page-aligned AllocRange succeeds") { + uint32_t addr = 0; + bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x1000, 0x1000, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr % 0x1000 == 0); + } + + SECTION("AllocRange with large alignment succeeds via bottom-up") { + // Bottom-up search picks a low parent address that translates to a + // guest address inside the child heap, so BaseHeap::AllocFixed accepts + // it. The PhysicalHeap alignment check is host-based + // ((addr + host_address_offset_) % alignment), so the misalignment of + // the guest address itself is not rejected here. + uint32_t alignment = 0x10000; + uint32_t addr = 0; + bool ok = heap.AllocRange(0xE0000000, 0xFFFCFFFF, 0x10000, alignment, + kMemoryAllocationReserve, kMemoryProtectRead, + false, &addr); + REQUIRE(ok); + REQUIRE(addr >= 0xE0000000); + } +} + +} // namespace test +} // namespace xe diff --git a/src/xenia/base/threading_posix.cc b/src/xenia/base/threading_posix.cc index 630e822a3..565a39a5f 100644 --- a/src/xenia/base/threading_posix.cc +++ b/src/xenia/base/threading_posix.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -611,6 +612,7 @@ class PosixCondition final : public PosixConditionBase { /// Thread::GetCurrentThread() on the main thread explicit PosixCondition(pthread_t thread) : thread_(thread), + tid_(static_cast(syscall(SYS_gettid))), signaled_(false), exit_code_(0), state_(State::kRunning), @@ -742,31 +744,48 @@ class PosixCondition final : public PosixConditionBase { int priority() const { WaitStarted(); - int policy; - sched_param param{}; - int ret = pthread_getschedparam(thread_, &policy, ¶m); - if (ret != 0) { - return -1; + if (!fifo_failed_) { + int policy; + sched_param param{}; + int ret = pthread_getschedparam(thread_, &policy, ¶m); + if (ret != 0) { + return -1; + } + return param.sched_priority; } - - return param.sched_priority; + // When using nice values, map back to the SCHED_FIFO range (1-32) + // so callers see a consistent priority space. + int nice_val = getpriority(PRIO_PROCESS, tid_); + // nice -19..19 → fifo 32..1 + return 16 - nice_val; } void set_priority(int new_priority) const { WaitStarted(); - sched_param param{}; - param.sched_priority = new_priority; - int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m); - if (res != 0) { - switch (res) { - case EPERM: - XELOGW("Permission denied while setting priority"); - break; - case EINVAL: - assert_always(); - default: - XELOGW("Unknown error while setting priority"); + if (!fifo_failed_) { + // Try real-time SCHED_FIFO for best priority control. + sched_param param{}; + param.sched_priority = new_priority; + int res = pthread_setschedparam(thread_, SCHED_FIFO, ¶m); + if (res == 0) { + return; } + if (res == EPERM) { + fifo_failed_ = true; + } else { + XELOGW("Unexpected error {} while setting SCHED_FIFO priority", res); + fifo_failed_ = true; + } + } + // Fall back to nice values under SCHED_OTHER. + // Map SCHED_FIFO range (1-32) to nice range (19 to -19). + // Center: fifo 16 → nice 0. + int nice_val = 16 - new_priority; + // Clamp to valid nice range. + if (nice_val < -20) nice_val = -20; + if (nice_val > 19) nice_val = 19; + if (tid_ > 0) { + setpriority(PRIO_PROCESS, tid_, nice_val); } } @@ -930,6 +949,8 @@ class PosixCondition final : public PosixConditionBase { sem_destroy(&suspend_sem_); } pthread_t thread_; + pid_t tid_ = 0; // Kernel TID for setpriority() fallback + mutable bool fifo_failed_ = false; // True after SCHED_FIFO was rejected bool signaled_; int exit_code_; State state_; // Protected by state_mutex_ @@ -1243,6 +1264,7 @@ void* PosixCondition::ThreadStartRoutine(void* parameter) { delete start_data; current_thread_ = thread; + thread->handle_.tid_ = static_cast(syscall(SYS_gettid)); { std::unique_lock lock(thread->handle_.state_mutex_); thread->handle_.state_ = diff --git a/src/xenia/cpu/backend/a64/a64_seq_memory.cc b/src/xenia/cpu/backend/a64/a64_seq_memory.cc index 501fb74f3..47d04a71f 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_memory.cc +++ b/src/xenia/cpu/backend/a64/a64_seq_memory.cc @@ -867,101 +867,6 @@ struct MEMSET_I64 EMITTER_OPCODE_TABLE(OPCODE_MEMSET, MEMSET_I64); // ============================================================================ -// OPCODE_ATOMIC_EXCHANGE -// ============================================================================ -// Note: src1 is a HOST address (not guest), matching the x64 backend. -struct ATOMIC_EXCHANGE_I8 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - // src1 is already a host address. - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, static_cast( - static_cast(i.src2.constant()) & 0xFF)); - } else { - e.and_(e.w0, i.src2, 0xFF); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpalb(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxrb(e.w1, ptr(e.x4)); - e.stlxrb(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -struct ATOMIC_EXCHANGE_I16 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, static_cast( - static_cast(i.src2.constant()) & 0xFFFF)); - } else { - e.and_(e.w0, i.src2, 0xFFFF); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpalh(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxrh(e.w1, ptr(e.x4)); - e.stlxrh(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -struct ATOMIC_EXCHANGE_I32 - : Sequence> { - static void Emit(A64Emitter& e, const EmitArgType& i) { - // src1 is a host address (not guest). - if (i.src1.is_constant) { - e.mov(e.x4, i.src1.constant()); - } else { - e.mov(e.x4, i.src1); - } - if (i.src2.is_constant) { - e.mov(e.w0, - static_cast(static_cast(i.src2.constant()))); - } else { - e.mov(e.w0, i.src2); - } - - if (e.IsFeatureEnabled(kA64EmitLSE)) { - e.swpal(e.w0, i.dest, ptr(e.x4)); - return; - } - - auto& retry = e.NewCachedLabel(); - e.L(retry); - e.ldaxr(e.w1, ptr(e.x4)); - e.stlxr(e.w2, e.w0, ptr(e.x4)); - e.cbnz(e.w2, retry); - e.mov(i.dest, e.w1); - } -}; -EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8, - ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32); - // ============================================================================ // OPCODE_ATOMIC_COMPARE_EXCHANGE // ============================================================================ diff --git a/src/xenia/cpu/backend/x64/x64_seq_memory.cc b/src/xenia/cpu/backend/x64/x64_seq_memory.cc index b543a57b8..ffccd4c31 100644 --- a/src/xenia/cpu/backend/x64/x64_seq_memory.cc +++ b/src/xenia/cpu/backend/x64/x64_seq_memory.cc @@ -295,68 +295,6 @@ RegExp ComputeMemoryAddressOffset(X64Emitter& e, const T& guest, } } -// ============================================================================ -// OPCODE_ATOMIC_EXCHANGE -// ============================================================================ -// Note that the address we use here is a real, host address! -// This is weird, and should be fixed. -template -void EmitAtomicExchangeXX(X64Emitter& e, const ARGS& i) { - if (i.dest == i.src1) { - e.mov(e.rax, i.src1); - if (i.dest != i.src2) { - if (i.src2.is_constant) { - e.mov(i.dest, i.src2.constant()); - } else { - e.mov(i.dest, i.src2); - } - } - e.lock(); - e.xchg(e.dword[e.rax], i.dest); - } else { - if (i.dest != i.src2) { - if (i.src2.is_constant) { - e.mov(i.dest, i.src2.constant()); - } else { - e.mov(i.dest, i.src2); - } - } - e.lock(); - e.xchg(e.dword[i.src1.reg()], i.dest); - } -} -struct ATOMIC_EXCHANGE_I8 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I16 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I32 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -struct ATOMIC_EXCHANGE_I64 - : Sequence> { - static void Emit(X64Emitter& e, const EmitArgType& i) { - EmitAtomicExchangeXX(e, i); - } -}; -EMITTER_OPCODE_TABLE(OPCODE_ATOMIC_EXCHANGE, ATOMIC_EXCHANGE_I8, - ATOMIC_EXCHANGE_I16, ATOMIC_EXCHANGE_I32, - ATOMIC_EXCHANGE_I64); - struct LVL_V128 : Sequence> { static void Emit(X64Emitter& e, const EmitArgType& i) { e.mov(e.edx, 0xf); diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.cc b/src/xenia/cpu/compiler/passes/simplification_pass.cc index d7fdf246a..2779f682a 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.cc +++ b/src/xenia/cpu/compiler/passes/simplification_pass.cc @@ -568,6 +568,10 @@ bool SimplificationPass::TryHandleANDROLORSHLSeq(hir::Instr* i, bool SimplificationPass::CheckAnd(hir::Instr* i, hir::HIRBuilder* builder) { retry_and_simplification: + if (SimplifyAndNot(i, builder)) { + return true; + } + auto [constant_value, variable_value] = i->BinaryValueArrangeAsConstAndVar(); if (!constant_value) { // added this for srawi @@ -1247,6 +1251,38 @@ bool SimplificationPass::SimplifyAddArith(hir::Instr* i, return false; } +bool SimplificationPass::SimplifyAndNot(hir::Instr* i, + hir::HIRBuilder* builder) { + // check if either of the 2 AND operands has just used NOT and fold into + // an AND_NOT opcode + Value* src1 = i->src1.value; + Value* src2 = i->src2.value; + + Instr* def1 = src1->def; + Instr* def2 = src2->def; + if (!def1 || !def2) return false; + + // Bypass the NOT from an incoming operand and combine it into AND_NOT. + // If the original NOT does not have any further uses, then the + // dead-code-elimination pass will delete it. Otherwise, if it still has uses, + // then there will still be a NOT operation. + if (def2->opcode == &OPCODE_NOT_info) { + // Fold src2's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src1); + i->set_src2(def2->src1.value); + return true; + } else if (def1->opcode == &OPCODE_NOT_info) { + // Swap operands and fold src1's NOT into AND_NOT + i->Replace(&OPCODE_AND_NOT_info, 0); + i->set_src1(src2); + i->set_src2(def1->src1.value); + return true; + } + + return false; +} + bool SimplificationPass::SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder) { /* diff --git a/src/xenia/cpu/compiler/passes/simplification_pass.h b/src/xenia/cpu/compiler/passes/simplification_pass.h index 1e69951e5..d9c8fc092 100644 --- a/src/xenia/cpu/compiler/passes/simplification_pass.h +++ b/src/xenia/cpu/compiler/passes/simplification_pass.h @@ -42,6 +42,7 @@ class SimplificationPass : public ConditionalGroupSubpass { bool SimplifyAddWithSHL(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddToSelf(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifyAddArith(hir::Instr* i, hir::HIRBuilder* builder); + bool SimplifyAndNot(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder); bool SimplifySHLArith(hir::Instr* i, hir::HIRBuilder* builder); // handle either or or xor with 0 diff --git a/src/xenia/cpu/hir/hir_builder.cc b/src/xenia/cpu/hir/hir_builder.cc index 44ac5d936..26bd58195 100644 --- a/src/xenia/cpu/hir/hir_builder.cc +++ b/src/xenia/cpu/hir/hir_builder.cc @@ -2225,17 +2225,6 @@ Value* HIRBuilder::Unpack(Value* value, uint32_t pack_flags) { return i->dest; } -Value* HIRBuilder::AtomicExchange(Value* address, Value* new_value) { - ASSERT_ADDRESS_TYPE(address); - ASSERT_INTEGER_TYPE(new_value); - Instr* i = - AppendInstr(OPCODE_ATOMIC_EXCHANGE_info, 0, AllocValue(new_value->type)); - i->set_src1(address); - i->set_src2(new_value); - i->src3.value = NULL; - return i->dest; -} - Value* HIRBuilder::AtomicCompareExchange(Value* address, Value* old_value, Value* new_value) { ASSERT_ADDRESS_TYPE(address); diff --git a/src/xenia/cpu/hir/hir_builder.h b/src/xenia/cpu/hir/hir_builder.h index 2621148ef..e8a80415a 100644 --- a/src/xenia/cpu/hir/hir_builder.h +++ b/src/xenia/cpu/hir/hir_builder.h @@ -297,7 +297,6 @@ class HIRBuilder { Value* Pack(Value* value1, Value* value2, uint32_t pack_flags = 0); Value* Unpack(Value* value, uint32_t pack_flags = 0); - Value* AtomicExchange(Value* address, Value* new_value); Value* AtomicCompareExchange(Value* address, Value* old_value, Value* new_value); Value* AtomicAdd(Value* address, Value* value); diff --git a/src/xenia/cpu/hir/opcodes.h b/src/xenia/cpu/hir/opcodes.h index 5a1bdc53b..43be8810a 100644 --- a/src/xenia/cpu/hir/opcodes.h +++ b/src/xenia/cpu/hir/opcodes.h @@ -282,7 +282,6 @@ enum Opcode { OPCODE_PACK, // break up into smaller operations and add a float16 convert // opcode OPCODE_UNPACK, - OPCODE_ATOMIC_EXCHANGE, OPCODE_ATOMIC_COMPARE_EXCHANGE, OPCODE_SET_ROUNDING_MODE, OPCODE_VECTOR_DENORMFLUSH, // converts denormals to signed zeros in a vector diff --git a/src/xenia/cpu/hir/opcodes.inl b/src/xenia/cpu/hir/opcodes.inl index c5c089e85..255b4fb80 100644 --- a/src/xenia/cpu/hir/opcodes.inl +++ b/src/xenia/cpu/hir/opcodes.inl @@ -650,12 +650,6 @@ DEFINE_OPCODE( OPCODE_SIG_V_V, 0) -DEFINE_OPCODE( - OPCODE_ATOMIC_EXCHANGE, - "atomic_exchange", - OPCODE_SIG_V_V_V, - OPCODE_FLAG_VOLATILE) - DEFINE_OPCODE( OPCODE_ATOMIC_COMPARE_EXCHANGE, "atomic_compare_exchange", diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index fdcf9414a..e907b0bf7 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -646,45 +646,6 @@ TEST_CASE("SET_NJM_OFF", "[backend]") { #endif } -// ============================================================================= -// Atomic Exchange I32 -// ============================================================================= -// Tests that AtomicExchange correctly swaps a value in memory and returns -// the old value. -// NOTE: OPCODE_ATOMIC_EXCHANGE uses a HOST address (not guest), per the -// x64 backend comment: "the address we use here is a real, host address!" -TEST_CASE("ATOMIC_EXCHANGE_I32", "[backend]") { - TestFunction test([](HIRBuilder& b) { - // r[4] holds the host address directly. - auto addr = LoadGPR(b, 4); - auto new_val = b.Truncate(LoadGPR(b, 5), hir::INT32_TYPE); - auto old_val = b.AtomicExchange(addr, new_val); - StoreGPR(b, 3, b.ZeroExtend(old_val, hir::INT64_TYPE)); - b.Return(); - }); - - // Allocate guest memory and compute the host pointer. - uint32_t guest_addr = test.memory->SystemHeapAlloc(4); - REQUIRE(guest_addr != 0); - auto* host_ptr = test.memory->TranslateVirtual(guest_addr); - - test.Run( - [&](PPCContext* ctx) { - *reinterpret_cast(host_ptr) = 0xAABBCCDD; - // Pass the HOST address in r[4]. - ctx->r[4] = reinterpret_cast(host_ptr); - ctx->r[5] = 0x11223344; - }, - [&](PPCContext* ctx) { - // r[3] should have the old value. - REQUIRE(static_cast(ctx->r[3]) == 0xAABBCCDD); - // Memory should now have the new value. - REQUIRE(*reinterpret_cast(host_ptr) == 0x11223344); - }); - - test.memory->SystemHeapFree(guest_addr); -} - // ============================================================================= // DOT_PRODUCT_3 — inline NEON dot product of first 3 vector elements // ============================================================================= diff --git a/src/xenia/cpu/testing/opcode_coverage_test.cc b/src/xenia/cpu/testing/opcode_coverage_test.cc index db1ece2ea..c70b051ab 100644 --- a/src/xenia/cpu/testing/opcode_coverage_test.cc +++ b/src/xenia/cpu/testing/opcode_coverage_test.cc @@ -491,8 +491,100 @@ TEST_CASE("ATOMIC_COMPARE_EXCHANGE_I32", "[atomic]") { // ============================================================================ // AND_NOT — bitwise AND with complement of second operand // ============================================================================ +TEST_CASE("AND_NOT_I8", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT8_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0x0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF0); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAA; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x00); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x12; + ctx->r[5] = 0x00; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x12); + }); +} + +TEST_CASE("AND_NOT_I16", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT16_TYPE))), + INT64_TYPE)); + StoreGPR(b, 3, + b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Truncate(LoadGPR(b, 5), INT16_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00; + ctx->r[5] = 0x0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0xF000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAA; + ctx->r[5] = 0xFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x0000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234; + ctx->r[5] = 0x0000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(static_cast(ctx->r[3]) == 0x1234); + }); +} + TEST_CASE("AND_NOT_I32", "[bitwise]") { TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, + b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Not(b.Truncate(LoadGPR(b, 5), INT32_TYPE))), + INT64_TYPE)); StoreGPR(b, 3, b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT32_TYPE), b.Truncate(LoadGPR(b, 5), INT32_TYPE)), @@ -506,6 +598,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x0F0F0F0F; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0xF000F000); }); // All bits masked out. @@ -515,6 +608,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0xFFFFFFFF; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x00000000); }); // No bits masked out. @@ -524,10 +618,87 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") { ctx->r[5] = 0x00000000; }, [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); REQUIRE(static_cast(ctx->r[3]) == 0x12345678); }); } +TEST_CASE("AND_NOT_I64", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 2, b.And(LoadGPR(b, 4), b.Not(LoadGPR(b, 5)))); + StoreGPR(b, 3, b.AndNot(LoadGPR(b, 4), LoadGPR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF00FF00FF00FF00; + ctx->r[5] = 0x0F0F0F0F0F0F0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0xF000F000F000F000); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xAAAAAAAAAAAAAAAA; + ctx->r[5] = 0xFFFFFFFFFFFFFFFF; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x0000000000000000); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x1234567812345678; + ctx->r[5] = 0x0000000000000000; + }, + [](PPCContext* ctx) { + REQUIRE(ctx->r[2] == ctx->r[3]); + REQUIRE(ctx->r[3] == 0x1234567812345678); + }); +} + +TEST_CASE("AND_NOT_V128", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreVR(b, 2, b.And(LoadVR(b, 4), b.Not(LoadVR(b, 5)))); + StoreVR(b, 3, b.AndNot(LoadVR(b, 4), LoadVR(b, 5))); + b.Return(); + }); + // result = src1 & ~src2 + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128s(0xFF00); + ctx->v[5] = vec128s(0x0F0F); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128s(0xF000)); + }); + // All bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128b(0xAA); + ctx->v[5] = vec128b(0xFF); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128b(0x00)); + }); + // No bits masked out. + test.Run( + [](PPCContext* ctx) { + ctx->v[4] = vec128i(0x12345678); + ctx->v[5] = vec128i(0x00000000); + }, + [](PPCContext* ctx) { + REQUIRE(ctx->v[2] == ctx->v[3]); + REQUIRE(ctx->v[3] == vec128i(0x12345678)); + }); +} + // ============================================================================ // TRUNCATE — integer narrowing // ============================================================================ diff --git a/src/xenia/hid/winkey/winkey_input_driver.cc b/src/xenia/hid/winkey/winkey_input_driver.cc index 3ff5beaff..995855349 100644 --- a/src/xenia/hid/winkey/winkey_input_driver.cc +++ b/src/xenia/hid/winkey/winkey_input_driver.cc @@ -41,6 +41,125 @@ namespace xe { namespace hid { namespace winkey { +static uint8_t VirtualKeyToHIDUsage(UINT vk) { + // Letters: contiguous in both VK and HID space + if (vk >= 'A' && vk <= 'Z') { + return vk - 'A' + 0x04; + } + + // Digits 1-9 (0 is irregular: 0x27) + if (vk >= '1' && vk <= '9') { + return vk - '1' + 0x1E; + } + + // F1-F12 + if (vk >= VK_F1 && vk <= VK_F12) { + return vk - VK_F1 + 0x3A; + } + + // F13-F24 + if (vk >= VK_F13 && vk <= VK_F24) { + return vk - VK_F13 + 0x68; + } + + // Numpad 1-9 (0 is irregular: 0x62) + if (vk >= VK_NUMPAD1 && vk <= VK_NUMPAD9) { + return vk - VK_NUMPAD1 + 0x59; + } + + // Modifiers (Left side starts at 0xE0, Right at 0xE4) + if (vk >= VK_LCONTROL && vk <= VK_LWIN) { + return vk - VK_LCONTROL + 0xE0; + } + if (vk >= VK_RCONTROL && vk <= VK_RWIN) { + return vk - VK_RCONTROL + 0xE4; + } + + switch (vk) { + case '0': + return 0x27; + case VK_RETURN: + return 0x28; + case VK_ESCAPE: + return 0x29; + case VK_BACK: + return 0x2A; + case VK_TAB: + return 0x2B; + case VK_SPACE: + return 0x2C; + case VK_OEM_MINUS: + return 0x2D; + case VK_OEM_PLUS: + return 0x2E; + case VK_OEM_4: + return 0x2F; + case VK_OEM_6: + return 0x30; + case VK_OEM_5: + return 0x31; + case VK_OEM_1: + return 0x33; + case VK_OEM_7: + return 0x34; + case VK_OEM_3: + return 0x35; + case VK_OEM_COMMA: + return 0x36; + case VK_OEM_PERIOD: + return 0x37; + case VK_OEM_2: + return 0x38; + case VK_CAPITAL: + return 0x39; + case VK_SNAPSHOT: + return 0x46; + case VK_SCROLL: + return 0x47; + case VK_PAUSE: + return 0x48; + case VK_INSERT: + return 0x49; + case VK_HOME: + return 0x4A; + case VK_PRIOR: + return 0x4B; + case VK_DELETE: + return 0x4C; + case VK_END: + return 0x4D; + case VK_NEXT: + return 0x4E; + case VK_RIGHT: + return 0x4F; + case VK_LEFT: + return 0x50; + case VK_DOWN: + return 0x51; + case VK_UP: + return 0x52; + case VK_NUMLOCK: + return 0x53; + case VK_DIVIDE: + return 0x54; + case VK_MULTIPLY: + return 0x55; + case VK_SUBTRACT: + return 0x56; + case VK_ADD: + return 0x57; + case VK_NUMPAD0: + return 0x62; + case VK_DECIMAL: + return 0x63; + case VK_APPS: + return 0x65; + default: + break; + } + return 0x00; +} + bool static IsPassthroughEnabled() { return static_cast(cvars::keyboard_mode) == KeyboardMode::Passthrough; @@ -344,10 +463,12 @@ X_RESULT WinKeyInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, } if (IsPassthroughEnabled()) { + const UINT vk = static_cast(xinput_virtual_key); + hid_code = VirtualKeyToHIDUsage(vk); if (GetKeyboardState(key_map_)) { + const UINT sc = MapVirtualKey(vk, MAPVK_VK_TO_VSC); WCHAR buf; - if (ToUnicode(uint8_t(xinput_virtual_key), 0, key_map_, &buf, 1, 0) == - 1) { + if (ToUnicode(vk, sc, key_map_, &buf, 1, 0) == 1) { keystroke_flags |= 0x1000; // XINPUT_KEYSTROKE_VALIDUNICODE unicode = buf; } diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 2a4284a23..42b0a5197 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -1330,15 +1330,16 @@ void KernelState::EmulateCPInterruptDPC(uint32_t interrupt_callback, } void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type, - char unk_18, char unk_19, char unk_1A) { + char priority_class, char default_priority, + char max_dynamic_priority) { uint32_t guest_kprocess = memory()->HostToGuestVirtual(process); uint32_t thread_list_guest_ptr = guest_kprocess + offsetof(X_KPROCESS, thread_list); - process->unk_18 = unk_18; - process->unk_19 = unk_19; - process->unk_1A = unk_1A; + process->process_priority_class = priority_class; + process->default_thread_priority = default_priority; + process->max_dynamic_priority = max_dynamic_priority; util::XeInitializeListHead(&process->thread_list, thread_list_guest_ptr); process->quantum = 60; // doubt any guest code uses this ptr, which i think probably has something to @@ -1346,7 +1347,7 @@ void KernelState::InitializeProcess(X_KPROCESS* process, uint32_t type, process->clrdataa_masked_ptr = 0; // clrdataa_ & ~(1U << 31); process->thread_count = 0; - process->unk_1B = 0x06; + process->disable_quantum_decay = 0x06; process->kernel_stack_size = 16 * 1024; process->tls_slot_size = 0x80; diff --git a/src/xenia/kernel/kernel_state.h b/src/xenia/kernel/kernel_state.h index 6ba068646..5b40ab9c7 100644 --- a/src/xenia/kernel/kernel_state.h +++ b/src/xenia/kernel/kernel_state.h @@ -66,10 +66,10 @@ struct X_KPROCESS { // so it sets this ptr to 0x1C0000 xe::be clrdataa_masked_ptr; xe::be thread_count; - uint8_t unk_18; - uint8_t unk_19; - uint8_t unk_1A; - uint8_t unk_1B; + uint8_t process_priority_class; + uint8_t default_thread_priority; + uint8_t max_dynamic_priority; + uint8_t disable_quantum_decay; xe::be kernel_stack_size; xe::be tls_static_data_address; xe::be tls_data_size; @@ -337,8 +337,9 @@ class KernelState { private: void LoadKernelModule(object_ref kernel_module); - void InitializeProcess(X_KPROCESS* process, uint32_t type, char unk_18, - char unk_19, char unk_1A); + void InitializeProcess(X_KPROCESS* process, uint32_t type, + char priority_class, char default_priority, + char max_dynamic_priority); void SetProcessTLSVars(X_KPROCESS* process, int num_slots, int tls_data_size, int tls_static_data_address); void InitializeKernelGuestGlobals(); @@ -388,6 +389,7 @@ class KernelState { uint32_t ke_timestamp_bundle_ptr_ = 0; std::unique_ptr timestamp_timer_; + uint32_t quantum_timer_counter_ = 0; cpu::backend::GuestTrampolineGroup kernel_trampoline_group_; // fixed address referenced by dashboards. Data is currently unknown uint32_t strange_hardcoded_page_ = 0x8E038634 & (~0xFFFF); diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc index 476e5de86..aeea35670 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc @@ -332,6 +332,10 @@ dword_result_t KeSetAffinityThread_entry(lpvoid_t thread_ptr, dword_t affinity, } auto thread = XObject::GetNativeObject(kernel_state(), thread_ptr); if (!thread) { + XELOGW( + "KeSetAffinityThread: guest thread pointer {:08X} did not resolve to " + "an XThread; returning STATUS_INVALID_HANDLE", + thread_ptr.guest_address()); return X_STATUS_INVALID_HANDLE; } if (previous_affinity_ptr) { @@ -709,8 +713,7 @@ uint32_t xeKeReleaseSemaphore(X_KSEMAPHORE* semaphore_ptr, uint32_t increment, return 0; } - // TODO(benvanik): increment thread priority? - // TODO(benvanik): wait? + sem->set_priority_increment(increment); int32_t previous_count = 0; [[maybe_unused]] bool success = @@ -1100,8 +1103,8 @@ DECLARE_XBOXKRNL_EXPORT3(NtWaitForMultipleObjectsEx, kThreading, kImplemented, dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle, dword_t wait_handle, + dword_t wait_mode, dword_t alertable, - dword_t r6, lpqword_t timeout_ptr) { X_STATUS result = X_STATUS_SUCCESS; // pre-lock for these two handle lookups @@ -1114,9 +1117,9 @@ dword_result_t NtSignalAndWaitForSingleObjectEx_entry(dword_t signal_handle, global_critical_region::mutex().unlock(); if (signal_object && wait_object) { uint64_t timeout = timeout_ptr ? static_cast(*timeout_ptr) : 0u; - result = - XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3, 1, - alertable, timeout_ptr ? &timeout : nullptr); + result = XObject::SignalAndWait(signal_object.get(), wait_object.get(), 3, + wait_mode, alertable, + timeout_ptr ? &timeout : nullptr); } else { result = X_STATUS_INVALID_HANDLE; } @@ -1139,11 +1142,34 @@ uint32_t xeKeKfAcquireSpinLock(PPCContext* ctx, X_KSPINLOCK* lock, PrefetchForCAS(lock); assert_true(lock->prcb_of_owner != static_cast(ctx->r[13])); + + uint32_t our_pcr = static_cast(ctx->r[13]); + uint8_t our_cpu = + ctx->TranslateVirtualGPR(our_pcr)->prcb_data.current_cpu; + // Lock. - while (!xe::atomic_cas(0, xe::byte_swap(static_cast(ctx->r[13])), - &lock->prcb_of_owner.value)) { - // Spin! - // TODO(benvanik): error on deadlock? + while ( + !xe::atomic_cas(0, xe::byte_swap(our_pcr), &lock->prcb_of_owner.value)) { + // On real hardware, threads sharing a Xenon HW thread are serialized by + // the kernel scheduler — the spinner would be preempted within one + // timeslice (~1ms) so the holder can make progress. In the naive + // host-thread model both threads run truly in parallel, so the spinner + // can burn its entire host quantum without giving the holder a chance. + // + // Check whether the lock holder is assigned to the same guest CPU as us. + // If so, yield the host thread aggressively (Sleep(0)) to force a host + // context switch and give the holder a chance to run and release. + // The relationship is stable — affinity doesn't change while a thread + // holds a spinlock — so one check per contention episode is sufficient. + uint32_t owner_pcr_be = lock->prcb_of_owner.value; + if (owner_pcr_be) { + uint32_t owner_pcr = xe::byte_swap(owner_pcr_be); + auto* owner_kpcr = ctx->TranslateVirtual(owner_pcr); + if (owner_kpcr->prcb_data.current_cpu == our_cpu) { + xe::threading::Sleep(std::chrono::milliseconds(0)); + continue; + } + } xe::threading::MaybeYield(); } @@ -1561,8 +1587,6 @@ DECLARE_XBOXKRNL_EXPORT2(KeInitializeDpc, kThreading, kImplemented, kSketchy); dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, dword_t arg2) { - assert_always("DPC does not dispatch yet; going to hang!"); - uint32_t list_entry_ptr = dpc.guest_address() + 4; // Lock dispatcher. @@ -1580,9 +1604,43 @@ dword_result_t KeInsertQueueDpc_entry(pointer_t dpc, dword_t arg1, dpc_list->Insert(list_entry_ptr); + // Dispatch the DPC inline on the calling thread. On real hardware DPCs + // are deferred to DISPATCH_IRQL on the target processor, but DPC routines + // access per-CPU state via r13 (KPCR) so they must run on a thread whose + // KPCR is valid for the target CPU. The calling thread's KPCR satisfies + // this for the common case (desired_cpu_number == 0, meaning current CPU). + // Inline dispatch also avoids latency issues with shared work queues. + uint32_t routine = dpc->routine; + if (routine) { + auto thread = XThread::GetCurrentThread(); + if (thread) { + auto thread_state = thread->thread_state(); + auto ppc_context = thread_state->context(); + auto kpcr = ppc_context->TranslateVirtualGPR(ppc_context->r[13]); + + // If we're already inside a DPC (reentrant KeInsertQueueDpc from a DPC + // routine), skip the impersonation — we're already at DISPATCH_IRQL. + bool already_in_dpc = kpcr->prcb_data.dpc_active != 0; + + DPCImpersonationScope dpc_scope{}; + if (!already_in_dpc) { + kernel_state()->BeginDPCImpersonation(ppc_context, dpc_scope); + } + + uint64_t args[] = {dpc.guest_address(), (uint64_t)dpc->context, + (uint64_t)arg1, (uint64_t)arg2}; + kernel_state()->processor()->Execute(thread_state, routine, args, + xe::countof(args)); + + if (!already_in_dpc) { + kernel_state()->EndDPCImpersonation(ppc_context, dpc_scope); + } + } + } + return 1; } -DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kStub, kSketchy); +DECLARE_XBOXKRNL_EXPORT2(KeInsertQueueDpc, kThreading, kImplemented, kSketchy); dword_result_t KeRemoveQueueDpc_entry(pointer_t dpc) { bool result = false; diff --git a/src/xenia/kernel/xevent.cc b/src/xenia/kernel/xevent.cc index bf1176af8..b583bf732 100644 --- a/src/xenia/kernel/xevent.cc +++ b/src/xenia/kernel/xevent.cc @@ -58,11 +58,13 @@ void XEvent::InitializeNative(void* native_ptr, X_DISPATCH_HEADER* header) { } int32_t XEvent::Set(uint32_t priority_increment, bool wait) { + set_priority_increment(priority_increment); event_->Set(); return 1; } int32_t XEvent::Pulse(uint32_t priority_increment, bool wait) { + set_priority_increment(priority_increment); event_->Pulse(); return 1; } diff --git a/src/xenia/kernel/xmutant.cc b/src/xenia/kernel/xmutant.cc index 58e379730..42da93d67 100644 --- a/src/xenia/kernel/xmutant.cc +++ b/src/xenia/kernel/xmutant.cc @@ -45,6 +45,8 @@ X_STATUS XMutant::ReleaseMutant(uint32_t priority_increment, bool abandon, owning_thread_ = nullptr; } + set_priority_increment(priority_increment); + // TODO(benvanik): abandoning. assert_false(abandon); if (mutant_->Release()) { diff --git a/src/xenia/kernel/xobject.cc b/src/xenia/kernel/xobject.cc index f3d95604b..ecc38ca9b 100644 --- a/src/xenia/kernel/xobject.cc +++ b/src/xenia/kernel/xobject.cc @@ -203,13 +203,20 @@ X_STATUS XObject::Wait(uint32_t wait_reason, uint32_t processor_mode, auto result = xe::threading::Wait(wait_handle, alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + case xe::threading::WaitResult::kUserCallback: { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->BoostOnWake(priority_increment()); + } + if (result == xe::threading::WaitResult::kSuccess) { + WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -231,13 +238,20 @@ X_STATUS XObject::SignalAndWait(XObject* signal_object, XObject* wait_object, auto result = xe::threading::SignalAndWait( signal_object->GetWaitHandle(), wait_object->GetWaitHandle(), alertable ? true : false, timeout_ms); + switch (result) { case xe::threading::WaitResult::kSuccess: - wait_object->WaitCallback(); - return X_STATUS_SUCCESS; - case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? + case xe::threading::WaitResult::kUserCallback: { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->BoostOnWake(wait_object->priority_increment()); + } + if (result == xe::threading::WaitResult::kSuccess) { + wait_object->WaitCallback(); + return X_STATUS_SUCCESS; + } return X_STATUS_USER_APC; + } case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); return X_STATUS_TIMEOUT; @@ -264,25 +278,31 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, TimeoutTicksToMs(*opt_timeout))) : std::chrono::milliseconds::max(); + X_STATUS status; + uint32_t boost_increment = 0; if (wait_type) { auto result = xe::threading::WaitAny(wait_handles, count, alertable ? true : false, timeout_ms); switch (result.first) { case xe::threading::WaitResult::kSuccess: objects[result.second]->WaitCallback(); - - return X_STATUS(result.second); + boost_increment = objects[result.second]->priority_increment(); + status = X_STATUS(result.second); + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; - default: + status = X_STATUS_TIMEOUT; + break; case xe::threading::WaitResult::kAbandoned: - return X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second); + status = X_STATUS(X_STATUS_ABANDONED_WAIT_0 + result.second); + break; + default: case xe::threading::WaitResult::kFailed: - return X_STATUS_UNSUCCESSFUL; + status = X_STATUS_UNSUCCESSFUL; + break; } } else { auto result = xe::threading::WaitAll(wait_handles, count, @@ -291,21 +311,38 @@ X_STATUS XObject::WaitMultiple(uint32_t count, XObject** objects, case xe::threading::WaitResult::kSuccess: for (uint32_t i = 0; i < count; i++) { objects[i]->WaitCallback(); + // Use the largest increment among the signaled objects. + if (objects[i]->priority_increment() > boost_increment) { + boost_increment = objects[i]->priority_increment(); + } } - - return X_STATUS_SUCCESS; + status = X_STATUS_SUCCESS; + break; case xe::threading::WaitResult::kUserCallback: - // Or X_STATUS_ALERTED? - return X_STATUS_USER_APC; + status = X_STATUS_USER_APC; + break; case xe::threading::WaitResult::kTimeout: xe::threading::MaybeYield(); - return X_STATUS_TIMEOUT; + status = X_STATUS_TIMEOUT; + break; default: case xe::threading::WaitResult::kAbandoned: case xe::threading::WaitResult::kFailed: - return X_STATUS_ABANDONED_WAIT_0; + status = X_STATUS_ABANDONED_WAIT_0; + break; } } + + // Apply priority boost if the thread actually blocked (not on + // timeout/failure). + if (status != X_STATUS_TIMEOUT && status != X_STATUS_UNSUCCESSFUL && + status != X_STATUS_ABANDONED_WAIT_0) { + auto current_thread = XThread::GetCurrentThread(); + if (current_thread) { + current_thread->BoostOnWake(boost_increment); + } + } + return status; } uint8_t* XObject::CreateNative(uint32_t size) { diff --git a/src/xenia/kernel/xobject.h b/src/xenia/kernel/xobject.h index 77fbf0870..da67be4c0 100644 --- a/src/xenia/kernel/xobject.h +++ b/src/xenia/kernel/xobject.h @@ -226,6 +226,12 @@ class XObject { void* native_ptr, int32_t as_type = -1, bool already_locked = false); + // Priority increment stored by the most recent signal operation + // (KeSetEvent, KeReleaseSemaphore, etc.). Read by the waiter on wake + // to apply a priority boost matching real Xenon scheduler behavior. + uint32_t priority_increment() const { return priority_increment_; } + void set_priority_increment(uint32_t inc) { priority_increment_ = inc; } + protected: bool SaveObject(ByteStream* stream); bool RestoreObject(ByteStream* stream); @@ -253,6 +259,8 @@ class XObject { KernelState* kernel_state_; + uint32_t priority_increment_ = 0; + // Host objects are persisted through resets/etc. bool host_object_ = false; diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index c73fd19f8..f8524ce97 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -14,6 +14,7 @@ #endif #include "xenia/base/byte_stream.h" +#include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/base/profiling.h" @@ -24,8 +25,9 @@ #include "xenia/kernel/user_module.h" #include "xenia/kernel/xboxkrnl/xboxkrnl_threading.h" -DEFINE_bool(ignore_thread_priorities, true, +DEFINE_bool(ignore_thread_priorities, false, "Ignores game-specified thread priorities.", "Kernel"); +UPDATE_from_bool(ignore_thread_priorities, 2026, 4, 9, 12, true); DEFINE_bool(ignore_thread_affinities, true, "Ignores game-specified thread affinities.", "Kernel"); @@ -155,13 +157,17 @@ static uint8_t next_cpu = 0; static uint8_t GetFakeCpuNumber(uint8_t proc_mask) { // NOTE: proc_mask is logical processors, not physical processors or cores. if (!proc_mask) { - next_cpu = (next_cpu + 1) % 6; - return next_cpu; // is this reasonable? - // TODO(Triang3l): Does the following apply here? + // On Xbox 360, threads without an explicit processor assignment stay on + // the same hardware thread as the parent. Preserve this so that the + // guest CPU assignment reflects the game's intent — parent-child thread + // pairs that share a HW thread may rely on implicit serialization. // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/coding-for-multiple-cores - // "On Xbox 360, you must explicitly assign software threads to a particular - // hardware thread by using XSetThreadProcessor. Otherwise, all child - // threads will stay on the same hardware thread as the parent." + XThread* parent = current_xthread_tls_; + if (parent) { + return parent->active_cpu(); + } + next_cpu = (next_cpu + 1) % 6; + return next_cpu; } assert_false(proc_mask & 0xC0); @@ -208,6 +214,19 @@ void XThread::InitializeGuestObject() { guest_thread->apc_lists[0].Initialize(memory()); guest_thread->apc_lists[1].Initialize(memory()); + guest_thread->process_priority_class = process->process_priority_class; + auto base_prio = process->default_thread_priority; + guest_thread->base_priority_copy = base_prio; + guest_thread->base_priority = base_prio; + guest_thread->priority = base_prio; + guest_thread->max_dynamic_priority = process->max_dynamic_priority; + guest_thread->quantum = process->quantum; + + // Sync the host-side priority tracking to match the guest defaults. + // Games may later override these via KeSetPriorityThread. + priority_ = base_prio; + base_priority_ = base_prio; + guest_thread->a_prcb_ptr = kpcrb; guest_thread->another_prcb_ptr = kpcrb; @@ -666,28 +685,129 @@ void XThread::RundownAPCs() { int32_t XThread::QueryPriority() { return thread_->priority(); } -void XThread::SetPriority(int32_t increment) { - if (is_guest_thread()) { - guest_object()->priority = static_cast(increment); - } - priority_ = increment; - int32_t target_priority = 0; - if (increment > 0x22) { - target_priority = xe::threading::ThreadPriority::kHighest; - } else if (increment > 0x11) { - target_priority = xe::threading::ThreadPriority::kAboveNormal; - } else if (increment < -0x22) { - target_priority = xe::threading::ThreadPriority::kLowest; - } else if (increment < -0x11) { - target_priority = xe::threading::ThreadPriority::kBelowNormal; +// Map Xenon's 0-31 priority range across the available host priority levels. +// Priority 18 (0x12) is the Xenon real-time threshold — threads at or above +// it don't get quantum decay on real hardware. +static int32_t GuestPriorityToHost(int32_t guest_priority) { + if (guest_priority >= 24) { + return xe::threading::ThreadPriority::kHighest; + } else if (guest_priority >= 17) { + return xe::threading::ThreadPriority::kAboveNormal; + } else if (guest_priority >= 10) { + return xe::threading::ThreadPriority::kNormal; + } else if (guest_priority >= 5) { + return xe::threading::ThreadPriority::kBelowNormal; } else { - target_priority = xe::threading::ThreadPriority::kNormal; + return xe::threading::ThreadPriority::kLowest; } +} + +void XThread::SetPriority(int32_t increment) { + // Clamp to valid Xenon priority range. Negative values can arrive via + // KeSetBasePriorityThread (signed offset from process base). + int32_t clamped = std::max(increment, 0); + if (is_guest_thread()) { + guest_object()->priority = static_cast(clamped); + } + priority_ = clamped; + base_priority_ = clamped; + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); if (!cvars::ignore_thread_priorities) { - thread_->set_priority(target_priority); + thread_->set_priority(GuestPriorityToHost(clamped)); } } +void XThread::CheckQuantumAndDecay() { + if (cvars::ignore_thread_priorities) return; + // Real-time threads (current priority >= 0x12) don't decay on Xenon. + if (priority_ >= 18) return; + + uint64_t now = Clock::QueryHostUptimeMillis(); + uint64_t elapsed = now - quantum_start_ms_; + // On Xenon, the clock interrupt fires every ~1ms and decrements the + // thread's quantum by 3. The process quantum is 60, so it takes ~20ms + // for quantum to expire. When it does, the scheduler decays the + // effective priority by exactly 1 and resets quantum. We approximate + // this by decaying 1 priority level per 20ms of elapsed wall-clock time. + constexpr uint64_t kQuantumPeriodMs = 20; + if (elapsed < kQuantumPeriodMs) return; + + int32_t decay_steps = static_cast(elapsed / kQuantumPeriodMs); + // On the first decay step, drain the accumulated priority boost as well. + // The real kernel computes: new_prio = priority - boost_accumulator - 1 + // then zeroes the accumulator. Additional decay steps (if the timer + // callback was late) each subtract 1 more. + int32_t total_decay = boost_amount_ + decay_steps; + boost_amount_ = 0; + + int32_t new_priority = priority_ - total_decay; + if (new_priority < base_priority_) { + new_priority = base_priority_; + } + if (new_priority != priority_) { + priority_ = new_priority; + if (is_guest_thread()) { + guest_object()->priority = static_cast(new_priority); + } + thread_->set_priority(GuestPriorityToHost(new_priority)); + } + quantum_start_ms_ = now; +} + +void XThread::BoostOnWake(int32_t increment) { + if (cvars::ignore_thread_priorities) return; + + // Real-time threads (priority >= 0x12) just get their quantum reset. + if (priority_ >= 18) { + boost_amount_ = 0; + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); + return; + } + + // Match the real kernel (xeEnqueueThreadPostWait): + // - Only apply boost if there is no pending decay (priority_decrement == 0) + // AND boost is not disabled on this thread. + // - Boosted priority = base + increment, clamped to max_priority_cap. + // - Only boost UP — never lower priority below its current value. + bool apply_boost = false; + if (increment > 0 && is_guest_thread()) { + auto* kthread = guest_object(); + if (kthread->priority_decrement == 0 && !kthread->boost_disabled) { + apply_boost = true; + } + } else if (increment > 0) { + // Host threads (non-guest): apply boost unconditionally. + apply_boost = true; + } + + if (apply_boost) { + int32_t boosted = base_priority_ + increment; + // Clamp to the per-thread max dynamic priority cap. + // For title threads this is 17 (just below real-time threshold). + int32_t max_cap = 17; + if (is_guest_thread()) { + uint8_t guest_cap = guest_object()->max_dynamic_priority; + if (guest_cap > 0) { + max_cap = guest_cap; + } + } + if (boosted > max_cap) { + boosted = max_cap; + } + // Only boost UP, never lower. + if (boosted > priority_) { + priority_ = boosted; + boost_amount_ = priority_ - base_priority_; + if (is_guest_thread()) { + guest_object()->priority = static_cast(priority_); + } + thread_->set_priority(GuestPriorityToHost(priority_)); + } + } + + quantum_start_ms_ = Clock::QueryHostUptimeMillis(); +} + void XThread::SetAffinity(uint32_t affinity) { SetActiveCpu(GetFakeCpuNumber(affinity)); } diff --git a/src/xenia/kernel/xthread.h b/src/xenia/kernel/xthread.h index dd0fa31b8..527e549c5 100644 --- a/src/xenia/kernel/xthread.h +++ b/src/xenia/kernel/xthread.h @@ -298,9 +298,9 @@ struct X_KTHREAD { uint8_t unk_A5[0xB]; // 0xA5 int32_t apc_disable_count; // 0xB0 xe::be quantum; // 0xB4 - uint8_t unk_B8; // 0xB8 - uint8_t unk_B9; // 0xB9 - uint8_t unk_BA; // 0xBA + uint8_t saturation_increment; // 0xB8 + uint8_t base_priority; // 0xB9 + uint8_t priority_decrement; // 0xBA uint8_t boost_disabled; // 0xBB uint8_t suspend_count; // 0xBC uint8_t was_preempted; // 0xBD @@ -310,9 +310,9 @@ struct X_KTHREAD { // all TypedGuestPointer a_prcb_ptr; // 0xC0 TypedGuestPointer another_prcb_ptr; // 0xC4 - uint8_t unk_C8; // 0xC8 - uint8_t unk_C9; // 0xC9 - uint8_t unk_CA; // 0xCA + uint8_t process_priority_class; // 0xC8 + uint8_t base_priority_copy; // 0xC9 + uint8_t max_dynamic_priority; // 0xCA uint8_t unk_CB; // 0xCB X_KSPINLOCK timer_list_lock; // 0xCC xe::be stack_alloc_base; // 0xD0 @@ -422,6 +422,22 @@ class XThread : public XObject, public cpu::Thread { int32_t QueryPriority(); void SetPriority(int32_t increment); + // Called periodically (~20ms) by KernelState's timestamp timer to simulate + // the Xenon scheduler's quantum-based priority decay for non-real-time + // threads (base_priority < 18). Threads that run for longer than one + // quantum (~20ms) have their effective priority decayed toward the base, + // which causes them to drop into lower host priority buckets and prevents + // starvation. On the first decay step the accumulated priority boost is + // also drained. + void CheckQuantumAndDecay(); + // Called when a thread wakes from a kernel wait. Applies a priority + // boost of |increment| above base_priority (matching the Xenon kernel's + // unwait-boost behavior) and restarts the quantum timer. The boost is + // drained on the next quantum expiry via CheckQuantumAndDecay(). + // If increment is 0 or the thread has boost disabled, the priority is + // simply restored to base_priority. + void BoostOnWake(int32_t increment); + // Xbox thread IDs: // 0 - core 0, thread 0 - user // 1 - core 0, thread 1 - user @@ -491,7 +507,10 @@ class XThread : public XObject, public cpu::Thread { bool main_thread_ = false; // Entry-point thread bool running_ = false; - int32_t priority_ = 0; + int32_t priority_ = 0; // current effective priority (may be decayed) + int32_t base_priority_ = 0; // priority floor — decay never goes below this + int32_t boost_amount_ = 0; // accumulated priority boost above base + uint64_t quantum_start_ms_ = 0; // host uptime (ms) when quantum last reset #if !XE_PLATFORM_WIN32 // Condition variable for thread self-suspension. diff --git a/src/xenia/memory.cc b/src/xenia/memory.cc index bfca9c7e5..70fe4b66b 100644 --- a/src/xenia/memory.cc +++ b/src/xenia/memory.cc @@ -1058,7 +1058,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, uint32_t protect) { alignment = xe::round_up(alignment, page_size_); size = xe::align(size, alignment); - assert_true(base_address % alignment == 0); + assert_true((base_address + host_address_offset_) % alignment == 0); uint32_t page_count = get_page_count(size, page_size_); uint32_t start_page_number = (base_address - heap_base_) / page_size_; uint32_t end_page_number = start_page_number + page_count - 1; @@ -1214,7 +1214,11 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, } // Compute the highest aligned start within this block and range. - uint32_t usable_end = std::min(block_end, high_page_number + 1); + // high_page_number is exclusive and rounded down to the stride, so + // the top stride of pages is never returned. + uint32_t high_aligned = + high_page_number - QuickMod(high_page_number, page_scan_stride); + uint32_t usable_end = std::min(block_end, high_aligned); if (usable_end < page_count) { continue; } @@ -1255,10 +1259,12 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, } // Compute the lowest aligned start within this block and range. + // high_page_number is treated as exclusive — the page at + // high_page_number itself is never returned. uint32_t earliest = std::max(block_start, low_page_number); uint32_t aligned_start = xe::round_up(earliest, page_scan_stride, false); if (aligned_start + page_count <= block_end && - aligned_start + page_count - 1 <= high_page_number) { + aligned_start + page_count <= high_page_number) { start_page_number = aligned_start; end_page_number = aligned_start + page_count - 1; break; @@ -1743,6 +1749,13 @@ void PhysicalHeap::Initialize(Memory* memory, uint8_t* membase, BaseHeap::Initialize(memory, membase, heap_type, heap_base, heap_size, page_size, host_address_offset); parent_heap_ = parent_heap; + + // The physical base offset (host_address_offset) must be a multiple of + // page_size. Otherwise, aligned parent allocations become misaligned after + // translation back to virtual addresses (parent_address + heap_base_ - + // GetPhysicalAddress(heap_base_) loses alignment). + xenia_assert(host_address_offset % page_size == 0); + system_page_size_ = uint32_t(xe::memory::page_size()); xenia_assert(xe::is_pow2(system_page_size_)); system_page_shift_ = xe::log2_floor(system_page_size_); @@ -1785,7 +1798,15 @@ bool PhysicalHeap::Alloc(uint32_t size, uint32_t alignment, // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = heap_base_ + parent_address - parent_heap_start; + const uint32_t address = heap_base_ + parent_address - parent_heap_start; + if ((address + host_address_offset_) % alignment != 0) { + XELOGE( + "PhysicalHeap::Alloc translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, parent_heap_start); + parent_heap_->Release(parent_address); + return false; + } if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( @@ -1821,12 +1842,17 @@ bool PhysicalHeap::AllocFixed(uint32_t base_address, uint32_t size, // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = + const uint32_t address = heap_base_ + parent_base_address - GetPhysicalAddress(heap_base_); - // The physical memory is already aligned properly by the parent heap. - // We only need page alignment for the virtual address since the actual - // memory alignment requirement has been satisfied in physical space. - if (!BaseHeap::AllocFixed(address, size, page_size_, allocation_type, + if ((address + host_address_offset_) % alignment != 0) { + XELOGE( + "PhysicalHeap::AllocFixed translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, GetPhysicalAddress(heap_base_)); + parent_heap_->Release(parent_base_address); + return false; + } + if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( "PhysicalHeap::AllocFixed unable to pin physical memory in physical " @@ -1866,15 +1892,19 @@ bool PhysicalHeap::AllocRange(uint32_t low_address, uint32_t high_address, parent_heap_->total_page_count()); return false; } - // Given the address we've reserved in the parent heap, pin that here. // Shouldn't be possible for it to be allocated already. - uint32_t address = + const uint32_t address = heap_base_ + parent_address - GetPhysicalAddress(heap_base_); - // The physical memory is already aligned properly by the parent heap. - // We only need page alignment for the virtual address since the actual - // memory alignment requirement has been satisfied in physical space. - if (!BaseHeap::AllocFixed(address, size, page_size_, allocation_type, + if ((address + host_address_offset_) % alignment != 0) { + XELOGE( + "PhysicalHeap::AllocRange translated address {:08X} misaligned " + "(alignment {:08X}, physical base offset {:08X})", + address, alignment, GetPhysicalAddress(heap_base_)); + parent_heap_->Release(parent_address); + return false; + } + if (!BaseHeap::AllocFixed(address, size, alignment, allocation_type, protect)) { XELOGE( "PhysicalHeap::AllocRange unable to pin physical memory in physical " diff --git a/src/xenia/ui/imgui_drawer.cc b/src/xenia/ui/imgui_drawer.cc index e3c7c736d..180e54404 100644 --- a/src/xenia/ui/imgui_drawer.cc +++ b/src/xenia/ui/imgui_drawer.cc @@ -151,6 +151,7 @@ void ImGuiDrawer::Initialize() { auto& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; const float font_size = std::max((float)cvars::font_size, 8.f); const float title_font_size = font_size + 6.f; diff --git a/src/xenia/ui/profile_dialogs.cc b/src/xenia/ui/profile_dialogs.cc index d9bde3465..06a22cdc8 100644 --- a/src/xenia/ui/profile_dialogs.cc +++ b/src/xenia/ui/profile_dialogs.cc @@ -61,6 +61,9 @@ void NoProfileDialog::OnDraw(ImGuiIO& io) { const auto content_files = xe::filesystem::ListDirectories( emulator_window_->emulator()->content_root()); + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } if (content_files.empty()) { if (ImGui::Button("Create Profile")) { new kernel::xam::ui::CreateProfileUI(emulator_window_->imgui_drawer(), diff --git a/src/xenia/xbox.h b/src/xenia/xbox.h index e28560b4f..f159a58ab 100644 --- a/src/xenia/xbox.h +++ b/src/xenia/xbox.h @@ -60,6 +60,7 @@ typedef uint32_t X_STATUS; #define X_STATUS_OBJECT_NAME_COLLISION ((X_STATUS)0xC0000035L) #define X_STATUS_INVALID_PAGE_PROTECTION ((X_STATUS)0xC0000045L) #define X_STATUS_MUTANT_NOT_OWNED ((X_STATUS)0xC0000046L) +#define X_STATUS_SEMAPHORE_LIMIT_EXCEEDED ((X_STATUS)0xC0000047L) #define X_STATUS_THREAD_IS_TERMINATING ((X_STATUS)0xC000004BL) #define X_STATUS_PROCEDURE_NOT_FOUND ((X_STATUS)0xC000007AL) #define X_STATUS_INVALID_IMAGE_FORMAT ((X_STATUS)0xC000007BL) @@ -72,7 +73,6 @@ typedef uint32_t X_STATUS; #define X_STATUS_INVALID_PARAMETER_2 ((X_STATUS)0xC00000F0L) #define X_STATUS_INVALID_PARAMETER_3 ((X_STATUS)0xC00000F1L) #define X_STATUS_PROCESS_IS_TERMINATING ((X_STATUS)0xC000010AL) -#define X_STATUS_SEMAPHORE_LIMIT_EXCEEDED ((X_STATUS)0xC000012BL) #define X_STATUS_DLL_NOT_FOUND ((X_STATUS)0xC0000135L) #define X_STATUS_ENTRYPOINT_NOT_FOUND ((X_STATUS)0xC0000139L) #define X_STATUS_MAPPED_ALIGNMENT ((X_STATUS)0xC0000220L)