From 9a4652eb7e110884300c846d3c4c2367aec7dbf7 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Fri, 30 Oct 2026 12:41:15 -0700 Subject: [PATCH] docs(prd): PRD-775 virtual shadow pages are managed and drawn on the GPU Ports Unreal 5.8.3's GPU page management in three phases ordered by measured CPU ceiling: one GPU-culled shadow pass per light (BuildPerPageDrawCommands), caster changes pushed by the scene (CacheManager change set), and the page table and physical pool on the GPU (PhysicalPageManagement). Folds PRD-457, 524, 652 and 773; maps the three VSM subsystem rows to PRD-775. --- README.md | 4 +- docs/PRDs/Unreal/research/port-history.json | 4 +- .../Unreal/research/unreal-port-progress.svg | 28 +-- ...l-visuals-and-performance-feature-map.html | 10 +- ...-pages-are-managed-and-drawn-on-the-gpu.md | 206 ++++++++++++++++++ 5 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 docs/PRDs/Unreal/shadows/PRD-775-virtual-shadow-pages-are-managed-and-drawn-on-the-gpu.md diff --git a/README.md b/README.md index fb8e46cc5f..de212f592b 100644 --- a/README.md +++ b/README.md @@ -185,13 +185,13 @@ Every Unreal 5.8.3 system that sets the look or the frame cost, split into 380 s measured in lines of the Unreal source. Each one maps to the ThreeNative PRD that ports it. **20% done · 24% in progress · 33% not started · 16% deferred · 5% with no PRD** (share of 676K Unreal lines; the tool-heavy Niagara, Landscape, World Partition, RHI and RenderCore modules are left out) -· 83 open PRDs on this branch · 72 subsystems with no PRD +· 84 open PRDs on this branch · 72 subsystems with no PRD [![Unreal port progress map](docs/PRDs/Unreal/research/unreal-port-status.svg)](docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html) **Pace:** 107 PRD boxes ticked a week over the last 2 weeks. The port order runs waves 1-4 first, so at that pace the default look is done about **Nov 21, 2026**, and everything filed today about -**Dec 14, 2026**. Scope grows as PRDs are filed, so read the dates as the earliest finish. +**Dec 15, 2026**. Scope grows as PRDs are filed, so read the dates as the earliest finish. [![Unreal port burn-up](docs/PRDs/Unreal/research/unreal-port-progress.svg)](docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html) diff --git a/docs/PRDs/Unreal/research/port-history.json b/docs/PRDs/Unreal/research/port-history.json index fccf668475..60a343282b 100644 --- a/docs/PRDs/Unreal/research/port-history.json +++ b/docs/PRDs/Unreal/research/port-history.json @@ -130,9 +130,9 @@ { "date": "2026-10-30", "done": 392, - "total": 1094, + "total": 1102, "lookDone": 138, "lookTotal": 489, - "open": 83 + "open": 84 } ] diff --git a/docs/PRDs/Unreal/research/unreal-port-progress.svg b/docs/PRDs/Unreal/research/unreal-port-progress.svg index 823331058b..01b9769007 100644 --- a/docs/PRDs/Unreal/research/unreal-port-progress.svg +++ b/docs/PRDs/Unreal/research/unreal-port-progress.svg @@ -1,7 +1,7 @@ -Unreal port burn-up: 392 of 1094 PRD boxes ticked · 107 a week over the last 2 weeks · 83 PRDs open +Unreal port burn-up: 392 of 1102 PRD boxes ticked · 107 a week over the last 2 weeks · 84 PRDs open Every filed PRD: share of boxes tickedDefault look (waves 1-4): share of boxes tickedDashed: finish at the last 2 weeks' pace 0% 25% @@ -9,17 +9,17 @@ 75% 100% Sep 6 -Sep 20 -Oct 4 -Oct 18 -Nov 1 -Nov 15 -Nov 29 -Dec 13 - - - -Everything filed ≈ Dec 14, 2026 - -Default look ≈ Nov 21, 2026 +Sep 20 +Oct 4 +Oct 18 +Nov 1 +Nov 15 +Nov 29 +Dec 13 + + + +Everything filed ≈ Dec 15, 2026 + +Default look ≈ Nov 21, 2026 diff --git a/docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html b/docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html index d6a88b6bad..9e477a840e 100644 --- a/docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html +++ b/docs/PRDs/Unreal/research/unreal-visuals-and-performance-feature-map.html @@ -285,7 +285,7 @@

Unreal 5.8.3 render features, sized and rated

const STATUS = { shipped: "Shipped", partial: "Partial", open: "Open", deferred: "Deferred", notfiled: "Not filed", rule: "Out of scope by rule" }; // PRD registry (title, path, box counts), generated from the PRD files by a script; do not hand-edit. // @prds-start -const PRDS = {"244":{"t":"The scene's BVH reaches the GPU","p":"docs/PRDs/done/PRD-244-the-scenes-bvh-reaches-the-gpu.md","r":"develop","s":"","d":0,"n":15,"f":"done"},"269":{"t":"motion vectors for skinned and instanced geometry, or the temporal filters lie","p":"docs/PRDs/done/PRD-269-motion-vectors-or-the-temporal-filters-lie.md","r":"develop","s":"DONE","d":13,"n":13,"f":"done"},"281":{"t":"a dense mesh bakes to a crack-free cluster DAG","p":"docs/PRDs/done/nanite-like/PRD-281-a-dense-mesh-bakes-to-a-crack-free-cluster-dag.md","r":"develop","s":"","d":8,"n":8,"f":"done"},"282":{"t":"the cut is chosen on the CPU first","p":"docs/PRDs/done/nanite-like/PRD-282-the-cut-is-chosen-on-the-cpu-first.md","r":"develop","s":"","d":8,"n":8,"f":"done"},"284":{"t":"the frame does not draw what the frame already hid","p":"docs/PRDs/done/nanite-like/PRD-284-the-frame-does-not-draw-what-the-frame-already-hid.md","r":"develop","s":"","d":0,"n":4,"f":"done"},"285":{"t":"clusters arrive when the camera asks for them","p":"docs/PRDs/done/nanite-like/PRD-285-clusters-arrive-when-the-camera-asks-for-them.md","r":"develop","s":"","d":0,"n":5,"f":"done"},"316":{"t":"forty-six VFX are generated render source, not an engine inside the engine","p":"docs/PRDs/Unreal/vfx/PRD-316-forty-six-vfx-are-generated-render-source-not-an-engine-inside-the-engine.md","r":"develop","s":"PROPOSED","d":0,"n":39,"f":"open"},"384":{"t":"Adaptive resolution must distinguish host delay from GPU cost","p":"docs/PRDs/done/PRD-384-adaptive-resolution-gpu-headroom.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"455":{"t":"Fewer rendered pixels reconstruct into a stable full-resolution frame","p":"docs/PRDs/done/PRD-455-temporal-reconstruction-from-dynamic-resolution.md","r":"develop","s":"DONE","d":7,"n":7,"f":"done"},"456":{"t":"Distant world cells stay visible after their detailed cells unload","p":"docs/PRDs/open-world/PRD-456-distant-world-cell-proxies.md","r":"develop","s":"PROPOSED","d":0,"n":6,"f":"open"},"457":{"t":"Virtual shadows page only when pages are cheaper than cached clip levels","p":"docs/PRDs/Unreal/rendering/PRD-457-virtual-shadows-scale-by-measurement.md","r":"develop","s":"PROPOSED","d":0,"n":5,"f":"open"},"459":{"t":"Smooth streaming: one admission budget per frame, prefetched ahead of the camera","p":"docs/PRDs/open-world/PRD-459-smooth-streaming-one-admission-budget-per-frame.md","r":"develop","s":"PARTIAL","d":4,"n":16,"f":"open"},"460":{"t":"Invisible streaming: a dithered fade for arriving props and a crossfade across LOD levels","p":"docs/PRDs/Unreal/open-world/PRD-460-invisible-streaming-transitions.md","r":"develop","s":"NOT STARTED","d":0,"n":15,"f":"open"},"486":{"t":"Characters get a LOD chain","p":"docs/PRDs/open-world/PRD-486-characters-get-a-lod-chain.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"488":{"t":"Foliage moves in the wind","p":"docs/PRDs/open-world/PRD-488-foliage-moves-in-the-wind.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"489":{"t":"Occlusion culling re-tested on the GPU scene","p":"docs/PRDs/done/PRD-489-gpu-scene-occlusion-culling.md","r":"develop","s":"CLOSED","d":2,"n":8,"f":"done"},"492":{"t":"Colour grading and film grain","p":"docs/PRDs/done/PRD-492-colour-grading-and-film-grain.md","r":"develop","s":"DONE","d":6,"n":6,"f":"done"},"493":{"t":"Terrain layers past sixteen textures","p":"docs/PRDs/open-world/PRD-493-terrain-layers-past-sixteen-textures.md","r":"develop","s":"PARTIAL","d":6,"n":8,"f":"open"},"523":{"t":"The render graph owns passes and history (N14a)","p":"docs/PRDs/native-engine/N14-native-render-chain-and-advanced-visuals/PRD-523-n14a-the-render-graph-owns-passes-and-history.md","r":"develop","s":"PROPOSED","d":0,"n":7,"f":"open"},"524":{"t":"Virtual shadows run native (N14b)","p":"docs/PRDs/native-engine/N14-native-render-chain-and-advanced-visuals/PRD-524-n14b-virtual-shadows-run-native.md","r":"develop","s":"PROPOSED","d":0,"n":5,"f":"open"},"538":{"t":"Shadow maps redraw only what streamed in","p":"docs/PRDs/done/PRD-538-shadow-maps-redraw-only-what-streamed-in.md","r":"develop","s":"DONE","d":4,"n":4,"f":"done"},"539":{"t":"A lower internal raster reconstructs into a stable full-resolution frame","p":"docs/PRDs/Unreal/performance/PRD-539-low-resolution-temporal-reconstruction-quality-and-cost.md","r":"develop","s":"NOT STARTED","d":0,"n":6,"f":"open"},"560":{"t":"Height fog follows the ground, and the sun shows through it","p":"docs/PRDs/done/PRD-560-height-fog-follows-the-ground.md","r":"develop","s":"LOCAL PHASES COMPLETE","d":11,"n":11,"f":"done"},"561":{"t":"Small things touch the ground: contact shadows from screen depth","p":"docs/PRDs/done/PRD-561-contact-shadows-from-screen-depth.md","r":"develop","s":"DECLINED","d":1,"n":7,"f":"done"},"562":{"t":"Distant land takes the colour of the sky it is seen through","p":"docs/PRDs/Unreal/rendering/PRD-562-aerial-perspective-from-a-froxel-volume.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"563":{"t":"The first frame starts at the tier the GPU family can hold","p":"docs/PRDs/done/PRD-563-the-first-frame-starts-at-the-tier-the-gpu-family-holds.md","r":"develop","s":"COMPLETE","d":15,"n":15,"f":"done"},"564":{"t":"The resolution scale changes without reallocating a render target","p":"docs/PRDs/done/PRD-564-the-resolution-scale-changes-without-reallocating-a-target.md","r":"develop","s":"DONE","d":7,"n":7,"f":"done"},"565":{"t":"Multisampled targets stay in tile memory on the Android host","p":"docs/PRDs/done/PRD-565-multisampled-targets-stay-in-tile-memory-on-android.md","r":"develop","s":"COMPLETE","d":4,"n":4,"f":"done"},"566":{"t":"Terrain LOD morphs on the GPU and picks its level by screen error","p":"docs/PRDs/done/PRD-566-terrain-lod-morphs-on-the-gpu-and-picks-by-screen-error.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"567":{"t":"Terrain layers blend by their own height, and the game owns the curve","p":"docs/PRDs/done/PRD-567-terrain-layers-blend-by-their-own-height.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"568":{"t":"Mobile builds ship GPU-ready ASTC, so no phone transcodes or uploads RGBA8","p":"docs/PRDs/done/PRD-568-mobile-builds-ship-gpu-ready-astc.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"569":{"t":"GPU particle emitters out of view stop costing, and effects yield to the frame budget","p":"docs/PRDs/done/PRD-569-gpu-particle-emitters-cull-and-yield-to-the-frame-budget.md","r":"develop","s":"DONE","d":12,"n":12,"f":"done"},"570":{"t":"A character costs what the camera sees of it","p":"docs/PRDs/done/PRD-570-a-character-costs-what-the-camera-sees-of-it.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"571":{"t":"Exposure meters a histogram and ignores the sun","p":"docs/PRDs/BLOCKED/requires-physical-device/PRD-571-exposure-meters-a-histogram-and-ignores-the-sun.md","r":"develop","s":"BLOCKED","d":8,"n":8,"f":"blocked"},"572":{"t":"Shadow casters sort themselves into static and moving","p":"docs/PRDs/done/PRD-572-shadow-casters-sort-themselves-into-static-and-moving.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"600":{"t":"A clustered mesh submits only the clusters the view holds","p":"docs/PRDs/Unreal/rendering/PRD-600-a-clustered-mesh-submits-only-the-clusters-the-view-holds.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"601":{"t":"Far clusters keep the shading of the source mesh","p":"docs/PRDs/Unreal/rendering/PRD-601-far-clusters-keep-the-shading-of-the-source-mesh.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"602":{"t":"A clustered shadow caster draws what its shadow texel resolves","p":"docs/PRDs/Unreal/rendering/PRD-602-a-clustered-shadow-caster-draws-what-its-shadow-texel-resolves.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"603":{"t":"Probes refresh by tracing the packed scene, not by drawing it six times","p":"docs/PRDs/Unreal/rendering/PRD-603-probes-refresh-by-tracing-the-packed-scene-not-by-drawing-it-six-times.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"604":{"t":"A surface is lit only by probes that can see it","p":"docs/PRDs/Unreal/rendering/PRD-604-a-surface-is-lit-only-by-probes-that-can-see-it.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"605":{"t":"The probe field follows the camera in nested rings","p":"docs/PRDs/Unreal/rendering/PRD-605-the-probe-field-follows-the-camera-in-nested-rings.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"606":{"t":"A fast-travel destination is ready before the cover lifts","p":"docs/PRDs/Unreal/open-world/PRD-606-a-fast-travel-destination-is-ready-before-the-cover-lifts.md","r":"develop","s":"NOT STARTED","d":0,"n":13,"f":"open"},"607":{"t":"Streaming reports when a hole is near and spends more to close it","p":"docs/PRDs/Unreal/open-world/PRD-607-streaming-reports-when-a-hole-is-near-and-spends-more-to-close-it.md","r":"develop","s":"NOT STARTED","d":0,"n":11,"f":"open"},"609":{"t":"A GPU burst starts where and when it is asked, and live particles stay put","p":"docs/PRDs/done/PRD-609-a-gpu-burst-starts-where-and-when-it-is-asked-and-live-particles-stay-put.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"610":{"t":"The native engine wins on real games","p":"docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md","r":"develop","s":"PARTIAL","d":3,"n":10,"f":"open"},"611":{"t":"The Linux HUD overlay keeps up with the game and stops burning a core","p":"docs/PRDs/performance/PRD-611-hud-overlay-latency-and-cpu.md","r":"develop","s":"PARTIAL","d":7,"n":9,"f":"open"},"631":{"t":"An Unreal reference scene measures the default look","p":"docs/PRDs/done/PRD-631-an-unreal-reference-scene-measures-the-default-look.md","r":"develop","s":"IN PROGRESS","d":15,"n":15,"f":"done"},"632":{"t":"The frame is exposed and tone-mapped the way a fresh Unreal project is","p":"docs/PRDs/Unreal/look/PRD-632-the-frame-is-exposed-and-tone-mapped-the-way-a-fresh-unreal-project-is.md","r":"develop","s":"IN PROGRESS","d":17,"n":25,"f":"open"},"633":{"t":"The sky is an atmosphere, and it lights the scene","p":"docs/PRDs/Unreal/look/PRD-633-the-sky-is-an-atmosphere-and-it-lights-the-scene.md","r":"develop","s":"PARTIAL","d":1,"n":11,"f":"open"},"634":{"t":"Sun shadows reach the horizon and soften with distance from the caster","p":"docs/PRDs/Unreal/look/PRD-634-sun-shadows-reach-the-horizon-and-soften-with-distance-from-the-caster.md","r":"develop","s":"PARTIAL","d":11,"n":18,"f":"open"},"635":{"t":"Ambient occlusion darkens only the light that comes from around","p":"docs/PRDs/Unreal/look/PRD-635-ambient-occlusion-darkens-only-the-light-that-comes-from-around.md","r":"develop","s":"IN PROGRESS","d":8,"n":17,"f":"open"},"636":{"t":"The finishing post chain matches Unreal's defaults","p":"docs/PRDs/Unreal/look/PRD-636-the-finishing-post-chain-matches-unreal-defaults.md","r":"develop","s":"PARTIAL","d":18,"n":29,"f":"open"},"639":{"t":"A Surface Cache gives traced rays the texture, glow and lights of what they hit","p":"docs/PRDs/Unreal/gi/PRD-639-a-surface-cache-gives-traced-rays-the-texture-glow-and-lights-of-what-they-hit.md","r":"develop","s":"IN PROGRESS","d":6,"n":9,"f":"open"},"640":{"t":"Pixels gather bounce light from screen probes, and the radiance cache answers the rest","p":"docs/PRDs/Unreal/gi/PRD-640-pixels-gather-bounce-light-from-screen-probes-and-the-radiance-cache-answers-the-rest.md","r":"develop","s":"PARTIAL","d":2,"n":9,"f":"open"},"641":{"t":"Bounce light holds still while the camera moves and follows a light that changes","p":"docs/PRDs/Unreal/gi/PRD-641-bounce-light-holds-still-while-the-camera-moves-and-follows-a-light-that-changes.md","r":"develop","s":"IN PROGRESS","d":5,"n":14,"f":"open"},"642":{"t":"Glossy surfaces reflect what the camera cannot see","p":"docs/PRDs/Unreal/gi/PRD-642-glossy-surfaces-reflect-what-the-camera-cannot-see.md","r":"develop","s":"PARTIAL","d":6,"n":11,"f":"open"},"643":{"t":"Glass, fog and leaves take the bounce light too","p":"docs/PRDs/Unreal/gi/PRD-643-glass-fog-and-leaves-take-the-bounce-light-too.md","r":"develop","s":"IN PROGRESS","d":6,"n":11,"f":"open"},"644":{"t":"The GI tier follows Unreal's quality levels and the measured GPU time","p":"docs/PRDs/Unreal/gi/PRD-644-the-gi-tier-follows-unreal-quality-levels-and-the-measured-gpu-time.md","r":"develop","s":"IN PROGRESS","d":9,"n":12,"f":"open"},"645":{"t":"A desktop GPU with ray tracing traces GI rays in hardware","p":"docs/PRDs/done/PRD-645-a-desktop-gpu-with-ray-tracing-traces-gi-rays-in-hardware.md","r":"develop","s":"DONE","d":4,"n":4,"f":"done"},"646":{"t":"ThreeNative recreates each Unreal calibration scene exactly","p":"docs/PRDs/Unreal/gi/PRD-646-threenative-recreates-each-unreal-calibration-scene-exactly.md","r":"develop","s":"PARTIAL","d":11,"n":15,"f":"open"},"647":{"t":"A visual judge calibrates GI against the Unreal scenes until parity","p":"docs/PRDs/done/PRD-647-a-visual-judge-calibrates-gi-against-the-unreal-scenes-until-parity.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"649":{"t":"Shadow calibration scenes are built in Unreal and judged until parity","p":"docs/PRDs/Unreal/shadows/PRD-649-shadow-calibration-scenes-are-built-in-unreal-and-judged-until-parity.md","r":"develop","s":"PARTIAL","d":3,"n":12,"f":"open"},"650":{"t":"A lamp's shadow is as sharp as the pixels it covers, and softens with the lamp's size","p":"docs/PRDs/done/PRD-650-a-lamp-shadow-is-as-sharp-as-the-pixels-it-covers-and-softens-with-the-lamp-size.md","r":"develop","s":"DONE","d":12,"n":12,"f":"done"},"651":{"t":"Leaves cast leaf shadows that sway near the camera","p":"docs/PRDs/Unreal/shadows/PRD-651-leaves-cast-leaf-shadows-that-sway-near-the-camera.md","r":"develop","s":"IN PROGRESS","d":7,"n":12,"f":"open"},"652":{"t":"The shadow tier follows Unreal's shadow quality levels","p":"docs/PRDs/Unreal/shadows/PRD-652-the-shadow-tier-follows-unreal-shadow-quality-levels.md","r":"develop","s":"IN PROGRESS","d":7,"n":11,"f":"open"},"660":{"t":"Many shadowed lamps cost one fixed budget, not one shadow map each","p":"docs/PRDs/Unreal/rendering/PRD-660-many-shadowed-lamps-cost-one-fixed-budget-not-one-shadow-map-each.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"661":{"t":"Bounce light stays clean at fewer rays by reusing the rays of neighbours and of the last frame","p":"docs/PRDs/Unreal/rendering/PRD-661-bounce-light-stays-clean-at-fewer-rays-by-reusing-neighbour-and-last-frame-rays.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"662":{"t":"With dynamic GI off, the sky light stops at nearby walls and roofs","p":"docs/PRDs/Unreal/rendering/PRD-662-with-dynamic-gi-off-the-sky-light-stops-at-nearby-walls-and-roofs.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"663":{"t":"A fog bank sits where the game puts it, and the sun lights it","p":"docs/PRDs/Unreal/rendering/PRD-663-a-fog-bank-sits-where-the-game-puts-it.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"664":{"t":"Smoke and explosions are lit volumes, not flat sprites","p":"docs/PRDs/Unreal/vfx/PRD-664-smoke-and-explosions-are-lit-volumes-not-flat-sprites.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"665":{"t":"Clouds shade the haze and fog below them, so light shafts show through the gaps","p":"docs/PRDs/Unreal/rendering/PRD-665-clouds-shade-the-haze-and-fog-below-them.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"667":{"t":"A bright sun can flare the lens, and bloom can take any kernel, when the game asks","p":"docs/PRDs/Unreal/rendering/PRD-667-a-bright-sun-can-flare-the-lens-when-the-game-asks.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"668":{"t":"A displaced surface changes its silhouette near the camera","p":"docs/PRDs/Unreal/rendering/PRD-668-a-displaced-surface-changes-its-silhouette-near-the-camera.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"669":{"t":"Far foliage keeps its volume when its clusters become voxels","p":"docs/PRDs/Unreal/rendering/PRD-669-far-foliage-keeps-its-volume-when-its-clusters-become-voxels.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"670":{"t":"Rough surfaces reflect light the way a fresh Unreal project shades them","p":"docs/PRDs/Unreal/rendering/PRD-670-rough-surfaces-reflect-light-the-way-a-fresh-unreal-project-shades-them.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"671":{"t":"A character wears strand hair that catches light along each strand","p":"docs/PRDs/Unreal/rendering/PRD-671-a-character-wears-strand-hair-that-catches-light-along-each-strand.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"672":{"t":"Terrain shades its layers once into cached pages, not every pixel every frame","p":"docs/PRDs/Unreal/performance/PRD-672-terrain-shades-its-layers-once-into-cached-pages.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"673":{"t":"The GPU scene tests a placement only when its grid cell is in view","p":"docs/PRDs/Unreal/performance/PRD-673-the-gpu-scene-tests-a-placement-only-when-its-grid-cell-is-in-view.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"674":{"t":"An ambient particle is a function of its age and seed, and costs no simulation","p":"docs/PRDs/Unreal/vfx/PRD-674-an-ambient-particle-is-a-function-of-its-age-and-seed-and-costs-no-simulation.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"675":{"t":"A game turns a named layer of its world on and off inside one streaming budget","p":"docs/PRDs/Unreal/open-world/PRD-675-a-game-turns-a-named-layer-of-its-world-on-and-off-inside-one-streaming-budget.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"676":{"t":"A playtest capture tells an agent whether a frame pays for pixels, clusters, shadow levels or effects","p":"docs/PRDs/Unreal/performance/PRD-676-a-playtest-capture-tells-an-agent-whether-a-frame-pays-for-pixels-clusters-shadow-levels-or-effects.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"677":{"t":"A dense glass or water mesh takes the cluster cut and blends like the source mesh","p":"docs/PRDs/Unreal/rendering/PRD-677-a-dense-glass-or-water-mesh-takes-the-cluster-cut-and-blends-like-the-source-mesh.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"678":{"t":"A dense skinned body takes the cluster cut in every view that sees it","p":"docs/PRDs/Unreal/rendering/PRD-678-a-dense-skinned-body-takes-the-cluster-cut-in-every-view-that-sees-it.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"679":{"t":"Sparks and glass in fog take the fog of their own distance and blend mode","p":"docs/PRDs/Unreal/rendering/PRD-679-sparks-and-glass-in-fog-take-the-fog-of-their-own-distance-and-blend-mode.md","r":"develop","s":"NOT STARTED","d":0,"n":11,"f":"open"},"682":{"t":"Flat parts of the frame shade at a coarser rate where the GPU allows it, and every other platform says why not","p":"docs/PRDs/Unreal/performance/PRD-682-flat-parts-of-the-frame-shade-at-a-coarser-rate-where-the-gpu-allows-it.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"683":{"t":"A frame skips the passes nothing reads and shares one target between passes that never overlap","p":"docs/PRDs/Unreal/performance/PRD-683-a-frame-skips-passes-nothing-reads-and-shares-targets-whose-lifetimes-do-not-overlap.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"684":{"t":"Screen probes fill the gaps at depth edges and aim their rays where the light comes from","p":"docs/PRDs/Unreal/rendering/PRD-684-screen-probes-fill-the-gaps-at-depth-edges-and-aim-their-rays-where-the-light-comes-from.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"685":{"t":"A ray the screen cannot answer reads the light that arrives from its own direction","p":"docs/PRDs/Unreal/rendering/PRD-685-a-ray-the-screen-cannot-answer-reads-the-light-that-arrives-from-its-own-direction.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"686":{"t":"A door that moves blocks and bounces GI rays where it now stands","p":"docs/PRDs/Unreal/rendering/PRD-686-a-door-that-moves-blocks-and-bounces-gi-rays-where-it-now-stands.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"687":{"t":"An agent sees what the GI rays see in a playtest capture","p":"docs/PRDs/Unreal/rendering/PRD-687-an-agent-sees-what-the-gi-rays-see-in-a-playtest-capture.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"694":{"t":"Traces and colliders see a clustered mesh at one fixed error, not at the camera cut","p":"docs/PRDs/Unreal/rendering/PRD-694-traces-and-colliders-see-a-clustered-mesh-at-one-fixed-error-not-at-the-camera-cut.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"695":{"t":"A clustered mesh takes its own cut in every view that draws it","p":"docs/PRDs/Unreal/rendering/PRD-695-a-clustered-mesh-takes-its-own-cut-in-every-view-that-draws-it.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"696":{"t":"Tiny triangles of a clustered shadow caster rasterize in compute","p":"docs/PRDs/Unreal/performance/PRD-696-tiny-triangles-of-a-clustered-shadow-caster-rasterize-in-compute.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"704":{"t":"A spark streaks along its own motion, and a far spark does not flicker","p":"docs/PRDs/Unreal/vfx/PRD-704-a-spark-streaks-along-its-own-motion-and-a-far-spark-does-not-flicker.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"705":{"t":"A sword swing leaves one connected trail, not a cloud of dashes","p":"docs/PRDs/Unreal/vfx/PRD-705-a-sword-swing-leaves-one-connected-trail-not-a-cloud-of-dashes.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"706":{"t":"An effect starts on the skin of a moving character","p":"docs/PRDs/Unreal/vfx/PRD-706-an-effect-starts-on-the-skin-of-a-moving-character.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"714":{"t":"The sun shadow keeps its texel size per screen pixel when the camera zooms or the window resizes","p":"docs/PRDs/Unreal/rendering/PRD-714-the-sun-shadow-keeps-one-texel-per-screen-pixel-when-the-camera-zooms-or-the-window-resizes.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"715":{"t":"A shadow lookup stops sampling where no edge is near and starts clear of its own surface","p":"docs/PRDs/Unreal/rendering/PRD-715-a-shadow-lookup-stops-sampling-where-no-edge-is-near-and-starts-clear-of-its-own-surface.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"716":{"t":"A shadow caster that no visible surface receives, or another caster hides, skips its draw","p":"docs/PRDs/Unreal/rendering/PRD-716-a-shadow-caster-that-no-visible-surface-receives-or-another-caster-hides-skips-its-draw.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"717":{"t":"A pixel a moving object just uncovered reprojects through the surface that left it","p":"docs/PRDs/Unreal/performance/PRD-717-a-pixel-a-moving-object-just-uncovered-reprojects-through-the-surface-that-left-it.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"718":{"t":"A surface that returns to view takes back the detail the history held before it left","p":"docs/PRDs/Unreal/performance/PRD-718-a-surface-that-returns-to-view-takes-back-the-detail-the-history-held-before-it-left.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"719":{"t":"History rejection judges a color change by whether the display can show it","p":"docs/PRDs/Unreal/performance/PRD-719-history-rejection-judges-a-color-change-by-whether-the-display-can-show-it.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"720":{"t":"The temporal resolve reads a wider input where history fails, and the tier sets how wide","p":"docs/PRDs/Unreal/performance/PRD-720-the-temporal-resolve-reads-a-wider-input-where-history-fails-and-the-tier-sets-how-wide.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"741":{"t":"The owned engine shades fewer pixels and its temporal resolve writes the display frame","p":"docs/PRDs/done/PRD-741-the-owned-engine-shades-fewer-pixels-and-its-temporal-resolve-writes-the-display-frame.md","r":"develop","s":"DONE","d":10,"n":10,"f":"done"},"773":{"t":"Virtual shadow pages draw only the instances that touch them","p":"docs/PRDs/performance/stress-lab-2026-10-30/PRD-773-vsm-per-page-instance-culling.md","r":"develop","s":"PARTIAL","d":4,"n":6,"f":"open"},"VQ-03":{"t":"Off-screen specular reflections come from a complete local probe","p":"docs/PRDs/rendering/PRD-VQ-03-local-reflection-probes.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-08":{"t":"Clouds and their ground attenuation share one authored field","p":"docs/PRDs/Unreal/rendering/PRD-VQ-08-clouds-and-overhead-transmittance.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-09":{"t":"A door that opens and a lamp that switches change the bounce light","p":"docs/PRDs/rendering/PRD-VQ-09-incremental-diffuse-relighting.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"VQ-10":{"t":"Texture residency releases real GPU memory rather than only biasing sampling","p":"docs/PRDs/Unreal/performance/PRD-VQ-10-texture-mip-residency.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"VQ-12":{"t":"Overlapping transparent effects have an explicit correct or bounded-approximate path","p":"docs/PRDs/rendering/PRD-VQ-12-transparent-effects-composition.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"VQ-13":{"t":"Depth of field and motion blur are qualified optional stages, not a second camera pipeline","p":"docs/PRDs/rendering/PRD-VQ-13-cinematic-stage-integration.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-15":{"t":"Hair cards preserve coverage, tangent highlights and shadow behavior across quality tiers","p":"docs/PRDs/rendering/PRD-VQ-15-hair-card-material-qualification.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"}}; +const PRDS = {"244":{"t":"The scene's BVH reaches the GPU","p":"docs/PRDs/done/PRD-244-the-scenes-bvh-reaches-the-gpu.md","r":"develop","s":"","d":0,"n":15,"f":"done"},"269":{"t":"motion vectors for skinned and instanced geometry, or the temporal filters lie","p":"docs/PRDs/done/PRD-269-motion-vectors-or-the-temporal-filters-lie.md","r":"develop","s":"DONE","d":13,"n":13,"f":"done"},"281":{"t":"a dense mesh bakes to a crack-free cluster DAG","p":"docs/PRDs/done/nanite-like/PRD-281-a-dense-mesh-bakes-to-a-crack-free-cluster-dag.md","r":"develop","s":"","d":8,"n":8,"f":"done"},"282":{"t":"the cut is chosen on the CPU first","p":"docs/PRDs/done/nanite-like/PRD-282-the-cut-is-chosen-on-the-cpu-first.md","r":"develop","s":"","d":8,"n":8,"f":"done"},"284":{"t":"the frame does not draw what the frame already hid","p":"docs/PRDs/done/nanite-like/PRD-284-the-frame-does-not-draw-what-the-frame-already-hid.md","r":"develop","s":"","d":0,"n":4,"f":"done"},"285":{"t":"clusters arrive when the camera asks for them","p":"docs/PRDs/done/nanite-like/PRD-285-clusters-arrive-when-the-camera-asks-for-them.md","r":"develop","s":"","d":0,"n":5,"f":"done"},"316":{"t":"forty-six VFX are generated render source, not an engine inside the engine","p":"docs/PRDs/Unreal/vfx/PRD-316-forty-six-vfx-are-generated-render-source-not-an-engine-inside-the-engine.md","r":"develop","s":"PROPOSED","d":0,"n":39,"f":"open"},"384":{"t":"Adaptive resolution must distinguish host delay from GPU cost","p":"docs/PRDs/done/PRD-384-adaptive-resolution-gpu-headroom.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"455":{"t":"Fewer rendered pixels reconstruct into a stable full-resolution frame","p":"docs/PRDs/done/PRD-455-temporal-reconstruction-from-dynamic-resolution.md","r":"develop","s":"DONE","d":7,"n":7,"f":"done"},"456":{"t":"Distant world cells stay visible after their detailed cells unload","p":"docs/PRDs/open-world/PRD-456-distant-world-cell-proxies.md","r":"develop","s":"PROPOSED","d":0,"n":6,"f":"open"},"457":{"t":"Virtual shadows page only when pages are cheaper than cached clip levels","p":"docs/PRDs/Unreal/rendering/PRD-457-virtual-shadows-scale-by-measurement.md","r":"develop","s":"PROPOSED","d":0,"n":5,"f":"open"},"459":{"t":"Smooth streaming: one admission budget per frame, prefetched ahead of the camera","p":"docs/PRDs/open-world/PRD-459-smooth-streaming-one-admission-budget-per-frame.md","r":"develop","s":"PARTIAL","d":4,"n":16,"f":"open"},"460":{"t":"Invisible streaming: a dithered fade for arriving props and a crossfade across LOD levels","p":"docs/PRDs/Unreal/open-world/PRD-460-invisible-streaming-transitions.md","r":"develop","s":"NOT STARTED","d":0,"n":15,"f":"open"},"486":{"t":"Characters get a LOD chain","p":"docs/PRDs/open-world/PRD-486-characters-get-a-lod-chain.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"488":{"t":"Foliage moves in the wind","p":"docs/PRDs/open-world/PRD-488-foliage-moves-in-the-wind.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"489":{"t":"Occlusion culling re-tested on the GPU scene","p":"docs/PRDs/done/PRD-489-gpu-scene-occlusion-culling.md","r":"develop","s":"CLOSED","d":2,"n":8,"f":"done"},"492":{"t":"Colour grading and film grain","p":"docs/PRDs/done/PRD-492-colour-grading-and-film-grain.md","r":"develop","s":"DONE","d":6,"n":6,"f":"done"},"493":{"t":"Terrain layers past sixteen textures","p":"docs/PRDs/open-world/PRD-493-terrain-layers-past-sixteen-textures.md","r":"develop","s":"PARTIAL","d":6,"n":8,"f":"open"},"523":{"t":"The render graph owns passes and history (N14a)","p":"docs/PRDs/native-engine/N14-native-render-chain-and-advanced-visuals/PRD-523-n14a-the-render-graph-owns-passes-and-history.md","r":"develop","s":"PROPOSED","d":0,"n":7,"f":"open"},"524":{"t":"Virtual shadows run native (N14b)","p":"docs/PRDs/native-engine/N14-native-render-chain-and-advanced-visuals/PRD-524-n14b-virtual-shadows-run-native.md","r":"develop","s":"PROPOSED","d":0,"n":5,"f":"open"},"538":{"t":"Shadow maps redraw only what streamed in","p":"docs/PRDs/done/PRD-538-shadow-maps-redraw-only-what-streamed-in.md","r":"develop","s":"DONE","d":4,"n":4,"f":"done"},"539":{"t":"A lower internal raster reconstructs into a stable full-resolution frame","p":"docs/PRDs/Unreal/performance/PRD-539-low-resolution-temporal-reconstruction-quality-and-cost.md","r":"develop","s":"NOT STARTED","d":0,"n":6,"f":"open"},"560":{"t":"Height fog follows the ground, and the sun shows through it","p":"docs/PRDs/done/PRD-560-height-fog-follows-the-ground.md","r":"develop","s":"LOCAL PHASES COMPLETE","d":11,"n":11,"f":"done"},"561":{"t":"Small things touch the ground: contact shadows from screen depth","p":"docs/PRDs/done/PRD-561-contact-shadows-from-screen-depth.md","r":"develop","s":"DECLINED","d":1,"n":7,"f":"done"},"562":{"t":"Distant land takes the colour of the sky it is seen through","p":"docs/PRDs/Unreal/rendering/PRD-562-aerial-perspective-from-a-froxel-volume.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"563":{"t":"The first frame starts at the tier the GPU family can hold","p":"docs/PRDs/done/PRD-563-the-first-frame-starts-at-the-tier-the-gpu-family-holds.md","r":"develop","s":"COMPLETE","d":15,"n":15,"f":"done"},"564":{"t":"The resolution scale changes without reallocating a render target","p":"docs/PRDs/done/PRD-564-the-resolution-scale-changes-without-reallocating-a-target.md","r":"develop","s":"DONE","d":7,"n":7,"f":"done"},"565":{"t":"Multisampled targets stay in tile memory on the Android host","p":"docs/PRDs/done/PRD-565-multisampled-targets-stay-in-tile-memory-on-android.md","r":"develop","s":"COMPLETE","d":4,"n":4,"f":"done"},"566":{"t":"Terrain LOD morphs on the GPU and picks its level by screen error","p":"docs/PRDs/done/PRD-566-terrain-lod-morphs-on-the-gpu-and-picks-by-screen-error.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"567":{"t":"Terrain layers blend by their own height, and the game owns the curve","p":"docs/PRDs/done/PRD-567-terrain-layers-blend-by-their-own-height.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"568":{"t":"Mobile builds ship GPU-ready ASTC, so no phone transcodes or uploads RGBA8","p":"docs/PRDs/done/PRD-568-mobile-builds-ship-gpu-ready-astc.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"569":{"t":"GPU particle emitters out of view stop costing, and effects yield to the frame budget","p":"docs/PRDs/done/PRD-569-gpu-particle-emitters-cull-and-yield-to-the-frame-budget.md","r":"develop","s":"DONE","d":12,"n":12,"f":"done"},"570":{"t":"A character costs what the camera sees of it","p":"docs/PRDs/done/PRD-570-a-character-costs-what-the-camera-sees-of-it.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"571":{"t":"Exposure meters a histogram and ignores the sun","p":"docs/PRDs/BLOCKED/requires-physical-device/PRD-571-exposure-meters-a-histogram-and-ignores-the-sun.md","r":"develop","s":"BLOCKED","d":8,"n":8,"f":"blocked"},"572":{"t":"Shadow casters sort themselves into static and moving","p":"docs/PRDs/done/PRD-572-shadow-casters-sort-themselves-into-static-and-moving.md","r":"develop","s":"DONE","d":9,"n":9,"f":"done"},"600":{"t":"A clustered mesh submits only the clusters the view holds","p":"docs/PRDs/Unreal/rendering/PRD-600-a-clustered-mesh-submits-only-the-clusters-the-view-holds.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"601":{"t":"Far clusters keep the shading of the source mesh","p":"docs/PRDs/Unreal/rendering/PRD-601-far-clusters-keep-the-shading-of-the-source-mesh.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"602":{"t":"A clustered shadow caster draws what its shadow texel resolves","p":"docs/PRDs/Unreal/rendering/PRD-602-a-clustered-shadow-caster-draws-what-its-shadow-texel-resolves.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"603":{"t":"Probes refresh by tracing the packed scene, not by drawing it six times","p":"docs/PRDs/Unreal/rendering/PRD-603-probes-refresh-by-tracing-the-packed-scene-not-by-drawing-it-six-times.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"604":{"t":"A surface is lit only by probes that can see it","p":"docs/PRDs/Unreal/rendering/PRD-604-a-surface-is-lit-only-by-probes-that-can-see-it.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"605":{"t":"The probe field follows the camera in nested rings","p":"docs/PRDs/Unreal/rendering/PRD-605-the-probe-field-follows-the-camera-in-nested-rings.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"606":{"t":"A fast-travel destination is ready before the cover lifts","p":"docs/PRDs/Unreal/open-world/PRD-606-a-fast-travel-destination-is-ready-before-the-cover-lifts.md","r":"develop","s":"NOT STARTED","d":0,"n":13,"f":"open"},"607":{"t":"Streaming reports when a hole is near and spends more to close it","p":"docs/PRDs/Unreal/open-world/PRD-607-streaming-reports-when-a-hole-is-near-and-spends-more-to-close-it.md","r":"develop","s":"NOT STARTED","d":0,"n":11,"f":"open"},"609":{"t":"A GPU burst starts where and when it is asked, and live particles stay put","p":"docs/PRDs/done/PRD-609-a-gpu-burst-starts-where-and-when-it-is-asked-and-live-particles-stay-put.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"610":{"t":"The native engine wins on real games","p":"docs/PRDs/performance/PRD-610-native-engine-wins-on-real-games.md","r":"develop","s":"PARTIAL","d":3,"n":10,"f":"open"},"611":{"t":"The Linux HUD overlay keeps up with the game and stops burning a core","p":"docs/PRDs/performance/PRD-611-hud-overlay-latency-and-cpu.md","r":"develop","s":"PARTIAL","d":7,"n":9,"f":"open"},"631":{"t":"An Unreal reference scene measures the default look","p":"docs/PRDs/done/PRD-631-an-unreal-reference-scene-measures-the-default-look.md","r":"develop","s":"IN PROGRESS","d":15,"n":15,"f":"done"},"632":{"t":"The frame is exposed and tone-mapped the way a fresh Unreal project is","p":"docs/PRDs/Unreal/look/PRD-632-the-frame-is-exposed-and-tone-mapped-the-way-a-fresh-unreal-project-is.md","r":"develop","s":"IN PROGRESS","d":17,"n":25,"f":"open"},"633":{"t":"The sky is an atmosphere, and it lights the scene","p":"docs/PRDs/Unreal/look/PRD-633-the-sky-is-an-atmosphere-and-it-lights-the-scene.md","r":"develop","s":"PARTIAL","d":1,"n":11,"f":"open"},"634":{"t":"Sun shadows reach the horizon and soften with distance from the caster","p":"docs/PRDs/Unreal/look/PRD-634-sun-shadows-reach-the-horizon-and-soften-with-distance-from-the-caster.md","r":"develop","s":"PARTIAL","d":11,"n":18,"f":"open"},"635":{"t":"Ambient occlusion darkens only the light that comes from around","p":"docs/PRDs/Unreal/look/PRD-635-ambient-occlusion-darkens-only-the-light-that-comes-from-around.md","r":"develop","s":"IN PROGRESS","d":8,"n":17,"f":"open"},"636":{"t":"The finishing post chain matches Unreal's defaults","p":"docs/PRDs/Unreal/look/PRD-636-the-finishing-post-chain-matches-unreal-defaults.md","r":"develop","s":"PARTIAL","d":18,"n":29,"f":"open"},"639":{"t":"A Surface Cache gives traced rays the texture, glow and lights of what they hit","p":"docs/PRDs/Unreal/gi/PRD-639-a-surface-cache-gives-traced-rays-the-texture-glow-and-lights-of-what-they-hit.md","r":"develop","s":"IN PROGRESS","d":6,"n":9,"f":"open"},"640":{"t":"Pixels gather bounce light from screen probes, and the radiance cache answers the rest","p":"docs/PRDs/Unreal/gi/PRD-640-pixels-gather-bounce-light-from-screen-probes-and-the-radiance-cache-answers-the-rest.md","r":"develop","s":"PARTIAL","d":2,"n":9,"f":"open"},"641":{"t":"Bounce light holds still while the camera moves and follows a light that changes","p":"docs/PRDs/Unreal/gi/PRD-641-bounce-light-holds-still-while-the-camera-moves-and-follows-a-light-that-changes.md","r":"develop","s":"IN PROGRESS","d":5,"n":14,"f":"open"},"642":{"t":"Glossy surfaces reflect what the camera cannot see","p":"docs/PRDs/Unreal/gi/PRD-642-glossy-surfaces-reflect-what-the-camera-cannot-see.md","r":"develop","s":"PARTIAL","d":6,"n":11,"f":"open"},"643":{"t":"Glass, fog and leaves take the bounce light too","p":"docs/PRDs/Unreal/gi/PRD-643-glass-fog-and-leaves-take-the-bounce-light-too.md","r":"develop","s":"IN PROGRESS","d":6,"n":11,"f":"open"},"644":{"t":"The GI tier follows Unreal's quality levels and the measured GPU time","p":"docs/PRDs/Unreal/gi/PRD-644-the-gi-tier-follows-unreal-quality-levels-and-the-measured-gpu-time.md","r":"develop","s":"IN PROGRESS","d":9,"n":12,"f":"open"},"645":{"t":"A desktop GPU with ray tracing traces GI rays in hardware","p":"docs/PRDs/done/PRD-645-a-desktop-gpu-with-ray-tracing-traces-gi-rays-in-hardware.md","r":"develop","s":"DONE","d":4,"n":4,"f":"done"},"646":{"t":"ThreeNative recreates each Unreal calibration scene exactly","p":"docs/PRDs/Unreal/gi/PRD-646-threenative-recreates-each-unreal-calibration-scene-exactly.md","r":"develop","s":"PARTIAL","d":11,"n":15,"f":"open"},"647":{"t":"A visual judge calibrates GI against the Unreal scenes until parity","p":"docs/PRDs/done/PRD-647-a-visual-judge-calibrates-gi-against-the-unreal-scenes-until-parity.md","r":"develop","s":"DONE","d":8,"n":8,"f":"done"},"649":{"t":"Shadow calibration scenes are built in Unreal and judged until parity","p":"docs/PRDs/Unreal/shadows/PRD-649-shadow-calibration-scenes-are-built-in-unreal-and-judged-until-parity.md","r":"develop","s":"PARTIAL","d":3,"n":12,"f":"open"},"650":{"t":"A lamp's shadow is as sharp as the pixels it covers, and softens with the lamp's size","p":"docs/PRDs/done/PRD-650-a-lamp-shadow-is-as-sharp-as-the-pixels-it-covers-and-softens-with-the-lamp-size.md","r":"develop","s":"DONE","d":12,"n":12,"f":"done"},"651":{"t":"Leaves cast leaf shadows that sway near the camera","p":"docs/PRDs/Unreal/shadows/PRD-651-leaves-cast-leaf-shadows-that-sway-near-the-camera.md","r":"develop","s":"IN PROGRESS","d":7,"n":12,"f":"open"},"652":{"t":"The shadow tier follows Unreal's shadow quality levels","p":"docs/PRDs/Unreal/shadows/PRD-652-the-shadow-tier-follows-unreal-shadow-quality-levels.md","r":"develop","s":"IN PROGRESS","d":7,"n":11,"f":"open"},"660":{"t":"Many shadowed lamps cost one fixed budget, not one shadow map each","p":"docs/PRDs/Unreal/rendering/PRD-660-many-shadowed-lamps-cost-one-fixed-budget-not-one-shadow-map-each.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"661":{"t":"Bounce light stays clean at fewer rays by reusing the rays of neighbours and of the last frame","p":"docs/PRDs/Unreal/rendering/PRD-661-bounce-light-stays-clean-at-fewer-rays-by-reusing-neighbour-and-last-frame-rays.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"662":{"t":"With dynamic GI off, the sky light stops at nearby walls and roofs","p":"docs/PRDs/Unreal/rendering/PRD-662-with-dynamic-gi-off-the-sky-light-stops-at-nearby-walls-and-roofs.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"663":{"t":"A fog bank sits where the game puts it, and the sun lights it","p":"docs/PRDs/Unreal/rendering/PRD-663-a-fog-bank-sits-where-the-game-puts-it.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"664":{"t":"Smoke and explosions are lit volumes, not flat sprites","p":"docs/PRDs/Unreal/vfx/PRD-664-smoke-and-explosions-are-lit-volumes-not-flat-sprites.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"665":{"t":"Clouds shade the haze and fog below them, so light shafts show through the gaps","p":"docs/PRDs/Unreal/rendering/PRD-665-clouds-shade-the-haze-and-fog-below-them.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"667":{"t":"A bright sun can flare the lens, and bloom can take any kernel, when the game asks","p":"docs/PRDs/Unreal/rendering/PRD-667-a-bright-sun-can-flare-the-lens-when-the-game-asks.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"668":{"t":"A displaced surface changes its silhouette near the camera","p":"docs/PRDs/Unreal/rendering/PRD-668-a-displaced-surface-changes-its-silhouette-near-the-camera.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"669":{"t":"Far foliage keeps its volume when its clusters become voxels","p":"docs/PRDs/Unreal/rendering/PRD-669-far-foliage-keeps-its-volume-when-its-clusters-become-voxels.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"670":{"t":"Rough surfaces reflect light the way a fresh Unreal project shades them","p":"docs/PRDs/Unreal/rendering/PRD-670-rough-surfaces-reflect-light-the-way-a-fresh-unreal-project-shades-them.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"671":{"t":"A character wears strand hair that catches light along each strand","p":"docs/PRDs/Unreal/rendering/PRD-671-a-character-wears-strand-hair-that-catches-light-along-each-strand.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"672":{"t":"Terrain shades its layers once into cached pages, not every pixel every frame","p":"docs/PRDs/Unreal/performance/PRD-672-terrain-shades-its-layers-once-into-cached-pages.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"673":{"t":"The GPU scene tests a placement only when its grid cell is in view","p":"docs/PRDs/Unreal/performance/PRD-673-the-gpu-scene-tests-a-placement-only-when-its-grid-cell-is-in-view.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"674":{"t":"An ambient particle is a function of its age and seed, and costs no simulation","p":"docs/PRDs/Unreal/vfx/PRD-674-an-ambient-particle-is-a-function-of-its-age-and-seed-and-costs-no-simulation.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"675":{"t":"A game turns a named layer of its world on and off inside one streaming budget","p":"docs/PRDs/Unreal/open-world/PRD-675-a-game-turns-a-named-layer-of-its-world-on-and-off-inside-one-streaming-budget.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"676":{"t":"A playtest capture tells an agent whether a frame pays for pixels, clusters, shadow levels or effects","p":"docs/PRDs/Unreal/performance/PRD-676-a-playtest-capture-tells-an-agent-whether-a-frame-pays-for-pixels-clusters-shadow-levels-or-effects.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"677":{"t":"A dense glass or water mesh takes the cluster cut and blends like the source mesh","p":"docs/PRDs/Unreal/rendering/PRD-677-a-dense-glass-or-water-mesh-takes-the-cluster-cut-and-blends-like-the-source-mesh.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"678":{"t":"A dense skinned body takes the cluster cut in every view that sees it","p":"docs/PRDs/Unreal/rendering/PRD-678-a-dense-skinned-body-takes-the-cluster-cut-in-every-view-that-sees-it.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"679":{"t":"Sparks and glass in fog take the fog of their own distance and blend mode","p":"docs/PRDs/Unreal/rendering/PRD-679-sparks-and-glass-in-fog-take-the-fog-of-their-own-distance-and-blend-mode.md","r":"develop","s":"NOT STARTED","d":0,"n":11,"f":"open"},"682":{"t":"Flat parts of the frame shade at a coarser rate where the GPU allows it, and every other platform says why not","p":"docs/PRDs/Unreal/performance/PRD-682-flat-parts-of-the-frame-shade-at-a-coarser-rate-where-the-gpu-allows-it.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"683":{"t":"A frame skips the passes nothing reads and shares one target between passes that never overlap","p":"docs/PRDs/Unreal/performance/PRD-683-a-frame-skips-passes-nothing-reads-and-shares-targets-whose-lifetimes-do-not-overlap.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"684":{"t":"Screen probes fill the gaps at depth edges and aim their rays where the light comes from","p":"docs/PRDs/Unreal/rendering/PRD-684-screen-probes-fill-the-gaps-at-depth-edges-and-aim-their-rays-where-the-light-comes-from.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"685":{"t":"A ray the screen cannot answer reads the light that arrives from its own direction","p":"docs/PRDs/Unreal/rendering/PRD-685-a-ray-the-screen-cannot-answer-reads-the-light-that-arrives-from-its-own-direction.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"686":{"t":"A door that moves blocks and bounces GI rays where it now stands","p":"docs/PRDs/Unreal/rendering/PRD-686-a-door-that-moves-blocks-and-bounces-gi-rays-where-it-now-stands.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"687":{"t":"An agent sees what the GI rays see in a playtest capture","p":"docs/PRDs/Unreal/rendering/PRD-687-an-agent-sees-what-the-gi-rays-see-in-a-playtest-capture.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"694":{"t":"Traces and colliders see a clustered mesh at one fixed error, not at the camera cut","p":"docs/PRDs/Unreal/rendering/PRD-694-traces-and-colliders-see-a-clustered-mesh-at-one-fixed-error-not-at-the-camera-cut.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"695":{"t":"A clustered mesh takes its own cut in every view that draws it","p":"docs/PRDs/Unreal/rendering/PRD-695-a-clustered-mesh-takes-its-own-cut-in-every-view-that-draws-it.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"696":{"t":"Tiny triangles of a clustered shadow caster rasterize in compute","p":"docs/PRDs/Unreal/performance/PRD-696-tiny-triangles-of-a-clustered-shadow-caster-rasterize-in-compute.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"704":{"t":"A spark streaks along its own motion, and a far spark does not flicker","p":"docs/PRDs/Unreal/vfx/PRD-704-a-spark-streaks-along-its-own-motion-and-a-far-spark-does-not-flicker.md","r":"develop","s":"NOT STARTED","d":0,"n":10,"f":"open"},"705":{"t":"A sword swing leaves one connected trail, not a cloud of dashes","p":"docs/PRDs/Unreal/vfx/PRD-705-a-sword-swing-leaves-one-connected-trail-not-a-cloud-of-dashes.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"706":{"t":"An effect starts on the skin of a moving character","p":"docs/PRDs/Unreal/vfx/PRD-706-an-effect-starts-on-the-skin-of-a-moving-character.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"714":{"t":"The sun shadow keeps its texel size per screen pixel when the camera zooms or the window resizes","p":"docs/PRDs/Unreal/rendering/PRD-714-the-sun-shadow-keeps-one-texel-per-screen-pixel-when-the-camera-zooms-or-the-window-resizes.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"715":{"t":"A shadow lookup stops sampling where no edge is near and starts clear of its own surface","p":"docs/PRDs/Unreal/rendering/PRD-715-a-shadow-lookup-stops-sampling-where-no-edge-is-near-and-starts-clear-of-its-own-surface.md","r":"develop","s":"NOT STARTED","d":0,"n":9,"f":"open"},"716":{"t":"A shadow caster that no visible surface receives, or another caster hides, skips its draw","p":"docs/PRDs/Unreal/rendering/PRD-716-a-shadow-caster-that-no-visible-surface-receives-or-another-caster-hides-skips-its-draw.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"717":{"t":"A pixel a moving object just uncovered reprojects through the surface that left it","p":"docs/PRDs/Unreal/performance/PRD-717-a-pixel-a-moving-object-just-uncovered-reprojects-through-the-surface-that-left-it.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"718":{"t":"A surface that returns to view takes back the detail the history held before it left","p":"docs/PRDs/Unreal/performance/PRD-718-a-surface-that-returns-to-view-takes-back-the-detail-the-history-held-before-it-left.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"719":{"t":"History rejection judges a color change by whether the display can show it","p":"docs/PRDs/Unreal/performance/PRD-719-history-rejection-judges-a-color-change-by-whether-the-display-can-show-it.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"720":{"t":"The temporal resolve reads a wider input where history fails, and the tier sets how wide","p":"docs/PRDs/Unreal/performance/PRD-720-the-temporal-resolve-reads-a-wider-input-where-history-fails-and-the-tier-sets-how-wide.md","r":"develop","s":"NOT STARTED","d":0,"n":7,"f":"open"},"741":{"t":"The owned engine shades fewer pixels and its temporal resolve writes the display frame","p":"docs/PRDs/done/PRD-741-the-owned-engine-shades-fewer-pixels-and-its-temporal-resolve-writes-the-display-frame.md","r":"develop","s":"DONE","d":10,"n":10,"f":"done"},"773":{"t":"Virtual shadow pages draw only the instances that touch them","p":"docs/PRDs/performance/stress-lab-2026-10-30/PRD-773-vsm-per-page-instance-culling.md","r":"develop","s":"PARTIAL","d":4,"n":6,"f":"open"},"775":{"t":"Virtual shadow pages are managed and drawn on the GPU, as in Unreal","p":"docs/PRDs/Unreal/shadows/PRD-775-virtual-shadow-pages-are-managed-and-drawn-on-the-gpu.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"VQ-03":{"t":"Off-screen specular reflections come from a complete local probe","p":"docs/PRDs/rendering/PRD-VQ-03-local-reflection-probes.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-08":{"t":"Clouds and their ground attenuation share one authored field","p":"docs/PRDs/Unreal/rendering/PRD-VQ-08-clouds-and-overhead-transmittance.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-09":{"t":"A door that opens and a lamp that switches change the bounce light","p":"docs/PRDs/rendering/PRD-VQ-09-incremental-diffuse-relighting.md","r":"develop","s":"NOT STARTED","d":0,"n":8,"f":"open"},"VQ-10":{"t":"Texture residency releases real GPU memory rather than only biasing sampling","p":"docs/PRDs/Unreal/performance/PRD-VQ-10-texture-mip-residency.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"VQ-12":{"t":"Overlapping transparent effects have an explicit correct or bounded-approximate path","p":"docs/PRDs/rendering/PRD-VQ-12-transparent-effects-composition.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"},"VQ-13":{"t":"Depth of field and motion blur are qualified optional stages, not a second camera pipeline","p":"docs/PRDs/rendering/PRD-VQ-13-cinematic-stage-integration.md","r":"develop","s":"PROPOSED","d":0,"n":9,"f":"open"},"VQ-15":{"t":"Hair cards preserve coverage, tangent highlights and shadow behavior across quality tiers","p":"docs/PRDs/rendering/PRD-VQ-15-hair-card-material-qualification.md","r":"develop","s":"PROPOSED","d":0,"n":8,"f":"open"}}; // @prds-end const fmt = n => n.toLocaleString("en-US"); const esc = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); @@ -319,12 +319,12 @@

Unreal 5.8.3 render features, sized and rated

["Shadow casters at shadow-texel detail", null, "Draws shadow pages from clusters (inside cull + raster).", "open", "PRD-602.", ["602"]], ]}, vsm: { name: "Virtual shadow maps", what: "One huge sparse shadow map per light, filled only where pixels need it.", subs: [ - ["Page marking and physical page pool", 11240, "Marks which shadow pages pixels need and allocates them from a pool.", "shipped", "VirtualShadowNode clip levels ship. Page cost: PRD-457.", ["457"]], + ["Page marking and physical page pool", 11240, "Marks which shadow pages pixels need and allocates them from a pool.", "shipped", "VirtualShadowNode clip levels ship. Page cost: PRD-457. The page table and pool move to the GPU (VirtualShadowMapPhysicalPageManagement.usf:236, :475): PRD-775.", ["457", "775"]], ["Projection + SMRT soft shadows", 4389, "Samples pages with shadow-map ray tracing for a source-angle penumbra; spot and point projection.", "open", "PRD-634 sun penumbra, PRD-650 lamp shadows.", ["634", "650"]], - ["Cache and invalidation", 3764, "Keeps static pages across frames; redraws only what moved.", "shipped", "PRD-538, PRD-572 done.", ["538", "572"]], + ["Cache and invalidation", 3764, "Keeps static pages across frames; redraws only what moved.", "shipped", "PRD-538, PRD-572 done. Caster changes pushed by the scene, not scanned (VirtualShadowMapCacheManager.cpp:2178): PRD-775.", ["538", "572", "775"]], ["Debug and stats", 1015, "Page usage and cache views.", "open", "PRD-676 shadow-level view.", ["676"]], ["Directional clipmaps", 831, "Nested levels around the camera for the sun.", "shipped", "Shipped; reach to the horizon in PRD-634.", ["634"]], - ["Per-page draw commands", 714, "Builds draw lists per dirty page, culling every instance against the pages it can touch (VirtualShadowMapBuildPerPageDrawCommands.usf:155, :309).", "partial", "Partial redraw ships. Per-page instance culling: PRD-773, native green, browser proof open.", ["773"]], + ["Per-page draw commands", 714, "Builds draw lists per dirty page, culling every instance against the pages it can touch (VirtualShadowMapBuildPerPageDrawCommands.usf:155, :309).", "partial", "Partial redraw ships. Per-page instance culling: PRD-773, native green, browser proof open. One batched GPU-culled pass instead of one pass per page: PRD-775.", ["773", "775"]], ["Leaf and alpha-masked casters", null, "Masked materials in the shadow pass.", "partial", "PRD-651 (5/12): an engine bug on the default engine.", ["651"]], ["Calibration and tiers", null, "Shadow quality levels Low / Medium / Epic.", "open", "PRD-649 calibration scenes, PRD-652 tiers.", ["649", "652"]], ]}, @@ -649,7 +649,7 @@

Unreal 5.8.3 render features, sized and rated

["Ambient occlusion","Lighting & GI",4501,2363,4,"Low","partial","GTAO ships in templates. PRD-635 applies it to indirect light only (#541).",0,"ao"], ["Substrate materials","Lighting & GI",12154,16982,8,"High","open","On in a fresh 5.8.3 project. three r185 matches its specular energy, not its rough diffuse: PRD-670.",0,null,["670"]], ["MegaLights","Lighting & GI",10136,9857,8,"High","open","PRD-660, gate first. Off in a fresh Unreal project.",0,"megalights"], - ["Virtual shadow maps","Shadows",11261,10692,9,"High","partial","Sun clipmap, page cache, mover maps shipped (PRD-538, 572). PRD-634, 649–652, 457 open. PRD-773 per-page instance culling in flight.",0,"vsm"], + ["Virtual shadow maps","Shadows",11261,10692,9,"High","partial","Sun clipmap, page cache, mover maps shipped (PRD-538, 572). PRD-634, 649–652, 457 open. PRD-773 per-page instance culling in flight. PRD-775 moves page management and page draws to the GPU.",0,"vsm"], ["Classic shadows (CSM, contact)","Shadows",18513,2411,6,"Medium","shipped","CSM replaced by the shipped virtual shadow map. Contact shadows deferred (PRD-561): revisit when a light needs a contact-shadow length.",0,null,["561"]], ["Sky atmosphere","Sky & volumes",2666,2318,6,"Medium","open","PRD-633, in PR #541.",0,"sky"], ["Volumetric clouds","Sky & volumes",3449,2695,7,"Medium","partial","Template clouds in rain. PRD-VQ-08 open.",0,"clouds"], diff --git a/docs/PRDs/Unreal/shadows/PRD-775-virtual-shadow-pages-are-managed-and-drawn-on-the-gpu.md b/docs/PRDs/Unreal/shadows/PRD-775-virtual-shadow-pages-are-managed-and-drawn-on-the-gpu.md new file mode 100644 index 0000000000..9dc5079727 --- /dev/null +++ b/docs/PRDs/Unreal/shadows/PRD-775-virtual-shadow-pages-are-managed-and-drawn-on-the-gpu.md @@ -0,0 +1,206 @@ +# PRD-775 — Virtual shadow pages are managed and drawn on the GPU, as in Unreal + +**Status:** PROPOSED +**Priority:** P1 — no box is ticked: the stress lab `?stress` CPU frame (p50 12.82, p95 17.64 ms) is behind Unreal 5.8.3 (p50 9.45, p95 11.03 ms) on the same scene, and the gap is the virtual shadow map's CPU work. +**Complexity:** 6 (MEDIUM) — 6-10 files (+2), a new GPU page module (+2), CPU/GPU page state in step (+2); risk override: HIGH, because every virtual-shadow game (the shooter kit ships VSM by default) takes the new path. +**Owner:** João · 2026-10-30 +**Depends on:** [PRD-773](../../performance/stress-lab-2026-10-30/PRD-773-vsm-per-page-instance-culling.md) Phase 1 (FG1 per-instance page culling, merged in `1882d6041`); PRD-770 fair window. Phase B starts from the stress-120 verdicts on FV1, FV3, FGL3 and FC1s-b. + +## Context + +**The gap.** On the stress lab (`sandbox/stress-lab`, `main.html?stress`, 1280x720, RTX 2080, uncapped): + +| arm | CPU frame p50 | CPU frame p95 | +| --- | --- | --- | +| Unreal 5.8.3, matched scene (provisional) | 9.45 | 11.03 | +| ThreeNative stack, virtual shadow map (VSM) | 12.82 | 17.64 | +| ThreeNative, plain sun map (`?stress&plainShadow`) | 9.09 | 10.7 | + +The plain-map arm is under Unreal on both numbers. So the AC is reachable when the VSM's CPU work falls to about the +plain map's cost. That delta (p50 +3.7, p95 +6.9 ms) is this PRD's ceiling. + +**Where the VSM CPU time goes** (native ms per frame, `hunt/HUNT-cpuvsm.md`; the Wasm stage costs about 4.3x native; +`stagesAtP95` names the p95 tail: shadow encode +1.4, VSM pages +1.4, shadow lists +1.0 ms): + +| work | steady frame | page-granting frame (12-15 % of frames) | file | +| --- | --- | --- | --- | +| caster bounds loop, every caster, every frame | 0.173 | 0.166 | `renderer.cpp:3010-3027` | +| caster state records + string signatures | 0.051 | 0.048 | `renderer.cpp:3036-3054` | +| `PageAtlas::updateCasters`: ordered set + per-caster compare | 0.184 | 0.182 | `atlas.cpp:309` | +| `PageAtlas::update`: protected ids, 256 page builds, LRU pool, `writeTable` | 0.064 | 0.240 | `atlas.cpp:133`, `pages.cpp:143`, `atlas.cpp:394` | +| per-level page index (FS9) + per-page caster selection | 0.091 | 0.277 | `renderer.cpp:3860-3950` | +| per-page rows (`CpuShadowBuild`) | 0.317 | 0.686 | `renderer.cpp:3952-4334` | +| encode: one render pass per page (256 on a grant), a page clear each | small | ≈0.9 ms web | `renderer.cpp:5530-5600` | + +**Our design today** (`packages/runtime-native/src/engine/renderer/shadows/virtual/`): the CPU owns everything. +`PageAtlas` keeps the page table, a CPU `PhysicalPagePool` with LRU eviction, the per-level clip windows and the +caster motion state; it maps every page of a level's window (`tiles²`, 256 at 2048 texels) and grants one level a +frame. The renderer builds an `AtlasPage` (two matrices) per granted page, selects each page's casters, plans rows +per page, and encodes one render pass per page with a viewport, a scissor and a clear draw. The page table is +re-uploaded each frame (`renderer.cpp:3242-3250`). Movers draw into one full mover map per level each frame. + +**What Unreal 5.8.3 does** (read 2026-10-30 at `/home/joao/projects/UnrealEngine`; ideas, defaults and guards only): + +- **The page table and physical pool live on the GPU.** `BuildPageAllocations` + (`UE 5.8.3: Engine/Source/Runtime/Renderer/Private/VirtualShadowMaps/VirtualShadowMapArray.cpp:3227`) runs + `UpdatePhysicalPages` (`:3306`), `PackAvailablePages` (`:3336`), `AllocateNewPageMappings` (`:3362`), + `SelectPagesToInitialize` (`:3438`) and an indirect page clear (`:3457`) as compute passes. The pool keeps four + ordered lists, LRU, AVAILABLE, EMPTY and REQUESTED (`UE 5.8.3: Engine/Shaders/Private/VirtualShadowMaps/VirtualShadowMapPhysicalPageManagement.usf:34-41`). + `UpdatePhysicalPages` (`:236`) walks last frame's REQUESTED list as the LRU order (`:252-260`), keeps a page that + is requested again or younger than `r.Shadow.Virtual.Cache.MaxPageAgeSinceLastRequest` (1000 frames, + `VirtualShadowMapCacheManager.cpp:127`), turns invalidation flags into static or dynamic "uncached" bits + (`:328-342`), and moves a dead page to EMPTY (`:460-466`). `PackAvailablePages` (`:686`) compacts LRU into AVAILABLE + with one group prefix sum; `AllocateNewPageMappings` (`:475`) pops AVAILABLE (`:487`) and first clears the page + table entry of the page it evicts (`:506`); `AppendPhysicalPageLists` (`:732`) keeps the order and never loses a + page on overflow (`:738-740`). `SelectPagesToInitializeCS` (`:852`) emits only uncached pages to clear. The CPU only + sizes the pool: `r.Shadow.Virtual.MaxPhysicalPages` 2048 (`VirtualShadowMapArray.cpp:173-175`). +- **Non-Nanite casters draw into all their pages in one batched pass, not one pass per page.** + `r.Shadow.Virtual.NonNanite.Batch` 1 and `.SinglePassBatched` 1 (`VirtualShadowMapArray.cpp:490-492`, `:637-639`). + `CullPerPageDrawCommandsCs` (`UE 5.8.3: Engine/Shaders/Private/VirtualShadowMaps/VirtualShadowMapBuildPerPageDrawCommands.usf:155`) + takes each instance's own bounds to a page rect (`:307`) and keeps it only where it overlaps a page to render + (`:309`); `AllocateCommandInstanceOutputSpaceCs` (`:454`) and `OutputCommandInstanceListsCs` (`:485`) write the + indirect args and a per-instance page record. The vertex shader reads that record + (`UE 5.8.3: Engine/Shaders/Private/ShadowDepthVertexShader.usf:210`), scales and biases clip space into the physical + page and sets four clip planes to the page rect (`:42-63`). The output buffer is sized conservatively on the CPU + (`VirtualShadowMapArray.cpp:3964-3970`, `r.Shadow.Virtual.NonNanite.CulledInstanceAllocationFactor` 1.0, `:444`). +- **Invalidation is pushed by the scene, not scanned.** The cache manager reads only the primitives the scene + update changed (`VirtualShadowMapCacheManager.cpp:2178`, `ForEachUpdateCommand(AddedUpdated, AllCulling)`), marks + them dynamic with a frame stamp (`:1767-1785`), and returns a dynamic primitive to the static cache after + `r.Shadow.Virtual.Cache.FramesStaticThreshold` (100, `:142-145`) frames, iterating only the dynamic set + (`:1803-1810`). The page marking itself runs on the GPU + (`VirtualShadowMapCacheGPUInvalidation.usf:66`, `:151`). + +## Solution + +Port the three Unreal ideas in the order of their measured CPU ceiling. Each phase stands alone, keeps pixels, and +keeps the CPU `PageAtlas` as the test oracle where the GPU replaces it. + +```mermaid +flowchart LR + subgraph CPU["CPU per frame (after Phase 3)"] + W[per-level window, matrices, grant decision] --> U[upload: level data, changed caster boxes, invalidated ranges] + U --> E[encode: compute dispatches + one shadow pass per light] + end + subgraph GPU + P[page manager: keep / free / allocate, page table, clear list] --> C[per-page instance cull -> indirect args + page records] + C --> R[one depth pass: clear quads, then one indirect draw per caster row] + R --> S[receivers sample the page table] + end + E --> P +``` + +1. **Phase 1 — one shadow pass, GPU-built page draws** (ceiling: rows 0.686 + index 0.277 native and ≈0.9-1.4 ms of + web encode on a granting frame; the p95 lever). On a granting frame the CPU uploads the list of pages to draw + (≤256 records: slot, level, page projection) and keeps a coarse cull: one `pageRange` per caster row against the + granted level's dirty rect. A compute kernel (shader IR `ComputePass`, `engine/renderer/compute.h`) runs FG1's + per-instance rule on the GPU: each (row, instance) box becomes a page rect, every drawn page in it gets a + (instance, page) record, and the row's indirect `instanceCount` counts them. The output region of a row is + `instances x reach²` records, with `reach` from the row's largest instance box, so no output can overflow (Unreal + sizes the same buffer conservatively). The shadow vertex shader takes the page record, applies the page + projection, and scales and biases clip space into the slot; clipping to the slot uses the `clip-distances` device + feature where present and a fragment discard against the slot rect elsewhere. One render pass per light replaces + the per-page passes: an instanced clear draw over the drawn slots, then one `drawIndexedIndirect` per row. No + `indirect-first-instance` is needed: the row's record offset rides in its per-draw uniform. Mover maps keep + today's path. +2. **Phase 2 — caster state is pushed, not scanned** (ceiling: 0.408 native ≈ 1.75 ms web on every frame; the p50 + lever). The render database already knows which draws changed (TR1's stamp, FM1's refresh guard). The VSM stage + takes that change set: a changed caster updates its bounds, motion state and invalidation; an unchanged one costs + nothing. Motion state is a flat array indexed by the frozen draw list (FS11's record) with a frame stamp, and the + static return walks only the moving set, as `UpdateCachePrimitiveAsDynamic` does. Invalidation stays on the CPU + by page range (FV3): for our changed-caster counts a range write costs less than a dispatch. +3. **Phase 3 — the page table and pool on the GPU** (ceiling: 0.06 native steady, 0.24 on a grant, plus the table + upload). The CPU keeps only per-level state: window origin, matrices, depth fit and the grant decision. A GPU page + manager keeps Unreal's four lists and runs keep/free, pack, allocate and select-to-clear. The request is still the + whole window of a granted level; [PRD-457](../rendering/PRD-457-virtual-shadows-scale-by-measurement.md) owns sparse + pages from receiver demand and gets the GPU request flags from this phase. The page records of Phase 1 come from + the allocation pass, so the 256 `AtlasPage` builds and `writeTable` leave the CPU. + +**Risks.** (a) WebGPU has no per-instance viewport: the slot remap is extra float math, so a page-edge texel can round +differently; each phase states a pixel target and runs the Opus judge. (b) The fragment-discard fallback disables +early depth on that pass; Phase 1 measures GPU p95 with both arms. (c) The web Wasm budget is 4 MiB (`pnpm budgets`); +the WGSL and setup code must fit. Web shadow bundles stay compiled out (FM5 closed). (d) One pass hides the per-level +GPU cost that `recordCost` reads; the pass time goes to the granted level, split evenly when several levels grant. + +## Validation scene + +- **Consumer and scene:** the stress lab, `main.html?stress` (4000 trees, 6000 rocks, 120 rigs, 300 bodies, VSM sun), + with its 40 s orbit, against the Unreal 5.8.3 matched scene (provisional numbers above). +- **Window:** the PRD-770 fair window, RTX 2080, 1280x720, uncapped and vsync passes, ABBA pairs; lane checks + through `measure.sh`. +- **Benefit workload:** `?stress` with the camera moving (granting frames for Phase 1, every frame for Phase 2). +- **Controls that charge the bookkeeping:** `?stress&still` (no grant, no motion: the new path must not cost CPU), + `?stress&plainShadow` (the floor this PRD aims at), and the shooter-kit holdout (many static non-instanced casters, + the case where one indirect row per caster could cost more encode than today's per-page rows). +- **Pass:** each phase's keep rule below; the PRD's ACs on the whole stack. + +## Acceptance criteria + +- [ ] AC-1 [local]: In a PRD-770 fair window, `?stress` CPU frame p95 is below 11.03 ms and p50 below 9.45 ms, GPU p95 is not worse than develop. proof: `TN_STRESS_LEDGER=/home/joao/projects/threenative/threenative-engine/docs/stress-loop/ledger.json node scripts/stress-loop.mjs fair ...` (ledger row) +- [ ] AC-2 [local]: A fresh Opus visual judge returns NEUTRAL or IMPROVEMENT for the whole stack against develop on `?stress&freezeAt=12` and `?stress&closeup`, 2+ runs per arm. proof: judge verdict on the PR (orphan `prd-775-screenshots` branch) + +## Blocked on + +- The keep rule for Phase 1: it cuts granting frames (12-15 % of frames), so it moves CPU p95, not p50. The brief's + rule asks for CPU p50 better by more than 3 %. Unblocked by the coordinator (stress-120): accept "CPU p95 better + by more than 3 % in both ABBA pairs, p50 not worse" for Phase 1, or keep the brief's rule. + +## Integration ledger + +| Capability | Reachable consumer/trigger | Replaces / disposition | Evidence | +| --- | --- | --- | --- | +| Static virtual pages draw from GPU rows | any `VirtualShadowNode` sun → `Renderer::renderPrepared` shadow stage (`renderer.cpp:3745`) | per-page selection, rows and passes (`renderer.cpp:3860-4334`, `:5530`) deleted for static pages | Phase 1 box 1 | +| Caster state from the change set | `RenderDatabase::render` → VSM stage (`renderer.cpp:2998`) | per-frame scan (`renderer.cpp:3010-3054`, `atlas.cpp:309`) deleted | Phase 2 box 1 | +| GPU page table and pool | VSM stage → GPU page manager; receivers read `s_vsmTable` | CPU `PhysicalPagePool` and `writeTable` leave the runtime path; kept as the test oracle | Phase 3 box 1 | + +## Decisions + +- 2026-10-30 (vsm-gpu): the phase order follows the measured CPU ceiling, not Unreal's pass order. Phase 1 needs no + GPU page table: it reads the CPU's granted page list. +- 2026-10-30 (vsm-gpu): GPU invalidation (`VSMUpdateViewInstanceStateCS`, `ProcessInvalidationQueueGPUCS`) is deferred. + With the change set of Phase 2 the CPU touches only changed casters, and a page-range write costs less than a + dispatch. Revisit when a measured frame invalidates more than about 1000 casters. +- 2026-10-30 (vsm-gpu): mover maps keep today's path. Unreal's dynamic page layer is a separate port; the stress lab's + movers cost a fixed three passes a frame. +- 2026-10-30 (vsm-gpu): closed levers stay closed. Pass count and fill count alone did not move GPU p95 (FP2), and + FG1b showed the per-level index was not FG1's CPU p95 cost. This PRD removes the CPU planning and encode of those + passes, not their GPU count. +- 2026-10-30 (vsm-gpu), folded plans: PRD-457 keeps sparse demand and the cost gate; PRD-524's parity cases + (`native-engine-vsm`, `-deformation`, `-cut`) are this PRD's pixel gates where they exist; PRD-652's tier + `maxPhysicalPages` sizes the GPU pool; PRD-773 Phase 3 (FG2 depth band) stays there; PRD-774 moves the plan off the + main thread and is independent of this work. + +## Execution phases + +Every phase: engine code only (`packages/`), red-green first, measured only through +`/home/joao/.cache/tmp-tn/stress-loop/measure.sh` and the fair window. **Keep rule** (brief): CPU p50 better by more +than 3 % in both ABBA pairs, CPU p95 and GPU p95 not worse, Opus judge NEUTRAL or IMPROVEMENT (2+ runs per arm), the +shooter-kit holdout shows a gain or no effect (`holdout-measure-shooter.sh`), `pnpm budgets` green. Then a ledger +row (decision keep), a `PERFORMANCE-LEARNINGS.md` entry and the box ticked with the proof. + +### Phase 1 — One shadow pass per light draws GPU-culled page instances + +**Status:** NOT STARTED +**Files:** new `packages/runtime-native/src/engine/renderer/shadows/virtual/gpu_pages.{h,cpp}` (cull kernel, buffers); +`renderer.cpp` (shadow stage, encode); the shadow vertex program generator under `engine/shader/` (page record path); +`tests/native-engine/render_database_test.cpp`. + +- [ ] A granted level draws in one render pass per light, from GPU rows. proof: new case `virtual_shadow_pages_draw_from_gpu_rows` in `tn-native-engine-render-database-test` — RED on develop (one pass per drawn page, 124+ passes); GREEN: one pass, the GPU's (page, instance) set equals FG1's CPU set (1,118 instances on 124 pages), receiver pixels max channel diff 0 on the case and on `ctest -R native_engine_render_vsm` +- [ ] Phase 1 passes the keep rule on the stress lab. proof: ledger rows (decision keep) from `measure.sh` and the fair window, judge verdict, shooter holdout + +### Phase 2 — The VSM stage reads the caster change set + +**Status:** NOT STARTED +**Files:** `renderer.cpp` (VSM stage), `render_database.{h,cpp}` (change set), `shadows/virtual/atlas.{h,cpp}` +(`updateCasters` over changed casters), `tests/native-engine/vsm/vsm_atlas_test.cpp`. + +- [ ] A frame where no caster changed visits no caster state, and the move, settle and remove invalidations equal today's. proof: new case `virtual_shadow_caster_changes_are_pushed` in `tn-native-engine-render-database-test` (RED: develop visits every caster; GREEN: 0 visits) plus every `tn-native-engine-vsm-atlas-test` case green +- [ ] Phase 2 passes the keep rule on the stress lab. proof: ledger rows (decision keep), judge verdict, shooter holdout + +### Phase 3 — The page table and physical pool live on the GPU + +**Status:** NOT STARTED +**Files:** `shadows/virtual/gpu_pages.{h,cpp}` (page manager passes), `shadows/virtual/atlas.{h,cpp}` (per-level state +only on the runtime path), `renderer.cpp` (table binding), `tests/native-engine/vsm/vsm_atlas_test.cpp`. + +- [ ] The GPU page manager maps the same virtual pages as the CPU `PageAtlas` over a recorded path with pans, a camera cut, a sun direction change and an invalidation, with no lost or duplicated physical page. proof: new differential case `gpu_page_table_matches_cpu_atlas` in `tn-native-engine-vsm-atlas-test`, receiver pixels max channel diff 0 on `ctest -R native_engine_render_vsm` +- [ ] Phase 3 passes the keep rule on the stress lab. proof: ledger rows (decision keep), judge verdict, shooter holdout