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

@@ -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)
+6
View File
@@ -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 ()
+5
View File
@@ -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;
+51
View File
@@ -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
+12
View File
@@ -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
+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);
}
+11 -1
View File
@@ -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,
+8
View File
@@ -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
+124
View File
@@ -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
+107
View File
@@ -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
+27
View File
@@ -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
+9
View File
@@ -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();
+124 -7
View File
@@ -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)
+21
View File
@@ -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
+279
View File
@@ -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, &region);
// 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
+1
View File
@@ -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
+179
View File
@@ -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