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

+79
View File
@@ -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);
}