Repository navigation
Conversation
added 3 commits
October 10, 2026 17:26
…te maps (foveation) With foveation enabled in the layer configuration, visionOS hands each eye a rasterization rate map and textures of its physical size; the engine ignored the maps, so apps had to keep foveation off, which also forbids a render quality above the platform default. The stereo path now attaches the eye's map to every pass that rasterizes the scene at drawable size and sets the eye's screen-space viewport there; the full-screen passes keep copying physical pixels one to one. SSAO rebuilds view positions from a texel's screen position, and the occlusion culls (mesh, Gaussian chunk, per-splat) move their projected rects into the pyramid's physical layout through the map the pyramid was drawn with, so culling stays on under foveation. The Gaussian fragment takes its offset from the splat centre from a varying rather than from its position. On by default when the drawable provides maps; `.foveatedRendering(.disabled)` ignores them for diagnosis. Kernels rebuilt for every platform.
A layer configured with foveation or a render quality aborts the app when
the frame is queried with the single-drawable queryDrawable() ("BUG IN
CLIENT: called cp_frame_query_drawable() which does not support setting
render quality or fovea"). The frame now takes the drawable array on
visionOS 26 (the old call on visionOS 2), presents every drawable, and
draws the scene into a capture drawable only while its textures match the
frame's resources; otherwise it is presented cleared rather than
reallocating every sizeable resource twice a frame.
… map is attached Under a rasterization rate map Metal reads a descriptor's renderTargetWidth and renderTargetHeight, like the viewport, in screen pixels. The scene descriptors carried the physical texture size there, which clipped every foveated frame to the top-left physical-size corner of the screen: on the headset a screen-fixed rectangle, different in each eye, outside which the board vanished. The attached descriptors now leave the size at 0 (the attachments' own) and get the texture size back when the map is detached; the eye's viewport is taken from the map's screen size.
This was referenced Oct 10, 2026
Collaborator
Author
|
Superseded by #1359, which carries this work linearised together with the rest of the headset rendering set. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On visionOS, a layer configured with
isFoveationEnabledhands the engine oneMTLRasterizationRateMapper eye and colour/depth textures of the map's physical size, smaller than the screen the eye sees; the GPU draws through the map (dense where the eyes look, coarse in the periphery) and the compositor unwarps the result. The engine ignored the maps, so apps had to keep foveation off — and a layer without foveation is also refused amaxRenderQualityabove the platform default ("Layer configuration not supported", code -18), which leaves the per-eye drawables at roughly 1900 px, about 2 mm per pixel at 2.4 m: thin wires and digits look soft next to SwiftUI windows, which draw at panel density.This PR makes the XR path draw through the compositor's rate maps (dedicated layout, the stereo path), on by default whenever the drawable provides them, with
setRendering(.foveatedRendering(.disabled))as a diagnostic switch.Pass changes
UntoldEngineXR.executeXRSystemPassmakes anXRFoveationFrameper eye (renderInfo.xrFoveation): the map, the eye's viewport in the map's screen space (view.textureMap.viewport) and the map's parameter data copied into a ring of shared buffers (the maps follow the gaze, so they are copied every frame; the ring is one slot deeper than the frames in flight because the depth pyramid carries its map into the next frame's culls).renderInfo.viewPortstays the texture size — the physical size — so every sizeable resource is allocated at it, exactly as before.applyXRFoveationToSceneRenderPassDescriptorsattaches the eye's map to the descriptors of the passes that rasterize the scene at drawable size — the G-buffer + light pass (offscreenRenderPassDescriptor, MSAA included), the deferred colour that transparency, wireframe, spatial/muscle debug and the model-surface extension draw into, environment/sky/grid, the Gaussian splats, the gizmos — and the two ad-hoc depth descriptors (mesh occluder shells, wireframe occlusion depth). Each of those encoders sets the eye's viewport (applyXRFoveationViewport): the default viewport is the texture's size, which would draw the scene into a corner of the screen. Render extensions that draw into the scene targets get the viewport throughSceneRenderTargetAccess.rasterization_rate_map_decoder): SSAO rebuilds view positions from a depth texel's screen position rather than its UV (otherwise the periphery's geometry bends and shades false creases); the occlusion culls —hzbCullVisibleEntities,gaussianChunkCulland the per-splat test shared bygaussianFrustumCullandgaussianChunkDecodePreprocess— move each projected rect or point into the pyramid's physical layout before sampling it (hzbRemapRectToPhysical/hzbScreenUVToPhysicalUVinHZBOcclusion.h), using the map the pyramid was drawn through (HZBPyramidFrame.rateMapData), so a foveated frame keeps occlusion culling instead of switching it off.[[position]], which a rate map places in the physical texture, not on the screen the centre was projected to.SmallObjectCullingmeasures its pixel threshold against the screen height.GaussianChunkCullConstantsgrows by onesimd_float4(192 → 208 bytes); the layout tests are updated. Every kernel that declares a rate map buffer gets a small placeholder bound when there is none and reads it only when the sizes word says there is a map.Without foveation (a layer configured without it, the simulator),
drawable.rasterizationRateMapsis empty and the frame is drawn uniformly as before; macOS/iOS are untouched beyond the extra (unread) bindings.Device result (Vision Pro, visionOS 26, Oche with foveation on, render quality 1.0, MSAA 4×)
✓ XR foveation: drawing through the compositor's rasterization rate maps, screen 6888×5525, textures 2624×2560 per eye— the eye's screen is 2.6× the texture's width, which is what a uniform layer could never afford.queryDrawables()(the old single-drawable call aborts: "BUG IN CLIENT: called cp_frame_query_drawable() which does not support setting render quality or fovea"), and under a rate map Metal readsrenderTargetWidth/Heightand the viewport in screen pixels (a physical-size target clipped the scene to a screen-fixed rectangle, different per eye; measured on a Mac GPU with a real rate map).Checklist
This project follows the Engineering Principles.
XRFoveationTests(render tests): the rate map data the shaders decode, which descriptors carry the map and which do not, and the HZB cull dropping a box only once its rect is moved through the map into the pyramid's layout (a real rate map on the Mac GPU, the real kernel); layout pins for the grown constants struct.RenderingProperty.foveatedRendering,getFoveatedRenderingEnabled(),renderInfo.xrFoveation,XRFoveationFrame,XRRasterizationRateMapData,HZBPyramidFrame.rateMapData(defaulted).swift build --build-tests, the touched render test suites,xcodebuildofUntoldEngineXRfor the visionOS simulator, kernels rebuilt with./buildkernels.shfor every platform.docs/Architecture/xrRenderingSystem.md,docs/API/UsingEngineAPI.md..msaaTaa, the whole command buffer as the Controls line reports it): 12 ms on average, 9 ms to 18 ms; the compositor's 90 Hz budget is 11.1 ms, so quality 1.0 with 4× MSAA and the temporal resolve sits at the edge of it and the game's render quality setting is what trades it off.Architectural notes / trade-offs
renderInfo.viewPortkeeps meaning "the size of the textures" so the existing allocation and resize path works unchanged; the screen size lives inrenderInfo.xrFoveation. The pixel-based cull threshold is the one system that wanted the screen size.RenderPassContextcould carry it); the layered and shared layouts, which the stereo path does not use.