mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 04:04:18 +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
@@ -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
|
||||
Reference in new issue
Block a user