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:
iChris4andClaude Opus 5.5 committed 2026-09-24 19:03:17 +02:00
1 parent ceeeba0332
commit cc653272c8
30 files changed
+1527 -14

No files matched your search

+27
View File
@@ -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();
+41
View File
@@ -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_;