mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 00:00:17 +02:00
Added foveated rendering for the Quest
- Eyes render under a VK_EXT_fragment_density_map: full rate around each eye's forward direction, 2x2 then 4x4 pixel blocks towards the edges ([vr] foveation = off|low|medium|high, default off). XR_FB_foveation cannot help here: the runtime's maps only shape passes drawing into its swapchain, and the eyes reach it through a copy. - aurora-main/patches/dawn/aurora_fdm.inc: Dawn enables the extension only on request and for dynamic rendering, flags every render pipeline, and chains an immutable RG8 map into any pass whose first color attachment is a view bound to one (ABI: include/aurora/dawn_fdm_abi.h). - android/Build-QuestDawn.ps1 builds the pinned Dawn revision with those patches for arm64 (dawn-build CI flags, protobuf off) into a cached package; Build-Quest.ps1 links it (-StockDawn opts out) and AuroraDawnProvider.cmake enables the ABI from its manifest. - lib/gfx/foveation.hpp generates the maps (32 px per texel, densities 255/127/63); an eye is foveated only when single_pass_eyes draws it in one render pass. Menus never are. - Live level from the headset panel's VR tab and the launcher; the launch decides whether the device has maps. debug.wiicompiled.foveation and debug.wiicompiled.fdm for A/B. - Tests: Foveation cases in gx_fifo_tests, mkw_vr_config_tests. Docs: OPENXR.md, quest-port.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ceeeba0332
commit
cc653272c8
30 files changed
+1527
-14
No files matched your search
@@ -177,6 +177,11 @@ its CPU and GPU domains: `boost`, `sustained_high`, `sustained_low`, `power_savi
|
||||
request (see `docs/quest-port.md`); desktop runtimes rarely offer the extension, and the setting
|
||||
then does nothing. It is read at launch, and the session log records whether the runtime accepted
|
||||
it and any later performance notification (a thermal or rendering warning).
|
||||
`foveation` (Quest only, default `off`) shades the edges of the immersive race view more coarsely:
|
||||
`off`, `low`, `medium` or `high`, see [Foveated rendering](#foveated-rendering). A session launched
|
||||
with it off runs without fragment density maps, so going from `off` to a level takes a restart;
|
||||
between levels, and back to `off`, it is live from the headset panel's VR tab. The launcher's
|
||||
Settings page has it too.
|
||||
|
||||
## Controllers
|
||||
|
||||
@@ -614,6 +619,44 @@ Kart Wii's minimap are treated as game art and remain eligible for the screen. A
|
||||
uses the full eye viewport and scissor because its recorded rectangle no longer describes where it
|
||||
ended up; its original viewport is folded into the projection instead.
|
||||
|
||||
## Foveated rendering
|
||||
|
||||
On the Quest, `foveation` shades the edges of the immersive race view in 2x2, then 4x4 pixel
|
||||
blocks, where the headset's lenses blur the picture anyway, and gives the GPU time back for a
|
||||
higher `render_scale` or a steadier frame rate. Each eye's render pass runs under a fragment
|
||||
density map (`VK_EXT_fragment_density_map`, attached through dynamic rendering). The map is centred
|
||||
on that eye's forward direction, which the asymmetric frustum places off the image centre, towards
|
||||
the nose. Its rings are angles from that direction (`aurora-main/lib/gfx/foveation.hpp`):
|
||||
|
||||
| Level | Full rate | Half (2x2) | Quarter (4x4) |
|
||||
| --- | --- | --- | --- |
|
||||
| `low` | within 30° | beyond | never |
|
||||
| `medium` | within 25° | 25° to 40° | beyond 40° |
|
||||
| `high` | within 18° | 18° to 34° | beyond 34° |
|
||||
|
||||
The HUD is drawn in the same render pass as the world and is foveated with it. `low` and `medium`
|
||||
keep the default HUD screen (2.4 m wide at 2 m) at half rate or better while you look straight
|
||||
ahead; `high` coarsens its corners. Menus and every other virtual screen, the settings panel, and
|
||||
anything drawn outside an immersive race are never foveated.
|
||||
|
||||
`XR_FB_foveation`, the extension DolphinXR uses by default, cannot help here. The runtime's density
|
||||
maps only shape render passes that draw into its swapchain images, and on the Quest Dawn draws each
|
||||
eye on its own device and hands it to the OpenXR device, which copies it into the swapchain. So the
|
||||
map has to go into Dawn's own eye passes. The stock Dawn package has no such feature, so the Quest
|
||||
build links a Dawn built with Aurora's patches (`aurora-main/patches/dawn`, built by
|
||||
`android/Build-QuestDawn.ps1`, see `docs/quest-port.md`). The patch enables the extension only when
|
||||
Aurora asks for it at device creation, and every render pipeline then carries the density-map
|
||||
pipeline flag. That is why the launch decides.
|
||||
|
||||
A density map forces Adreno into binned rendering, where every extra render pass in an eye stores
|
||||
and reloads the whole eye. DolphinXR measured foveation as a net loss on Mario Kart Wii for exactly
|
||||
that reason (its bloom chain splits the frame about 20 times). An eye is therefore foveated only
|
||||
when it is drawn in a single render pass (`single_pass_eyes`); an eye that a partial clear still
|
||||
splits is drawn at full rate. The session log reports what happened: "Fragment density maps:
|
||||
enabled" at startup, one "eye foveation" line per eye and level with the map's size, and the "Eye
|
||||
replay plan" lines. `debug.wiicompiled.foveation <0-3>` overrides the level for A/B timing, and
|
||||
`debug.wiicompiled.fdm 0` launches without density maps at all (`docs/quest-port.md`).
|
||||
|
||||
## Diagnostics
|
||||
|
||||
**F10 > Diagnostics** holds two bug-report aids.
|
||||
|
||||
+12
-1
@@ -15,13 +15,19 @@
|
||||
# Prerequisites (see docs/quest-port.md): JDK 17, the Android SDK with NDK
|
||||
# 29.0.14206865 and CMake 3.22.1, and android/Prepare-QuestDependencies.ps1
|
||||
# having staged the SDL3 AAR.
|
||||
#
|
||||
# Dawn is built from source with Aurora's patches (android/Build-QuestDawn.ps1:
|
||||
# fragment density maps for foveated rendering) and cached under
|
||||
# .scratch/quest-dawn; the first build takes a while. -StockDawn links the
|
||||
# stock prebuilt package instead, which leaves foveated rendering unavailable.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Generated = '',
|
||||
[string]$Dependencies = '',
|
||||
[string]$CMakeDir = '',
|
||||
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
||||
[switch]$Install
|
||||
[switch]$Install,
|
||||
[switch]$StockDawn
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
@@ -79,6 +85,11 @@ $gradleArgs = @(
|
||||
"-PmkwGeneratedDir=$Generated"
|
||||
)
|
||||
if ($Dependencies) { $gradleArgs += "-PmkwDependenciesDir=$Dependencies" }
|
||||
if (-not $StockDawn) {
|
||||
$dawnWork = Join-Path $repo '.scratch\quest-dawn'
|
||||
& (Join-Path $root 'Build-QuestDawn.ps1') -WorkDirectory $dawnWork
|
||||
$gradleArgs += "-PmkwQuestDawnDir=$(Join-Path $dawnWork 'package')"
|
||||
}
|
||||
$gradleArgs += $task
|
||||
Write-Host "gradlew $($gradleArgs -join ' ')"
|
||||
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Builds the pinned Dawn for the Quest (Android arm64) with Aurora's patches (aurora-main/patches/dawn).
|
||||
# The stock prebuilt package has no fragment density maps, which the headset's foveated rendering
|
||||
# attaches to the eye render passes. The result is an install tree in the stock package's layout;
|
||||
# Build-Quest.ps1 hands it to the native build through -PmkwQuestDawnDir.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Build-QuestDawn.ps1 [-WorkDirectory <dir>] [-Force]
|
||||
#
|
||||
# The package is cached. Its aurora-dawn.json records the source revision, the patch files' hash, the
|
||||
# NDK and the flags it was built with, and a later run with the same inputs reuses it. That matters
|
||||
# beyond build time: every Quest game kit fingerprints the Dawn archive, so a rebuilt archive would
|
||||
# make players rebuild their games.
|
||||
#
|
||||
# Prerequisites: Git, Python 3.12+, CMake 3.25+, and the Android SDK with NDK 29.0.14206865 and CMake
|
||||
# 3.22.1 (for Ninja). The first build compiles all of Dawn and Tint and takes a while.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$WorkDirectory = '',
|
||||
[string]$Python = 'python',
|
||||
[int]$Jobs = [Environment]::ProcessorCount,
|
||||
[switch]$Force
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repo = (Resolve-Path (Join-Path $root '..')).Path
|
||||
if (-not $WorkDirectory) { $WorkDirectory = Join-Path $repo '.scratch\quest-dawn' }
|
||||
$WorkDirectory = [IO.Path]::GetFullPath($WorkDirectory)
|
||||
[IO.Directory]::CreateDirectory($WorkDirectory) | Out-Null
|
||||
|
||||
# The revision the stock package and the Windows Vulkan DLL (Launcher/Build-DawnVulkan.ps1) are
|
||||
# built from, and the dawn-build CI's Android configuration.
|
||||
$revision = '13abc3bc8ea2d3c2050f9e77a12d012108ceee24'
|
||||
$archiveHash = '713bea5b92d4f6c5175752fd7cbf1c3c5ce36598ff5dd98685d8a1216614ebba'
|
||||
$ndkVersion = '29.0.14206865'
|
||||
$platform = 'android-28'
|
||||
$flags = @(
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DANDROID_ABI=arm64-v8a',
|
||||
"-DANDROID_PLATFORM=$platform",
|
||||
'-DANDROID_STL=c++_shared',
|
||||
'-DDAWN_FETCH_DEPENDENCIES=ON',
|
||||
'-DDAWN_BUILD_MONOLITHIC_LIBRARY=STATIC',
|
||||
'-DBUILD_SHARED_LIBS=OFF',
|
||||
'-DDAWN_ENABLE_INSTALL=ON',
|
||||
'-DDAWN_BUILD_SAMPLES=OFF',
|
||||
'-DDAWN_BUILD_TESTS=OFF',
|
||||
'-DDAWN_BUILD_BENCHMARKS=OFF',
|
||||
'-DDAWN_USE_GLFW=OFF',
|
||||
'-DTINT_BUILD_TESTS=OFF',
|
||||
'-DTINT_BUILD_CMD_TOOLS=OFF',
|
||||
'-DTINT_BUILD_IR_BINARY=OFF',
|
||||
# Only the IR binary format needs protobuf, and building it would run the protoc it cross-compiled
|
||||
# for Android (the CI hands it a host protoc instead).
|
||||
'-DDAWN_BUILD_PROTOBUF=OFF'
|
||||
)
|
||||
|
||||
$sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
|
||||
$ndk = Join-Path $sdkRoot "ndk\$ndkVersion"
|
||||
$toolchainFile = Join-Path $ndk 'build\cmake\android.toolchain.cmake'
|
||||
if (-not (Test-Path $toolchainFile)) { throw "NDK $ndkVersion not found under $sdkRoot (set ANDROID_HOME)" }
|
||||
$llvmBin = Join-Path $ndk 'toolchains\llvm\prebuilt\windows-x86_64\bin'
|
||||
$ninja = Get-ChildItem -Path (Join-Path $sdkRoot 'cmake') -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending | ForEach-Object { Join-Path $_.FullName 'bin\ninja.exe' } |
|
||||
Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $ninja) { throw "No Ninja under $sdkRoot\cmake; install the SDK's CMake 3.22.1" }
|
||||
$pythonPath = (Get-Command $Python -ErrorAction Stop).Source
|
||||
|
||||
function Get-TextSha256([string]$text) {
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($text))
|
||||
return -join ($bytes | ForEach-Object { $_.ToString('x2') })
|
||||
} finally { $sha.Dispose() }
|
||||
}
|
||||
|
||||
$patchDir = Join-Path $repo 'aurora-main\patches\dawn'
|
||||
$patchFiles = @(
|
||||
(Join-Path $patchDir 'apply.py'),
|
||||
(Join-Path $patchDir 'aurora_vulkan_hooks.h'),
|
||||
(Join-Path $patchDir 'aurora_vulkan_interop.inc'),
|
||||
(Join-Path $patchDir 'aurora_fdm.h'),
|
||||
(Join-Path $patchDir 'aurora_fdm.inc'),
|
||||
(Join-Path $repo 'aurora-main\include\aurora\dawn_vulkan_abi.h'),
|
||||
(Join-Path $repo 'aurora-main\include\aurora\dawn_fdm_abi.h')
|
||||
)
|
||||
# Line endings vary between checkouts; hash the patch text with LF only.
|
||||
$patchHash = Get-TextSha256 (($patchFiles | ForEach-Object {
|
||||
"$([IO.Path]::GetFileName($_))`n$(([IO.File]::ReadAllText($_)).Replace("`r`n", "`n"))"
|
||||
}) -join "`n")
|
||||
$cacheKey = Get-TextSha256 "$revision|$archiveHash|$patchHash|$ndkVersion|$($flags -join ' ')"
|
||||
|
||||
$package = Join-Path $WorkDirectory 'package'
|
||||
$manifestPath = Join-Path $package 'aurora-dawn.json'
|
||||
$library = Join-Path $package 'lib\libwebgpu_dawn.a'
|
||||
if (-not $Force -and (Test-Path $manifestPath) -and (Test-Path $library)) {
|
||||
$manifest = [IO.File]::ReadAllText($manifestPath) | ConvertFrom-Json
|
||||
$libraryHash = (Get-FileHash -LiteralPath $library -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($manifest.CacheKey -eq $cacheKey -and $manifest.ArchiveSha256 -eq $libraryHash) {
|
||||
Write-Host "Patched Dawn for the Quest is up to date: $package"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$archive = Join-Path $WorkDirectory 'dawn-source.tar.gz'
|
||||
if (-not (Test-Path -LiteralPath $archive)) {
|
||||
Invoke-WebRequest "https://github.com/google/dawn/archive/$revision.tar.gz" -OutFile $archive -UseBasicParsing
|
||||
}
|
||||
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $archiveHash) {
|
||||
throw 'Dawn source archive does not match the pinned SHA-256.'
|
||||
}
|
||||
|
||||
# The patches are applied to pristine sources: src/ is extracted again whenever the patches changed,
|
||||
# while third_party/, which DAWN_FETCH_DEPENDENCIES fills, is kept. A tree already patched with these
|
||||
# patches is reused, so a flag change does not recompile all of Dawn.
|
||||
$source = Join-Path $WorkDirectory "dawn-$revision"
|
||||
$patchMarker = Join-Path $source 'aurora-patches.sha256'
|
||||
$patched = (Test-Path -LiteralPath $patchMarker) -and ([IO.File]::ReadAllText($patchMarker).Trim() -eq $patchHash)
|
||||
$extract = @'
|
||||
import sys, tarfile
|
||||
archive, destination, only_src = sys.argv[1], sys.argv[2], sys.argv[3] == "1"
|
||||
with tarfile.open(archive) as tar:
|
||||
members = tar.getmembers()
|
||||
if only_src:
|
||||
members = [m for m in members if m.name.split("/", 2)[1:2] == ["src"]]
|
||||
tar.extractall(destination, members=members, filter="data")
|
||||
'@
|
||||
if (-not $patched) {
|
||||
$extractScript = Join-Path $WorkDirectory 'extract.py'
|
||||
[IO.File]::WriteAllText($extractScript, $extract)
|
||||
$onlySource = Test-Path -LiteralPath $source
|
||||
if ($onlySource) { Remove-Item -LiteralPath (Join-Path $source 'src') -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
& $pythonPath $extractScript $archive $WorkDirectory $(if ($onlySource) { '1' } else { '0' })
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dawn source extraction failed (Python 3.12+ required).' }
|
||||
& $pythonPath (Join-Path $patchDir 'apply.py') $source
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Applying the Aurora Dawn patches failed.' }
|
||||
[IO.File]::WriteAllText($patchMarker, $patchHash)
|
||||
}
|
||||
|
||||
$build = Join-Path $WorkDirectory 'build'
|
||||
& cmake -S $source -B $build -G Ninja "-DCMAKE_MAKE_PROGRAM=$ninja" "-DCMAKE_TOOLCHAIN_FILE=$toolchainFile" `
|
||||
"-DPython3_EXECUTABLE=$pythonPath" @flags "-DCMAKE_INSTALL_PREFIX=$package"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dawn configuration failed.' }
|
||||
& cmake --build $build --parallel $Jobs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dawn build failed.' }
|
||||
if (Test-Path -LiteralPath $package) { Remove-Item -LiteralPath $package -Recurse -Force }
|
||||
& cmake --install $build
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dawn installation failed.' }
|
||||
if (-not (Test-Path -LiteralPath $library)) { throw "Dawn archive missing: $library" }
|
||||
# As the stock package: debug info would multiply the archive the APK carries in its game kit.
|
||||
& (Join-Path $llvmBin 'llvm-strip.exe') --strip-debug $library
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Stripping the Dawn archive failed.' }
|
||||
|
||||
$manifestJson = [ordered]@{
|
||||
SourceRevision = $revision
|
||||
SourceSha256 = $archiveHash
|
||||
PatchSha256 = $patchHash
|
||||
AuroraVulkanAbi = 1
|
||||
AuroraFdmAbi = 1
|
||||
Ndk = $ndkVersion
|
||||
Flags = ($flags -join ' ')
|
||||
CacheKey = $cacheKey
|
||||
ArchiveSha256 = (Get-FileHash -LiteralPath $library -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
} | ConvertTo-Json
|
||||
# No byte order mark: CMake's string(JSON) reads this file.
|
||||
[IO.File]::WriteAllText($manifestPath, $manifestJson, (New-Object Text.UTF8Encoding $false))
|
||||
Write-Host "Patched Dawn for the Quest ready: $package"
|
||||
@@ -12,6 +12,9 @@ val mkwGeneratedDir = providers.gradleProperty("mkwGeneratedDir").orNull
|
||||
// Optional: a directory of already-fetched dependency sources (the installer's
|
||||
// BuildWorkspace/Dependencies) so the native configure does not download them.
|
||||
val mkwDependenciesDir = providers.gradleProperty("mkwDependenciesDir").orNull
|
||||
// Optional: the Dawn package android/Build-QuestDawn.ps1 built with Aurora's patches (fragment density
|
||||
// maps for foveated rendering). Without it the stock prebuilt Dawn is used and foveation is off.
|
||||
val mkwQuestDawnDir = providers.gradleProperty("mkwQuestDawnDir").orNull
|
||||
// Optional: a directory holding a second SDL3 AAR/prefab is not needed; the
|
||||
// AAR in app/libs is produced by Prepare-QuestDependencies.ps1.
|
||||
val mkwRepoRoot = rootProject.file("..").canonicalFile
|
||||
@@ -135,6 +138,9 @@ android {
|
||||
if (mkwDependenciesDir != null) {
|
||||
arguments += "-DMKW_DEPENDENCIES_DIR=${File(mkwDependenciesDir).canonicalPath.replace('\\', '/')}"
|
||||
}
|
||||
// Always passed, empty when absent, so the native build drops a previous override.
|
||||
arguments += "-DMKW_QUEST_DAWN_PACKAGE_DIR=" +
|
||||
(mkwQuestDawnDir?.let { File(it).canonicalPath.replace('\\', '/') } ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ if(DEFINED MKW_DEPENDENCIES_DIR AND IS_DIRECTORY "${MKW_DEPENDENCIES_DIR}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The Dawn package android/Build-QuestDawn.ps1 built with Aurora's patches (fragment density maps for
|
||||
# foveated rendering) stands in for the stock prebuilt one. The override lives in this build
|
||||
# directory's cache, so a build without the package clears it rather than keep the last one.
|
||||
if(DEFINED MKW_QUEST_DAWN_PACKAGE_DIR AND NOT MKW_QUEST_DAWN_PACKAGE_DIR STREQUAL "")
|
||||
if(NOT EXISTS "${MKW_QUEST_DAWN_PACKAGE_DIR}/aurora-dawn.json")
|
||||
message(FATAL_ERROR "No patched Dawn package at ${MKW_QUEST_DAWN_PACKAGE_DIR}; run android/Build-QuestDawn.ps1")
|
||||
endif()
|
||||
set(FETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT "${MKW_QUEST_DAWN_PACKAGE_DIR}" CACHE PATH "" FORCE)
|
||||
else()
|
||||
unset(FETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT CACHE)
|
||||
endif()
|
||||
|
||||
find_package(SDL3 REQUIRED CONFIG)
|
||||
set(AURORA_SDL3_PROVIDER system CACHE STRING "" FORCE)
|
||||
set(AURORA_SDL3_LINKAGE shared CACHE STRING "" FORCE)
|
||||
|
||||
@@ -200,6 +200,15 @@ class SettingsPage(
|
||||
read = { stringIndex(it, "vr", "performance_level", PERFORMANCE_LEVELS) },
|
||||
write = { c, index -> c.setString("vr", "performance_level", PERFORMANCE_LEVELS[index]) },
|
||||
)
|
||||
choice(
|
||||
R.string.vr_foveation, R.string.vr_foveation_helper,
|
||||
listOf(
|
||||
R.string.vr_foveation_off, R.string.vr_foveation_low,
|
||||
R.string.vr_foveation_medium, R.string.vr_foveation_high,
|
||||
),
|
||||
read = { stringIndex(it, "vr", "foveation", FOVEATION_LEVELS) },
|
||||
write = { c, index -> c.setString("vr", "foveation", FOVEATION_LEVELS[index]) },
|
||||
)
|
||||
choice(
|
||||
R.string.vr_interpolation, R.string.vr_interpolation_helper,
|
||||
listOf(activity.getString(R.string.vr_interpolation_off), activity.getString(R.string.vr_interpolation_auto), "72 FPS", "90 FPS", "120 FPS"),
|
||||
@@ -725,6 +734,8 @@ class SettingsPage(
|
||||
val SEATS = listOf("cockpit", "custom")
|
||||
// The runtime's default ("boost") first: an absent key reads as index 0.
|
||||
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
|
||||
// runtime_config.h's kVrFoveationLevels, the default ("off") first.
|
||||
val FOVEATION_LEVELS = listOf("off", "low", "medium", "high")
|
||||
val CONTROLLER_MODES = listOf("wii_remote", "gamepad")
|
||||
val INTERPOLATION_FPS = listOf(0L, 1L, 72L, 90L, 120L)
|
||||
val RESOLUTIONS = listOf(1.0, 1.5, 2.0, 3.0, 4.0)
|
||||
|
||||
@@ -543,6 +543,12 @@
|
||||
<string name="vr_performance_level_sustained_low">Sustained low</string>
|
||||
<string name="vr_performance_level_power_savings">Power savings</string>
|
||||
<string name="vr_performance_level_default">Headset default</string>
|
||||
<string name="vr_foveation">Foveated rendering</string>
|
||||
<string name="vr_foveation_helper">Shades the edges of the race view in coarser blocks, where the lenses blur the picture anyway, to free GPU time for a higher render scale or a steadier frame rate. Higher levels start closer to the centre; High also coarsens the corners of the HUD. Menus are never foveated.</string>
|
||||
<string name="vr_foveation_off">Off</string>
|
||||
<string name="vr_foveation_low">Low</string>
|
||||
<string name="vr_foveation_medium">Medium</string>
|
||||
<string name="vr_foveation_high">High</string>
|
||||
<string name="vr_interpolation">VR frame interpolation</string>
|
||||
<string name="vr_interpolation_helper">Experimental. Renders extra frames between game frames at the headset\'s rate. Needs GPU headroom and adds one game frame of latency.</string>
|
||||
<string name="vr_interpolation_off">Off</string>
|
||||
|
||||
@@ -265,6 +265,20 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
"The package must be a Dawn install tree built with DAWN_ENABLE_INSTALL=ON.")
|
||||
endif ()
|
||||
|
||||
# A package built with Aurora's patches (android/Build-QuestDawn.ps1) describes them in
|
||||
# aurora-dawn.json. Only such a package has the fragment density map ABI
|
||||
# (include/aurora/dawn_fdm_abi.h); aurora_core compiles its callers against it.
|
||||
set(AURORA_DAWN_FDM_ABI 0 PARENT_SCOPE)
|
||||
if (EXISTS "${_dawn_pkg_dir}/aurora-dawn.json")
|
||||
file(READ "${_dawn_pkg_dir}/aurora-dawn.json" _aurora_dawn_manifest)
|
||||
string(JSON _aurora_dawn_fdm_abi ERROR_VARIABLE _aurora_dawn_manifest_error
|
||||
GET "${_aurora_dawn_manifest}" AuroraFdmAbi)
|
||||
if (NOT _aurora_dawn_manifest_error AND _aurora_dawn_fdm_abi GREATER 0)
|
||||
set(AURORA_DAWN_FDM_ABI ${_aurora_dawn_fdm_abi} PARENT_SCOPE)
|
||||
message(STATUS "aurora: Dawn package carries the fragment density map ABI ${_aurora_dawn_fdm_abi}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
_aurora_dawn_set_platform_backends()
|
||||
|
||||
get_target_property(_dawn_pkg_type dawn::webgpu_dawn TYPE)
|
||||
|
||||
@@ -45,6 +45,12 @@ if (AURORA_ENABLE_GX)
|
||||
# integration links everywhere.
|
||||
target_sources(aurora_core PRIVATE lib/webgpu/vulkan_interop.cpp)
|
||||
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
|
||||
# Fragment density maps for foveated eye rendering, from a Dawn built with Aurora's patches; the
|
||||
# value is the package's ABI version (include/aurora/dawn_fdm_abi.h).
|
||||
target_sources(aurora_core PRIVATE lib/webgpu/fdm.cpp)
|
||||
if (AURORA_DAWN_FDM_ABI)
|
||||
target_compile_definitions(aurora_core PRIVATE AURORA_DAWN_FDM=${AURORA_DAWN_FDM_ABI})
|
||||
endif ()
|
||||
if (DAWN_ENABLE_VULKAN)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
|
||||
endif ()
|
||||
|
||||
@@ -245,6 +245,11 @@ typedef struct {
|
||||
// Enables renderer features needed by an external XR compositor. The normal
|
||||
// desktop path is unchanged when false.
|
||||
bool xrInterop;
|
||||
// Asks for fragment density maps on the Vulkan device, for foveated eye
|
||||
// rendering (aurora_set_stereo_foveation). Only a Dawn built with Aurora's
|
||||
// patches has them (the Quest build). Every render pipeline is then built to
|
||||
// run under a density map, so set it only when foveation may be used.
|
||||
bool xrFragmentDensityMap;
|
||||
// Optional OpenXR-selected D3D adapter. Supplying the runtime's LUID before
|
||||
// device creation keeps Dawn and the compositor on the same physical GPU.
|
||||
bool hasD3D12AdapterLuid;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
// Versioned C ABI for the fragment density maps the patched Dawn attaches to Aurora's immersive
|
||||
// eye render passes (VK_EXT_fragment_density_map through dynamic rendering, see
|
||||
// patches/dawn/aurora_fdm.inc). The Quest build links that Dawn statically and
|
||||
// AuroraDawnProvider.cmake defines AURORA_DAWN_FDM_ABI when the package carries it. Devices and
|
||||
// texture views are WGPUDevice and WGPUTextureView handles.
|
||||
#define AURORA_DAWN_FDM_ABI 1
|
||||
|
||||
#if defined(AURORA_DAWN_FDM_IMPLEMENTATION)
|
||||
#define AURORA_DAWN_FDM_API DAWN_NATIVE_EXPORT
|
||||
#else
|
||||
#define AURORA_DAWN_FDM_API
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct {
|
||||
// VK_EXT_fragment_density_map is enabled on the device, for non-subsampled attachments.
|
||||
uint32_t enabled;
|
||||
// The framebuffer area one density map texel may cover.
|
||||
uint32_t minTexelWidth;
|
||||
uint32_t minTexelHeight;
|
||||
uint32_t maxTexelWidth;
|
||||
uint32_t maxTexelHeight;
|
||||
} AuroraDawnFdmCaps;
|
||||
|
||||
AURORA_DAWN_FDM_API uint32_t AuroraDawnFdmVersion(void);
|
||||
// Asks the next Vulkan device Dawn creates for fragment density maps. They are only enabled when
|
||||
// that device renders through dynamic rendering and supports non-subsampled attachments. Call it
|
||||
// before requesting the device.
|
||||
AURORA_DAWN_FDM_API void AuroraDawnFdmRequest(int enable);
|
||||
// Returns caps->enabled.
|
||||
AURORA_DAWN_FDM_API int AuroraDawnFdmQuery(void* device, AuroraDawnFdmCaps* caps);
|
||||
// Uploads an immutable RG8 density map, `width` by `height` texels in packed rows, and returns its
|
||||
// id, or 0 on failure. A map becomes usable once its upload has completed on the GPU: the driver
|
||||
// reads a non-dynamic map on the CPU when a render pass using it is recorded.
|
||||
AURORA_DAWN_FDM_API uint64_t AuroraDawnFdmCreateMap(void* device, uint32_t width, uint32_t height,
|
||||
const uint8_t* rg8);
|
||||
AURORA_DAWN_FDM_API int AuroraDawnFdmMapReady(void* device, uint64_t map);
|
||||
// Frees the map once the GPU is done with it, and unbinds it from any view.
|
||||
AURORA_DAWN_FDM_API void AuroraDawnFdmReleaseMap(void* device, uint64_t map);
|
||||
// Every render pass whose first color attachment is `view` gets `map` as its fragment density map
|
||||
// while the map is ready; 0 unbinds. The binding keeps the view alive until it is unbound. Returns 0
|
||||
// when the map is unknown or too small to cover the view.
|
||||
AURORA_DAWN_FDM_API int AuroraDawnFdmBind(void* device, void* view, uint64_t map);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -107,6 +107,18 @@ bool aurora_get_stereo_skip_copy_clears();
|
||||
void aurora_set_stereo_single_pass_eyes(bool enabled);
|
||||
bool aurora_get_stereo_single_pass_eyes();
|
||||
|
||||
// Fixed foveated rendering of the immersive eyes: 0 off, 1 low, 2 medium, 3
|
||||
// high. Each eye's render pass runs under a fragment density map that shades
|
||||
// the periphery in 2x2, then 4x4 pixel blocks, the higher the level the closer
|
||||
// to the centre. Only an eye drawn in a single render pass (single_pass_eyes)
|
||||
// is foveated; menus on the virtual screen never are. Live, but it needs a
|
||||
// device created with AuroraConfig::xrFragmentDensityMap and a Dawn built with
|
||||
// Aurora's patches (the Quest build); aurora_stereo_foveation_available says
|
||||
// whether this session has both.
|
||||
void aurora_set_stereo_foveation(uint32_t level);
|
||||
uint32_t aurora_get_stereo_foveation();
|
||||
bool aurora_stereo_foveation_available();
|
||||
|
||||
// Places orthographic GX draws (menus, HUD, 2D overlays) on a fixed virtual
|
||||
// screen during immersive replay instead of stretching them across the whole
|
||||
// eye viewport. The screen hangs `distance` world units straight ahead of the
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
#include "gfx/common.hpp"
|
||||
#include "gfx/efb_ram_copy.hpp"
|
||||
#include "gfx/foveation.hpp"
|
||||
#include "gfx/stereo_replay.hpp"
|
||||
#include "gx/fifo.hpp"
|
||||
#include "gx/shader_info.hpp"
|
||||
@@ -11,6 +12,7 @@
|
||||
#include "stereo_mirror.hpp"
|
||||
#include "stereo_interpolation.hpp"
|
||||
#include "stereo_overlay.hpp"
|
||||
#include "webgpu/fdm.hpp"
|
||||
#include "webgpu/gpu.hpp"
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#endif
|
||||
@@ -612,12 +614,27 @@ struct StereoEyeTarget {
|
||||
// Built on demand for the desktop mirror only, and dropped with the rest of
|
||||
// the target when ensure_stereo_eye_target replaces the textures.
|
||||
wgpu::BindGroup copyBindGroup;
|
||||
// Foveated rendering: a second view of `color` for the immersive eye passes,
|
||||
// which the patched Dawn binds to this eye's fragment density map
|
||||
// (webgpu/fdm.hpp), and what that map was built for.
|
||||
wgpu::TextureView foveatedView;
|
||||
uint64_t densityMap = 0;
|
||||
std::array<int32_t, 7> densityKey{};
|
||||
|
||||
const webgpu::TextureWithSampler& output() const noexcept { return resolvedColor.texture ? resolvedColor : color; }
|
||||
};
|
||||
std::array<StereoEyeTarget, AURORA_STEREO_EYE_COUNT> g_stereoEyeTargets;
|
||||
stereo::MirrorState g_stereoMirrorState;
|
||||
|
||||
// The map's binding holds the foveated view, and with it the eye texture, until it is released.
|
||||
void release_eye_density_map(StereoEyeTarget& target) noexcept {
|
||||
if (target.densityMap != 0) {
|
||||
webgpu::fdm::release_map(target.densityMap);
|
||||
target.densityMap = 0;
|
||||
}
|
||||
target.densityKey = {};
|
||||
}
|
||||
|
||||
// The eye targets outlive a frame, so the mirror samples them through a bind
|
||||
// group cached beside them rather than one built per presentation slot.
|
||||
wgpu::BindGroup stereo_eye_copy_bind_group(uint32_t eyeIndex) {
|
||||
@@ -637,6 +654,7 @@ void ensure_stereo_eye_target(uint32_t eyeIndex, uint32_t width, uint32_t height
|
||||
return;
|
||||
}
|
||||
|
||||
release_eye_density_map(target);
|
||||
target = {};
|
||||
target.color = webgpu::create_render_texture(width, height, samples > 1);
|
||||
if (samples > 1) {
|
||||
@@ -665,6 +683,56 @@ void ensure_stereo_eye_target(uint32_t eyeIndex, uint32_t width, uint32_t height
|
||||
target.depthFormat = target.depth.format;
|
||||
}
|
||||
|
||||
// The view an immersive eye's passes render through while foveated, or none. The eye's fragment
|
||||
// density map is rebuilt whenever its size, field of view or level changes (a map is immutable), and
|
||||
// is used once its upload has completed.
|
||||
wgpu::TextureView foveated_eye_view(uint32_t eyeIndex, const AuroraStereoEye& input) {
|
||||
auto& target = g_stereoEyeTargets[eyeIndex];
|
||||
const auto level = static_cast<gfx::foveation::Level>(gfx::get_stereo_foveation());
|
||||
if (level == gfx::foveation::Level::Off || target.samples > 1 || !webgpu::fdm::available()) {
|
||||
return {};
|
||||
}
|
||||
const auto fov = gfx::foveation::fov_from_projection(input.projection);
|
||||
// Hundredths of a tangent: finer than a map texel, coarse enough to ignore pose noise.
|
||||
const auto hundredths = [](float value) { return static_cast<int32_t>(std::lround(value * 100.0f)); };
|
||||
const std::array<int32_t, 7> key{static_cast<int32_t>(target.color.size.width),
|
||||
static_cast<int32_t>(target.color.size.height),
|
||||
static_cast<int32_t>(level),
|
||||
hundredths(fov.tanLeft),
|
||||
hundredths(fov.tanRight),
|
||||
hundredths(fov.tanDown),
|
||||
hundredths(fov.tanUp)};
|
||||
if (key != target.densityKey) {
|
||||
release_eye_density_map(target);
|
||||
target.densityKey = key;
|
||||
if (!target.foveatedView) {
|
||||
const wgpu::TextureViewDescriptor descriptor{
|
||||
.label = eyeIndex == 0 ? "Foveated left eye" : "Foveated right eye",
|
||||
.usage = wgpu::TextureUsage::RenderAttachment,
|
||||
};
|
||||
target.foveatedView = target.color.texture.CreateView(&descriptor);
|
||||
}
|
||||
gfx::foveation::Map map;
|
||||
gfx::foveation::build(target.color.size.width, target.color.size.height, webgpu::fdm::texel_size(), fov, level,
|
||||
map);
|
||||
target.densityMap = webgpu::fdm::create_map(map.width, map.height, map.rg8.data());
|
||||
if (target.densityMap != 0 && !webgpu::fdm::bind(target.foveatedView, target.densityMap)) {
|
||||
webgpu::fdm::release_map(target.densityMap);
|
||||
target.densityMap = 0;
|
||||
}
|
||||
static constexpr std::array<const char*, gfx::foveation::kLevelCount> kLevelNames{"off", "low", "medium", "high"};
|
||||
if (target.densityMap != 0) {
|
||||
Log.info("{} eye foveation {}: {}x{} density map, {} pixels per texel", eyeIndex == 0 ? "Left" : "Right",
|
||||
kLevelNames[static_cast<uint32_t>(level)], map.width, map.height, webgpu::fdm::texel_size());
|
||||
} else {
|
||||
Log.warn("{} eye foveation {}: the {}x{} density map could not be created", eyeIndex == 0 ? "Left" : "Right",
|
||||
kLevelNames[static_cast<uint32_t>(level)], map.width, map.height);
|
||||
}
|
||||
}
|
||||
return target.densityMap != 0 && webgpu::fdm::map_ready(target.densityMap) ? target.foveatedView
|
||||
: wgpu::TextureView{};
|
||||
}
|
||||
|
||||
std::optional<AuroraStereoFrame> request_stereo_frame(uint32_t logicalFrame, uint64_t contentTag) noexcept {
|
||||
StereoProviderRegistration registration;
|
||||
{
|
||||
@@ -772,6 +840,9 @@ gfx::StereoReplayFrame make_stereo_replay_frame(const AuroraStereoFrame& input,
|
||||
.msaaSamples = webgpu::g_graphicsConfig.msaaSamples,
|
||||
.depthFormat = owned.depth.format,
|
||||
};
|
||||
if (input.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY) {
|
||||
view.target.foveatedColorView = foveated_eye_view(eye, input.eyes[eye]);
|
||||
}
|
||||
std::memcpy(&view.projection, input.eyes[eye].projection, sizeof(view.projection));
|
||||
std::memcpy(&view.viewFromCenter, input.eyes[eye].viewFromCenter, sizeof(view.viewFromCenter));
|
||||
if (unitRatio != 1.f) {
|
||||
@@ -1702,6 +1773,9 @@ void shutdown() noexcept {
|
||||
stop_frame_worker();
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
stop_presenter();
|
||||
for (auto& target : g_stereoEyeTargets) {
|
||||
release_eye_density_map(target);
|
||||
}
|
||||
g_stereoEyeTargets = {};
|
||||
g_stereoMirrorState.Reset();
|
||||
g_presentationImagePools = {};
|
||||
@@ -2859,6 +2933,11 @@ void aurora_set_stereo_skip_copy_clears(bool enabled) { aurora::gfx::set_stereo_
|
||||
bool aurora_get_stereo_skip_copy_clears() { return aurora::gfx::get_stereo_skip_copy_clears(); }
|
||||
void aurora_set_stereo_single_pass_eyes(bool enabled) { aurora::gfx::set_stereo_single_pass_eyes(enabled); }
|
||||
bool aurora_get_stereo_single_pass_eyes() { return aurora::gfx::get_stereo_single_pass_eyes(); }
|
||||
void aurora_set_stereo_foveation(uint32_t level) {
|
||||
aurora::gfx::set_stereo_foveation(std::min(level, aurora::gfx::foveation::kLevelCount - 1));
|
||||
}
|
||||
uint32_t aurora_get_stereo_foveation() { return aurora::gfx::get_stereo_foveation(); }
|
||||
bool aurora_stereo_foveation_available() { return aurora::webgpu::fdm::available(); }
|
||||
void aurora_set_stereo_hud_screen(bool enabled, float width, float distance) {
|
||||
aurora::gfx::set_stereo_hud_screen(enabled, width, distance);
|
||||
}
|
||||
|
||||
@@ -221,6 +221,8 @@ static std::atomic_bool g_stereoSkipCopyClears{true};
|
||||
// Replays each eye in as few render passes as its clears allow (eye_pass_plan.hpp) rather than one
|
||||
// per recorded pass. Same image, fewer tile loads and stores.
|
||||
static std::atomic_bool g_stereoSinglePassEyes{true};
|
||||
// foveation::Level of the immersive eyes.
|
||||
static std::atomic_uint32_t g_stereoFoveation{0};
|
||||
|
||||
void set_stereo_stop_at_display_copy(bool value) noexcept {
|
||||
g_stereoStopAtDisplayCopy.store(value, std::memory_order_relaxed);
|
||||
@@ -234,6 +236,10 @@ void set_stereo_single_pass_eyes(bool value) noexcept {
|
||||
g_stereoSinglePassEyes.store(value, std::memory_order_relaxed);
|
||||
}
|
||||
bool get_stereo_single_pass_eyes() noexcept { return g_stereoSinglePassEyes.load(std::memory_order_relaxed); }
|
||||
void set_stereo_foveation(uint32_t level) noexcept {
|
||||
g_stereoFoveation.store(level, std::memory_order_relaxed);
|
||||
}
|
||||
uint32_t get_stereo_foveation() noexcept { return g_stereoFoveation.load(std::memory_order_relaxed); }
|
||||
|
||||
// The fixed virtual screen orthographic draws are placed on during immersive
|
||||
// replay, in game world units. Written from the settings overlay and read by
|
||||
@@ -1789,6 +1795,10 @@ static void render_eye_planned(std::vector<RenderPass>& renderPasses, wgpu::Comm
|
||||
const bool stereoStencil = target.depthFormat == wgpu::TextureFormat::Depth24PlusStencil8;
|
||||
const GpuTimingCategory timingCategory =
|
||||
invocation.stereoEye == 0 ? GpuTimingCategory::EyeLeft : GpuTimingCategory::EyeRight;
|
||||
// Foveated only as a single render pass: loading a finished eye back under a density map costs a
|
||||
// full tile load per split, which is what made foveation a net loss in DolphinXR.
|
||||
const wgpu::TextureView& colorView =
|
||||
target.foveatedColorView && plan.renderPasses == 1 ? target.foveatedColorView : target.colorView;
|
||||
wgpu::RenderPassEncoder pass;
|
||||
for (const auto& step : plan.steps) {
|
||||
const auto& passInfo = renderPasses[step.pass];
|
||||
@@ -1797,7 +1807,7 @@ static void render_eye_planned(std::vector<RenderPass>& renderPasses, wgpu::Comm
|
||||
pass.End();
|
||||
}
|
||||
const wgpu::RenderPassColorAttachment colorAttachment{
|
||||
.view = target.colorView,
|
||||
.view = colorView,
|
||||
.resolveTarget = target.resolveView,
|
||||
.loadOp = step.clearColor ? wgpu::LoadOp::Clear : wgpu::LoadOp::Load,
|
||||
.storeOp = wgpu::StoreOp::Store,
|
||||
|
||||
@@ -284,6 +284,10 @@ bool resume_frame();
|
||||
void abort_frame() noexcept;
|
||||
struct ReplayTarget {
|
||||
wgpu::TextureView colorView;
|
||||
// A view of the same texture that a fragment density map is bound to (webgpu/fdm.hpp), set when
|
||||
// this frame's eyes are foveated. Only an eye drawn in a single render pass renders through it: a
|
||||
// later render pass would load the eye back under the density map.
|
||||
wgpu::TextureView foveatedColorView;
|
||||
wgpu::TextureView resolveView;
|
||||
wgpu::TextureView depthView;
|
||||
wgpu::Texture copySourceTexture;
|
||||
@@ -409,6 +413,10 @@ bool get_stereo_skip_copy_clears() noexcept;
|
||||
// default and live, like the two above.
|
||||
void set_stereo_single_pass_eyes(bool value) noexcept;
|
||||
bool get_stereo_single_pass_eyes() noexcept;
|
||||
// Foveated rendering level for the immersive eyes (gfx/foveation.hpp Level). Live: the next frame's
|
||||
// eyes use it, provided the device has fragment density maps.
|
||||
void set_stereo_foveation(uint32_t level) noexcept;
|
||||
uint32_t get_stereo_foveation() noexcept;
|
||||
|
||||
// Places orthographic draws on a fixed virtual screen during immersive replay.
|
||||
// `width` and `distance` are in game world units; the screen's height follows
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Fixed foveated rendering for the immersive eyes: the fragment density map an eye's render pass
|
||||
// runs under (webgpu/fdm.hpp). Each texel says how finely the framebuffer area it covers is shaded:
|
||||
// fully at the centre of the view, in 2x2 then 4x4 pixel blocks towards the edges, where the
|
||||
// headset's lenses blur the picture anyway.
|
||||
namespace aurora::gfx::foveation {
|
||||
|
||||
enum class Level : uint32_t {
|
||||
Off = 0,
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
};
|
||||
inline constexpr uint32_t kLevelCount = 4;
|
||||
|
||||
// A texel's density is its byte over 255 and a fragment covers 1/density pixels in that direction,
|
||||
// rounded down to a size the GPU supports. A half is therefore written just below 128, so that it
|
||||
// cannot round back to a single pixel.
|
||||
inline constexpr uint8_t kFullDensity = 255;
|
||||
inline constexpr uint8_t kHalfDensity = 127;
|
||||
inline constexpr uint8_t kQuarterDensity = 63;
|
||||
|
||||
// Tangents of an eye's field of view, left and down negative.
|
||||
struct EyeFov {
|
||||
float tanLeft = -1.0f;
|
||||
float tanRight = 1.0f;
|
||||
float tanDown = -1.0f;
|
||||
float tanUp = 1.0f;
|
||||
};
|
||||
|
||||
// From AuroraStereoEye::projection, row-major: [0] = 2/(r-l), [2] = (r+l)/(r-l), [5] = 2/(u-d),
|
||||
// [6] = (u+d)/(u-d), with l, r, d, u the tangents (openxr_integration.cpp, ProjectionFromFov).
|
||||
inline EyeFov fov_from_projection(const float* projection) noexcept {
|
||||
const float sx = projection[0];
|
||||
const float cx = projection[2];
|
||||
const float sy = projection[5];
|
||||
const float cy = projection[6];
|
||||
if (!(sx > 0.0f) || !(sy > 0.0f)) {
|
||||
return {};
|
||||
}
|
||||
return EyeFov{
|
||||
.tanLeft = (cx - 1.0f) / sx,
|
||||
.tanRight = (cx + 1.0f) / sx,
|
||||
.tanDown = (cy - 1.0f) / sy,
|
||||
.tanUp = (cy + 1.0f) / sy,
|
||||
};
|
||||
}
|
||||
|
||||
// Angles from the eye's forward direction, in degrees, below which a level shades fully and then at
|
||||
// half density; beyond the second, a quarter. Low never drops below half. The default HUD screen
|
||||
// (2.4 m wide at 2 m) reaches about 37 degrees at its corners with a 4:3 picture, so Low and Medium
|
||||
// keep it at half density or better when looking straight ahead.
|
||||
struct Rings {
|
||||
float full = 90.0f;
|
||||
float half = 90.0f;
|
||||
};
|
||||
|
||||
inline Rings rings(Level level) noexcept {
|
||||
switch (level) {
|
||||
case Level::Low:
|
||||
return {.full = 30.0f, .half = 90.0f};
|
||||
case Level::Medium:
|
||||
return {.full = 25.0f, .half = 40.0f};
|
||||
case Level::High:
|
||||
return {.full = 18.0f, .half = 34.0f};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// The angle between the forward direction and the ray through a point at tangents (x, y).
|
||||
inline float eccentricity_degrees(float tanX, float tanY) noexcept {
|
||||
return std::atan(std::sqrt(tanX * tanX + tanY * tanY)) * (180.0f / 3.14159265358979f);
|
||||
}
|
||||
|
||||
inline uint8_t density(Level level, float eccentricity) noexcept {
|
||||
const Rings ring = rings(level);
|
||||
if (eccentricity < ring.full) {
|
||||
return kFullDensity;
|
||||
}
|
||||
return eccentricity < ring.half ? kHalfDensity : kQuarterDensity;
|
||||
}
|
||||
|
||||
struct Map {
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
// Two bytes per texel, horizontal then vertical density, rows packed top to bottom.
|
||||
std::vector<uint8_t> rg8;
|
||||
};
|
||||
|
||||
// The map for an eye of `eyeWidth` by `eyeHeight` pixels whose field of view is `fov`, `texel` pixels
|
||||
// per map texel. The map covers the whole eye, its last row and column possibly overhanging it.
|
||||
inline void build(uint32_t eyeWidth, uint32_t eyeHeight, uint32_t texel, const EyeFov& fov, Level level,
|
||||
Map& map) {
|
||||
map.width = texel > 0 ? (eyeWidth + texel - 1) / texel : 0;
|
||||
map.height = texel > 0 ? (eyeHeight + texel - 1) / texel : 0;
|
||||
map.rg8.assign(static_cast<size_t>(map.width) * map.height * 2, kFullDensity);
|
||||
if (level == Level::Off || eyeWidth == 0 || eyeHeight == 0) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
// Texel centres, clamped to the eye for an overhanging last row or column.
|
||||
const float v = std::min((static_cast<float>(y) + 0.5f) * static_cast<float>(texel), static_cast<float>(eyeHeight)) /
|
||||
static_cast<float>(eyeHeight);
|
||||
const float tanY = fov.tanUp + (fov.tanDown - fov.tanUp) * v;
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
const float u = std::min((static_cast<float>(x) + 0.5f) * static_cast<float>(texel), static_cast<float>(eyeWidth)) /
|
||||
static_cast<float>(eyeWidth);
|
||||
const float tanX = fov.tanLeft + (fov.tanRight - fov.tanLeft) * u;
|
||||
const uint8_t value = density(level, eccentricity_degrees(tanX, tanY));
|
||||
uint8_t* texelBytes = &map.rg8[(static_cast<size_t>(y) * map.width + x) * 2];
|
||||
texelBytes[0] = value;
|
||||
texelBytes[1] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::foveation
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "fdm.hpp"
|
||||
|
||||
#include "../internal.hpp"
|
||||
#include "gpu.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
// AURORA_DAWN_FDM carries the ABI version of the Dawn package's patches (AuroraDawnProvider.cmake).
|
||||
#if defined(__ANDROID__) && defined(AURORA_DAWN_FDM)
|
||||
#include <aurora/dawn_fdm_abi.h>
|
||||
static_assert(AURORA_DAWN_FDM == AURORA_DAWN_FDM_ABI,
|
||||
"The Dawn package's fragment density map ABI does not match include/aurora/dawn_fdm_abi.h");
|
||||
#define AURORA_FDM_SUPPORTED 1
|
||||
#endif
|
||||
|
||||
namespace aurora::webgpu::fdm {
|
||||
namespace {
|
||||
Module Log("aurora::webgpu::fdm");
|
||||
|
||||
bool g_requested = false;
|
||||
bool g_available = false;
|
||||
uint32_t g_texelSize = 0;
|
||||
|
||||
// Meta's own maps use 32: rings smooth enough, in a map of a few hundred texels.
|
||||
constexpr uint32_t kPreferredTexelSize = 32;
|
||||
} // namespace
|
||||
|
||||
void request(bool wanted) noexcept {
|
||||
g_requested = wanted;
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
AuroraDawnFdmRequest(wanted ? 1 : 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void device_created() noexcept {
|
||||
g_available = false;
|
||||
g_texelSize = 0;
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
AuroraDawnFdmCaps caps{};
|
||||
if (AuroraDawnFdmQuery(g_device.Get(), &caps) != 0) {
|
||||
// Density texels cover a power-of-two area within the device's range in each direction.
|
||||
const uint32_t smallest = std::max({caps.minTexelWidth, caps.minTexelHeight, 1u});
|
||||
const uint32_t largest = std::max(std::min(caps.maxTexelWidth, caps.maxTexelHeight), smallest);
|
||||
g_texelSize = std::bit_ceil(std::clamp(kPreferredTexelSize, smallest, largest));
|
||||
g_available = true;
|
||||
Log.info("Fragment density maps: enabled, {}x{} to {}x{} pixels per texel, using {}", caps.minTexelWidth,
|
||||
caps.minTexelHeight, caps.maxTexelWidth, caps.maxTexelHeight, g_texelSize);
|
||||
} else if (g_requested) {
|
||||
Log.warn("Fragment density maps: unavailable; the device lacks VK_EXT_fragment_density_map for "
|
||||
"non-subsampled images, or Dawn does not render through dynamic rendering");
|
||||
}
|
||||
#else
|
||||
if (g_requested) {
|
||||
Log.warn("Fragment density maps: unavailable; this build links a Dawn without Aurora's patches "
|
||||
"(android/Build-QuestDawn.ps1)");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool available() noexcept { return g_available; }
|
||||
|
||||
uint32_t texel_size() noexcept { return g_texelSize; }
|
||||
|
||||
uint64_t create_map(uint32_t width, uint32_t height, const uint8_t* rg8) noexcept {
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
if (g_available) {
|
||||
return AuroraDawnFdmCreateMap(g_device.Get(), width, height, rg8);
|
||||
}
|
||||
#endif
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)rg8;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool map_ready(uint64_t map) noexcept {
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
if (g_available && map != 0) {
|
||||
return AuroraDawnFdmMapReady(g_device.Get(), map) != 0;
|
||||
}
|
||||
#endif
|
||||
(void)map;
|
||||
return false;
|
||||
}
|
||||
|
||||
void release_map(uint64_t map) noexcept {
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
if (g_available && map != 0) {
|
||||
AuroraDawnFdmReleaseMap(g_device.Get(), map);
|
||||
}
|
||||
#endif
|
||||
(void)map;
|
||||
}
|
||||
|
||||
bool bind(const wgpu::TextureView& view, uint64_t map) noexcept {
|
||||
#ifdef AURORA_FDM_SUPPORTED
|
||||
if (g_available && view) {
|
||||
return AuroraDawnFdmBind(g_device.Get(), view.Get(), map) != 0;
|
||||
}
|
||||
#endif
|
||||
(void)view;
|
||||
(void)map;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace aurora::webgpu::fdm
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
// Fragment density maps for foveated eye rendering, through the ABI of a Dawn built with Aurora's
|
||||
// patches (include/aurora/dawn_fdm_abi.h, the Quest build). Against any other Dawn the maps are
|
||||
// simply unavailable.
|
||||
namespace aurora::webgpu::fdm {
|
||||
|
||||
// Before the device is requested: whether to ask Dawn for VK_EXT_fragment_density_map.
|
||||
void request(bool wanted) noexcept;
|
||||
// Once the device exists: reads what Dawn enabled, and logs it.
|
||||
void device_created() noexcept;
|
||||
bool available() noexcept;
|
||||
// Framebuffer pixels per density map texel, in each direction.
|
||||
uint32_t texel_size() noexcept;
|
||||
|
||||
// An immutable RG8 map of `width` by `height` texels, or 0. Usable once map_ready.
|
||||
uint64_t create_map(uint32_t width, uint32_t height, const uint8_t* rg8) noexcept;
|
||||
bool map_ready(uint64_t map) noexcept;
|
||||
void release_map(uint64_t map) noexcept;
|
||||
// Render passes whose first color attachment is `view` get `map` while it is ready; 0 unbinds.
|
||||
bool bind(const wgpu::TextureView& view, uint64_t map) noexcept;
|
||||
|
||||
} // namespace aurora::webgpu::fdm
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
#include "../android_debug.hpp"
|
||||
#include "fdm.hpp"
|
||||
#include "../gfx/common.hpp"
|
||||
#include "../internal.hpp"
|
||||
#include "../window.hpp"
|
||||
@@ -823,6 +824,13 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_deviceLostReason.store(reason, std::memory_order_relaxed);
|
||||
g_deviceLost.store(true, std::memory_order_release);
|
||||
});
|
||||
#if defined(__ANDROID__)
|
||||
// Foveated eye rendering (fdm.hpp). `adb shell setprop debug.wiicompiled.fdm 0` keeps the density
|
||||
// maps off, and their pipeline flag with them, whatever the settings say; 1 asks for them anyway.
|
||||
const int fdmOverride = android_debug::property_int("debug.wiicompiled.fdm", -1);
|
||||
fdm::request(g_backendType == wgpu::BackendType::Vulkan &&
|
||||
(fdmOverride >= 0 ? fdmOverride == 1 : g_config.xrFragmentDensityMap));
|
||||
#endif
|
||||
const auto future =
|
||||
g_adapter.RequestDevice(&deviceDescriptor, wgpu::CallbackMode::WaitAnyOnly,
|
||||
[](wgpu::RequestDeviceStatus status, wgpu::Device device, wgpu::StringView message) {
|
||||
@@ -860,6 +868,7 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
}
|
||||
Log.report(level, "WebGPU message: {}", message);
|
||||
});
|
||||
fdm::device_created();
|
||||
}
|
||||
g_queue = g_device.GetQueue();
|
||||
|
||||
|
||||
@@ -1,13 +1,47 @@
|
||||
"""Apply the versioned Aurora native Vulkan ABI to the pinned Dawn source tree."""
|
||||
"""Apply Aurora's native Dawn patches to the pinned Dawn source tree.
|
||||
|
||||
- The versioned native Vulkan ABI (aurora_vulkan_interop.inc): the OpenXR runtime creates Dawn's
|
||||
Vulkan instance and device, used by the Windows Vulkan OpenXR binding.
|
||||
- Fragment density maps (aurora_fdm.inc): VK_EXT_fragment_density_map on Aurora's immersive eye
|
||||
render passes, used by the Quest build.
|
||||
|
||||
Every edit is anchored on Dawn source text, asserts that the anchor exists, and is skipped when it
|
||||
is already applied, so the script can run again on a patched tree.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import shutil, sys
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
here = Path(__file__).resolve().parent
|
||||
for name in ("aurora_vulkan_hooks.h", "aurora_vulkan_interop.inc"):
|
||||
shutil.copyfile(here / name, root / "src/dawn/native/vulkan" / name)
|
||||
shutil.copyfile(here.parent.parent / "include/aurora/dawn_vulkan_abi.h",
|
||||
root / "src/dawn/native/vulkan/aurora_dawn_vulkan_abi.h")
|
||||
p = root / "src/dawn/native/vulkan/VulkanBackend.cpp"
|
||||
vulkan = root / "src/dawn/native/vulkan"
|
||||
include = here.parent.parent / "include/aurora"
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, encoding="utf-8", newline="") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write(path, text):
|
||||
with open(path, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def insert(path, anchor, text, after=True):
|
||||
"""Insert `text` after (or before) the one occurrence of `anchor` unless it is already there."""
|
||||
s = read(path)
|
||||
if text in s:
|
||||
return
|
||||
assert s.count(anchor) == 1, f"{path.name}: expected one occurrence of {anchor!r}"
|
||||
s = s.replace(anchor, anchor + text if after else text + anchor, 1)
|
||||
write(path, s)
|
||||
|
||||
|
||||
for name in ("aurora_vulkan_hooks.h", "aurora_vulkan_interop.inc", "aurora_fdm.h", "aurora_fdm.inc"):
|
||||
shutil.copyfile(here / name, vulkan / name)
|
||||
shutil.copyfile(include / "dawn_vulkan_abi.h", vulkan / "aurora_dawn_vulkan_abi.h")
|
||||
shutil.copyfile(include / "dawn_fdm_abi.h", vulkan / "aurora_dawn_fdm_abi.h")
|
||||
|
||||
p = vulkan / "VulkanBackend.cpp"
|
||||
s = p.read_text()
|
||||
if '#include "aurora_vulkan_interop.inc"' not in s:
|
||||
s += '\n#include "aurora_vulkan_interop.inc"\n'
|
||||
@@ -23,7 +57,7 @@ new = '''((filename.size() > 2 && filename[1] == ':') || filename.starts_with("\
|
||||
if old in s:
|
||||
s = s.replace(old, new, 1)
|
||||
p.write_text(s)
|
||||
p = root / "src/dawn/native/vulkan/VulkanFunctions.cpp"
|
||||
p = vulkan / "VulkanFunctions.cpp"
|
||||
s = p.read_text()
|
||||
if '#include "aurora_vulkan_hooks.h"' not in s:
|
||||
marker = 'namespace dawn::native::vulkan {'
|
||||
@@ -36,3 +70,86 @@ if '#include "aurora_vulkan_hooks.h"' not in s:
|
||||
assert old in s, old
|
||||
s = s.replace(old, old + '\n ' + new, 1)
|
||||
p.write_text(s)
|
||||
|
||||
# Fragment density maps.
|
||||
p = vulkan / "VulkanBackend.cpp"
|
||||
s = read(p)
|
||||
if '#include "aurora_fdm.inc"' not in s:
|
||||
write(p, s + '#include "aurora_fdm.inc"\n')
|
||||
|
||||
insert(vulkan / "VulkanExtensions.h", " RasterizationOrderAttachmentAccess,\n",
|
||||
" FragmentDensityMap,\n")
|
||||
insert(vulkan / "VulkanExtensions.cpp",
|
||||
' {DeviceExt::RasterizationOrderAttachmentAccess, "VK_EXT_rasterization_order_attachment_access"},\n',
|
||||
' {DeviceExt::FragmentDensityMap, "VK_EXT_fragment_density_map"},\n')
|
||||
insert(vulkan / "VulkanExtensions.cpp", " case DeviceExt::EnumCount:\n",
|
||||
" // Aurora only attaches maps to dynamic rendering passes (aurora_fdm.inc).\n"
|
||||
" case DeviceExt::FragmentDensityMap:\n"
|
||||
" hasDependencies = HasDep(DeviceExt::DynamicRendering);\n"
|
||||
" break;\n\n",
|
||||
after=False)
|
||||
|
||||
insert(vulkan / "VulkanInfo.h", " VkPhysicalDeviceExtendedDynamicStateFeaturesEXT extendedDynamicStateFeatures;\n",
|
||||
" VkPhysicalDeviceFragmentDensityMapFeaturesEXT fragmentDensityMapFeatures;\n")
|
||||
insert(vulkan / "VulkanInfo.h", " VkPhysicalDeviceDrmPropertiesEXT drmProperties;\n",
|
||||
" VkPhysicalDeviceFragmentDensityMapPropertiesEXT fragmentDensityMapProperties;\n")
|
||||
insert(vulkan / "VulkanInfo.cpp",
|
||||
" // Use vkGetPhysicalDevice{Features,Properties}2 if required to gather information about\n",
|
||||
" if (info.extensions[DeviceExt::FragmentDensityMap]) {\n"
|
||||
" featuresChain.Add(&info.fragmentDensityMapFeatures,\n"
|
||||
" VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT);\n"
|
||||
" propertiesChain.Add(&info.fragmentDensityMapProperties,\n"
|
||||
" VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_PROPERTIES_EXT);\n"
|
||||
" }\n\n",
|
||||
after=False)
|
||||
|
||||
insert(vulkan / "DeviceVk.cpp", '#include "src/dawn/native/vulkan/DeviceVk.h"\n',
|
||||
'#include "aurora_fdm.h"\n')
|
||||
insert(vulkan / "DeviceVk.cpp", " usedKnobs.extensions = mDeviceInfo.extensions;\n",
|
||||
" // Aurora: fragment density maps for its immersive eye passes, only on request\n"
|
||||
" // (aurora_fdm.inc).\n"
|
||||
" if (!AuroraFdmWanted(mDeviceInfo, IsToggleEnabled(Toggle::VulkanUseDynamicRendering) &&\n"
|
||||
" !HasFeature(Feature::DawnLoadResolveTexture))) {\n"
|
||||
" usedKnobs.extensions.set(DeviceExt::FragmentDensityMap, false);\n"
|
||||
" }\n")
|
||||
insert(vulkan / "DeviceVk.cpp",
|
||||
" usedKnobs.extendedDynamicStateFeatures = mDeviceInfo.extendedDynamicStateFeatures;\n"
|
||||
" featuresChain.Add(&usedKnobs.extendedDynamicStateFeatures);\n"
|
||||
" }\n",
|
||||
"\n"
|
||||
" if (usedKnobs.HasExt(DeviceExt::FragmentDensityMap)) {\n"
|
||||
" usedKnobs.fragmentDensityMapFeatures = mDeviceInfo.fragmentDensityMapFeatures;\n"
|
||||
" // Aurora's maps are immutable, read when a render pass is recorded.\n"
|
||||
" usedKnobs.fragmentDensityMapFeatures.fragmentDensityMapDynamic = VK_FALSE;\n"
|
||||
" featuresChain.Add(&usedKnobs.fragmentDensityMapFeatures);\n"
|
||||
" }\n")
|
||||
insert(vulkan / "DeviceVk.cpp", " mStaticSamplerCache.clear();\n",
|
||||
"\n AuroraFdmDestroy(this);\n")
|
||||
|
||||
insert(vulkan / "RenderPipelineVk.cpp", " createInfo.renderPass = nullRenderPass;\n",
|
||||
"\n"
|
||||
" // Aurora: any pipeline may draw in an eye pass with a fragment density map\n"
|
||||
" // (aurora_fdm.inc).\n"
|
||||
" if (device->GetDeviceInfo().HasExt(DeviceExt::FragmentDensityMap)) {\n"
|
||||
" createInfo.flags |= VK_PIPELINE_CREATE_RENDERING_FRAGMENT_DENSITY_MAP_ATTACHMENT_BIT_EXT;\n"
|
||||
" }\n")
|
||||
|
||||
insert(vulkan / "CommandBufferVk.cpp", '#include "src/dawn/native/vulkan/CommandBufferVk.h"\n',
|
||||
'#include "aurora_fdm.h"\n')
|
||||
insert(vulkan / "CommandBufferVk.cpp", " // TODO(crbug.com/463893794): Handle ExpandResolveTexture.\n",
|
||||
" // Aurora: the fragment density map bound to the first color attachment's view\n"
|
||||
" // (aurora_fdm.inc).\n"
|
||||
" VkRenderingFragmentDensityMapAttachmentInfoEXT fragmentDensityMap;\n"
|
||||
" if (attachmentMask[ColorAttachmentIndex(uint8_t(0))]) {\n"
|
||||
" const ::VkImageView densityMap = AuroraFdmViewFor(\n"
|
||||
" device, renderPass->colorAttachments[ColorAttachmentIndex(uint8_t(0))].view.Get());\n"
|
||||
" if (densityMap != VK_NULL_HANDLE) {\n"
|
||||
" fragmentDensityMap.sType =\n"
|
||||
" VK_STRUCTURE_TYPE_RENDERING_FRAGMENT_DENSITY_MAP_ATTACHMENT_INFO_EXT;\n"
|
||||
" fragmentDensityMap.pNext = renderInfo.pNext;\n"
|
||||
" fragmentDensityMap.imageView = densityMap;\n"
|
||||
" fragmentDensityMap.imageLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT;\n"
|
||||
" renderInfo.pNext = &fragmentDensityMap;\n"
|
||||
" }\n"
|
||||
" }\n\n",
|
||||
after=False)
|
||||
@@ -0,0 +1,21 @@
|
||||
// Included only by the pinned Dawn source build (apply.py): fragment density maps for Aurora's
|
||||
// immersive eye render passes. Implemented in aurora_fdm.inc.
|
||||
#pragma once
|
||||
#include "src/dawn/common/vulkan_platform.h"
|
||||
|
||||
namespace dawn::native {
|
||||
class TextureViewBase;
|
||||
}
|
||||
|
||||
namespace dawn::native::vulkan {
|
||||
class Device;
|
||||
struct VulkanDeviceInfo;
|
||||
|
||||
// Device::CreateDevice keeps VK_EXT_fragment_density_map only when Aurora asked for it and the
|
||||
// device can attach a map to a dynamic rendering pass over ordinary (non-subsampled) images.
|
||||
bool AuroraFdmWanted(const VulkanDeviceInfo& info, bool dynamicRendering);
|
||||
// The density map for a render pass whose first color attachment is `view`, or a null handle.
|
||||
::VkImageView AuroraFdmViewFor(Device* device, TextureViewBase* view);
|
||||
// Device::DestroyImpl: releases every map while the fenced deleter still runs.
|
||||
void AuroraFdmDestroy(Device* device);
|
||||
} // namespace dawn::native::vulkan
|
||||
@@ -0,0 +1,279 @@
|
||||
// Compiled inside VulkanBackend.cpp, using the pinned Dawn implementation (apply.py).
|
||||
//
|
||||
// Fragment density maps for Aurora's immersive eye render passes. Aurora uploads immutable RG8
|
||||
// maps, binds each to the texture view its eye passes render through, and
|
||||
// RecordBeginDynamicRenderPass chains the bound map into those passes. The maps are raw VkImages
|
||||
// that Dawn's usage tracking never sees: they stay in FRAGMENT_DENSITY_MAP_OPTIMAL from their
|
||||
// upload until the fenced deleter frees them.
|
||||
#define AURORA_DAWN_FDM_IMPLEMENTATION
|
||||
#include "aurora_dawn_fdm_abi.h"
|
||||
#include "aurora_fdm.h"
|
||||
#include "src/dawn/native/DynamicUploader.h"
|
||||
#include "src/dawn/native/ResourceMemoryAllocation.h"
|
||||
#include "src/dawn/native/vulkan/BufferVk.h"
|
||||
#include "src/dawn/native/vulkan/CommandRecordingContextVk.h"
|
||||
#include "src/dawn/native/vulkan/FencedDeleter.h"
|
||||
#include "src/dawn/native/vulkan/QueueVk.h"
|
||||
#include "src/dawn/native/vulkan/ResourceHeapVk.h"
|
||||
#include "src/dawn/native/vulkan/ResourceMemoryAllocatorVk.h"
|
||||
#include "src/dawn/native/vulkan/VulkanError.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
namespace dawn::native::vulkan {
|
||||
namespace {
|
||||
std::atomic<bool> auroraFdmRequested{false};
|
||||
|
||||
struct AuroraFdmMap {
|
||||
VkImage image;
|
||||
VkImageView view;
|
||||
ResourceMemoryAllocation memory;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
ExecutionSerial readySerial = kMaxExecutionSerial;
|
||||
};
|
||||
struct AuroraFdmBinding {
|
||||
Ref<TextureViewBase> view;
|
||||
uint64_t map = 0;
|
||||
};
|
||||
struct AuroraFdmState {
|
||||
uint64_t nextMap = 1;
|
||||
absl::flat_hash_map<uint64_t, AuroraFdmMap> maps;
|
||||
absl::flat_hash_map<TextureViewBase*, AuroraFdmBinding> bindings;
|
||||
};
|
||||
// Per device. Also taken while a render pass is recorded, under the device lock Queue::Submit holds.
|
||||
std::mutex auroraFdmMutex;
|
||||
absl::flat_hash_map<Device*, std::unique_ptr<AuroraFdmState>> auroraFdmStates;
|
||||
|
||||
Device* AuroraFdmDevice(void* handle) {
|
||||
return ToBackend(FromAPI(static_cast<WGPUDevice>(handle)));
|
||||
}
|
||||
|
||||
bool AuroraFdmEnabled(Device* device) {
|
||||
return device->GetDeviceInfo().HasExt(DeviceExt::FragmentDensityMap);
|
||||
}
|
||||
|
||||
AuroraFdmState* AuroraFdmStateFor(Device* device, bool create) {
|
||||
auto it = auroraFdmStates.find(device);
|
||||
if (it != auroraFdmStates.end()) return it->second.get();
|
||||
if (!create) return nullptr;
|
||||
return auroraFdmStates.emplace(device, std::make_unique<AuroraFdmState>()).first->second.get();
|
||||
}
|
||||
|
||||
bool AuroraFdmReady(Device* device, const AuroraFdmMap& map) {
|
||||
return device->GetQueue()->GetCompletedCommandSerial() >= map.readySerial;
|
||||
}
|
||||
|
||||
void AuroraFdmRelease(Device* device, AuroraFdmMap& map) {
|
||||
if (map.view != VK_NULL_HANDLE) device->GetFencedDeleter()->DeleteWhenUnused(map.view);
|
||||
if (map.image != VK_NULL_HANDLE) device->GetFencedDeleter()->DeleteWhenUnused(map.image);
|
||||
device->GetResourceMemoryAllocator()->Deallocate(&map.memory);
|
||||
map.view = VkImageView{};
|
||||
map.image = VkImage{};
|
||||
}
|
||||
|
||||
MaybeError AuroraFdmUpload(Device* device, uint32_t width, uint32_t height, const uint8_t* rg8, AuroraFdmMap& map) {
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
imageInfo.format = VK_FORMAT_R8G8_UNORM;
|
||||
imageInfo.extent = {width, height, 1};
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
DAWN_TRY(CheckVkSuccess(device->fn.CreateImage(device->GetVkDevice(), &imageInfo, nullptr, &*map.image),
|
||||
"CreateImage (fragment density map)"));
|
||||
|
||||
VkMemoryRequirements requirements;
|
||||
device->fn.GetImageMemoryRequirements(device->GetVkDevice(), map.image, &requirements);
|
||||
DAWN_TRY_ASSIGN(map.memory,
|
||||
device->GetResourceMemoryAllocator()->Allocate(requirements, MemoryKind::DeviceLocal));
|
||||
DAWN_TRY(CheckVkSuccess(device->fn.BindImageMemory(device->GetVkDevice(), map.image,
|
||||
ToBackend(map.memory.GetResourceHeap())->GetMemory(),
|
||||
map.memory.GetOffset()),
|
||||
"BindImageMemory (fragment density map)"));
|
||||
|
||||
VkImageViewCreateInfo viewInfo{};
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
viewInfo.image = map.image;
|
||||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.format = VK_FORMAT_R8G8_UNORM;
|
||||
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
DAWN_TRY(CheckVkSuccess(device->fn.CreateImageView(device->GetVkDevice(), &viewInfo, nullptr, &*map.view),
|
||||
"CreateImageView (fragment density map)"));
|
||||
|
||||
Queue* queue = ToBackend(device->GetQueue());
|
||||
const uint64_t size = uint64_t(width) * height * 2;
|
||||
DAWN_TRY(device->GetDynamicUploader()->WithUploadReservation(
|
||||
size, device->GetOptimalBufferToTextureCopyOffsetAlignment(),
|
||||
[&](UploadReservation reservation) -> MaybeError {
|
||||
std::memcpy(reservation.mappedPointer.get(), rg8, size);
|
||||
CommandRecordingContext* context = queue->GetPendingRecordingContext();
|
||||
|
||||
VkImageMemoryBarrier barrier{};
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.srcAccessMask = 0;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = map.image;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
device->fn.CmdPipelineBarrier(context->commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
VkBufferImageCopy region{};
|
||||
region.bufferOffset = reservation.offsetInBuffer;
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageExtent = {width, height, 1};
|
||||
device->fn.CmdCopyBufferToImage(context->commandBuffer, ToBackend(reservation.buffer.Get())->GetHandle(),
|
||||
map.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
||||
|
||||
// A map without VK_IMAGE_VIEW_CREATE_FRAGMENT_DENSITY_MAP_DYNAMIC_BIT_EXT is read by the
|
||||
// host when a render pass using it is recorded, so the upload is also made visible to
|
||||
// host reads. The map is only used once this submission has completed.
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT | VK_ACCESS_HOST_READ_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_FRAGMENT_DENSITY_MAP_OPTIMAL_EXT;
|
||||
device->fn.CmdPipelineBarrier(context->commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT |
|
||||
VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
return {};
|
||||
}));
|
||||
map.width = width;
|
||||
map.height = height;
|
||||
map.readySerial = queue->GetPendingCommandSerial();
|
||||
DAWN_TRY(queue->SubmitPendingCommands());
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool AuroraFdmWanted(const VulkanDeviceInfo& info, bool dynamicRendering) {
|
||||
return auroraFdmRequested.load() && dynamicRendering && info.HasExt(DeviceExt::FragmentDensityMap) &&
|
||||
info.fragmentDensityMapFeatures.fragmentDensityMap == VK_TRUE &&
|
||||
info.fragmentDensityMapFeatures.fragmentDensityMapNonSubsampledImages == VK_TRUE;
|
||||
}
|
||||
|
||||
::VkImageView AuroraFdmViewFor(Device* device, TextureViewBase* view) {
|
||||
if (view == nullptr || !AuroraFdmEnabled(device)) return VK_NULL_HANDLE;
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
AuroraFdmState* state = AuroraFdmStateFor(device, false);
|
||||
if (state == nullptr) return VK_NULL_HANDLE;
|
||||
auto binding = state->bindings.find(view);
|
||||
if (binding == state->bindings.end()) return VK_NULL_HANDLE;
|
||||
auto map = state->maps.find(binding->second.map);
|
||||
if (map == state->maps.end() || !AuroraFdmReady(device, map->second)) return VK_NULL_HANDLE;
|
||||
return map->second.view;
|
||||
}
|
||||
|
||||
void AuroraFdmDestroy(Device* device) {
|
||||
std::unique_ptr<AuroraFdmState> state;
|
||||
{
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
auto it = auroraFdmStates.find(device);
|
||||
if (it == auroraFdmStates.end()) return;
|
||||
state = std::move(it->second);
|
||||
auroraFdmStates.erase(it);
|
||||
}
|
||||
state->bindings.clear();
|
||||
for (auto& [id, map] : state->maps) AuroraFdmRelease(device, map);
|
||||
}
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT uint32_t AuroraDawnFdmVersion(void) { return AURORA_DAWN_FDM_ABI; }
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT void AuroraDawnFdmRequest(int enable) { auroraFdmRequested.store(enable != 0); }
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnFdmQuery(void* handle, AuroraDawnFdmCaps* caps) {
|
||||
Device* device = AuroraFdmDevice(handle);
|
||||
auto guard = device->GetGuard();
|
||||
const VulkanDeviceInfo& info = device->GetDeviceInfo();
|
||||
*caps = {};
|
||||
caps->enabled = AuroraFdmEnabled(device) ? 1 : 0;
|
||||
if (info.HasExt(DeviceExt::FragmentDensityMap)) {
|
||||
const auto& properties = info.fragmentDensityMapProperties;
|
||||
caps->minTexelWidth = properties.minFragmentDensityTexelSize.width;
|
||||
caps->minTexelHeight = properties.minFragmentDensityTexelSize.height;
|
||||
caps->maxTexelWidth = properties.maxFragmentDensityTexelSize.width;
|
||||
caps->maxTexelHeight = properties.maxFragmentDensityTexelSize.height;
|
||||
}
|
||||
return static_cast<int>(caps->enabled);
|
||||
}
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT uint64_t AuroraDawnFdmCreateMap(void* handle, uint32_t width, uint32_t height,
|
||||
const uint8_t* rg8) {
|
||||
Device* device = AuroraFdmDevice(handle);
|
||||
auto guard = device->GetGuard();
|
||||
if (!AuroraFdmEnabled(device) || width == 0 || height == 0 || rg8 == nullptr) return 0;
|
||||
AuroraFdmMap map;
|
||||
if (device->ConsumedError(AuroraFdmUpload(device, width, height, rg8, map))) {
|
||||
AuroraFdmRelease(device, map);
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
AuroraFdmState* state = AuroraFdmStateFor(device, true);
|
||||
const uint64_t id = state->nextMap++;
|
||||
state->maps.emplace(id, std::move(map));
|
||||
return id;
|
||||
}
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnFdmMapReady(void* handle, uint64_t id) {
|
||||
Device* device = AuroraFdmDevice(handle);
|
||||
auto guard = device->GetGuard();
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
AuroraFdmState* state = AuroraFdmStateFor(device, false);
|
||||
if (state == nullptr) return 0;
|
||||
auto map = state->maps.find(id);
|
||||
return map != state->maps.end() && AuroraFdmReady(device, map->second) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT void AuroraDawnFdmReleaseMap(void* handle, uint64_t id) {
|
||||
Device* device = AuroraFdmDevice(handle);
|
||||
auto guard = device->GetGuard();
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
AuroraFdmState* state = AuroraFdmStateFor(device, false);
|
||||
if (state == nullptr) return;
|
||||
auto map = state->maps.find(id);
|
||||
if (map == state->maps.end()) return;
|
||||
absl::erase_if(state->bindings, [id](const auto& binding) { return binding.second.map == id; });
|
||||
AuroraFdmRelease(device, map->second);
|
||||
state->maps.erase(map);
|
||||
}
|
||||
|
||||
extern "C" DAWN_NATIVE_EXPORT int AuroraDawnFdmBind(void* handle, void* viewHandle, uint64_t id) {
|
||||
Device* device = AuroraFdmDevice(handle);
|
||||
auto guard = device->GetGuard();
|
||||
TextureViewBase* view = FromAPI(static_cast<WGPUTextureView>(viewHandle));
|
||||
if (view == nullptr) return 0;
|
||||
std::lock_guard lock(auroraFdmMutex);
|
||||
AuroraFdmState* state = AuroraFdmStateFor(device, id != 0);
|
||||
if (state == nullptr) return 1;
|
||||
if (id == 0) {
|
||||
state->bindings.erase(view);
|
||||
return 1;
|
||||
}
|
||||
auto map = state->maps.find(id);
|
||||
if (map == state->maps.end()) return 0;
|
||||
// VkRenderingInfo requires the map to reach the render area at the largest texel size.
|
||||
const Extent3D size = view->GetSingleSubresourceVirtualSize();
|
||||
const auto& properties = device->GetDeviceInfo().fragmentDensityMapProperties;
|
||||
const uint64_t maxTexelWidth = std::max<uint32_t>(properties.maxFragmentDensityTexelSize.width, 1);
|
||||
const uint64_t maxTexelHeight = std::max<uint32_t>(properties.maxFragmentDensityTexelSize.height, 1);
|
||||
if (map->second.width * maxTexelWidth < size.width || map->second.height * maxTexelHeight < size.height) {
|
||||
return 0;
|
||||
}
|
||||
state->bindings[view] = AuroraFdmBinding{Ref<TextureViewBase>(view), id};
|
||||
return 1;
|
||||
}
|
||||
} // namespace dawn::native::vulkan
|
||||
@@ -56,6 +56,7 @@ if (AURORA_ENABLE_GX)
|
||||
stereo_interpolation_test.cpp
|
||||
stereo_mirror_test.cpp
|
||||
eye_pass_plan_test.cpp
|
||||
foveation_test.cpp
|
||||
native_wheel_test.cpp
|
||||
cockpit_geometry_test.cpp
|
||||
texture_bind_group_cache_key_test.cpp
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "gfx/foveation.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace aurora::gfx::foveation {
|
||||
namespace {
|
||||
|
||||
constexpr float kDegrees = 3.14159265358979f / 180.0f;
|
||||
|
||||
// Roughly a Quest 3 left eye: the wider side is the outer (left) one.
|
||||
EyeFov left_eye() {
|
||||
return EyeFov{.tanLeft = std::tan(-54.0f * kDegrees),
|
||||
.tanRight = std::tan(43.0f * kDegrees),
|
||||
.tanDown = std::tan(-50.0f * kDegrees),
|
||||
.tanUp = std::tan(47.0f * kDegrees)};
|
||||
}
|
||||
|
||||
EyeFov right_eye() {
|
||||
const EyeFov left = left_eye();
|
||||
return EyeFov{.tanLeft = -left.tanRight, .tanRight = -left.tanLeft, .tanDown = left.tanDown, .tanUp = left.tanUp};
|
||||
}
|
||||
|
||||
Map build_map(Level level, const EyeFov& fov = left_eye(), uint32_t width = 1344, uint32_t height = 1408,
|
||||
uint32_t texel = 32) {
|
||||
Map map;
|
||||
foveation::build(width, height, texel, fov, level, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
uint8_t at(const Map& map, uint32_t x, uint32_t y) { return map.rg8[(static_cast<size_t>(y) * map.width + x) * 2]; }
|
||||
|
||||
// The tangents at a texel centre, as build computes them.
|
||||
std::pair<float, float> tangents(const Map& map, const EyeFov& fov, uint32_t x, uint32_t y, uint32_t width,
|
||||
uint32_t height, uint32_t texel) {
|
||||
const float u = std::min((x + 0.5f) * texel, static_cast<float>(width)) / width;
|
||||
const float v = std::min((y + 0.5f) * texel, static_cast<float>(height)) / height;
|
||||
return {fov.tanLeft + (fov.tanRight - fov.tanLeft) * u, fov.tanUp + (fov.tanDown - fov.tanUp) * v};
|
||||
}
|
||||
|
||||
TEST(Foveation, MapCoversTheWholeEye) {
|
||||
const Map quest = build_map(Level::Medium);
|
||||
EXPECT_EQ(quest.width, 42u);
|
||||
EXPECT_EQ(quest.height, 44u);
|
||||
EXPECT_EQ(quest.rg8.size(), 42u * 44u * 2u);
|
||||
|
||||
// render_scale 0.75: the last column and row overhang the eye.
|
||||
const Map scaled = build_map(Level::Medium, left_eye(), 1260, 1320);
|
||||
EXPECT_EQ(scaled.width, 40u);
|
||||
EXPECT_EQ(scaled.height, 42u);
|
||||
}
|
||||
|
||||
TEST(Foveation, WritesOnlyWholeHalfAndQuarterDensities) {
|
||||
for (Level level : {Level::Low, Level::Medium, Level::High}) {
|
||||
const Map map = build_map(level);
|
||||
for (size_t i = 0; i < map.rg8.size(); i += 2) {
|
||||
const uint8_t value = map.rg8[i];
|
||||
EXPECT_TRUE(value == kFullDensity || value == kHalfDensity || value == kQuarterDensity) << int(value);
|
||||
// The same density in both directions.
|
||||
EXPECT_EQ(map.rg8[i], map.rg8[i + 1]);
|
||||
}
|
||||
}
|
||||
// A half must stay below 1/2 so the fragment size cannot round down to a single pixel.
|
||||
EXPECT_LE(kHalfDensity / 255.0f, 0.5f);
|
||||
EXPECT_LE(kQuarterDensity / 255.0f, 0.25f);
|
||||
EXPECT_GT(kHalfDensity / 255.0f, 0.25f);
|
||||
}
|
||||
|
||||
TEST(Foveation, OffShadesEverythingFully) {
|
||||
const Map map = build_map(Level::Off);
|
||||
EXPECT_TRUE(std::all_of(map.rg8.begin(), map.rg8.end(), [](uint8_t value) { return value == kFullDensity; }));
|
||||
}
|
||||
|
||||
TEST(Foveation, DensityNeverRisesAwayFromTheForwardDirection) {
|
||||
const EyeFov fov = left_eye();
|
||||
for (Level level : {Level::Low, Level::Medium, Level::High}) {
|
||||
const Map map = build_map(level, fov);
|
||||
std::vector<std::pair<float, uint8_t>> texels;
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
const auto [tanX, tanY] = tangents(map, fov, x, y, 1344, 1408, 32);
|
||||
texels.emplace_back(eccentricity_degrees(tanX, tanY), at(map, x, y));
|
||||
}
|
||||
}
|
||||
std::sort(texels.begin(), texels.end());
|
||||
for (size_t i = 1; i < texels.size(); ++i) {
|
||||
EXPECT_LE(texels[i].second, texels[i - 1].second);
|
||||
}
|
||||
// Every level shades the centre fully and saves something at the edges.
|
||||
EXPECT_EQ(texels.front().second, kFullDensity);
|
||||
EXPECT_LT(texels.back().second, kFullDensity);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Foveation, EachEyeCentresOnItsOwnForwardDirection) {
|
||||
// The asymmetric frustum puts the forward direction off the image centre, towards the nose.
|
||||
const auto fullColumns = [](const Map& map) {
|
||||
double sum = 0.0;
|
||||
uint32_t count = 0;
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
if (at(map, x, y) == kFullDensity) {
|
||||
sum += x + 0.5;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / count : 0.0;
|
||||
};
|
||||
const Map left = build_map(Level::High, left_eye());
|
||||
const Map right = build_map(Level::High, right_eye());
|
||||
const EyeFov fov = left_eye();
|
||||
const double forward = -fov.tanLeft / (fov.tanRight - fov.tanLeft) * left.width;
|
||||
EXPECT_NEAR(fullColumns(left), forward, 1.0);
|
||||
EXPECT_GT(fullColumns(left), left.width / 2.0);
|
||||
EXPECT_NEAR(fullColumns(right), right.width - fullColumns(left), 1.0);
|
||||
}
|
||||
|
||||
TEST(Foveation, HigherLevelsNeverShadeMore) {
|
||||
const Map low = build_map(Level::Low);
|
||||
const Map medium = build_map(Level::Medium);
|
||||
const Map high = build_map(Level::High);
|
||||
for (size_t i = 0; i < low.rg8.size(); ++i) {
|
||||
EXPECT_LE(medium.rg8[i], low.rg8[i]);
|
||||
EXPECT_LE(high.rg8[i], medium.rg8[i]);
|
||||
}
|
||||
// Low never goes below half.
|
||||
EXPECT_TRUE(std::none_of(low.rg8.begin(), low.rg8.end(), [](uint8_t value) { return value == kQuarterDensity; }));
|
||||
}
|
||||
|
||||
TEST(Foveation, LowAndMediumKeepTheHudScreenAtHalfDensity) {
|
||||
// The default HUD screen: 2.4 m wide at 2 m, with a 4:3 picture, looking straight ahead.
|
||||
constexpr float kHalfWidth = 1.2f / 2.0f;
|
||||
constexpr float kHalfHeight = 0.9f / 2.0f;
|
||||
const EyeFov fov = left_eye();
|
||||
for (Level level : {Level::Low, Level::Medium}) {
|
||||
const Map map = build_map(level, fov);
|
||||
uint32_t covered = 0;
|
||||
for (uint32_t y = 0; y < map.height; ++y) {
|
||||
for (uint32_t x = 0; x < map.width; ++x) {
|
||||
const auto [tanX, tanY] = tangents(map, fov, x, y, 1344, 1408, 32);
|
||||
if (std::abs(tanX) <= kHalfWidth && std::abs(tanY) <= kHalfHeight) {
|
||||
EXPECT_GE(at(map, x, y), kHalfDensity) << "level " << int(level) << " at " << x << "," << y;
|
||||
++covered;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_GT(covered, 100u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Foveation, ReadsTheFieldOfViewBackFromTheEyeProjection) {
|
||||
const EyeFov fov = left_eye();
|
||||
std::array<float, 16> projection{};
|
||||
// openxr_integration.cpp's ProjectionFromFov.
|
||||
projection[0] = 2.0f / (fov.tanRight - fov.tanLeft);
|
||||
projection[2] = (fov.tanRight + fov.tanLeft) / (fov.tanRight - fov.tanLeft);
|
||||
projection[5] = 2.0f / (fov.tanUp - fov.tanDown);
|
||||
projection[6] = (fov.tanUp + fov.tanDown) / (fov.tanUp - fov.tanDown);
|
||||
const EyeFov read = fov_from_projection(projection.data());
|
||||
EXPECT_NEAR(read.tanLeft, fov.tanLeft, 1e-5f);
|
||||
EXPECT_NEAR(read.tanRight, fov.tanRight, 1e-5f);
|
||||
EXPECT_NEAR(read.tanDown, fov.tanDown, 1e-5f);
|
||||
EXPECT_NEAR(read.tanUp, fov.tanUp, 1e-5f);
|
||||
|
||||
// A projection without a frustum scale leaves the symmetric default.
|
||||
const std::array<float, 16> empty{};
|
||||
const EyeFov fallback = fov_from_projection(empty.data());
|
||||
EXPECT_EQ(fallback.tanLeft, -1.0f);
|
||||
EXPECT_EQ(fallback.tanUp, 1.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace aurora::gfx::foveation
|
||||
+65
-5
@@ -89,14 +89,68 @@ marker, a lost device above all, ends the session. Three hundred skipped copies
|
||||
in a row end it too.
|
||||
|
||||
The cost is one extra GPU copy per eye per frame, a few hundred microseconds
|
||||
at Quest eye resolutions; the benefit is that stock Dawn is used unchanged
|
||||
and the OpenXR device outlives Aurora's, which is exactly the failure DolphinXR
|
||||
hit on Vulkan when a game's device was destroyed under the compositor.
|
||||
at Quest eye resolutions; the benefit is that the bridge needs nothing from
|
||||
Dawn beyond its public API, and the OpenXR device outlives Aurora's, which is
|
||||
exactly the failure DolphinXR hit on Vulkan when a game's device was destroyed
|
||||
under the compositor. (The Quest's Dawn carries Aurora's patches for foveated
|
||||
rendering only; see below.)
|
||||
|
||||
`gpu.cpp` steers Aurora to an RGBA8 surface format under `xrInterop` on Android
|
||||
(there is no BGRA `AHardwareBuffer` format) and requests the two Dawn features
|
||||
the bridge needs.
|
||||
|
||||
### Foveated rendering
|
||||
|
||||
`[vr] foveation` (`OPENXR.md`, Foveated rendering) shades the edges of the
|
||||
immersive eyes more coarsely under a `VK_EXT_fragment_density_map`. The Quest 3
|
||||
(Adreno 740, Vulkan 1.3) exposes it with non-subsampled attachments, as well as
|
||||
dynamic rendering, `VK_EXT_fragment_density_map2`,
|
||||
`VK_QCOM_fragment_density_map_offset` and `VK_KHR_fragment_shading_rate`
|
||||
(`adb shell cmd gpu vkjson`).
|
||||
|
||||
`XR_FB_foveation` cannot be used: its maps only shape render passes that draw
|
||||
into the runtime's swapchain images on the runtime's device, and the eyes reach
|
||||
that device through the copy above. The map has to be attached to Dawn's own eye
|
||||
passes, which the stock package cannot do. So `Build-Quest.ps1` first runs
|
||||
`android/Build-QuestDawn.ps1`, which builds the pinned Dawn revision
|
||||
(`13abc3bc`, the stock package's) with `aurora-main/patches/dawn`, then links it
|
||||
instead of the stock package:
|
||||
|
||||
- The patch (`aurora_fdm.inc`, applied by `apply.py`) adds the extension to
|
||||
Dawn's Vulkan backend and enables it only when Aurora asks before creating
|
||||
the device, and only for dynamic rendering. It then flags every render
|
||||
pipeline for density-mapped passes, and chains a density map into any render
|
||||
pass whose first color attachment is a texture view bound to one. Maps are raw
|
||||
`VkImage`s uploaded once through Dawn's queue. They are read on the CPU when a
|
||||
render pass is recorded, so a map is used only after its upload has completed.
|
||||
Its C ABI is `aurora-main/include/aurora/dawn_fdm_abi.h`.
|
||||
- The build mirrors the dawn-build CI's Android configuration (NDK
|
||||
`29.0.14206865`, `android-28`, static monolithic library, samples, tests and
|
||||
tools off, `llvm-strip --strip-debug`). It also sets `DAWN_BUILD_PROTOBUF=OFF`:
|
||||
the stock CI hands the build a host `protoc`, and without one the build tried
|
||||
to run the `protoc` it had cross-compiled for Android.
|
||||
- The package lands in `.scratch/quest-dawn/package` with an `aurora-dawn.json`
|
||||
recording the revision, the patch files' hash, the NDK and the flags. A later
|
||||
run with the same inputs reuses it and takes no time. That matters: the game
|
||||
kit fingerprints the Dawn archive, so a rebuilt one would make every player
|
||||
rebuild their game. The first build compiles all of Dawn and Tint and takes a
|
||||
while; a patch change re-extracts `src/` and rebuilds Dawn's own sources
|
||||
only, keeping the fetched `third_party/`.
|
||||
- `AuroraDawnProvider.cmake` reads that manifest. Only when `AuroraFdmAbi` is
|
||||
present does `aurora_core` compile against the ABI (`AURORA_DAWN_FDM`), so
|
||||
`Build-Quest.ps1 -StockDawn` still builds, with foveation unavailable.
|
||||
|
||||
Aurora (`lib/webgpu/fdm.cpp`, `lib/gfx/foveation.hpp`) builds one map per eye,
|
||||
32 pixels per texel (42x44 for 1344x1408 eyes). The map is centred on that eye's
|
||||
forward direction and rebuilt when the eye's size, field of view or level
|
||||
changes. It is bound to a second view of the eye texture that only a
|
||||
single-render-pass immersive eye renders through (`single_pass_eyes`). Density
|
||||
bytes are 255, 127 and 63: a fragment covers 1/density pixels rounded down to a
|
||||
supported size, so a half written as 128 could round back to one pixel.
|
||||
Changing the level is live. The launch decides whether the device has density
|
||||
maps at all, because every pipeline carries the flag and Dawn's pipeline cache
|
||||
keys on it: the first launch with foveation on recompiles every pipeline once.
|
||||
|
||||
### Controllers
|
||||
|
||||
Quest Touch controllers are not HID gamepads, so `openxr_input.cpp` syncs an
|
||||
@@ -642,6 +696,9 @@ Android facts this design rests on, all measured on a Quest 3:
|
||||
android-aarch64 package (digest pinned in `AuroraDawnProvider.cmake`, which
|
||||
also rewrites the package's absolute `liblog.so` path and looks the package
|
||||
up with `NO_CMAKE_FIND_ROOT_PATH` so the NDK sysroot rule does not hide it),
|
||||
or, as `Build-Quest.ps1` passes it (`-PmkwQuestDawnDir`, turned into
|
||||
`FETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT` by `android/app/src/main/cpp/CMakeLists.txt`),
|
||||
the same revision built with Aurora's patches (Foveated rendering, above),
|
||||
SDL3 built shared (or `-DAURORA_SDL3_PROVIDER=system` for the AAR prefab),
|
||||
tests off, products built as `libmain.so` / `libmain_retro_rewind.so`,
|
||||
`-mcpu=cortex-a77` (Quest 2's XR2 Gen 1; Quest 3/Pro are supersets).
|
||||
@@ -683,7 +740,7 @@ installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline)
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed (the first run also builds Dawn from source)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
|
||||
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||
@@ -810,6 +867,8 @@ the app:
|
||||
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
||||
| `debug.wiicompiled.panel_layer 0` | Draws the headset settings panel into the eye images instead of on its own quad layer (`OPENXR.md`, Settings in the headset); read about once a second, so it can be switched while the panel is open |
|
||||
| `debug.wiicompiled.eye_passes 0` | Replays each eye in one render pass per recorded pass, as before `single_pass_eyes` (`OPENXR.md`); `1` forces the single pass and an empty value restores the setting. Read about once a second, for A/B timing inside one session |
|
||||
| `debug.wiicompiled.foveation <0-3>` | Overrides the foveation level (off, low, medium, high) within one session; an empty value restores the setting. Needs a session launched with foveation on. Read about once a second |
|
||||
| `debug.wiicompiled.fdm 0` | Launches without fragment density maps at all, whatever `foveation` says, which also drops their flag from every pipeline; `1` asks for them even with `foveation = "off"` |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes. As a Wii Remote, `x`/`y`/`start` are 1/2/+, the directions push the Nunchuk stick, and `home`, `c` and `z` also exist. `panel` presses the settings panel's button (left Y, or both thumbsticks as a gamepad), opening or closing it (see `OPENXR.md`) |
|
||||
| `debug.wiicompiled.fpslog 1` | Logs the game's rendered frame rate every 5 s, with per-frame averages of the producer's waits for the frame worker's DONE and SEALED phases and of the worker's seal, permit wait, prepare and encode stretches, and of the draw calls the recorded frame holds and the primitives that merged into them (an overlay that stops draws merging shows up there first). A third line reports the GX thread's command ring (records, waits, busy share). A second line gives the GPU time per frame from timestamp queries on every pass (`mono` native render, `eyeL`/`eyeR` replays, `screen`, `panel`, `efbcopy`, `palette`, `peek`, plus `passes-span` from the first pass begin to the last pass end and `between-passes` for copies and idle gaps). The compositor's `VrApi` log line gives headset FPS, `GPU%`, `CPU%`, clock levels and app GPU time (`App=`) |
|
||||
|
||||
@@ -1003,7 +1062,8 @@ or `EndAccess` errors); a black mirror too points at Aurora itself.
|
||||
(about half a minute); later runs load Dawn's pipeline cache from `Cache/`
|
||||
next to `DATA`. `render_scale` defaults to 0.8 here (1.0 on
|
||||
PC); lower it further if the compositor reports missed frames.
|
||||
`XR_FB_foveation` is not used yet.
|
||||
Foveated rendering (above) is off by default until its device measurements
|
||||
pick a level.
|
||||
- **Lifecycle.** Backgrounding (the Quest menu, guardian) pauses the session
|
||||
through the ordinary `STOPPING`/`READY` events; SDL's Android surface loss is
|
||||
handled by Aurora's existing Android paths. Neither has been exercised.
|
||||
|
||||
@@ -395,6 +395,12 @@ target_include_directories(mkw_frame_interpolation_pacing_tests PRIVATE "${CMAKE
|
||||
target_link_libraries(mkw_frame_interpolation_pacing_tests PRIVATE mkw::toml11)
|
||||
target_compile_features(mkw_frame_interpolation_pacing_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_frame_interpolation_pacing_tests COMMAND mkw_frame_interpolation_pacing_tests)
|
||||
# [vr] settings the headset renderer reads: single_pass_eyes and the Quest's foveation.
|
||||
add_executable(mkw_vr_config_tests tests/vr_config_tests.cpp)
|
||||
target_include_directories(mkw_vr_config_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_link_libraries(mkw_vr_config_tests PRIVATE mkw::toml11)
|
||||
target_compile_features(mkw_vr_config_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_vr_config_tests COMMAND mkw_vr_config_tests)
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
|
||||
@@ -87,6 +87,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<float> vrWheelTrackingGrace;
|
||||
std::optional<bool> vrWheelHaptics;
|
||||
std::optional<std::string> vrPerformanceLevel;
|
||||
std::optional<std::string> vrFoveation;
|
||||
std::optional<std::string> vrRecenterKey;
|
||||
std::optional<float> vrLeanBackDegrees;
|
||||
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
|
||||
@@ -247,6 +248,24 @@ inline bool IsSupportedVrPerformanceLevel(std::string_view value) {
|
||||
return value == "default" || value == "power_savings" || value == "sustained_low" ||
|
||||
value == "sustained_high" || value == "boost";
|
||||
}
|
||||
// Fixed foveated rendering of the immersive eyes on the Quest, in the order of
|
||||
// aurora_set_stereo_foveation's levels: the periphery is shaded in 2x2, then
|
||||
// 4x4 pixel blocks, the higher the level the closer to the centre. Whether the
|
||||
// GPU device gets fragment density maps at all is decided at launch, so going
|
||||
// from "off" to a level takes a restart; between levels and back to "off" it is
|
||||
// live.
|
||||
inline constexpr const char* kVrFoveationDefault = "off";
|
||||
inline constexpr std::array<std::string_view, 4> kVrFoveationLevels{"off", "low", "medium", "high"};
|
||||
|
||||
inline bool IsSupportedVrFoveation(std::string_view value) {
|
||||
return std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value) != kVrFoveationLevels.end();
|
||||
}
|
||||
|
||||
// The level aurora_set_stereo_foveation takes; anything unknown is off.
|
||||
inline uint32_t VrFoveationLevelIndex(std::string_view value) {
|
||||
const auto it = std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value);
|
||||
return it == kVrFoveationLevels.end() ? 0u : static_cast<uint32_t>(it - kVrFoveationLevels.begin());
|
||||
}
|
||||
// What the desktop window shows while the headset is running: "normal" leaves
|
||||
// the ordinary desktop view alone, "both", "left" and "right" mirror the
|
||||
// headset's eyes, and "none" blacks the window out. Matches
|
||||
@@ -801,6 +820,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && IsSupportedVrPerformanceLevel(*value)) {
|
||||
config.vrPerformanceLevel = *value;
|
||||
}
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "foveation");
|
||||
value && IsSupportedVrFoveation(*value)) {
|
||||
config.vrFoveation = *value;
|
||||
}
|
||||
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
@@ -1189,6 +1212,14 @@ inline bool SetVrPerformanceLevel(std::string value) {
|
||||
return WriteSetting("vr", "performance_level", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrFoveation(std::string value) {
|
||||
if (!IsSupportedVrFoveation(value)) {
|
||||
return false;
|
||||
}
|
||||
Mutable().vrFoveation = value;
|
||||
return WriteSetting("vr", "foveation", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonSeat(std::string value) {
|
||||
if (!IsSupportedVrFirstPersonSeat(value)) {
|
||||
return false;
|
||||
@@ -1641,6 +1672,11 @@ inline std::string VrPerformanceLevel(std::string fallback = kVrPerformanceLevel
|
||||
return value && IsSupportedVrPerformanceLevel(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline std::string VrFoveation(std::string fallback = kVrFoveationDefault) {
|
||||
const auto& value = Get().vrFoveation;
|
||||
return value && IsSupportedVrFoveation(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline int32_t VrFirstPersonHiddenModel(int32_t fallback = kVrFirstPersonHiddenModelDefault) {
|
||||
return std::clamp(Get().vrFirstPersonHiddenModel.value_or(fallback), -1, 31);
|
||||
}
|
||||
|
||||
@@ -145,6 +145,11 @@ bool g_vrHudVirtualScreen = RuntimeConfigFile::VrHudVirtualScreen(true);
|
||||
bool g_vrFlatScreen = RuntimeConfigFile::VrFlatScreen();
|
||||
#if defined(__ANDROID__)
|
||||
bool g_vrPassthrough = RuntimeConfigFile::VrPassthrough();
|
||||
// Menu labels for the foveation levels, index-matched to RuntimeConfigFile::kVrFoveationLevels and to
|
||||
// aurora_set_stereo_foveation.
|
||||
constexpr std::array<const char*, 4> kVrFoveationLabels{"Off", "Low", "Medium", "High"};
|
||||
static_assert(kVrFoveationLabels.size() == RuntimeConfigFile::kVrFoveationLevels.size());
|
||||
int g_vrFoveation = static_cast<int>(RuntimeConfigFile::VrFoveationLevelIndex(RuntimeConfigFile::VrFoveation()));
|
||||
#endif
|
||||
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
||||
bool g_vrFirstPersonToggleClick = RuntimeConfigFile::VrFirstPersonToggleClick();
|
||||
@@ -1512,6 +1517,25 @@ void DrawVrSettings() {
|
||||
"fully virtual; the Flat Screen race has the room around it too. "
|
||||
"Applies immediately.");
|
||||
}
|
||||
// Shows the live level, which debug.wiicompiled.foveation can override.
|
||||
g_vrFoveation = static_cast<int>(aurora_get_stereo_foveation());
|
||||
if (ImGui::Combo("Foveated rendering", &g_vrFoveation, kVrFoveationLabels.data(),
|
||||
static_cast<int>(kVrFoveationLabels.size()))) {
|
||||
aurora_set_stereo_foveation(static_cast<uint32_t>(g_vrFoveation));
|
||||
RuntimeConfigFile::SetVrFoveation(
|
||||
std::string(RuntimeConfigFile::kVrFoveationLevels[static_cast<size_t>(g_vrFoveation)]));
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"%s", aurora_stereo_foveation_available()
|
||||
? "Shades the edges of the race view in 2x2, then 4x4 pixel blocks, where the "
|
||||
"lenses blur the picture anyway, to free GPU time for a higher render scale or a "
|
||||
"steadier frame rate. Higher levels start closer to the centre; High also "
|
||||
"coarsens the corners of the HUD. Menus are never foveated. Applies immediately."
|
||||
: "Shades the edges of the race view in 2x2, then 4x4 pixel blocks, where the "
|
||||
"lenses blur the picture anyway, to free GPU time. This session started with it "
|
||||
"off, or without a GPU that supports it: a new level applies after a restart.");
|
||||
}
|
||||
#endif
|
||||
ImGui::Separator();
|
||||
ImGui::Text("VR camera");
|
||||
@@ -2331,6 +2355,9 @@ void InitializeRuntimeSettings() noexcept {
|
||||
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
|
||||
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
|
||||
aurora_set_stereo_single_pass_eyes(g_vrSinglePassEyes);
|
||||
#if defined(__ANDROID__)
|
||||
aurora_set_stereo_foveation(static_cast<uint32_t>(g_vrFoveation));
|
||||
#endif
|
||||
aurora_set_stereo_mirror_view(static_cast<AuroraStereoMirrorView>(g_vrMirrorView));
|
||||
mkw::vr::OpenXRSetControllerMode(static_cast<mkw::vr::OpenXRControllerMode>(g_vrControllerMode));
|
||||
ApplyVrHudVirtualScreen();
|
||||
|
||||
@@ -618,6 +618,11 @@ private:
|
||||
void ApplyGraphicsRequirements(AuroraConfig& aurora_config) {
|
||||
aurora_config.desiredBackend = kRequiredAuroraBackend;
|
||||
aurora_config.xrInterop = true;
|
||||
#if defined(__ANDROID__)
|
||||
// Foveated rendering: fragment density maps are decided with the device. They put a flag on
|
||||
// every render pipeline, so a session launched with foveation off does without them.
|
||||
aurora_config.xrFragmentDensityMap = RuntimeConfigFile::VrFoveation() != "off";
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
if (kRequiredAuroraBackend != BACKEND_D3D12) return;
|
||||
const auto& requirements = backend_->GraphicsRequirements();
|
||||
@@ -851,6 +856,7 @@ private:
|
||||
const bool panel_layer = backend_->PanelLayerAvailable() && !PanelLayerForcedOff();
|
||||
aurora_set_stereo_panel_layer(panel_layer);
|
||||
PollEyePassesOverride();
|
||||
PollFoveationOverride();
|
||||
presentation.panel.requested = panel_layer && OpenXRSettingsPanelOpen();
|
||||
|
||||
// Pipeline caches are stored where their stall is least visible: once when a race
|
||||
@@ -1481,6 +1487,39 @@ private:
|
||||
#endif
|
||||
}
|
||||
|
||||
// Android: `adb shell setprop debug.wiicompiled.foveation <0-3>` overrides the foveation level
|
||||
// (off, low, medium, high) within one session, for A/B timing; an empty value hands it back to
|
||||
// the settings. Levels need a session launched with foveation on (see VrFoveation). Read about
|
||||
// once a second.
|
||||
void PollFoveationOverride() noexcept {
|
||||
#if defined(__ANDROID__)
|
||||
if (foveation_poll_ != 0) {
|
||||
--foveation_poll_;
|
||||
return;
|
||||
}
|
||||
foveation_poll_ = 72;
|
||||
char value[PROP_VALUE_MAX] = {};
|
||||
int override_value = -1;
|
||||
if (__system_property_get("debug.wiicompiled.foveation", value) > 0 && value[0] >= '0' &&
|
||||
value[0] <= '3' && value[1] == '\0') {
|
||||
override_value = value[0] - '0';
|
||||
}
|
||||
if (override_value == foveation_override_) {
|
||||
return;
|
||||
}
|
||||
foveation_override_ = override_value;
|
||||
const uint32_t level = override_value >= 0
|
||||
? static_cast<uint32_t>(override_value)
|
||||
: RuntimeConfigFile::VrFoveationLevelIndex(RuntimeConfigFile::VrFoveation());
|
||||
aurora_set_stereo_foveation(level);
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: foveation " << RuntimeConfigFile::kVrFoveationLevels[level]
|
||||
<< (override_value >= 0 ? " (debug.wiicompiled.foveation)"
|
||||
: " (debug.wiicompiled.foveation cleared)")
|
||||
<< (aurora_stereo_foveation_available() ? "" : "; unavailable in this session")
|
||||
<< std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The settings panel's layer hangs exactly where its pointer hits are
|
||||
// tested, the rectangle it used to cover in the eyes.
|
||||
static void PlacePanelLayer(OpenXRBackendFrame& frame, const OpenXRPointerScreen& screen) noexcept {
|
||||
@@ -1706,6 +1745,8 @@ private:
|
||||
bool panel_layer_forced_off_ = false;
|
||||
uint32_t eye_passes_poll_ = 0;
|
||||
int eye_passes_override_ = -1;
|
||||
uint32_t foveation_poll_ = 0;
|
||||
int foveation_override_ = -1;
|
||||
#endif
|
||||
std::unique_ptr<OpenXRInput> input_;
|
||||
std::thread pacing_thread_;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "runtime_config.h"
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
static void Require(bool condition) {
|
||||
if (!condition) std::abort();
|
||||
}
|
||||
|
||||
static RuntimeUserConfig Parse(const std::string& text) {
|
||||
std::istringstream input(text);
|
||||
return RuntimeConfigFile::ParseConfig(input);
|
||||
}
|
||||
|
||||
int main() {
|
||||
// [vr] foveation: the Quest's foveated rendering level, index-matched to
|
||||
// aurora_set_stereo_foveation.
|
||||
for (std::string_view level : RuntimeConfigFile::kVrFoveationLevels) {
|
||||
Require(Parse("[vr]\nfoveation = \"" + std::string(level) + "\"\n").vrFoveation == std::string(level));
|
||||
}
|
||||
Require(!Parse("[vr]\nfoveation = \"ultra\"\n").vrFoveation.has_value());
|
||||
Require(!Parse("[vr]\nfoveation = 2\n").vrFoveation.has_value());
|
||||
Require(!Parse("[vr]\n").vrFoveation.has_value());
|
||||
Require(std::string_view(RuntimeConfigFile::kVrFoveationDefault) == "off");
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("off") == 0);
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("low") == 1);
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("medium") == 2);
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("high") == 3);
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("ultra") == 0);
|
||||
|
||||
// [vr] single_pass_eyes: each eye replayed in one render pass.
|
||||
Require(Parse("[vr]\nsingle_pass_eyes = true\n").vrSinglePassEyes == true);
|
||||
Require(Parse("[vr]\nsingle_pass_eyes = false\n").vrSinglePassEyes == false);
|
||||
Require(!Parse("[vr]\n").vrSinglePassEyes.has_value());
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user