@woosh/meep-engine 3.11.3 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/editor/SelectionVisualizer.js +127 -127
- package/editor/entity_world_bounds.js +27 -27
- package/editor/particles/rebuildParticleEmitter.js +31 -31
- package/editor/process/SymbolicDisplayProcess.js +1 -1
- package/editor/process/symbolic/CameraSymbolicDisplay.js +146 -146
- package/editor/process/symbolic/LightSymbolicDisplay.js +172 -172
- package/editor/prototypeEditorShell.js +156 -156
- package/editor/selection/editor_pick.js +130 -130
- package/editor/selection/screen_rect_frustum.js +119 -119
- package/editor/tools/v2/TransformControls.js +971 -971
- package/package.json +1 -1
- package/src/engine/Engine.d.ts +3 -3
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +726 -726
- package/src/engine/EngineHarness.d.ts +1 -1
- package/src/engine/EngineHarness.js +590 -590
- package/src/engine/ecs/HIERARCHY_REVIEW.md +717 -717
- package/src/engine/ecs/terrain/ecs/BuildLightTexture.d.ts +1 -1
- package/src/engine/ecs/terrain/ecs/BuildLightTexture.js +27 -27
- package/src/engine/ecs/terrain/ecs/Terrain.d.ts +1 -5
- package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/Terrain.js +790 -790
- package/src/engine/ecs/terrain/util/loadVisibleTerrainTiles.js +129 -129
- package/src/engine/graphics/ecs/camera/camera_find_active.d.ts +1 -1
- package/src/engine/graphics/ecs/camera/camera_find_active.js +28 -28
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.d.ts +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +428 -428
- package/src/engine/graphics/ecs/water/Water.d.ts +1 -1
- package/src/engine/graphics/ecs/water/Water.js +104 -104
- package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts +1 -1
- package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +1 -1
- package/src/engine/graphics3/AnimationGraphSystem.d.ts +65 -0
- package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -0
- package/src/engine/graphics3/AnimationGraphSystem.js +329 -0
- package/src/engine/graphics3/AnimationGraphSystem3.d.ts +6 -61
- package/src/engine/graphics3/AnimationGraphSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationGraphSystem3.js +6 -325
- package/src/engine/graphics3/AnimationSystem.d.ts +76 -0
- package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -0
- package/src/engine/graphics3/AnimationSystem.js +352 -0
- package/src/engine/graphics3/AnimationSystem3.d.ts +6 -72
- package/src/engine/graphics3/AnimationSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationSystem3.js +6 -348
- package/src/engine/graphics3/CameraSystem.d.ts +49 -0
- package/src/engine/graphics3/CameraSystem.d.ts.map +1 -0
- package/src/engine/graphics3/CameraSystem.js +170 -0
- package/src/engine/graphics3/CameraSystem3.d.ts +6 -45
- package/src/engine/graphics3/CameraSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/CameraSystem3.js +6 -166
- package/src/engine/graphics3/DebugDrawSystem.d.ts +72 -0
- package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -0
- package/src/engine/graphics3/DebugDrawSystem.js +205 -0
- package/src/engine/graphics3/DebugDrawSystem3.d.ts +6 -68
- package/src/engine/graphics3/DebugDrawSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/DebugDrawSystem3.js +6 -201
- package/src/engine/graphics3/DecalSystem.d.ts +81 -0
- package/src/engine/graphics3/DecalSystem.d.ts.map +1 -0
- package/src/engine/graphics3/DecalSystem.js +441 -0
- package/src/engine/graphics3/DecalSystem3.d.ts +6 -77
- package/src/engine/graphics3/DecalSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/DecalSystem3.js +6 -437
- package/src/engine/graphics3/FogOfWarSystem.d.ts +74 -0
- package/src/engine/graphics3/FogOfWarSystem.d.ts.map +1 -0
- package/src/engine/graphics3/FogOfWarSystem.js +265 -0
- package/src/engine/graphics3/FogOfWarSystem3.d.ts +6 -70
- package/src/engine/graphics3/FogOfWarSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/FogOfWarSystem3.js +6 -261
- package/src/engine/graphics3/GraphicsEngine.d.ts +340 -0
- package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -0
- package/src/engine/graphics3/GraphicsEngine.js +790 -0
- package/src/engine/graphics3/GraphicsEngine3.d.ts +6 -336
- package/src/engine/graphics3/GraphicsEngine3.d.ts.map +1 -1
- package/src/engine/graphics3/GraphicsEngine3.js +6 -786
- package/src/engine/graphics3/HighlightSystem.d.ts +82 -0
- package/src/engine/graphics3/HighlightSystem.d.ts.map +1 -0
- package/src/engine/graphics3/HighlightSystem.js +241 -0
- package/src/engine/graphics3/HighlightSystem3.d.ts +6 -78
- package/src/engine/graphics3/HighlightSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/HighlightSystem3.js +6 -237
- package/src/engine/graphics3/LightSystem.d.ts +40 -0
- package/src/engine/graphics3/LightSystem.d.ts.map +1 -0
- package/src/engine/graphics3/LightSystem.js +211 -0
- package/src/engine/graphics3/LightSystem3.d.ts +6 -36
- package/src/engine/graphics3/LightSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/LightSystem3.js +6 -207
- package/src/engine/graphics3/MeshSystem.d.ts +106 -0
- package/src/engine/graphics3/MeshSystem.d.ts.map +1 -0
- package/src/engine/graphics3/MeshSystem.js +647 -0
- package/src/engine/graphics3/MeshSystem3.d.ts +6 -102
- package/src/engine/graphics3/MeshSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/MeshSystem3.js +6 -642
- package/src/engine/graphics3/ParticipatingMedia.d.ts +2 -2
- package/src/engine/graphics3/ParticipatingMedia.js +203 -203
- package/src/engine/graphics3/ParticipatingMediaSystem.d.ts +87 -0
- package/src/engine/graphics3/ParticipatingMediaSystem.d.ts.map +1 -0
- package/src/engine/graphics3/ParticipatingMediaSystem.js +184 -0
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +6 -83
- package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/ParticipatingMediaSystem3.js +6 -180
- package/src/engine/graphics3/ParticleEmitterSystem.d.ts +76 -0
- package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +1 -0
- package/src/engine/graphics3/ParticleEmitterSystem.js +289 -0
- package/src/engine/graphics3/ParticleEmitterSystem3.d.ts +6 -72
- package/src/engine/graphics3/ParticleEmitterSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/ParticleEmitterSystem3.js +6 -285
- package/src/engine/graphics3/PathDisplaySystem.d.ts +70 -0
- package/src/engine/graphics3/PathDisplaySystem.d.ts.map +1 -0
- package/src/engine/graphics3/PathDisplaySystem.js +309 -0
- package/src/engine/graphics3/PathDisplaySystem3.d.ts +6 -66
- package/src/engine/graphics3/PathDisplaySystem3.d.ts.map +1 -1
- package/src/engine/graphics3/PathDisplaySystem3.js +6 -305
- package/src/engine/graphics3/Pickable.d.ts +1 -1
- package/src/engine/graphics3/Pickable.js +65 -65
- package/src/engine/graphics3/PickingQuery.d.ts +1 -1
- package/src/engine/graphics3/PickingQuery.js +48 -48
- package/src/engine/graphics3/PickingSystem.d.ts +67 -0
- package/src/engine/graphics3/PickingSystem.d.ts.map +1 -0
- package/src/engine/graphics3/PickingSystem.js +273 -0
- package/src/engine/graphics3/PickingSystem3.d.ts +6 -63
- package/src/engine/graphics3/PickingSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/PickingSystem3.js +6 -269
- package/src/engine/graphics3/ShadedGeometrySystem.d.ts +47 -0
- package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -0
- package/src/engine/graphics3/ShadedGeometrySystem.js +169 -0
- package/src/engine/graphics3/ShadedGeometrySystem3.d.ts +6 -43
- package/src/engine/graphics3/ShadedGeometrySystem3.d.ts.map +1 -1
- package/src/engine/graphics3/ShadedGeometrySystem3.js +6 -165
- package/src/engine/graphics3/TerrainSystem.d.ts +80 -0
- package/src/engine/graphics3/TerrainSystem.d.ts.map +1 -0
- package/src/engine/graphics3/TerrainSystem.js +566 -0
- package/src/engine/graphics3/TerrainSystem3.d.ts +6 -76
- package/src/engine/graphics3/TerrainSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/TerrainSystem3.js +6 -562
- package/src/engine/graphics3/TooltipComponentSystem.d.ts +65 -0
- package/src/engine/graphics3/TooltipComponentSystem.d.ts.map +1 -0
- package/src/engine/graphics3/TooltipComponentSystem.js +328 -0
- package/src/engine/graphics3/TooltipComponentSystem3.d.ts +6 -61
- package/src/engine/graphics3/TooltipComponentSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/TooltipComponentSystem3.js +6 -324
- package/src/engine/graphics3/Trail3DSystem.d.ts +73 -0
- package/src/engine/graphics3/Trail3DSystem.d.ts.map +1 -0
- package/src/engine/graphics3/Trail3DSystem.js +221 -0
- package/src/engine/graphics3/Trail3DSystem3.d.ts +6 -69
- package/src/engine/graphics3/Trail3DSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/Trail3DSystem3.js +6 -217
- package/src/engine/graphics3/VolumetricLightMap.d.ts +2 -2
- package/src/engine/graphics3/VolumetricLightMap.js +2 -2
- package/src/engine/graphics3/VolumetricLightMapSystem.d.ts +98 -0
- package/src/engine/graphics3/VolumetricLightMapSystem.d.ts.map +1 -0
- package/src/engine/graphics3/VolumetricLightMapSystem.js +261 -0
- package/src/engine/graphics3/VolumetricLightMapSystem3.d.ts +6 -94
- package/src/engine/graphics3/VolumetricLightMapSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/VolumetricLightMapSystem3.js +10 -261
- package/src/engine/graphics3/WaterSystem.d.ts +59 -0
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -0
- package/src/engine/graphics3/WaterSystem.js +293 -0
- package/src/engine/graphics3/WaterSystem3.d.ts +6 -55
- package/src/engine/graphics3/WaterSystem3.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem3.js +6 -289
- package/src/engine/graphics3/animation/AnimationGraphController.d.ts +1 -1
- package/src/engine/graphics3/animation/AnimationGraphController.js +518 -518
- package/src/engine/graphics3/animation/ClipListPlayer.d.ts +1 -1
- package/src/engine/graphics3/animation/ClipListPlayer.js +263 -263
- package/src/engine/graphics3/animation/ClipPlayback.d.ts +2 -2
- package/src/engine/graphics3/animation/ClipPlayback.js +84 -84
- package/src/engine/graphics3/decal/graph_build_decal_clusters.d.ts.map +1 -1
- package/src/engine/graphics3/decal/graph_build_decal_clusters.js +6 -8
- package/src/engine/graphics3/decal/shader_cluster_assign_decals.d.ts +0 -7
- package/src/engine/graphics3/decal/shader_cluster_assign_decals.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +1 -1
- package/src/engine/graphics3/decal/shader_cull_decals_frustum.d.ts +0 -4
- package/src/engine/graphics3/decal/shader_cull_decals_frustum.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_cull_decals_frustum.js +1 -1
- package/src/engine/graphics3/highlight/pack_highlight_table.d.ts +2 -2
- package/src/engine/graphics3/highlight/pack_highlight_table.d.ts.map +1 -1
- package/src/engine/graphics3/highlight/pack_highlight_table.js +74 -74
- package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts +1 -1
- package/src/engine/graphics3/pose/query_entity_node_world_pose.js +101 -101
- package/src/engine/graphics3/preview/make_model_thumbnail.d.ts +3 -3
- package/src/engine/graphics3/preview/make_model_thumbnail.d.ts.map +1 -1
- package/src/engine/graphics3/preview/make_model_thumbnail.js +48 -48
- package/src/engine/graphics3/shade_camera_projection_ray.d.ts +1 -1
- package/src/engine/graphics3/shade_camera_projection_ray.js +77 -77
- package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +1 -1
- package/src/engine/graphics3/shade_node_to_entity_composition.js +84 -84
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.js +0 -1
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.js +0 -1
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.js +133 -134
- package/src/shade/playground/skinned_blas_refit/verify_leaf_triangles.js +202 -202
- package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.js +249 -249
- package/src/shade/renderer/STATIC_GRAPHICS_ENGINE_ASSETS.js +1 -1
- package/src/shade/renderer/animation/pose/PosePlayback.d.ts +2 -1
- package/src/shade/renderer/animation/pose/PosePlayback.d.ts.map +1 -1
- package/src/shade/renderer/animation/pose/PosePlayback.js +2 -1
- package/src/shade/renderer/animation/shader_animation_apply.d.ts.map +1 -1
- package/src/shade/renderer/animation/shader_animation_apply.js +128 -129
- package/src/shade/renderer/animation/shader_animation_pose_resolve.d.ts.map +1 -1
- package/src/shade/renderer/animation/shader_animation_pose_resolve.js +176 -177
- package/src/shade/renderer/animation/shader_animation_tick.d.ts.map +1 -1
- package/src/shade/renderer/animation/shader_animation_tick.js +125 -126
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.js +98 -99
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.js +212 -213
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.js +120 -121
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.js +75 -76
- package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.js +371 -372
- package/src/shade/renderer/animation/skinning/shader_skin_matrix_prep.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/shader_skin_matrix_prep.js +104 -105
- package/src/shade/renderer/atmosphere/shader_multiscatter_lut.d.ts.map +1 -1
- package/src/shade/renderer/atmosphere/shader_multiscatter_lut.js +0 -1
- package/src/shade/renderer/buffer/graph_inspect_gpu_buffer.js +136 -136
- package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUDatabase.js +2 -3
- package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.js +319 -319
- package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
- package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +41 -42
- package/src/shade/renderer/buffer/util/shader_buffer_intra_copy_regions.d.ts.map +1 -1
- package/src/shade/renderer/buffer/util/shader_buffer_intra_copy_regions.js +83 -84
- package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/shader_dynamic_mesh.js +177 -170
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +918 -918
- package/src/shade/renderer/geometry/bvh/record_blas_refit.js +4 -4
- package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.d.ts.map +1 -1
- package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.js +121 -122
- package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +774 -774
- package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts +1 -1
- package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.js +69 -70
- package/src/shade/renderer/global_illumination/lpv/placement/graph_generate_probe_locations.js +367 -367
- package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.js +184 -184
- package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +1115 -1115
- package/src/shade/renderer/gpu_primitive/bvh/graph_bvh_build_lbvh.js +217 -217
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.js +142 -143
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.js +0 -1
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.js +140 -141
- package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.js +0 -1
- package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.js +0 -1
- package/src/shade/renderer/light/cluster/cull/graph_cull_lights.d.ts.map +1 -1
- package/src/shade/renderer/light/cluster/cull/graph_cull_lights.js +191 -198
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_frustum.d.ts +0 -6
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_frustum.d.ts.map +1 -1
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_frustum.js +120 -120
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_hzb.d.ts +0 -4
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_hzb.d.ts.map +1 -1
- package/src/shade/renderer/light/cluster/cull/shader_cull_lights_hzb.js +112 -112
- package/src/shade/renderer/light/cluster/graph_assign_clsuters.d.ts.map +1 -1
- package/src/shade/renderer/light/cluster/graph_assign_clsuters.js +4 -7
- package/src/shade/renderer/light/cluster/shader_cluster_assign_lights.d.ts +0 -6
- package/src/shade/renderer/light/cluster/shader_cluster_assign_lights.d.ts.map +1 -1
- package/src/shade/renderer/light/cluster/shader_cluster_assign_lights.js +1 -1
- package/src/shade/renderer/lightmap/bake/raster/shader_lightmap_to_viz_buffer.js +222 -222
- package/src/shade/renderer/lightmap/bake/tile/shader_lightmap_tile_composit.js +81 -81
- package/src/shade/renderer/material/GPUMaterialContext.js +380 -380
- package/src/shade/renderer/material/standard/fragment_gbuffer_vt.js +169 -169
- package/src/shade/renderer/object_property/shader_object_property_process.d.ts.map +1 -1
- package/src/shade/renderer/object_property/shader_object_property_process.js +83 -84
- package/src/shade/renderer/object_property/shader_refresh_all_mesh_bounds.d.ts.map +1 -1
- package/src/shade/renderer/object_property/shader_refresh_all_mesh_bounds.js +57 -58
- package/src/shade/renderer/particles/prototypeParticleSystem.js +2 -3
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +117 -118
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +77 -79
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +190 -183
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +99 -100
- package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts.map +1 -1
- package/src/shade/renderer/particles/sort/shader_particle_sort.js +158 -161
- package/src/shade/renderer/path_tracer/graph_path_trace_scene.js +184 -184
- package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.js +119 -119
- package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.js +3 -4
- package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.js +266 -267
- package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.js +270 -271
- package/src/shade/renderer/postprocess/nss/shader_nss_concat.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_concat.js +68 -69
- package/src/shade/renderer/postprocess/nss/shader_nss_conv_layer.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_conv_layer.js +225 -226
- package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_extract_feedback.js +49 -50
- package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.js +291 -292
- package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.js +230 -231
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts +16 -2
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.js +12 -12
- package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +2 -2
- package/src/shade/renderer/rasterize/bucket/shader_count_meshlets_by_material.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/shader_count_meshlets_by_material.js +68 -69
- package/src/shade/renderer/rasterize/bucket/shader_make_meshlet_indirect_draw_commands_by_material.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/shader_make_meshlet_indirect_draw_commands_by_material.js +59 -60
- package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +0 -1
- package/src/shade/renderer/rasterize/bucket/shader_sort_meshlets_by_material.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/shader_sort_meshlets_by_material.js +64 -65
- package/src/shade/renderer/rasterize/compute/graph_mesh_frustum_cull.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/compute/graph_mesh_frustum_cull.js +168 -169
- package/src/shade/renderer/rasterize/compute/graph_mesh_sphere_cull.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/compute/graph_mesh_sphere_cull.js +160 -161
- package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +109 -110
- package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +115 -116
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_classify_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_classify_bucket.js +139 -140
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_count_by_rasterization_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_count_by_rasterization_bucket.js +91 -92
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js +88 -89
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.js +90 -91
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +65 -66
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +89 -90
- package/src/shade/renderer/rasterize/expand/graph_mesh_filter_one_way.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/graph_mesh_filter_one_way.js +169 -170
- package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.js +89 -90
- package/src/shade/renderer/rasterize/expand/mesh/shader_meshes_to_meshlet_counts.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/mesh/shader_meshes_to_meshlet_counts.js +61 -62
- package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.js +153 -154
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +144 -144
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +88 -88
- package/src/shade/renderer/rasterize/native/oit/shader_oit_generate_moments.js +217 -217
- package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.js +179 -179
- package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.js +178 -178
- package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +194 -194
- package/src/shade/renderer/rasterize/native/viz/viz_rasterization_opaque_pass_descriptor.js +76 -76
- package/src/shade/renderer/restir/di/shader_restir_di_denoise.d.ts.map +1 -1
- package/src/shade/renderer/restir/di/shader_restir_di_denoise.js +136 -137
- package/src/shade/renderer/restir/di/shader_restir_di_resample.d.ts.map +1 -1
- package/src/shade/renderer/restir/di/shader_restir_di_resample.js +129 -130
- package/src/shade/renderer/restir/di/shader_restir_di_resolve.d.ts.map +1 -1
- package/src/shade/renderer/restir/di/shader_restir_di_resolve.js +99 -100
- package/src/shade/renderer/restir/di/shader_restir_di_spatial.d.ts.map +1 -1
- package/src/shade/renderer/restir/di/shader_restir_di_spatial.js +116 -117
- package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.js +383 -383
- package/src/shade/renderer/scene/hierarchy/graph_scene_update_transform_hierarchy_naive.js +123 -123
- package/src/shade/renderer/scene/hierarchy/shader_nodes_record_input_positions.d.ts.map +1 -1
- package/src/shade/renderer/scene/hierarchy/shader_nodes_record_input_positions.js +64 -65
- package/src/shade/renderer/scene/hierarchy/shader_nodes_update_transform_hierarchy.d.ts.map +1 -1
- package/src/shade/renderer/scene/hierarchy/shader_nodes_update_transform_hierarchy.js +372 -373
- package/src/shade/renderer/scene/hierarchy/shader_nodes_update_transform_hierarchy_naive.d.ts.map +1 -1
- package/src/shade/renderer/scene/hierarchy/shader_nodes_update_transform_hierarchy_naive.js +111 -112
- package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.d.ts.map +1 -1
- package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.js +0 -1
- package/src/shade/renderer/shader/ComputeShader.d.ts +24 -3
- package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
- package/src/shade/renderer/shader/ComputeShader.js +350 -296
- package/src/shade/renderer/shader/ImageShader.js +217 -217
- package/src/shade/renderer/shader/ShaderDescriptor.d.ts +5 -0
- package/src/shade/renderer/shader/ShaderDescriptor.d.ts.map +1 -1
- package/src/shade/renderer/shader/ShaderDescriptor.js +20 -0
- package/src/shade/renderer/shader/util/compute_infer_workgroup_size_from_source.d.ts +47 -0
- package/src/shade/renderer/shader/util/compute_infer_workgroup_size_from_source.d.ts.map +1 -0
- package/src/shade/renderer/shader/util/compute_infer_workgroup_size_from_source.js +448 -0
- package/src/shade/renderer/shadow/map/csm/graph_csm_setup_cascades.js +3 -3
- package/src/shade/renderer/shadow/ray/staged/gen/graph_gen_rays.js +3 -3
- package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.js +2 -2
- package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.js +4 -4
- package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.d.ts.map +1 -1
- package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.js +231 -232
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +12 -15
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts +3 -2
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +3 -2
- package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.d.ts +0 -2
- package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +17 -6
- package/src/shade/renderer/water/shader_water.js +246 -246
- package/src/shade/vitest.setup.mjs +1 -1
- package/src/shade/wgsl/emulator/ComputeShaderEmulator.js +291 -291
- package/src/view/minimap/Minimap.d.ts +2 -2
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +225 -225
- package/src/view/minimap/dom/MinimapCameraView.d.ts +3 -3
- package/src/view/minimap/dom/MinimapCameraView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapCameraView.js +206 -206
- package/src/shade/renderer/shadow/map/csm/shader/shader_setup_cascades.d.ts +0 -3
- package/src/shade/renderer/shadow/map/csm/shader/shader_setup_cascades.d.ts.map +0 -1
- package/src/shade/renderer/shadow/map/csm/shader/shader_setup_cascades.js +0 -20
|
@@ -1,918 +1,918 @@
|
|
|
1
|
-
# Render extensions — design
|
|
2
|
-
|
|
3
|
-
Replaces `Renderer.add_pass` / `remove_pass`, `FramePhase`, `FramePassContext` and
|
|
4
|
-
`FramePassRegistry` (MEEP 3, phase 2, item S3).
|
|
5
|
-
|
|
6
|
-
Status: **design, not built.** Nothing in §3 through §11 is committed code.
|
|
7
|
-
|
|
8
|
-
**Revised 2026-08-24** against the frame-graph work that has landed since the first draft:
|
|
9
|
-
`FrameGraphState`, `validate()`, recording scopes, record-time producers, and the `exportToJson`
|
|
10
|
-
fix. Those came out of this document's §4 and §13 and are tracked in
|
|
11
|
-
`engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`. §1.2, §3.4, §4, §5 and §13
|
|
12
|
-
describe what the graph does now, not what it did — the net effect is that the design leans on the
|
|
13
|
-
graph in three more places and asks it for nothing.
|
|
14
|
-
|
|
15
|
-
The same revision **withdrew the cull diagnostic** and the engine change it needed, on sponsor
|
|
16
|
-
review: culling is what a frame graph is for, and a check that calls it a failure when an extension
|
|
17
|
-
is the recorder invents a distinction that does not exist and outlaws the speculative producer.
|
|
18
|
-
§5.1 is the argument; the bar it sets — a check may only fire on something that cannot be true —
|
|
19
|
-
also took out a provenance check in §3.4.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 0. Summary
|
|
24
|
-
|
|
25
|
-
An extension is a **named object with declarations** — a phase, ordering constraints, the optional
|
|
26
|
-
frame products it needs — and one method that records into the frame's graph. Everything it exchanges
|
|
27
|
-
with the frame goes through **typed per-frame records** holding frame-graph handles: it reads a
|
|
28
|
-
handle out of a record and writes a replacement back. There is no return value and no privileged
|
|
29
|
-
resource.
|
|
30
|
-
|
|
31
|
-
Three properties follow, and they are the point:
|
|
32
|
-
|
|
33
|
-
1. **Every resource the frame carries is exchangeable**, not just the colour. The G-buffer stops
|
|
34
|
-
being a documented special case, depth stops being read-only, and an extension can publish a
|
|
35
|
-
record of its own that another extension reads.
|
|
36
|
-
2. **Order is declared**, not emergent from the order in which ECS systems happened to reach their
|
|
37
|
-
first `await`.
|
|
38
|
-
3. **The frame can be described without being rendered, and read back attributed once it has been.**
|
|
39
|
-
The set of extensions, their order per phase, and the products they force on are data, so
|
|
40
|
-
`describe()` prints the pipeline; and the registry records each extension inside its own graph
|
|
41
|
-
scope, so a DOT export of the frame groups by who asked for what instead of by whatever the
|
|
42
|
-
`graph_*` helpers named things. That pair is what "the frame graph is a first-class construct"
|
|
43
|
-
has to mean at this seam.
|
|
44
|
-
|
|
45
|
-
Underneath all three, one correction that is not an abstraction: **the upscale is a phase boundary
|
|
46
|
-
and the surface says so.** Three phases are before it and two are after, the before case is the
|
|
47
|
-
common one, and the size of what an extension is drawing into is derived from the live handle rather
|
|
48
|
-
than handed over as a field the renderer fills in once for the whole frame — which is what the
|
|
49
|
-
current mechanism does, and gets wrong at two of its five phases (§1.3b).
|
|
50
|
-
|
|
51
|
-
The shape is Unity 6 URP's — `ContextContainer` / `ContextItem`, `ScriptableRenderPass`,
|
|
52
|
-
`ConfigureInput` — with URP's two worst traits removed (§2).
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 1. What is wrong with `add_pass`
|
|
57
|
-
|
|
58
|
-
### 1.1 One resource is modifiable
|
|
59
|
-
|
|
60
|
-
`FramePassRegistry.run` threads exactly one value:
|
|
61
|
-
|
|
62
|
-
```js
|
|
63
|
-
const produced = passes[i](context);
|
|
64
|
-
if (produced === undefined) { continue; }
|
|
65
|
-
context.color = produced;
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
A pass's only sanctioned output is a colour handle. Everything else on `FramePassContext` — depth,
|
|
69
|
-
previous depth, the four G-buffer targets, the two visibility targets — is an input. The one
|
|
70
|
-
exception, the G-buffer being in/out at `AfterGBuffer`, is implemented by letting the pass reach into
|
|
71
|
-
`context.gbuffer` and assign, and costs three paragraphs of prose in `FramePassContext` to explain
|
|
72
|
-
why replacing beats writing in place.
|
|
73
|
-
|
|
74
|
-
What that closes off, concretely: writing depth (terrain and water both rasterize geometry that
|
|
75
|
-
should own depth), publishing a resource an extension computed for other extensions to read (fog of
|
|
76
|
-
war's visibility grid is exactly this and currently lives outside the graph), and reading anything
|
|
77
|
-
the frame builds but the struct does not name — velocity, the Hi-Z pyramid, the TAA history, the
|
|
78
|
-
shadow atlas, the light cluster tables.
|
|
79
|
-
|
|
80
|
-
The struct is closed by construction: adding a field is an edit to Shade. That is the wrong cost for
|
|
81
|
-
"my game needs one more thing from the frame".
|
|
82
|
-
|
|
83
|
-
### 1.2 The graph is an escape hatch, not the mechanism
|
|
84
|
-
|
|
85
|
-
`context.graph` hands the pass the `FrameGraph` and steps back. The registry never learns what the
|
|
86
|
-
pass read or wrote, so:
|
|
87
|
-
|
|
88
|
-
- **It cannot order passes.** It orders by registration order.
|
|
89
|
-
- **It cannot attribute anything.** `exportToDot` shows the nodes an extension created with whatever
|
|
90
|
-
names its helpers gave them, unrelated to the extension. The graph has since gained recording
|
|
91
|
-
scopes, which are exactly the missing mechanism — but they are opt-in and `add_pass` opens none,
|
|
92
|
-
so a frame's DOT export is still one flat wall of passes.
|
|
93
|
-
- **It cannot validate.** Reading `context.color` at `AfterGBuffer` yields `-1` and fails somewhere
|
|
94
|
-
inside a binding builder. Retaining a handle across frames is undetectable. Publishing a stale
|
|
95
|
-
handle is now caught, but by the graph at `compile()` and against the *reader* — the pass that
|
|
96
|
-
consumed it — so the report names the built-in pass that broke, never the extension that handed
|
|
97
|
-
it the bad handle.
|
|
98
|
-
- **`AfterGBuffer` has no return channel, so the culling contract cannot be met there.** The rule
|
|
99
|
-
itself is the graph's, and it is right everywhere: work nothing consumes is elided. But the only
|
|
100
|
-
way this mechanism lets a pass thread its result into the frame is by returning a colour handle,
|
|
101
|
-
and at `AfterGBuffer` there is no colour — so a pass whose G-buffer work *should* be consumed
|
|
102
|
-
reaches for `make_side_effect()` instead, which does not say "my output is used", it says "run me
|
|
103
|
-
anyway". Three paragraphs of `FramePhase` document the workaround. The record model removes the
|
|
104
|
-
reason for it (§3.3) and leaves the culling rule alone (§5.1).
|
|
105
|
-
|
|
106
|
-
And the injection points themselves are call sites: a phase is wherever `#run_frame_passes` was
|
|
107
|
-
typed inside an 800-line method. There are five because five were typed.
|
|
108
|
-
|
|
109
|
-
### 1.3 What this has already produced
|
|
110
|
-
|
|
111
|
-
Four findings from reading the current code, each independent of the design that replaces it.
|
|
112
|
-
|
|
113
|
-
**(a) Order is an accident, and the code says so.** `
|
|
114
|
-
|
|
115
|
-
> `// before anything is awaited, so that pass order within the phase is the profile's system order —`
|
|
116
|
-
> `// the decal composite has to see the textured ground, not precede it`
|
|
117
|
-
|
|
118
|
-
An ordering requirement between two extensions is being met by placing a registration call above an
|
|
119
|
-
`await`. Adding an `await` to a system's startup silently reorders the frame.
|
|
120
|
-
|
|
121
|
-
The same mechanism decides the six-deep chain at `AfterTransparency`, and **it has already produced a
|
|
122
|
-
shipped bug**. In profile order that chain is fog of war, particles, path display, highlight, debug
|
|
123
|
-
draw, trails — so the fog composites *first* and everything drawn after it is unfogged. Confirmed
|
|
124
|
-
with the sponsor: that is not intended. Fog of war conceals, so it belongs over everything it
|
|
125
|
-
conceals; particles drawing on top of it defeat what it is for.
|
|
126
|
-
|
|
127
|
-
Nobody chose that order. Nothing recorded it. Nothing failed when it turned out wrong. That is the
|
|
128
|
-
argument for §3.6 in one paragraph.
|
|
129
|
-
|
|
130
|
-
**(b) The one size the context states is wrong at two phases.** `Renderer` fills
|
|
131
|
-
`pass_context.resolution` from `resolution_internal.asArray()`, once, for all five phases. But TAA
|
|
132
|
-
and NSS upscale to `output_resolution` before `BeforePresent`, and the tonemap writes the canvas at
|
|
133
|
-
output resolution — so at `BeforePresent` and `Overlay` the colour is *not* the size the context
|
|
134
|
-
reports, whenever TAA is on. With TAA off it is, because the upscale is missing (there is a `TODO`
|
|
135
|
-
in `Renderer` saying so). The size of the frame's colour at `BeforePresent` is therefore not
|
|
136
|
-
derivable from the context at all; it depends on a renderer feature flag the extension cannot see.
|
|
137
|
-
|
|
138
|
-
`probeShadePassInjection` — the gate for the current mechanism — does
|
|
139
|
-
`output_resolution: context.resolution` at `BeforePresent`. It passes because the probe runs at 100%
|
|
140
|
-
scale. The game ships with dynamic resolution scaling enabled against a 30 fps floor, and at any
|
|
141
|
-
scale below 100% that pass produces a cropped, wrongly-scaled frame while the gate still measures a
|
|
142
|
-
luminance change and reports green.
|
|
143
|
-
|
|
144
|
-
The single field is the tell: the surface was built as though a frame had one resolution. Three of
|
|
145
|
-
the five phases are before the upscale and two are after it, and nothing in the surface says which
|
|
146
|
-
is which — `FramePhase`'s own text for `BeforePresent` says "still HDR" and never mentions that the
|
|
147
|
-
colour has been upscaled by then. §3.5 makes the boundary part of what a phase *is*.
|
|
148
|
-
|
|
149
|
-
**(c) `-1` is a sentinel with no guard.** `color` is `-1` at `AfterGBuffer`. Nothing stops a pass
|
|
150
|
-
reading it. The house rule is that the common case is the only case and preconditions are asserted,
|
|
151
|
-
not encoded as magic values.
|
|
152
|
-
|
|
153
|
-
**(d) The G-buffer special case exists only because colour owns the return channel.** Given a
|
|
154
|
-
uniform way to publish any handle, "the G-buffer is in/out at one phase" stops being a rule and
|
|
155
|
-
becomes an instance of the general one.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## 2. What is borrowed from URP, and what is not
|
|
160
|
-
|
|
161
|
-
Borrowed:
|
|
162
|
-
|
|
163
|
-
| URP 6 | Here | Why |
|
|
164
|
-
| --- | --- | --- |
|
|
165
|
-
| `ContextContainer` + `ContextItem` | `FrameContext.get(RecordType)` + frame records | The open, typed resource vocabulary. This is the fix for §1.1, and the reason it is keyed by type rather than by string is that the house rule forbids string-keyed registries. |
|
|
166
|
-
| `UniversalResourceData` | `SceneColor`, `GBufferTextures`, `ViewTextures`, `PresentTarget` | Built-in resources are ordinary classes with named fields, carried in the same container as anyone else's. One mechanism, not two. |
|
|
167
|
-
| `ScriptableRenderPass` | `RenderExtension` | A declaring object, not a closure. Declarations are what makes ordering, validation and description possible. |
|
|
168
|
-
| `ConfigureInput(ScriptableRenderPassInput.Motion)` | `RenderExtension.requires` + `FrameProduct` | An extension states what the frame must produce and the renderer turns it on. This is the answer to "velocity exists, is built, and is unreachable". |
|
|
169
|
-
| Renderer feature list, ordered | The registry's resolved order, printable | The pipeline is inspectable before it runs. |
|
|
170
|
-
|
|
171
|
-
Not borrowed, deliberately:
|
|
172
|
-
|
|
173
|
-
- **`RenderPassEvent` + integer offsets.** URP orders same-event passes by an `int` you add to the
|
|
174
|
-
enum. It is the same defect as registration order with an extra step: unreadable, unstable under
|
|
175
|
-
insertion, and it makes every ordering question a numerology question. Replaced by explicit
|
|
176
|
-
`after` / `before` constraints over extension classes (§3.6).
|
|
177
|
-
- **`SetGlobalTexture` / global shader state.** An implicit channel between passes that the graph
|
|
178
|
-
cannot see. Records are the channel.
|
|
179
|
-
- **`AddUnsafePass`.** URP needs a legacy `CommandBuffer` escape hatch. There is no legacy to carry
|
|
180
|
-
here, and adding an unchecked door on day one guarantees it becomes the door.
|
|
181
|
-
- **Two-level feature/pass split.** URP splits `ScriptableRendererFeature` from
|
|
182
|
-
`ScriptableRenderPass` because a feature is a `ScriptableObject` living on an asset. That reason
|
|
183
|
-
does not exist here, so there is one class.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## 3. The model
|
|
188
|
-
|
|
189
|
-
### 3.1 `RenderExtension`
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
export class RenderExtension {
|
|
193
|
-
/**
|
|
194
|
-
* Stable identity. Used in ordering diagnostics, error decoration and `describe()`.
|
|
195
|
-
* @type {string}
|
|
196
|
-
*/
|
|
197
|
-
name = "";
|
|
198
|
-
|
|
199
|
-
/** @type {FramePhase} */
|
|
200
|
-
phase = FramePhase.AfterTransparency;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Extension classes this one records after / before. Constructors, not instances — an
|
|
204
|
-
* extension does not need a reference to another system's object to state an order.
|
|
205
|
-
* @type {Function[]}
|
|
206
|
-
*/
|
|
207
|
-
after = [];
|
|
208
|
-
before = [];
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Optional frame products this extension reads. A bitmask of {@link FrameProduct}.
|
|
212
|
-
* Reading a product nobody required is an assert failure, not a silent `-1`.
|
|
213
|
-
* @type {number}
|
|
214
|
-
*/
|
|
215
|
-
requires = 0;
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Record this extension's work into the frame. Called once per frame, when the phase comes
|
|
219
|
-
* round. Returns nothing: everything this produces reaches the rest of the frame by
|
|
220
|
-
* publication (§3.4).
|
|
221
|
-
*
|
|
222
|
-
* @param {FrameContext} frame
|
|
223
|
-
*/
|
|
224
|
-
record(frame) {}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
`record` returning `void` is load-bearing. It is what makes colour stop being privileged.
|
|
229
|
-
|
|
230
|
-
An extension that has nothing to draw this frame returns without recording; that is the only "off"
|
|
231
|
-
switch and there is no `enabled` flag beside it.
|
|
232
|
-
|
|
233
|
-
### 3.2 `FrameContext`
|
|
234
|
-
|
|
235
|
-
```js
|
|
236
|
-
export class FrameContext {
|
|
237
|
-
/** @type {FrameGraph} the frame's graph — record into this with the `graph_*` helpers */
|
|
238
|
-
graph;
|
|
239
|
-
/** @type {ViewRenderContext} */
|
|
240
|
-
view;
|
|
241
|
-
/** @type {FramePhase} */
|
|
242
|
-
phase;
|
|
243
|
-
|
|
244
|
-
/** @template T @param {new()=>T} type @returns {T} throws if not present at this phase */
|
|
245
|
-
get(type) {}
|
|
246
|
-
|
|
247
|
-
/** @param {Function} type @returns {boolean} */
|
|
248
|
-
has(type) {}
|
|
249
|
-
|
|
250
|
-
/** @template T @param {new()=>T} type @returns {T} adds a record this extension owns */
|
|
251
|
-
create(type) {}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Pixel size of the target this phase carries, derived from the live handle rather than filled
|
|
255
|
-
* in by the renderer.
|
|
256
|
-
* @returns {number[]}
|
|
257
|
-
*/
|
|
258
|
-
get resolution() {}
|
|
259
|
-
|
|
260
|
-
/** @param {number} handle @returns {ResourceDescriptor} size, format and usage of a handle */
|
|
261
|
-
describe(handle) {}
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Three accessors rather than one `get_or_create`, because "absent" has to stay distinguishable from
|
|
266
|
-
"empty" — that is what lets `get(SceneColor)` at `AfterGBuffer` throw with a sentence instead of
|
|
267
|
-
handing back `-1`.
|
|
268
|
-
|
|
269
|
-
**`resolution` is derived, and that is the whole of the fix for defect (b).** It reads the graph
|
|
270
|
-
through the handle this phase's target actually is, so it is right before the upscale, right after
|
|
271
|
-
it, right when TAA is off and the upscale does not happen at all, and right at every dynamic
|
|
272
|
-
resolution scale. The common case — "how big is the thing I am drawing into" — stays one property
|
|
273
|
-
access and cannot be wrong.
|
|
274
|
-
|
|
275
|
-
`describe(handle)` is the general form, and it is needed whenever two things in one pass are
|
|
276
|
-
different sizes: an extension at `BeforePresent` reading the G-buffer is working across the upscale
|
|
277
|
-
boundary and has to ask for both.
|
|
278
|
-
|
|
279
|
-
> Both lean on descriptors being trustworthy, which was checked: transient and imported resources
|
|
280
|
-
> both carry a real `TextureResourceDescriptor` holding absolute pixels.
|
|
281
|
-
> `TextureResourceDescriptor.resolution_domain` says otherwise by default and is vestigial — nothing
|
|
282
|
-
> branches on it, only `hash()` folds it in. It stays; it is now documented as vestigial on the field
|
|
283
|
-
> itself so the next reader does not take it for a contract.
|
|
284
|
-
|
|
285
|
-
### 3.3 Frame records
|
|
286
|
-
|
|
287
|
-
A record is a plain class holding frame-graph handles. The renderer publishes the built-in ones as
|
|
288
|
-
the frame reaches the phase where they are meaningful.
|
|
289
|
-
|
|
290
|
-
```js
|
|
291
|
-
/** present from AfterGBuffer */
|
|
292
|
-
class GBufferTextures { albedo; normal; pbr; emissive; }
|
|
293
|
-
|
|
294
|
-
/** present from AfterGBuffer */
|
|
295
|
-
class ViewTextures {
|
|
296
|
-
depth; depth_previous;
|
|
297
|
-
visibility_mesh; visibility_triangle;
|
|
298
|
-
velocity; // requires FrameProduct.Velocity
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** present from AfterLighting through BeforePresent */
|
|
302
|
-
class SceneColor {
|
|
303
|
-
color;
|
|
304
|
-
copy; // requires FrameProduct.ColorCopy
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** present at Overlay, and only there */
|
|
308
|
-
class PresentTarget { canvas; }
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
Presence by phase:
|
|
312
|
-
|
|
313
|
-
| phase | records | resolution |
|
|
314
|
-
| --- | --- | --- |
|
|
315
|
-
| `FrameStart` | — (the view only) | internal |
|
|
316
|
-
| `AfterGBuffer` | `GBufferTextures` (writable), `ViewTextures` | internal |
|
|
317
|
-
| `AfterLighting` | `GBufferTextures` (frozen), `ViewTextures`, `SceneColor` | internal |
|
|
318
|
-
| `AfterTransparency` | as above | internal |
|
|
319
|
-
| `BeforePresent` | as above — but `SceneColor` and `GBufferTextures` are now different sizes | output |
|
|
320
|
-
| `Overlay` | `PresentTarget` | output |
|
|
321
|
-
|
|
322
|
-
Two of those rows are decisions worth naming.
|
|
323
|
-
|
|
324
|
-
**`SceneColor` is absent at `Overlay`, and `PresentTarget` is not `SceneColor`.** The current
|
|
325
|
-
mechanism hands the tonemapped colour to `Overlay` passes and then documents, at length, that they
|
|
326
|
-
must not read it — a texture cannot be a render attachment and a sampled source in the same pass.
|
|
327
|
-
That was found by measurement after the fact. Handing back a differently-named record with one field
|
|
328
|
-
says the same thing structurally: there is no `color` at `Overlay` to be tempted by, and an
|
|
329
|
-
extension that wants the finished image as *input* discovers at `get` time that it belongs at
|
|
330
|
-
`BeforePresent`.
|
|
331
|
-
|
|
332
|
-
**`GBufferTextures` is present but frozen after `AfterGBuffer`.** Outline and highlight effects
|
|
333
|
-
legitimately read the G-buffer and the visibility buffer late. Publishing into it late is
|
|
334
|
-
meaningless — shading has run — so the record is frozen and the assert says why (§5). At
|
|
335
|
-
`BeforePresent` it is also on the far side of the upscale from the colour (§3.5), so the two are
|
|
336
|
-
different sizes; `describe()` is how a pass learns the ratio, and the record's own doc comment says
|
|
337
|
-
to expect it.
|
|
338
|
-
|
|
339
|
-
Extensions add their own records with `create`:
|
|
340
|
-
|
|
341
|
-
```js
|
|
342
|
-
export class FogOfWarVisibility { grid; } // a graph buffer handle
|
|
343
|
-
|
|
344
|
-
// producer, at FrameStart
|
|
345
|
-
frame.create(FogOfWarVisibility).grid = this.#update_grid(frame.graph);
|
|
346
|
-
|
|
347
|
-
// consumer, at AfterTransparency
|
|
348
|
-
if (frame.has(FogOfWarVisibility)) { ... }
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Which is the second half of the fix for §1.1: the vocabulary is open, and extending it costs no
|
|
352
|
-
edit to Shade.
|
|
353
|
-
|
|
354
|
-
### 3.4 Publication
|
|
355
|
-
|
|
356
|
-
A field on a record is an accessor. Writing it is publication:
|
|
357
|
-
|
|
358
|
-
```js
|
|
359
|
-
const scene = frame.get(SceneColor);
|
|
360
|
-
|
|
361
|
-
const [fogged] = graph_image_pass({
|
|
362
|
-
graph: frame.graph,
|
|
363
|
-
shader: this.#shader,
|
|
364
|
-
output_resolution: frame.describe(scene.color).resolution,
|
|
365
|
-
inputs: { input_color: scene.color, input_depth: frame.get(ViewTextures).depth }
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
scene.color = fogged; // the rest of the frame carries this
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
The setter asserts three things, all free in production:
|
|
372
|
-
|
|
373
|
-
1. The record is not frozen (§3.3), and the frame index matches — a handle captured last frame
|
|
374
|
-
cannot be published this frame.
|
|
375
|
-
2. `graph.is_valid_resource(handle)` — the handle is the current version of its resource.
|
|
376
|
-
3. The current phase is one where the record is writable.
|
|
377
|
-
|
|
378
|
-
Check 2 exists for **attribution**, not detection. The graph catches a superseded handle on its
|
|
379
|
-
own: whoever reads it next trips `validate()`'s superseded-read check at `compile()` (§4). But that
|
|
380
|
-
report names the *reader* — some built-in pass downstream — and says nothing about which extension
|
|
381
|
-
put the bad handle into the record. Checking at the setter fails at the line that caused it, with
|
|
382
|
-
the extension's name on it. It forbids nothing: one `ResourceEntry` owns one concrete resource, so
|
|
383
|
-
an older node is not a snapshot of older contents and publishing one is wrong in every case.
|
|
384
|
-
|
|
385
|
-
**A provenance check was considered here and rejected** — "the handle's producer is a pass recorded
|
|
386
|
-
inside this extension's scope", which the record-time producer (§4) made possible. It would have
|
|
387
|
-
forbidden a legitimate thing: `import_resource` leaves a node with no producer, so an extension that
|
|
388
|
-
imports a texture it owns and publishes it as the frame's colour — a debug view, video playback —
|
|
389
|
-
would have failed a check aimed at typos. What it uniquely caught, publishing a handle you read
|
|
390
|
-
rather than wrote, surfaces immediately downstream anyway.
|
|
391
|
-
|
|
392
|
-
Reading a field asserts frame identity and, for product-gated fields, that the *recording extension*
|
|
393
|
-
declared the product (§3.7).
|
|
394
|
-
|
|
395
|
-
**Publication is not the dependency edge.** The graph already has the edge: `FramePassBuilder.write`
|
|
396
|
-
renames a written resource into a versioned clone and records the read, so "this pass consumed the
|
|
397
|
-
colour as it stood and produced a new one" is already a graph fact. Publication is the *naming*
|
|
398
|
-
step — which handle currently means "the scene colour". Keeping those two ideas separate is what
|
|
399
|
-
lets the record model be this small.
|
|
400
|
-
|
|
401
|
-
### 3.5 Phases
|
|
402
|
-
|
|
403
|
-
`FrameStart`, `AfterGBuffer`, `AfterLighting`, `AfterTransparency`, `BeforePresent`, `Overlay`.
|
|
404
|
-
|
|
405
|
-
**The upscale is a phase boundary, and saying so is part of what a phase means.** `AfterGBuffer`,
|
|
406
|
-
`AfterLighting` and `AfterTransparency` are before it and run at internal resolution; TAA and NSS
|
|
407
|
-
upscale to output resolution, so `BeforePresent` and `Overlay` are after it and run at output
|
|
408
|
-
resolution.
|
|
409
|
-
|
|
410
|
-
**Before the upscale is the common case, and the design leans on it.** An extension drawing into the
|
|
411
|
-
scene belongs at `AfterTransparency`: its work is then temporally resolved, upscaled and sharpened
|
|
412
|
-
along with everything else, and it is the cheapest place to draw because it draws at internal
|
|
413
|
-
resolution. `BeforePresent` is the deliberate other choice — full output resolution, still HDR,
|
|
414
|
-
nothing after it but the tonemap — and it is a trade, not an upgrade: work there gets no temporal
|
|
415
|
-
history and costs full-resolution pixels. An extension that does not have a reason to be after the
|
|
416
|
-
upscale should not be.
|
|
417
|
-
|
|
418
|
-
One wrinkle, and it belongs to the renderer rather than to this design: with
|
|
419
|
-
`feature_taa_enabled === false` the upscale does not happen at all — there is a `TODO` in `Renderer`
|
|
420
|
-
saying exactly that — so in that configuration `BeforePresent` is at internal resolution. A derived
|
|
421
|
-
`frame.resolution` (§3.2) is right in both configurations. A documented constant would be wrong in
|
|
422
|
-
one, which is how the current mechanism ended up wrong in two.
|
|
423
|
-
|
|
424
|
-
Five phases are the existing ones, unchanged in meaning. **`FrameStart` is the one addition**: a point after
|
|
425
|
-
the view is updated and before anything is rasterized. It exists because per-frame GPU compute that
|
|
426
|
-
later phases depend on currently has nowhere to go — fog of war's grid update, a custom cull, a
|
|
427
|
-
procedural buffer build — so it is either bolted onto a draw pass at a later phase, which puts it in
|
|
428
|
-
the wrong place in the graph and mis-attributes it in a capture, or done outside the graph entirely,
|
|
429
|
-
which is precisely the aliasing-and-lifetime blindness §1.2 is about.
|
|
430
|
-
|
|
431
|
-
Considered and rejected: `BeforeGBuffer` (nothing distinguishes it from `FrameStart` for anything an
|
|
432
|
-
extension can do), `AfterShadows` (shadow rendering is per-light and interior to
|
|
433
|
-
`GPULightCollection`; a phase there would be a lie about what is stable), per-post-effect phases
|
|
434
|
-
(URP has seventeen events and their number is a symptom, not a feature).
|
|
435
|
-
|
|
436
|
-
### 3.6 Ordering
|
|
437
|
-
|
|
438
|
-
Within a phase, order is the topological order of the `after` / `before` edges. Cycles throw at
|
|
439
|
-
registration, naming the cycle. Both directions exist because an extension that must precede one it
|
|
440
|
-
cannot edit is exactly the case an extension mechanism is for.
|
|
441
|
-
|
|
442
|
-
Unconstrained pairs are broken by `name`, ascending. Stable and reproducible; deliberately *not*
|
|
443
|
-
registration order, so that adding an `await` to a system's startup cannot change the picture.
|
|
444
|
-
|
|
445
|
-
Name-order is stable, not correct. So the resolver also reports **ambiguity**: two extensions on one
|
|
446
|
-
phase that publish the same field of the same record, with no ordering path between them, is an
|
|
447
|
-
assert failure naming both and the field. Publication goes through our setters, so the frame knows
|
|
448
|
-
who published what — no extra syntax on the author's side. This is the check that would have made
|
|
449
|
-
somebody write down what the six-deep `AfterTransparency` chain is supposed to be.
|
|
450
|
-
|
|
451
|
-
The ambiguity check is per-frame and assert-only. It costs nothing in production and fires on the
|
|
452
|
-
first frame in development.
|
|
453
|
-
|
|
454
|
-
### 3.7 Products
|
|
455
|
-
|
|
456
|
-
`FrameProduct` is a bitmask of things the frame *can* build but does not always:
|
|
457
|
-
|
|
458
|
-
```js
|
|
459
|
-
export const FrameProduct = {
|
|
460
|
-
/** per-object + camera velocity, built even when TAA and motion blur are off */
|
|
461
|
-
Velocity: 1,
|
|
462
|
-
/** a readable snapshot of the scene colour as it stood when the phase began */
|
|
463
|
-
ColorCopy: 2,
|
|
464
|
-
};
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
The union over registered extensions is computed once per registration change, and the renderer
|
|
468
|
-
consults it the way it consults its own feature flags. `ColorCopy` is produced immediately before
|
|
469
|
-
the passes of each phase that requires it.
|
|
470
|
-
|
|
471
|
-
Two members, because two have a nameable consumer: `Velocity` for any effect an extension wants to
|
|
472
|
-
temporally reproject itself (currently built for TAA, unreachable by anyone else), and `ColorCopy`
|
|
473
|
-
for refraction — a pass that rasterizes geometry *into* the live colour with a `load` op and must
|
|
474
|
-
also sample what is behind it, which is refractive water and distortion particles. A full-screen
|
|
475
|
-
pass that reads the colour and writes a new one does not need it and must not ask for it.
|
|
476
|
-
|
|
477
|
-
Considered and deferred with reasons in §9: `HiZ`, `PreviousColor`, `SceneDepthPyramid`.
|
|
478
|
-
|
|
479
|
-
This is the part of the design with the weakest present-day demand and it ships last (§10).
|
|
480
|
-
|
|
481
|
-
---
|
|
482
|
-
|
|
483
|
-
## 4. What the frame graph does, and what it does not
|
|
484
|
-
|
|
485
|
-
Stated plainly, because the design would be dishonest without it.
|
|
486
|
-
|
|
487
|
-
`FrameGraph.execute` iterates `__pass_nodes` **in insertion order**. It does not topologically sort,
|
|
488
|
-
and there is no plan for it to. What the graph gives is culling (`ref_count` propagation from
|
|
489
|
-
unreferenced resources), transient resource lifetime and aliasing, versioned renaming of written
|
|
490
|
-
resources, and export.
|
|
491
|
-
|
|
492
|
-
So: **the graph does not order extensions; §3.6 does.** Every claim in this document about ordering
|
|
493
|
-
is a claim about the resolver, not about the graph.
|
|
494
|
-
|
|
495
|
-
**What the graph does instead of sorting is check.** The first draft of this document listed two
|
|
496
|
-
implementation constraints the design had to route around. Both are now fixed, and a third thing
|
|
497
|
-
landed that the design had assumed away, so the division of labour is different from what §3.6 was
|
|
498
|
-
written against. The four that matter here — the full list and its evidence are in
|
|
499
|
-
`engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`:
|
|
500
|
-
|
|
501
|
-
- **`validate()` proves the insertion order is a valid topological order**, and `compile()` asserts
|
|
502
|
-
on it. It reports a *read before write* (a pass reading a node a later pass produces) and a
|
|
503
|
-
*superseded read* (a pass reading a version an earlier pass has already replaced — one
|
|
504
|
-
`ResourceEntry` owns one concrete resource, so an older node is not a snapshot). This is the
|
|
505
|
-
honest answer to insertion-order execution: not to sort, but to refuse a recording whose order is
|
|
506
|
-
wrong. It is asserted, so it costs nothing shipped and fails the frame in development.
|
|
507
|
-
- **The lifecycle is one-shot and stated.** `FrameGraphState`: `Recording` → `Compiled` →
|
|
508
|
-
`Executed`, monotonic, asserted at every mutating entry point. `compile()` was not idempotent and
|
|
509
|
-
`execute()` could not tell whether `compile()` had run — a graph executed uncompiled culls itself
|
|
510
|
-
entirely and says nothing. The design's old "the extension layer must never compile the graph
|
|
511
|
-
itself" caveat is deleted: the graph says so now.
|
|
512
|
-
- **`ResourceNode.producer` is assigned at record time**, by `FramePassBuilder`, rather than in
|
|
513
|
-
`compile()`. So "this handle was produced by the pass that is publishing it" is answerable at
|
|
514
|
-
publication (§3.4).
|
|
515
|
-
- **Recording scopes exist.** `push_scope(name)` / `pop_scope()` stamp every pass and resource node
|
|
516
|
-
created inside them, nesting through a parent chain. This supersedes §13's request for a
|
|
517
|
-
`pass_count` getter and is what §1.2's attribution complaint was asking for.
|
|
518
|
-
|
|
519
|
-
One finding from that work belongs here rather than there, because it is the strongest available
|
|
520
|
-
argument for the whole model: `validate()`, on its first run against the live 61-pass `Shading`
|
|
521
|
-
graph, found that **Shade's own rasterization was not threading its colour targets.** Depth was
|
|
522
|
-
threaded through the two-phase opaque pass and colour was not, so every rasterization pass read
|
|
523
|
-
`out_color/0` at version 0 while depth advanced. It was harmless only because those passes write an
|
|
524
|
-
imported resource and therefore always execute, and because insertion order happened to give the
|
|
525
|
-
right picture — the colour dependency between them was simply not a graph fact. That is what
|
|
526
|
-
"working around the graph rather than through it" costs, in the engine's own code, found the day
|
|
527
|
-
someone checked.
|
|
528
|
-
|
|
529
|
-
What remains true, and what §3.6 rests on: the graph checks that a recording order is *consistent*,
|
|
530
|
-
never that it is the one anybody wanted. Two extensions each reading the colour and writing a new
|
|
531
|
-
one are valid in either order. Data-order violations are the graph's; visual-order mistakes are
|
|
532
|
-
§3.6's, and nothing else will catch them.
|
|
533
|
-
|
|
534
|
-
---
|
|
535
|
-
|
|
536
|
-
## 5. Validation and diagnostics
|
|
537
|
-
|
|
538
|
-
All of it under `assert`, which is compiled out of production builds — so the price of every check
|
|
539
|
-
here is zero in a shipped game and the checks may be as thorough as they like.
|
|
540
|
-
|
|
541
|
-
| check | when | what it catches |
|
|
542
|
-
| --- | --- | --- |
|
|
543
|
-
| record present at phase | `get` | reading `SceneColor` at `AfterGBuffer` — replaces `-1` |
|
|
544
|
-
| record frozen | publish | publishing a G-buffer handle after shading |
|
|
545
|
-
| frame identity | read + publish | a handle retained across frames |
|
|
546
|
-
| `is_valid_resource` | publish | publishing a superseded handle, attributed to the extension |
|
|
547
|
-
| product declared by the recording extension | read | reading `velocity` because *someone else* required it |
|
|
548
|
-
| ordering cycle | registration | `after`/`before` cycle, named |
|
|
549
|
-
| publication ambiguity | end of phase | two extensions writing one field with no order between them |
|
|
550
|
-
|
|
551
|
-
Beneath all of them the graph runs its own `validate()` inside `compile()` (§4), which catches the
|
|
552
|
-
data-order failures this layer cannot see.
|
|
553
|
-
|
|
554
|
-
Every one of these fires on a **statement that cannot be true** — a superseded handle names one
|
|
555
|
-
physical resource holding newer contents, a cycle has no order, an undeclared collision has no
|
|
556
|
-
answer. None of them fires on an extension that is behaving correctly. That is the bar; §5.1 is
|
|
557
|
-
what happens when a check is written without holding to it.
|
|
558
|
-
|
|
559
|
-
### 5.1 Culling is not an error, and no diagnostic here treats it as one
|
|
560
|
-
|
|
561
|
-
An earlier revision of this document asked the graph to report per-scope culling so that "an
|
|
562
|
-
extension whose entire recorded work was culled" could be an assert. **That was wrong and it is
|
|
563
|
-
removed**, along with the engine change it needed. Three reasons, in increasing order of weight.
|
|
564
|
-
|
|
565
|
-
Culling is what a frame graph is *for*. Frostbite's paper is about eliminating work nobody
|
|
566
|
-
consumes; that is why the pattern spread. A mechanism cannot adopt the construct and then treat its
|
|
567
|
-
central behaviour as a failure.
|
|
568
|
-
|
|
569
|
-
The distinction it drew does not exist. "Culled built-in pass: optimisation. Culled extension pass:
|
|
570
|
-
bug." Nothing about the recorder changes what culling means, and encoding the split would have made
|
|
571
|
-
extensions second-class in the one place the design claims they are not.
|
|
572
|
-
|
|
573
|
-
And it forbids the pattern this design exists to enable. §3.3's own worked example is an extension
|
|
574
|
-
publishing `FogOfWarVisibility` at `FrameStart` for a consumer at `AfterTransparency`. Take the
|
|
575
|
-
consumer out — disable fog — and the producer's work is dead, the graph elides it, and the frame is
|
|
576
|
-
correct and cheaper. Under the assert that is a hard error. A speculative producer whose consumer
|
|
577
|
-
may or may not be present is the whole point of an open record vocabulary, and the check would have
|
|
578
|
-
outlawed it on day one.
|
|
579
|
-
|
|
580
|
-
**What replaces it: nothing, because nothing is missing.** `exportToJson` already reports
|
|
581
|
-
`culled` per pass and `exportToDot` already greys culled passes out; with the registry's scopes
|
|
582
|
-
(§5.2) a culled extension is a grey cluster with its own name on it. An author asking "why did my
|
|
583
|
-
pass not run" dumps the graph and sees it. That is a tool, not a tribunal.
|
|
584
|
-
|
|
585
|
-
The culling contract itself is stated once, on `RenderExtension.record`, in the same terms it holds
|
|
586
|
-
for every other pass in the graph: work whose output nothing consumes is elided; thread your result
|
|
587
|
-
into a record and it is consumed; if you want work that runs regardless, `make_side_effect()` says
|
|
588
|
-
so, exactly as it does for a built-in pass.
|
|
589
|
-
|
|
590
|
-
### 5.2 Recording scopes
|
|
591
|
-
|
|
592
|
-
**The registry opens a recording scope per extension**, named after it, around the `record()` call:
|
|
593
|
-
|
|
594
|
-
```js
|
|
595
|
-
graph.push_scope(extension.name);
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
extension.record(frame);
|
|
599
|
-
} finally {
|
|
600
|
-
graph.pop_scope();
|
|
601
|
-
}
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
The `finally` is not error handling — nothing is caught and nothing is swallowed. It is there
|
|
605
|
-
because `compile()` asserts the scope stack came back empty, so an extension that throws while
|
|
606
|
-
recording would otherwise fail on an unbalanced scope and bury the real error behind it. The throw
|
|
607
|
-
propagates, decorated with the extension's name the way `FrameGraph` already decorates a failing
|
|
608
|
-
pass with its own.
|
|
609
|
-
|
|
610
|
-
**Scopes are for reading the frame back, not for policing it.** `exportToDot` puts each extension's
|
|
611
|
-
passes in its own labelled cluster, nested as the scopes nest, and a graph that opens no scope
|
|
612
|
-
exports byte-identically to before. That is the whole of §1.2's attribution complaint, answered by
|
|
613
|
-
an API that already exists and that nothing currently calls — and it is what makes §5.1's answer to
|
|
614
|
-
culling a real answer rather than a shrug: the grey box in the dump has the extension's name on it.
|
|
615
|
-
|
|
616
|
-
No check depends on scopes. The one that did is gone (§5.1), and the provenance check that would
|
|
617
|
-
have used `is_within` was rejected on its own merits (§3.4).
|
|
618
|
-
|
|
619
|
-
`RenderExtensionRegistry.describe()` returns the resolved pipeline as text: per phase, the extensions
|
|
620
|
-
in resolved order, each with the products it required and the records it publishes into. Printable
|
|
621
|
-
without a device, which is what makes it testable without one.
|
|
622
|
-
|
|
623
|
-
---
|
|
624
|
-
|
|
625
|
-
## 6. Lifecycle
|
|
626
|
-
|
|
627
|
-
Registration is device-free and survives device loss — the current mechanism's one clearly right
|
|
628
|
-
decision, kept. `
|
|
629
|
-
re-populated from it. A system registers once at startup without knowing whether a device exists.
|
|
630
|
-
|
|
631
|
-
```js
|
|
632
|
-
graphics.add_extension(extension); // returns the extension
|
|
633
|
-
graphics.remove_extension(extension); // returns whether it was registered
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
Identity is the object, so the phase no longer has to be passed to removal. Registering or removing
|
|
637
|
-
during `record` is an assert failure.
|
|
638
|
-
|
|
639
|
-
`
|
|
640
|
-
one-for-one, so D14's "the facade does not grow" holds. `pass_count(phase)` becomes
|
|
641
|
-
`extension_count(phase)`, which the start → stop → start leak gate needs.
|
|
642
|
-
|
|
643
|
-
**Views.** There is one injection site today: the primary view inside `render_to_target`. Shadow
|
|
644
|
-
views, probe bakes and picking do not run extensions and this design does not change that. When a
|
|
645
|
-
second view kind gains injection points, the filter belongs on the registry and the extension gains
|
|
646
|
-
a declaration for it; building that now would be inventing a contract for a caller that does not
|
|
647
|
-
exist.
|
|
648
|
-
|
|
649
|
-
---
|
|
650
|
-
|
|
651
|
-
## 7. The whole API
|
|
652
|
-
|
|
653
|
-
That is the entire surface.
|
|
654
|
-
|
|
655
|
-
```js
|
|
656
|
-
// declaration
|
|
657
|
-
class RenderExtension { name; phase; after; before; requires; record(frame); }
|
|
658
|
-
const FramePhase = { FrameStart, AfterGBuffer, AfterLighting, AfterTransparency, BeforePresent, Overlay };
|
|
659
|
-
const FrameProduct = { Velocity, ColorCopy };
|
|
660
|
-
|
|
661
|
-
// recording
|
|
662
|
-
class FrameContext { graph; view; phase; resolution; get(type); has(type); create(type); describe(handle); }
|
|
663
|
-
|
|
664
|
-
// built-in records
|
|
665
|
-
class GBufferTextures { albedo; normal; pbr; emissive; }
|
|
666
|
-
class ViewTextures { depth; depth_previous; visibility_mesh; visibility_triangle; velocity; }
|
|
667
|
-
class SceneColor { color; copy; }
|
|
668
|
-
class PresentTarget { canvas; }
|
|
669
|
-
|
|
670
|
-
// registration
|
|
671
|
-
class RenderExtensionRegistry { add(e); remove(e); count(phase); describe(); run(frame); }
|
|
672
|
-
// on
|
|
673
|
-
add_extension(e); remove_extension(e); extension_count(phase);
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
---
|
|
677
|
-
|
|
678
|
-
## 8. Migration
|
|
679
|
-
|
|
680
|
-
Nine live systems, one probe, one prototype viewer. Every one of them is the same mechanical change:
|
|
681
|
-
a closure becomes a class, `context.color` becomes `frame.get(SceneColor).color`, and the return
|
|
682
|
-
value becomes a publication.
|
|
683
|
-
|
|
684
|
-
| consumer | phase | change beyond the mechanical |
|
|
685
|
-
| --- | --- | --- |
|
|
686
|
-
| `
|
|
687
|
-
| `
|
|
688
|
-
| `
|
|
689
|
-
| `
|
|
690
|
-
| `
|
|
691
|
-
| `
|
|
692
|
-
| `
|
|
693
|
-
| `
|
|
694
|
-
| `
|
|
695
|
-
| `probeShadePassInjection` | all | the gate is rewritten and gains the two checks it is currently blind to: a G-buffer replacement, and an order flip |
|
|
696
|
-
| `prototypeVGEOFormatViewer` | `AfterTransparency` | prototype; mechanical |
|
|
697
|
-
|
|
698
|
-
**The `AfterTransparency` chain is settled, not open.** Fog of war is last among the content
|
|
699
|
-
extensions, because it conceals and therefore belongs over everything it is meant to conceal —
|
|
700
|
-
particles, trails, path display and the selection highlight all draw before it and are all subject
|
|
701
|
-
to it. Debug draw is the single deliberate exception, `after: [FogOfWarExtension]`: developer
|
|
702
|
-
geometry that the fog can hide is a debugging tool that stops working exactly where debugging is
|
|
703
|
-
needed.
|
|
704
|
-
|
|
705
|
-
That is six extensions and two declarations. Written down once, in the two files that care, instead
|
|
706
|
-
of being a property of where an `await` sits.
|
|
707
|
-
|
|
708
|
-
**The bug does not have to wait for the migration.** Today's ordering is registration order, so
|
|
709
|
-
moving `
|
|
710
|
-
the cost of it being another undeclared ordering fact, which is what this design exists to end.
|
|
711
|
-
|
|
712
|
-
---
|
|
713
|
-
|
|
714
|
-
## 9. Non-goals and known limits
|
|
715
|
-
|
|
716
|
-
**Shade's own frame is not rewritten into a list of extensions.** URP's real strength is that
|
|
717
|
-
built-in and injected passes are the same type. Getting there means restructuring
|
|
718
|
-
`Renderer.render_to_target`, and renderer internals are hands-off. The consequence, stated so it is
|
|
719
|
-
not discovered later: injection points remain hand-placed call sites, so the phase set is only as
|
|
720
|
-
good as the places the renderer publishes records, and an extension cannot insert itself between two
|
|
721
|
-
built-in post passes.
|
|
722
|
-
|
|
723
|
-
**Extensions cannot participate in OIT.** Work at `AfterTransparency` composites over the resolved
|
|
724
|
-
transparency; it cannot interleave with sorted transparents. True today, unchanged, and the honest
|
|
725
|
-
reason the phase model has a ceiling.
|
|
726
|
-
|
|
727
|
-
**Extensions cannot add to a built-in pass** — no extra material variants in the G-buffer raster, no
|
|
728
|
-
extra lights. A new pass is the only unit.
|
|
729
|
-
|
|
730
|
-
**No `FrameProduct` for `HiZ` / `PreviousColor` / depth pyramid yet.** Each is built by the renderer
|
|
731
|
-
and each would be a one-line addition, and none has a consumer today. Adding them speculatively puts
|
|
732
|
-
three untested branches in the renderer's product wiring.
|
|
733
|
-
|
|
734
|
-
**No view kinds.** §6.
|
|
735
|
-
|
|
736
|
-
**Ordering ambiguity is caught at frame time, not registration time**, because publication is
|
|
737
|
-
observed rather than declared. Declaring publications up front would catch it earlier at the cost of
|
|
738
|
-
a second declaration that can disagree with the code; observation cannot disagree with the code.
|
|
739
|
-
|
|
740
|
-
---
|
|
741
|
-
|
|
742
|
-
## 10. Delivery
|
|
743
|
-
|
|
744
|
-
Three steps, each independently green.
|
|
745
|
-
|
|
746
|
-
- **A — the model.** Records, `FrameContext`, publication with all four checks, registry with
|
|
747
|
-
ordering, cycle detection and scoped recording. Port the nine systems and the viewer. Delete
|
|
748
|
-
`FramePassContext`, `FramePassRegistry` and `add_pass` / `remove_pass`. `FrameStart` and
|
|
749
|
-
`PresentTarget` land here, because they are what the fog-of-war split and the `Overlay` rule need.
|
|
750
|
-
- **B — diagnostics.** Ambiguity check and `describe()`. Nothing here waits on the engine; scoped
|
|
751
|
-
recording lands in A, because it needs nothing that is not already there.
|
|
752
|
-
- **C — products.** `FrameProduct.Velocity` and `ColorCopy`, and the renderer wiring that honours
|
|
753
|
-
them.
|
|
754
|
-
|
|
755
|
-
A is the whole of the user-visible design. B is what makes it hold. C is the part with the weakest
|
|
756
|
-
demand and it is separable on purpose.
|
|
757
|
-
|
|
758
|
-
---
|
|
759
|
-
|
|
760
|
-
## 11. Tests and gates
|
|
761
|
-
|
|
762
|
-
Device-free (`vitest`, from the moh root), black-box, real objects:
|
|
763
|
-
|
|
764
|
-
- resolved order for a set of extensions with `after` / `before`; a cycle throws and names both;
|
|
765
|
-
unconstrained pairs resolve by name and the result does not change when registration order does.
|
|
766
|
-
- `get` of an absent record throws; `create` twice throws; `has` agrees with both.
|
|
767
|
-
- publication of a superseded handle fails; publication into a frozen record fails; a handle from a
|
|
768
|
-
previous frame fails. `FrameGraph` and a `SoftwareGPUDevice` are enough for all of these — no real
|
|
769
|
-
device, per the standing rule.
|
|
770
|
-
- **an extension whose output nothing consumes is culled, and that is not an error** — the frame
|
|
771
|
-
renders, no assert fires, and the pass reports `culled` in the JSON export. Pinned as a test
|
|
772
|
-
because it is a property somebody will otherwise "fix" (§5.1).
|
|
773
|
-
- an extension's passes land inside its own DOT cluster, and a graph recorded with no extensions
|
|
774
|
-
exports exactly as it did before. The scope machinery is already specced on the graph; what this
|
|
775
|
-
adds is that the registry opens and closes one per extension, including when `record()` throws.
|
|
776
|
-
- `describe()` output is stable and contains every registered extension.
|
|
777
|
-
- registry survives a simulated renderer replacement with the same extensions in the same order.
|
|
778
|
-
|
|
779
|
-
On a device, extending `probeShadePassInjection`:
|
|
780
|
-
|
|
781
|
-
- a colour-writing extension at each colour-carrying phase, in and out, restoring to within the
|
|
782
|
-
existing 25% bar.
|
|
783
|
-
- **a G-buffer-replacing extension at `AfterGBuffer`** — writes a flat albedo, and the lit frame
|
|
784
|
-
changes. The current gate has no such check, which is why the in/out G-buffer has never been
|
|
785
|
-
exercised outside the decal system.
|
|
786
|
-
- **an order flip** — two extensions publishing the colour, with the `after` edge reversed between
|
|
787
|
-
runs, producing different frames. This is the property the current mechanism cannot state at all.
|
|
788
|
-
- **the upscale boundary, at 60% dynamic resolution scale**: an extension at `AfterTransparency` and
|
|
789
|
-
one at `BeforePresent`, each sizing its output from `frame.resolution`, both producing a full,
|
|
790
|
-
uncropped frame. They must resolve to *different* sizes. The `BeforePresent` half of this fails on
|
|
791
|
-
the current mechanism (§1.3b), and the same check run with `feature_taa_enabled = false` must
|
|
792
|
-
still pass, which is what pins `resolution` as derived rather than declared.
|
|
793
|
-
- **fog of war over particles** — the `AfterTransparency` chain resolved from declarations, with a
|
|
794
|
-
particle emitter inside the concealed region staying concealed. This is §1.3a's bug, as a gate.
|
|
795
|
-
|
|
796
|
-
---
|
|
797
|
-
|
|
798
|
-
## 12. Adversarial review
|
|
799
|
-
|
|
800
|
-
The design above is the post-review version. What follows is what was attacked and what changed.
|
|
801
|
-
|
|
802
|
-
**"A type-keyed container is a registry, and the house rule forbids registries."** The rule forbids
|
|
803
|
-
*string*-keyed factories chosen to save boilerplate. A class is a type; the container is a typed
|
|
804
|
-
map. The alternative — a struct with fixed fields — is defect §1.1 restated. *Survived, with the
|
|
805
|
-
concession that every built-in record is an ordinary class with named fields, so the container is
|
|
806
|
-
the only indirection.*
|
|
807
|
-
|
|
808
|
-
**"You claim the graph orders passes. It executes in insertion order."** Correct, and the first
|
|
809
|
-
draft leaned on the graph for ordering. **Changed:** §4 now states the execution model outright, and
|
|
810
|
-
every ordering claim is attributed to the resolver. The design would have been wrong without this.
|
|
811
|
-
|
|
812
|
-
> *Since.* The graph answered this from its own side rather than by being routed around: `validate()`
|
|
813
|
-
> now proves that the recorded insertion order is a valid topological order and `compile()` asserts
|
|
814
|
-
> on it. The design's position is unchanged — §3.6 still owns ordering — but the failure mode it was
|
|
815
|
-
> protecting against narrowed sharply. A recording order that is *inconsistent* is now the graph's
|
|
816
|
-
> to reject; §3.6 is left owning the orders that are consistent and still wrong, which is exactly
|
|
817
|
-
> the fog-over-particles case (§1.3a) and is the only kind of ordering bug the game actually had.
|
|
818
|
-
|
|
819
|
-
**"`scene.color = handle` is untracked mutation — the same side channel you criticised
|
|
820
|
-
`context.gbuffer` for being."** Half right. The criticism of `context.gbuffer` was that it was the
|
|
821
|
-
*only* extra mutable thing, at *one* phase, invisible in the pass signature. **Changed:** publication
|
|
822
|
-
is now uniform across every record and validated at the setter — freeze state, frame identity, and
|
|
823
|
-
`is_valid_resource` against the graph. The last of those catches a class of silent wrong-picture bug
|
|
824
|
-
that neither the current mechanism nor URP catches at all.
|
|
825
|
-
|
|
826
|
-
**"You deleted `resolution` and told every extension to call `describe()`. That is a worse API for
|
|
827
|
-
the case everyone has."** Fair, and the first draft did exactly that in reaction to defect (b).
|
|
828
|
-
**Changed:** `frame.resolution` came back as a derived getter over the phase's live target handle,
|
|
829
|
-
so the common case is one property access *and* correct on both sides of the upscale, with
|
|
830
|
-
`describe()` left for the genuinely two-sized case. The lesson from (b) is that the number must not
|
|
831
|
-
be a field somebody fills in, not that the number should be hard to get.
|
|
832
|
-
|
|
833
|
-
**"Ambiguity detection needs the frame to know who is recording — hidden global state."** One field,
|
|
834
|
-
set by the loop that calls `record`. Re-entrant recording is an assert failure. *Survived.*
|
|
835
|
-
|
|
836
|
-
**"Alphabetical tie-breaking is arbitrary. Why is arbitrary-by-name better than
|
|
837
|
-
arbitrary-by-registration?"** Because it does not change when a system's `startup` gains an `await`.
|
|
838
|
-
Reproducibility is the property, not correctness. **Changed:** the ambiguity check was added in the
|
|
839
|
-
same pass, so an arbitrary order between two extensions that actually conflict is an error rather
|
|
840
|
-
than a silently-chosen order.
|
|
841
|
-
|
|
842
|
-
**"`FrameProduct` is a closed enum, so §1.1 is only half fixed."** Concede. The frame's own optional
|
|
843
|
-
products are Shade's features and enumerating them is correct; the *open* half is the record
|
|
844
|
-
vocabulary, which needs no engine edit. **Changed:** §3.7 says this, the enum was cut to two members
|
|
845
|
-
with named consumers, and it was moved to the last delivery step.
|
|
846
|
-
|
|
847
|
-
**"An `enabled` flag and an early return from `record` are two ways to do one thing."** Agreed.
|
|
848
|
-
**Changed:** the flag was cut.
|
|
849
|
-
|
|
850
|
-
**"`get_or_create` is friendlier than three methods."** It is, and it destroys the property that
|
|
851
|
-
made `-1` go away: absent has to stay distinguishable from empty. *Rejected.*
|
|
852
|
-
|
|
853
|
-
**"Six phases plus a blackboard plus a topological sort, to replace two methods, for nine
|
|
854
|
-
consumers."** The two methods currently need four paragraphs of documentation about culling, carry a
|
|
855
|
-
wrong resolution at two of five phases, and enforce a documented ordering requirement through the
|
|
856
|
-
placement of an `await`. The sort is thirty lines over at most a dozen nodes. *Survived, and it is
|
|
857
|
-
the reason §10 splits diagnostics out: if the sort and the records are the value, B and C can be
|
|
858
|
-
judged separately.*
|
|
859
|
-
|
|
860
|
-
**"`SceneColor` absent at `Overlay` means an extension that wants the tonemapped image just gets a
|
|
861
|
-
throw."** Yes, with a message naming `BeforePresent`. That is strictly better than the current
|
|
862
|
-
outcome, which is a validation error from inside a binding builder, and it is better than the
|
|
863
|
-
documentation-only fix that shipped, which was discovered by measurement after the fact. *Survived.*
|
|
864
|
-
|
|
865
|
-
**"The migration makes somebody decide the `AfterTransparency` order, and they may get it wrong."**
|
|
866
|
-
It was decided while this document was under review, and the order in the shipped game turned out to
|
|
867
|
-
be wrong (§1.3a). That is the argument for declaring it, not a counter to it — a mechanism where the
|
|
868
|
-
answer is written down is a mechanism where being wrong is a diff. **Changed:** §8 states the
|
|
869
|
-
resolved chain instead of leaving it open.
|
|
870
|
-
|
|
871
|
-
**"`describe()` is a feature nobody asked for."** It is the only thing that makes "the frame graph is
|
|
872
|
-
a first-class construct" mean something testable at this seam — a pipeline you can print without a
|
|
873
|
-
device is a pipeline you can spec without a device. *Survived, in delivery step B.*
|
|
874
|
-
|
|
875
|
-
**"Culling is what a frame graph is for. You made it an error for one class of user."** Sponsor, on
|
|
876
|
-
the revision that asked the graph to report per-scope culling so a fully-culled extension could
|
|
877
|
-
assert. **The objection is correct and the check is gone (§5.1).** Three ways it was wrong, and the
|
|
878
|
-
review that produced the rest of this document did not catch any of them:
|
|
879
|
-
|
|
880
|
-
- It adopted the construct and then treated its central behaviour as a failure.
|
|
881
|
-
- The line it drew — culled built-in pass is optimisation, culled extension pass is a bug — has
|
|
882
|
-
nothing behind it. Who recorded a pass does not change what culling means.
|
|
883
|
-
- Fatally, it outlawed a pattern this document's own §3.3 puts forward as the reason the record
|
|
884
|
-
vocabulary is open: an extension publishing `FogOfWarVisibility` for a consumer that may not be
|
|
885
|
-
registered. Disable the consumer and the producer is correctly elided — and the assert would have
|
|
886
|
-
called that a hard error. A speculative producer is the point, not an accident.
|
|
887
|
-
|
|
888
|
-
Withdrawn with it: `FRAME_GRAPH_RECOMMENDATIONS.md` item 5, which was the last open engine change,
|
|
889
|
-
and the provenance check in §3.4, which fell to the same standard once it was applied consistently
|
|
890
|
-
— it forbade publishing an imported texture in order to catch a typo. What the argument leaves
|
|
891
|
-
standing is a bar the remaining checks are now stated against: **a check may only fire on something
|
|
892
|
-
that cannot be true**, never on an extension behaving correctly.
|
|
893
|
-
|
|
894
|
-
---
|
|
895
|
-
|
|
896
|
-
## 13. Engine changes required
|
|
897
|
-
|
|
898
|
-
Small, and enumerated so the blast radius is visible. **`FrameGraph` needs nothing.** The graph-side
|
|
899
|
-
items this document originally asked for have either landed or been withdrawn — the last of them,
|
|
900
|
-
the culling report, was withdrawn on the reasoning in §5.1. Everything below is renderer and facade
|
|
901
|
-
work.
|
|
902
|
-
|
|
903
|
-
1. `Renderer` publishes records at six points instead of filling one context at five. The record
|
|
904
|
-
objects are built where `#frame_pass_context` is built today.
|
|
905
|
-
2. `Renderer` gains a `FrameStart` call site, and (delivery C) honours the product union when
|
|
906
|
-
deciding whether to build velocity and the colour copy.
|
|
907
|
-
3. `
|
|
908
|
-
`add_extension`/`remove_extension`/`extension_count`. No net surface change.
|
|
909
|
-
4. `src/shade/renderer/pass/` becomes `src/shade/renderer/extension/`. The folder is MEEP 3's, not
|
|
910
|
-
Shade's, and "pass" collides with `FramePassNode` — the graph's passes and an extension are
|
|
911
|
-
different things and should not share a word.
|
|
912
|
-
|
|
913
|
-
Nothing in Shade's frame structure, no pass reordering, no change to how built-in passes record.
|
|
914
|
-
|
|
915
|
-
**Already done, and nothing further is needed from them** — `FrameGraphState` and its asserts,
|
|
916
|
-
`validate()` with both order checks, `push_scope` / `pop_scope` and DOT clustering,
|
|
917
|
-
`ResourceNode.producer` at record time, and the `exportToJson` fix. `getDescriptor`,
|
|
918
|
-
`is_valid_resource` and `onExecuted` were already there and the design uses them as they stand.
|
|
1
|
+
# Render extensions — design
|
|
2
|
+
|
|
3
|
+
Replaces `Renderer.add_pass` / `remove_pass`, `FramePhase`, `FramePassContext` and
|
|
4
|
+
`FramePassRegistry` (MEEP 3, phase 2, item S3).
|
|
5
|
+
|
|
6
|
+
Status: **design, not built.** Nothing in §3 through §11 is committed code.
|
|
7
|
+
|
|
8
|
+
**Revised 2026-08-24** against the frame-graph work that has landed since the first draft:
|
|
9
|
+
`FrameGraphState`, `validate()`, recording scopes, record-time producers, and the `exportToJson`
|
|
10
|
+
fix. Those came out of this document's §4 and §13 and are tracked in
|
|
11
|
+
`engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`. §1.2, §3.4, §4, §5 and §13
|
|
12
|
+
describe what the graph does now, not what it did — the net effect is that the design leans on the
|
|
13
|
+
graph in three more places and asks it for nothing.
|
|
14
|
+
|
|
15
|
+
The same revision **withdrew the cull diagnostic** and the engine change it needed, on sponsor
|
|
16
|
+
review: culling is what a frame graph is for, and a check that calls it a failure when an extension
|
|
17
|
+
is the recorder invents a distinction that does not exist and outlaws the speculative producer.
|
|
18
|
+
§5.1 is the argument; the bar it sets — a check may only fire on something that cannot be true —
|
|
19
|
+
also took out a provenance check in §3.4.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 0. Summary
|
|
24
|
+
|
|
25
|
+
An extension is a **named object with declarations** — a phase, ordering constraints, the optional
|
|
26
|
+
frame products it needs — and one method that records into the frame's graph. Everything it exchanges
|
|
27
|
+
with the frame goes through **typed per-frame records** holding frame-graph handles: it reads a
|
|
28
|
+
handle out of a record and writes a replacement back. There is no return value and no privileged
|
|
29
|
+
resource.
|
|
30
|
+
|
|
31
|
+
Three properties follow, and they are the point:
|
|
32
|
+
|
|
33
|
+
1. **Every resource the frame carries is exchangeable**, not just the colour. The G-buffer stops
|
|
34
|
+
being a documented special case, depth stops being read-only, and an extension can publish a
|
|
35
|
+
record of its own that another extension reads.
|
|
36
|
+
2. **Order is declared**, not emergent from the order in which ECS systems happened to reach their
|
|
37
|
+
first `await`.
|
|
38
|
+
3. **The frame can be described without being rendered, and read back attributed once it has been.**
|
|
39
|
+
The set of extensions, their order per phase, and the products they force on are data, so
|
|
40
|
+
`describe()` prints the pipeline; and the registry records each extension inside its own graph
|
|
41
|
+
scope, so a DOT export of the frame groups by who asked for what instead of by whatever the
|
|
42
|
+
`graph_*` helpers named things. That pair is what "the frame graph is a first-class construct"
|
|
43
|
+
has to mean at this seam.
|
|
44
|
+
|
|
45
|
+
Underneath all three, one correction that is not an abstraction: **the upscale is a phase boundary
|
|
46
|
+
and the surface says so.** Three phases are before it and two are after, the before case is the
|
|
47
|
+
common one, and the size of what an extension is drawing into is derived from the live handle rather
|
|
48
|
+
than handed over as a field the renderer fills in once for the whole frame — which is what the
|
|
49
|
+
current mechanism does, and gets wrong at two of its five phases (§1.3b).
|
|
50
|
+
|
|
51
|
+
The shape is Unity 6 URP's — `ContextContainer` / `ContextItem`, `ScriptableRenderPass`,
|
|
52
|
+
`ConfigureInput` — with URP's two worst traits removed (§2).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 1. What is wrong with `add_pass`
|
|
57
|
+
|
|
58
|
+
### 1.1 One resource is modifiable
|
|
59
|
+
|
|
60
|
+
`FramePassRegistry.run` threads exactly one value:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const produced = passes[i](context);
|
|
64
|
+
if (produced === undefined) { continue; }
|
|
65
|
+
context.color = produced;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A pass's only sanctioned output is a colour handle. Everything else on `FramePassContext` — depth,
|
|
69
|
+
previous depth, the four G-buffer targets, the two visibility targets — is an input. The one
|
|
70
|
+
exception, the G-buffer being in/out at `AfterGBuffer`, is implemented by letting the pass reach into
|
|
71
|
+
`context.gbuffer` and assign, and costs three paragraphs of prose in `FramePassContext` to explain
|
|
72
|
+
why replacing beats writing in place.
|
|
73
|
+
|
|
74
|
+
What that closes off, concretely: writing depth (terrain and water both rasterize geometry that
|
|
75
|
+
should own depth), publishing a resource an extension computed for other extensions to read (fog of
|
|
76
|
+
war's visibility grid is exactly this and currently lives outside the graph), and reading anything
|
|
77
|
+
the frame builds but the struct does not name — velocity, the Hi-Z pyramid, the TAA history, the
|
|
78
|
+
shadow atlas, the light cluster tables.
|
|
79
|
+
|
|
80
|
+
The struct is closed by construction: adding a field is an edit to Shade. That is the wrong cost for
|
|
81
|
+
"my game needs one more thing from the frame".
|
|
82
|
+
|
|
83
|
+
### 1.2 The graph is an escape hatch, not the mechanism
|
|
84
|
+
|
|
85
|
+
`context.graph` hands the pass the `FrameGraph` and steps back. The registry never learns what the
|
|
86
|
+
pass read or wrote, so:
|
|
87
|
+
|
|
88
|
+
- **It cannot order passes.** It orders by registration order.
|
|
89
|
+
- **It cannot attribute anything.** `exportToDot` shows the nodes an extension created with whatever
|
|
90
|
+
names its helpers gave them, unrelated to the extension. The graph has since gained recording
|
|
91
|
+
scopes, which are exactly the missing mechanism — but they are opt-in and `add_pass` opens none,
|
|
92
|
+
so a frame's DOT export is still one flat wall of passes.
|
|
93
|
+
- **It cannot validate.** Reading `context.color` at `AfterGBuffer` yields `-1` and fails somewhere
|
|
94
|
+
inside a binding builder. Retaining a handle across frames is undetectable. Publishing a stale
|
|
95
|
+
handle is now caught, but by the graph at `compile()` and against the *reader* — the pass that
|
|
96
|
+
consumed it — so the report names the built-in pass that broke, never the extension that handed
|
|
97
|
+
it the bad handle.
|
|
98
|
+
- **`AfterGBuffer` has no return channel, so the culling contract cannot be met there.** The rule
|
|
99
|
+
itself is the graph's, and it is right everywhere: work nothing consumes is elided. But the only
|
|
100
|
+
way this mechanism lets a pass thread its result into the frame is by returning a colour handle,
|
|
101
|
+
and at `AfterGBuffer` there is no colour — so a pass whose G-buffer work *should* be consumed
|
|
102
|
+
reaches for `make_side_effect()` instead, which does not say "my output is used", it says "run me
|
|
103
|
+
anyway". Three paragraphs of `FramePhase` document the workaround. The record model removes the
|
|
104
|
+
reason for it (§3.3) and leaves the culling rule alone (§5.1).
|
|
105
|
+
|
|
106
|
+
And the injection points themselves are call sites: a phase is wherever `#run_frame_passes` was
|
|
107
|
+
typed inside an 800-line method. There are five because five were typed.
|
|
108
|
+
|
|
109
|
+
### 1.3 What this has already produced
|
|
110
|
+
|
|
111
|
+
Four findings from reading the current code, each independent of the design that replaces it.
|
|
112
|
+
|
|
113
|
+
**(a) Order is an accident, and the code says so.** `TerrainSystem.startup`:
|
|
114
|
+
|
|
115
|
+
> `// before anything is awaited, so that pass order within the phase is the profile's system order —`
|
|
116
|
+
> `// the decal composite has to see the textured ground, not precede it`
|
|
117
|
+
|
|
118
|
+
An ordering requirement between two extensions is being met by placing a registration call above an
|
|
119
|
+
`await`. Adding an `await` to a system's startup silently reorders the frame.
|
|
120
|
+
|
|
121
|
+
The same mechanism decides the six-deep chain at `AfterTransparency`, and **it has already produced a
|
|
122
|
+
shipped bug**. In profile order that chain is fog of war, particles, path display, highlight, debug
|
|
123
|
+
draw, trails — so the fog composites *first* and everything drawn after it is unfogged. Confirmed
|
|
124
|
+
with the sponsor: that is not intended. Fog of war conceals, so it belongs over everything it
|
|
125
|
+
conceals; particles drawing on top of it defeat what it is for.
|
|
126
|
+
|
|
127
|
+
Nobody chose that order. Nothing recorded it. Nothing failed when it turned out wrong. That is the
|
|
128
|
+
argument for §3.6 in one paragraph.
|
|
129
|
+
|
|
130
|
+
**(b) The one size the context states is wrong at two phases.** `Renderer` fills
|
|
131
|
+
`pass_context.resolution` from `resolution_internal.asArray()`, once, for all five phases. But TAA
|
|
132
|
+
and NSS upscale to `output_resolution` before `BeforePresent`, and the tonemap writes the canvas at
|
|
133
|
+
output resolution — so at `BeforePresent` and `Overlay` the colour is *not* the size the context
|
|
134
|
+
reports, whenever TAA is on. With TAA off it is, because the upscale is missing (there is a `TODO`
|
|
135
|
+
in `Renderer` saying so). The size of the frame's colour at `BeforePresent` is therefore not
|
|
136
|
+
derivable from the context at all; it depends on a renderer feature flag the extension cannot see.
|
|
137
|
+
|
|
138
|
+
`probeShadePassInjection` — the gate for the current mechanism — does
|
|
139
|
+
`output_resolution: context.resolution` at `BeforePresent`. It passes because the probe runs at 100%
|
|
140
|
+
scale. The game ships with dynamic resolution scaling enabled against a 30 fps floor, and at any
|
|
141
|
+
scale below 100% that pass produces a cropped, wrongly-scaled frame while the gate still measures a
|
|
142
|
+
luminance change and reports green.
|
|
143
|
+
|
|
144
|
+
The single field is the tell: the surface was built as though a frame had one resolution. Three of
|
|
145
|
+
the five phases are before the upscale and two are after it, and nothing in the surface says which
|
|
146
|
+
is which — `FramePhase`'s own text for `BeforePresent` says "still HDR" and never mentions that the
|
|
147
|
+
colour has been upscaled by then. §3.5 makes the boundary part of what a phase *is*.
|
|
148
|
+
|
|
149
|
+
**(c) `-1` is a sentinel with no guard.** `color` is `-1` at `AfterGBuffer`. Nothing stops a pass
|
|
150
|
+
reading it. The house rule is that the common case is the only case and preconditions are asserted,
|
|
151
|
+
not encoded as magic values.
|
|
152
|
+
|
|
153
|
+
**(d) The G-buffer special case exists only because colour owns the return channel.** Given a
|
|
154
|
+
uniform way to publish any handle, "the G-buffer is in/out at one phase" stops being a rule and
|
|
155
|
+
becomes an instance of the general one.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 2. What is borrowed from URP, and what is not
|
|
160
|
+
|
|
161
|
+
Borrowed:
|
|
162
|
+
|
|
163
|
+
| URP 6 | Here | Why |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `ContextContainer` + `ContextItem` | `FrameContext.get(RecordType)` + frame records | The open, typed resource vocabulary. This is the fix for §1.1, and the reason it is keyed by type rather than by string is that the house rule forbids string-keyed registries. |
|
|
166
|
+
| `UniversalResourceData` | `SceneColor`, `GBufferTextures`, `ViewTextures`, `PresentTarget` | Built-in resources are ordinary classes with named fields, carried in the same container as anyone else's. One mechanism, not two. |
|
|
167
|
+
| `ScriptableRenderPass` | `RenderExtension` | A declaring object, not a closure. Declarations are what makes ordering, validation and description possible. |
|
|
168
|
+
| `ConfigureInput(ScriptableRenderPassInput.Motion)` | `RenderExtension.requires` + `FrameProduct` | An extension states what the frame must produce and the renderer turns it on. This is the answer to "velocity exists, is built, and is unreachable". |
|
|
169
|
+
| Renderer feature list, ordered | The registry's resolved order, printable | The pipeline is inspectable before it runs. |
|
|
170
|
+
|
|
171
|
+
Not borrowed, deliberately:
|
|
172
|
+
|
|
173
|
+
- **`RenderPassEvent` + integer offsets.** URP orders same-event passes by an `int` you add to the
|
|
174
|
+
enum. It is the same defect as registration order with an extra step: unreadable, unstable under
|
|
175
|
+
insertion, and it makes every ordering question a numerology question. Replaced by explicit
|
|
176
|
+
`after` / `before` constraints over extension classes (§3.6).
|
|
177
|
+
- **`SetGlobalTexture` / global shader state.** An implicit channel between passes that the graph
|
|
178
|
+
cannot see. Records are the channel.
|
|
179
|
+
- **`AddUnsafePass`.** URP needs a legacy `CommandBuffer` escape hatch. There is no legacy to carry
|
|
180
|
+
here, and adding an unchecked door on day one guarantees it becomes the door.
|
|
181
|
+
- **Two-level feature/pass split.** URP splits `ScriptableRendererFeature` from
|
|
182
|
+
`ScriptableRenderPass` because a feature is a `ScriptableObject` living on an asset. That reason
|
|
183
|
+
does not exist here, so there is one class.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 3. The model
|
|
188
|
+
|
|
189
|
+
### 3.1 `RenderExtension`
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
export class RenderExtension {
|
|
193
|
+
/**
|
|
194
|
+
* Stable identity. Used in ordering diagnostics, error decoration and `describe()`.
|
|
195
|
+
* @type {string}
|
|
196
|
+
*/
|
|
197
|
+
name = "";
|
|
198
|
+
|
|
199
|
+
/** @type {FramePhase} */
|
|
200
|
+
phase = FramePhase.AfterTransparency;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extension classes this one records after / before. Constructors, not instances — an
|
|
204
|
+
* extension does not need a reference to another system's object to state an order.
|
|
205
|
+
* @type {Function[]}
|
|
206
|
+
*/
|
|
207
|
+
after = [];
|
|
208
|
+
before = [];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Optional frame products this extension reads. A bitmask of {@link FrameProduct}.
|
|
212
|
+
* Reading a product nobody required is an assert failure, not a silent `-1`.
|
|
213
|
+
* @type {number}
|
|
214
|
+
*/
|
|
215
|
+
requires = 0;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Record this extension's work into the frame. Called once per frame, when the phase comes
|
|
219
|
+
* round. Returns nothing: everything this produces reaches the rest of the frame by
|
|
220
|
+
* publication (§3.4).
|
|
221
|
+
*
|
|
222
|
+
* @param {FrameContext} frame
|
|
223
|
+
*/
|
|
224
|
+
record(frame) {}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`record` returning `void` is load-bearing. It is what makes colour stop being privileged.
|
|
229
|
+
|
|
230
|
+
An extension that has nothing to draw this frame returns without recording; that is the only "off"
|
|
231
|
+
switch and there is no `enabled` flag beside it.
|
|
232
|
+
|
|
233
|
+
### 3.2 `FrameContext`
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
export class FrameContext {
|
|
237
|
+
/** @type {FrameGraph} the frame's graph — record into this with the `graph_*` helpers */
|
|
238
|
+
graph;
|
|
239
|
+
/** @type {ViewRenderContext} */
|
|
240
|
+
view;
|
|
241
|
+
/** @type {FramePhase} */
|
|
242
|
+
phase;
|
|
243
|
+
|
|
244
|
+
/** @template T @param {new()=>T} type @returns {T} throws if not present at this phase */
|
|
245
|
+
get(type) {}
|
|
246
|
+
|
|
247
|
+
/** @param {Function} type @returns {boolean} */
|
|
248
|
+
has(type) {}
|
|
249
|
+
|
|
250
|
+
/** @template T @param {new()=>T} type @returns {T} adds a record this extension owns */
|
|
251
|
+
create(type) {}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Pixel size of the target this phase carries, derived from the live handle rather than filled
|
|
255
|
+
* in by the renderer.
|
|
256
|
+
* @returns {number[]}
|
|
257
|
+
*/
|
|
258
|
+
get resolution() {}
|
|
259
|
+
|
|
260
|
+
/** @param {number} handle @returns {ResourceDescriptor} size, format and usage of a handle */
|
|
261
|
+
describe(handle) {}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Three accessors rather than one `get_or_create`, because "absent" has to stay distinguishable from
|
|
266
|
+
"empty" — that is what lets `get(SceneColor)` at `AfterGBuffer` throw with a sentence instead of
|
|
267
|
+
handing back `-1`.
|
|
268
|
+
|
|
269
|
+
**`resolution` is derived, and that is the whole of the fix for defect (b).** It reads the graph
|
|
270
|
+
through the handle this phase's target actually is, so it is right before the upscale, right after
|
|
271
|
+
it, right when TAA is off and the upscale does not happen at all, and right at every dynamic
|
|
272
|
+
resolution scale. The common case — "how big is the thing I am drawing into" — stays one property
|
|
273
|
+
access and cannot be wrong.
|
|
274
|
+
|
|
275
|
+
`describe(handle)` is the general form, and it is needed whenever two things in one pass are
|
|
276
|
+
different sizes: an extension at `BeforePresent` reading the G-buffer is working across the upscale
|
|
277
|
+
boundary and has to ask for both.
|
|
278
|
+
|
|
279
|
+
> Both lean on descriptors being trustworthy, which was checked: transient and imported resources
|
|
280
|
+
> both carry a real `TextureResourceDescriptor` holding absolute pixels.
|
|
281
|
+
> `TextureResourceDescriptor.resolution_domain` says otherwise by default and is vestigial — nothing
|
|
282
|
+
> branches on it, only `hash()` folds it in. It stays; it is now documented as vestigial on the field
|
|
283
|
+
> itself so the next reader does not take it for a contract.
|
|
284
|
+
|
|
285
|
+
### 3.3 Frame records
|
|
286
|
+
|
|
287
|
+
A record is a plain class holding frame-graph handles. The renderer publishes the built-in ones as
|
|
288
|
+
the frame reaches the phase where they are meaningful.
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
/** present from AfterGBuffer */
|
|
292
|
+
class GBufferTextures { albedo; normal; pbr; emissive; }
|
|
293
|
+
|
|
294
|
+
/** present from AfterGBuffer */
|
|
295
|
+
class ViewTextures {
|
|
296
|
+
depth; depth_previous;
|
|
297
|
+
visibility_mesh; visibility_triangle;
|
|
298
|
+
velocity; // requires FrameProduct.Velocity
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** present from AfterLighting through BeforePresent */
|
|
302
|
+
class SceneColor {
|
|
303
|
+
color;
|
|
304
|
+
copy; // requires FrameProduct.ColorCopy
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** present at Overlay, and only there */
|
|
308
|
+
class PresentTarget { canvas; }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Presence by phase:
|
|
312
|
+
|
|
313
|
+
| phase | records | resolution |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| `FrameStart` | — (the view only) | internal |
|
|
316
|
+
| `AfterGBuffer` | `GBufferTextures` (writable), `ViewTextures` | internal |
|
|
317
|
+
| `AfterLighting` | `GBufferTextures` (frozen), `ViewTextures`, `SceneColor` | internal |
|
|
318
|
+
| `AfterTransparency` | as above | internal |
|
|
319
|
+
| `BeforePresent` | as above — but `SceneColor` and `GBufferTextures` are now different sizes | output |
|
|
320
|
+
| `Overlay` | `PresentTarget` | output |
|
|
321
|
+
|
|
322
|
+
Two of those rows are decisions worth naming.
|
|
323
|
+
|
|
324
|
+
**`SceneColor` is absent at `Overlay`, and `PresentTarget` is not `SceneColor`.** The current
|
|
325
|
+
mechanism hands the tonemapped colour to `Overlay` passes and then documents, at length, that they
|
|
326
|
+
must not read it — a texture cannot be a render attachment and a sampled source in the same pass.
|
|
327
|
+
That was found by measurement after the fact. Handing back a differently-named record with one field
|
|
328
|
+
says the same thing structurally: there is no `color` at `Overlay` to be tempted by, and an
|
|
329
|
+
extension that wants the finished image as *input* discovers at `get` time that it belongs at
|
|
330
|
+
`BeforePresent`.
|
|
331
|
+
|
|
332
|
+
**`GBufferTextures` is present but frozen after `AfterGBuffer`.** Outline and highlight effects
|
|
333
|
+
legitimately read the G-buffer and the visibility buffer late. Publishing into it late is
|
|
334
|
+
meaningless — shading has run — so the record is frozen and the assert says why (§5). At
|
|
335
|
+
`BeforePresent` it is also on the far side of the upscale from the colour (§3.5), so the two are
|
|
336
|
+
different sizes; `describe()` is how a pass learns the ratio, and the record's own doc comment says
|
|
337
|
+
to expect it.
|
|
338
|
+
|
|
339
|
+
Extensions add their own records with `create`:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
export class FogOfWarVisibility { grid; } // a graph buffer handle
|
|
343
|
+
|
|
344
|
+
// producer, at FrameStart
|
|
345
|
+
frame.create(FogOfWarVisibility).grid = this.#update_grid(frame.graph);
|
|
346
|
+
|
|
347
|
+
// consumer, at AfterTransparency
|
|
348
|
+
if (frame.has(FogOfWarVisibility)) { ... }
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Which is the second half of the fix for §1.1: the vocabulary is open, and extending it costs no
|
|
352
|
+
edit to Shade.
|
|
353
|
+
|
|
354
|
+
### 3.4 Publication
|
|
355
|
+
|
|
356
|
+
A field on a record is an accessor. Writing it is publication:
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
const scene = frame.get(SceneColor);
|
|
360
|
+
|
|
361
|
+
const [fogged] = graph_image_pass({
|
|
362
|
+
graph: frame.graph,
|
|
363
|
+
shader: this.#shader,
|
|
364
|
+
output_resolution: frame.describe(scene.color).resolution,
|
|
365
|
+
inputs: { input_color: scene.color, input_depth: frame.get(ViewTextures).depth }
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
scene.color = fogged; // the rest of the frame carries this
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The setter asserts three things, all free in production:
|
|
372
|
+
|
|
373
|
+
1. The record is not frozen (§3.3), and the frame index matches — a handle captured last frame
|
|
374
|
+
cannot be published this frame.
|
|
375
|
+
2. `graph.is_valid_resource(handle)` — the handle is the current version of its resource.
|
|
376
|
+
3. The current phase is one where the record is writable.
|
|
377
|
+
|
|
378
|
+
Check 2 exists for **attribution**, not detection. The graph catches a superseded handle on its
|
|
379
|
+
own: whoever reads it next trips `validate()`'s superseded-read check at `compile()` (§4). But that
|
|
380
|
+
report names the *reader* — some built-in pass downstream — and says nothing about which extension
|
|
381
|
+
put the bad handle into the record. Checking at the setter fails at the line that caused it, with
|
|
382
|
+
the extension's name on it. It forbids nothing: one `ResourceEntry` owns one concrete resource, so
|
|
383
|
+
an older node is not a snapshot of older contents and publishing one is wrong in every case.
|
|
384
|
+
|
|
385
|
+
**A provenance check was considered here and rejected** — "the handle's producer is a pass recorded
|
|
386
|
+
inside this extension's scope", which the record-time producer (§4) made possible. It would have
|
|
387
|
+
forbidden a legitimate thing: `import_resource` leaves a node with no producer, so an extension that
|
|
388
|
+
imports a texture it owns and publishes it as the frame's colour — a debug view, video playback —
|
|
389
|
+
would have failed a check aimed at typos. What it uniquely caught, publishing a handle you read
|
|
390
|
+
rather than wrote, surfaces immediately downstream anyway.
|
|
391
|
+
|
|
392
|
+
Reading a field asserts frame identity and, for product-gated fields, that the *recording extension*
|
|
393
|
+
declared the product (§3.7).
|
|
394
|
+
|
|
395
|
+
**Publication is not the dependency edge.** The graph already has the edge: `FramePassBuilder.write`
|
|
396
|
+
renames a written resource into a versioned clone and records the read, so "this pass consumed the
|
|
397
|
+
colour as it stood and produced a new one" is already a graph fact. Publication is the *naming*
|
|
398
|
+
step — which handle currently means "the scene colour". Keeping those two ideas separate is what
|
|
399
|
+
lets the record model be this small.
|
|
400
|
+
|
|
401
|
+
### 3.5 Phases
|
|
402
|
+
|
|
403
|
+
`FrameStart`, `AfterGBuffer`, `AfterLighting`, `AfterTransparency`, `BeforePresent`, `Overlay`.
|
|
404
|
+
|
|
405
|
+
**The upscale is a phase boundary, and saying so is part of what a phase means.** `AfterGBuffer`,
|
|
406
|
+
`AfterLighting` and `AfterTransparency` are before it and run at internal resolution; TAA and NSS
|
|
407
|
+
upscale to output resolution, so `BeforePresent` and `Overlay` are after it and run at output
|
|
408
|
+
resolution.
|
|
409
|
+
|
|
410
|
+
**Before the upscale is the common case, and the design leans on it.** An extension drawing into the
|
|
411
|
+
scene belongs at `AfterTransparency`: its work is then temporally resolved, upscaled and sharpened
|
|
412
|
+
along with everything else, and it is the cheapest place to draw because it draws at internal
|
|
413
|
+
resolution. `BeforePresent` is the deliberate other choice — full output resolution, still HDR,
|
|
414
|
+
nothing after it but the tonemap — and it is a trade, not an upgrade: work there gets no temporal
|
|
415
|
+
history and costs full-resolution pixels. An extension that does not have a reason to be after the
|
|
416
|
+
upscale should not be.
|
|
417
|
+
|
|
418
|
+
One wrinkle, and it belongs to the renderer rather than to this design: with
|
|
419
|
+
`feature_taa_enabled === false` the upscale does not happen at all — there is a `TODO` in `Renderer`
|
|
420
|
+
saying exactly that — so in that configuration `BeforePresent` is at internal resolution. A derived
|
|
421
|
+
`frame.resolution` (§3.2) is right in both configurations. A documented constant would be wrong in
|
|
422
|
+
one, which is how the current mechanism ended up wrong in two.
|
|
423
|
+
|
|
424
|
+
Five phases are the existing ones, unchanged in meaning. **`FrameStart` is the one addition**: a point after
|
|
425
|
+
the view is updated and before anything is rasterized. It exists because per-frame GPU compute that
|
|
426
|
+
later phases depend on currently has nowhere to go — fog of war's grid update, a custom cull, a
|
|
427
|
+
procedural buffer build — so it is either bolted onto a draw pass at a later phase, which puts it in
|
|
428
|
+
the wrong place in the graph and mis-attributes it in a capture, or done outside the graph entirely,
|
|
429
|
+
which is precisely the aliasing-and-lifetime blindness §1.2 is about.
|
|
430
|
+
|
|
431
|
+
Considered and rejected: `BeforeGBuffer` (nothing distinguishes it from `FrameStart` for anything an
|
|
432
|
+
extension can do), `AfterShadows` (shadow rendering is per-light and interior to
|
|
433
|
+
`GPULightCollection`; a phase there would be a lie about what is stable), per-post-effect phases
|
|
434
|
+
(URP has seventeen events and their number is a symptom, not a feature).
|
|
435
|
+
|
|
436
|
+
### 3.6 Ordering
|
|
437
|
+
|
|
438
|
+
Within a phase, order is the topological order of the `after` / `before` edges. Cycles throw at
|
|
439
|
+
registration, naming the cycle. Both directions exist because an extension that must precede one it
|
|
440
|
+
cannot edit is exactly the case an extension mechanism is for.
|
|
441
|
+
|
|
442
|
+
Unconstrained pairs are broken by `name`, ascending. Stable and reproducible; deliberately *not*
|
|
443
|
+
registration order, so that adding an `await` to a system's startup cannot change the picture.
|
|
444
|
+
|
|
445
|
+
Name-order is stable, not correct. So the resolver also reports **ambiguity**: two extensions on one
|
|
446
|
+
phase that publish the same field of the same record, with no ordering path between them, is an
|
|
447
|
+
assert failure naming both and the field. Publication goes through our setters, so the frame knows
|
|
448
|
+
who published what — no extra syntax on the author's side. This is the check that would have made
|
|
449
|
+
somebody write down what the six-deep `AfterTransparency` chain is supposed to be.
|
|
450
|
+
|
|
451
|
+
The ambiguity check is per-frame and assert-only. It costs nothing in production and fires on the
|
|
452
|
+
first frame in development.
|
|
453
|
+
|
|
454
|
+
### 3.7 Products
|
|
455
|
+
|
|
456
|
+
`FrameProduct` is a bitmask of things the frame *can* build but does not always:
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
export const FrameProduct = {
|
|
460
|
+
/** per-object + camera velocity, built even when TAA and motion blur are off */
|
|
461
|
+
Velocity: 1,
|
|
462
|
+
/** a readable snapshot of the scene colour as it stood when the phase began */
|
|
463
|
+
ColorCopy: 2,
|
|
464
|
+
};
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The union over registered extensions is computed once per registration change, and the renderer
|
|
468
|
+
consults it the way it consults its own feature flags. `ColorCopy` is produced immediately before
|
|
469
|
+
the passes of each phase that requires it.
|
|
470
|
+
|
|
471
|
+
Two members, because two have a nameable consumer: `Velocity` for any effect an extension wants to
|
|
472
|
+
temporally reproject itself (currently built for TAA, unreachable by anyone else), and `ColorCopy`
|
|
473
|
+
for refraction — a pass that rasterizes geometry *into* the live colour with a `load` op and must
|
|
474
|
+
also sample what is behind it, which is refractive water and distortion particles. A full-screen
|
|
475
|
+
pass that reads the colour and writes a new one does not need it and must not ask for it.
|
|
476
|
+
|
|
477
|
+
Considered and deferred with reasons in §9: `HiZ`, `PreviousColor`, `SceneDepthPyramid`.
|
|
478
|
+
|
|
479
|
+
This is the part of the design with the weakest present-day demand and it ships last (§10).
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 4. What the frame graph does, and what it does not
|
|
484
|
+
|
|
485
|
+
Stated plainly, because the design would be dishonest without it.
|
|
486
|
+
|
|
487
|
+
`FrameGraph.execute` iterates `__pass_nodes` **in insertion order**. It does not topologically sort,
|
|
488
|
+
and there is no plan for it to. What the graph gives is culling (`ref_count` propagation from
|
|
489
|
+
unreferenced resources), transient resource lifetime and aliasing, versioned renaming of written
|
|
490
|
+
resources, and export.
|
|
491
|
+
|
|
492
|
+
So: **the graph does not order extensions; §3.6 does.** Every claim in this document about ordering
|
|
493
|
+
is a claim about the resolver, not about the graph.
|
|
494
|
+
|
|
495
|
+
**What the graph does instead of sorting is check.** The first draft of this document listed two
|
|
496
|
+
implementation constraints the design had to route around. Both are now fixed, and a third thing
|
|
497
|
+
landed that the design had assumed away, so the division of labour is different from what §3.6 was
|
|
498
|
+
written against. The four that matter here — the full list and its evidence are in
|
|
499
|
+
`engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`:
|
|
500
|
+
|
|
501
|
+
- **`validate()` proves the insertion order is a valid topological order**, and `compile()` asserts
|
|
502
|
+
on it. It reports a *read before write* (a pass reading a node a later pass produces) and a
|
|
503
|
+
*superseded read* (a pass reading a version an earlier pass has already replaced — one
|
|
504
|
+
`ResourceEntry` owns one concrete resource, so an older node is not a snapshot). This is the
|
|
505
|
+
honest answer to insertion-order execution: not to sort, but to refuse a recording whose order is
|
|
506
|
+
wrong. It is asserted, so it costs nothing shipped and fails the frame in development.
|
|
507
|
+
- **The lifecycle is one-shot and stated.** `FrameGraphState`: `Recording` → `Compiled` →
|
|
508
|
+
`Executed`, monotonic, asserted at every mutating entry point. `compile()` was not idempotent and
|
|
509
|
+
`execute()` could not tell whether `compile()` had run — a graph executed uncompiled culls itself
|
|
510
|
+
entirely and says nothing. The design's old "the extension layer must never compile the graph
|
|
511
|
+
itself" caveat is deleted: the graph says so now.
|
|
512
|
+
- **`ResourceNode.producer` is assigned at record time**, by `FramePassBuilder`, rather than in
|
|
513
|
+
`compile()`. So "this handle was produced by the pass that is publishing it" is answerable at
|
|
514
|
+
publication (§3.4).
|
|
515
|
+
- **Recording scopes exist.** `push_scope(name)` / `pop_scope()` stamp every pass and resource node
|
|
516
|
+
created inside them, nesting through a parent chain. This supersedes §13's request for a
|
|
517
|
+
`pass_count` getter and is what §1.2's attribution complaint was asking for.
|
|
518
|
+
|
|
519
|
+
One finding from that work belongs here rather than there, because it is the strongest available
|
|
520
|
+
argument for the whole model: `validate()`, on its first run against the live 61-pass `Shading`
|
|
521
|
+
graph, found that **Shade's own rasterization was not threading its colour targets.** Depth was
|
|
522
|
+
threaded through the two-phase opaque pass and colour was not, so every rasterization pass read
|
|
523
|
+
`out_color/0` at version 0 while depth advanced. It was harmless only because those passes write an
|
|
524
|
+
imported resource and therefore always execute, and because insertion order happened to give the
|
|
525
|
+
right picture — the colour dependency between them was simply not a graph fact. That is what
|
|
526
|
+
"working around the graph rather than through it" costs, in the engine's own code, found the day
|
|
527
|
+
someone checked.
|
|
528
|
+
|
|
529
|
+
What remains true, and what §3.6 rests on: the graph checks that a recording order is *consistent*,
|
|
530
|
+
never that it is the one anybody wanted. Two extensions each reading the colour and writing a new
|
|
531
|
+
one are valid in either order. Data-order violations are the graph's; visual-order mistakes are
|
|
532
|
+
§3.6's, and nothing else will catch them.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## 5. Validation and diagnostics
|
|
537
|
+
|
|
538
|
+
All of it under `assert`, which is compiled out of production builds — so the price of every check
|
|
539
|
+
here is zero in a shipped game and the checks may be as thorough as they like.
|
|
540
|
+
|
|
541
|
+
| check | when | what it catches |
|
|
542
|
+
| --- | --- | --- |
|
|
543
|
+
| record present at phase | `get` | reading `SceneColor` at `AfterGBuffer` — replaces `-1` |
|
|
544
|
+
| record frozen | publish | publishing a G-buffer handle after shading |
|
|
545
|
+
| frame identity | read + publish | a handle retained across frames |
|
|
546
|
+
| `is_valid_resource` | publish | publishing a superseded handle, attributed to the extension |
|
|
547
|
+
| product declared by the recording extension | read | reading `velocity` because *someone else* required it |
|
|
548
|
+
| ordering cycle | registration | `after`/`before` cycle, named |
|
|
549
|
+
| publication ambiguity | end of phase | two extensions writing one field with no order between them |
|
|
550
|
+
|
|
551
|
+
Beneath all of them the graph runs its own `validate()` inside `compile()` (§4), which catches the
|
|
552
|
+
data-order failures this layer cannot see.
|
|
553
|
+
|
|
554
|
+
Every one of these fires on a **statement that cannot be true** — a superseded handle names one
|
|
555
|
+
physical resource holding newer contents, a cycle has no order, an undeclared collision has no
|
|
556
|
+
answer. None of them fires on an extension that is behaving correctly. That is the bar; §5.1 is
|
|
557
|
+
what happens when a check is written without holding to it.
|
|
558
|
+
|
|
559
|
+
### 5.1 Culling is not an error, and no diagnostic here treats it as one
|
|
560
|
+
|
|
561
|
+
An earlier revision of this document asked the graph to report per-scope culling so that "an
|
|
562
|
+
extension whose entire recorded work was culled" could be an assert. **That was wrong and it is
|
|
563
|
+
removed**, along with the engine change it needed. Three reasons, in increasing order of weight.
|
|
564
|
+
|
|
565
|
+
Culling is what a frame graph is *for*. Frostbite's paper is about eliminating work nobody
|
|
566
|
+
consumes; that is why the pattern spread. A mechanism cannot adopt the construct and then treat its
|
|
567
|
+
central behaviour as a failure.
|
|
568
|
+
|
|
569
|
+
The distinction it drew does not exist. "Culled built-in pass: optimisation. Culled extension pass:
|
|
570
|
+
bug." Nothing about the recorder changes what culling means, and encoding the split would have made
|
|
571
|
+
extensions second-class in the one place the design claims they are not.
|
|
572
|
+
|
|
573
|
+
And it forbids the pattern this design exists to enable. §3.3's own worked example is an extension
|
|
574
|
+
publishing `FogOfWarVisibility` at `FrameStart` for a consumer at `AfterTransparency`. Take the
|
|
575
|
+
consumer out — disable fog — and the producer's work is dead, the graph elides it, and the frame is
|
|
576
|
+
correct and cheaper. Under the assert that is a hard error. A speculative producer whose consumer
|
|
577
|
+
may or may not be present is the whole point of an open record vocabulary, and the check would have
|
|
578
|
+
outlawed it on day one.
|
|
579
|
+
|
|
580
|
+
**What replaces it: nothing, because nothing is missing.** `exportToJson` already reports
|
|
581
|
+
`culled` per pass and `exportToDot` already greys culled passes out; with the registry's scopes
|
|
582
|
+
(§5.2) a culled extension is a grey cluster with its own name on it. An author asking "why did my
|
|
583
|
+
pass not run" dumps the graph and sees it. That is a tool, not a tribunal.
|
|
584
|
+
|
|
585
|
+
The culling contract itself is stated once, on `RenderExtension.record`, in the same terms it holds
|
|
586
|
+
for every other pass in the graph: work whose output nothing consumes is elided; thread your result
|
|
587
|
+
into a record and it is consumed; if you want work that runs regardless, `make_side_effect()` says
|
|
588
|
+
so, exactly as it does for a built-in pass.
|
|
589
|
+
|
|
590
|
+
### 5.2 Recording scopes
|
|
591
|
+
|
|
592
|
+
**The registry opens a recording scope per extension**, named after it, around the `record()` call:
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
graph.push_scope(extension.name);
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
extension.record(frame);
|
|
599
|
+
} finally {
|
|
600
|
+
graph.pop_scope();
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The `finally` is not error handling — nothing is caught and nothing is swallowed. It is there
|
|
605
|
+
because `compile()` asserts the scope stack came back empty, so an extension that throws while
|
|
606
|
+
recording would otherwise fail on an unbalanced scope and bury the real error behind it. The throw
|
|
607
|
+
propagates, decorated with the extension's name the way `FrameGraph` already decorates a failing
|
|
608
|
+
pass with its own.
|
|
609
|
+
|
|
610
|
+
**Scopes are for reading the frame back, not for policing it.** `exportToDot` puts each extension's
|
|
611
|
+
passes in its own labelled cluster, nested as the scopes nest, and a graph that opens no scope
|
|
612
|
+
exports byte-identically to before. That is the whole of §1.2's attribution complaint, answered by
|
|
613
|
+
an API that already exists and that nothing currently calls — and it is what makes §5.1's answer to
|
|
614
|
+
culling a real answer rather than a shrug: the grey box in the dump has the extension's name on it.
|
|
615
|
+
|
|
616
|
+
No check depends on scopes. The one that did is gone (§5.1), and the provenance check that would
|
|
617
|
+
have used `is_within` was rejected on its own merits (§3.4).
|
|
618
|
+
|
|
619
|
+
`RenderExtensionRegistry.describe()` returns the resolved pipeline as text: per phase, the extensions
|
|
620
|
+
in resolved order, each with the products it required and the records it publishes into. Printable
|
|
621
|
+
without a device, which is what makes it testable without one.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 6. Lifecycle
|
|
626
|
+
|
|
627
|
+
Registration is device-free and survives device loss — the current mechanism's one clearly right
|
|
628
|
+
decision, kept. `GraphicsEngine` holds the extension list; a renderer replaced by a restart is
|
|
629
|
+
re-populated from it. A system registers once at startup without knowing whether a device exists.
|
|
630
|
+
|
|
631
|
+
```js
|
|
632
|
+
graphics.add_extension(extension); // returns the extension
|
|
633
|
+
graphics.remove_extension(extension); // returns whether it was registered
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Identity is the object, so the phase no longer has to be passed to removal. Registering or removing
|
|
637
|
+
during `record` is an assert failure.
|
|
638
|
+
|
|
639
|
+
`GraphicsEngine` gains no surface it did not have: this replaces `add_pass` / `remove_pass`
|
|
640
|
+
one-for-one, so D14's "the facade does not grow" holds. `pass_count(phase)` becomes
|
|
641
|
+
`extension_count(phase)`, which the start → stop → start leak gate needs.
|
|
642
|
+
|
|
643
|
+
**Views.** There is one injection site today: the primary view inside `render_to_target`. Shadow
|
|
644
|
+
views, probe bakes and picking do not run extensions and this design does not change that. When a
|
|
645
|
+
second view kind gains injection points, the filter belongs on the registry and the extension gains
|
|
646
|
+
a declaration for it; building that now would be inventing a contract for a caller that does not
|
|
647
|
+
exist.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## 7. The whole API
|
|
652
|
+
|
|
653
|
+
That is the entire surface.
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
// declaration
|
|
657
|
+
class RenderExtension { name; phase; after; before; requires; record(frame); }
|
|
658
|
+
const FramePhase = { FrameStart, AfterGBuffer, AfterLighting, AfterTransparency, BeforePresent, Overlay };
|
|
659
|
+
const FrameProduct = { Velocity, ColorCopy };
|
|
660
|
+
|
|
661
|
+
// recording
|
|
662
|
+
class FrameContext { graph; view; phase; resolution; get(type); has(type); create(type); describe(handle); }
|
|
663
|
+
|
|
664
|
+
// built-in records
|
|
665
|
+
class GBufferTextures { albedo; normal; pbr; emissive; }
|
|
666
|
+
class ViewTextures { depth; depth_previous; visibility_mesh; visibility_triangle; velocity; }
|
|
667
|
+
class SceneColor { color; copy; }
|
|
668
|
+
class PresentTarget { canvas; }
|
|
669
|
+
|
|
670
|
+
// registration
|
|
671
|
+
class RenderExtensionRegistry { add(e); remove(e); count(phase); describe(); run(frame); }
|
|
672
|
+
// on GraphicsEngine / Renderer
|
|
673
|
+
add_extension(e); remove_extension(e); extension_count(phase);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 8. Migration
|
|
679
|
+
|
|
680
|
+
Nine live systems, one probe, one prototype viewer. Every one of them is the same mechanical change:
|
|
681
|
+
a closure becomes a class, `context.color` becomes `frame.get(SceneColor).color`, and the return
|
|
682
|
+
value becomes a publication.
|
|
683
|
+
|
|
684
|
+
| consumer | phase | change beyond the mechanical |
|
|
685
|
+
| --- | --- | --- |
|
|
686
|
+
| `TerrainSystem` | `AfterGBuffer` | drops the registration-before-`await` comment; nothing else |
|
|
687
|
+
| `DecalSystem` | `AfterGBuffer` | `after: [TerrainExtension]`, which is the constraint the comment in `TerrainSystem` was hand-enforcing; stops assigning through `context.gbuffer` |
|
|
688
|
+
| `WaterSystem` | `AfterLighting` | publication instead of return; a candidate first consumer of `FrameProduct.ColorCopy` if refraction is wanted |
|
|
689
|
+
| `FogOfWarSystem` | `AfterTransparency` | the grid update becomes a `FrameStart` extension publishing `FogOfWarVisibility`, and the composite reads it; declares `after` the four content extensions — **this is where §1.3a's shipped bug is fixed** |
|
|
690
|
+
| `ParticleEmitterSystem` | `AfterTransparency` | — (now before the fog, by declaration) |
|
|
691
|
+
| `PathDisplaySystem` | `AfterTransparency` | — |
|
|
692
|
+
| `HighlightSystem` | `AfterTransparency` | reads `ViewTextures.visibility_*`, unchanged in meaning |
|
|
693
|
+
| `DebugDrawSystem` | `AfterTransparency` | `after: [FogOfWarExtension]` |
|
|
694
|
+
| `Trail3DSystem` | `AfterTransparency` | — |
|
|
695
|
+
| `probeShadePassInjection` | all | the gate is rewritten and gains the two checks it is currently blind to: a G-buffer replacement, and an order flip |
|
|
696
|
+
| `prototypeVGEOFormatViewer` | `AfterTransparency` | prototype; mechanical |
|
|
697
|
+
|
|
698
|
+
**The `AfterTransparency` chain is settled, not open.** Fog of war is last among the content
|
|
699
|
+
extensions, because it conceals and therefore belongs over everything it is meant to conceal —
|
|
700
|
+
particles, trails, path display and the selection highlight all draw before it and are all subject
|
|
701
|
+
to it. Debug draw is the single deliberate exception, `after: [FogOfWarExtension]`: developer
|
|
702
|
+
geometry that the fog can hide is a debugging tool that stops working exactly where debugging is
|
|
703
|
+
needed.
|
|
704
|
+
|
|
705
|
+
That is six extensions and two declarations. Written down once, in the two files that care, instead
|
|
706
|
+
of being a property of where an `await` sits.
|
|
707
|
+
|
|
708
|
+
**The bug does not have to wait for the migration.** Today's ordering is registration order, so
|
|
709
|
+
moving `FogOfWarSystem` below `Trail3DSystem` in `graphics_profile.js` fixes the picture now — at
|
|
710
|
+
the cost of it being another undeclared ordering fact, which is what this design exists to end.
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## 9. Non-goals and known limits
|
|
715
|
+
|
|
716
|
+
**Shade's own frame is not rewritten into a list of extensions.** URP's real strength is that
|
|
717
|
+
built-in and injected passes are the same type. Getting there means restructuring
|
|
718
|
+
`Renderer.render_to_target`, and renderer internals are hands-off. The consequence, stated so it is
|
|
719
|
+
not discovered later: injection points remain hand-placed call sites, so the phase set is only as
|
|
720
|
+
good as the places the renderer publishes records, and an extension cannot insert itself between two
|
|
721
|
+
built-in post passes.
|
|
722
|
+
|
|
723
|
+
**Extensions cannot participate in OIT.** Work at `AfterTransparency` composites over the resolved
|
|
724
|
+
transparency; it cannot interleave with sorted transparents. True today, unchanged, and the honest
|
|
725
|
+
reason the phase model has a ceiling.
|
|
726
|
+
|
|
727
|
+
**Extensions cannot add to a built-in pass** — no extra material variants in the G-buffer raster, no
|
|
728
|
+
extra lights. A new pass is the only unit.
|
|
729
|
+
|
|
730
|
+
**No `FrameProduct` for `HiZ` / `PreviousColor` / depth pyramid yet.** Each is built by the renderer
|
|
731
|
+
and each would be a one-line addition, and none has a consumer today. Adding them speculatively puts
|
|
732
|
+
three untested branches in the renderer's product wiring.
|
|
733
|
+
|
|
734
|
+
**No view kinds.** §6.
|
|
735
|
+
|
|
736
|
+
**Ordering ambiguity is caught at frame time, not registration time**, because publication is
|
|
737
|
+
observed rather than declared. Declaring publications up front would catch it earlier at the cost of
|
|
738
|
+
a second declaration that can disagree with the code; observation cannot disagree with the code.
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## 10. Delivery
|
|
743
|
+
|
|
744
|
+
Three steps, each independently green.
|
|
745
|
+
|
|
746
|
+
- **A — the model.** Records, `FrameContext`, publication with all four checks, registry with
|
|
747
|
+
ordering, cycle detection and scoped recording. Port the nine systems and the viewer. Delete
|
|
748
|
+
`FramePassContext`, `FramePassRegistry` and `add_pass` / `remove_pass`. `FrameStart` and
|
|
749
|
+
`PresentTarget` land here, because they are what the fog-of-war split and the `Overlay` rule need.
|
|
750
|
+
- **B — diagnostics.** Ambiguity check and `describe()`. Nothing here waits on the engine; scoped
|
|
751
|
+
recording lands in A, because it needs nothing that is not already there.
|
|
752
|
+
- **C — products.** `FrameProduct.Velocity` and `ColorCopy`, and the renderer wiring that honours
|
|
753
|
+
them.
|
|
754
|
+
|
|
755
|
+
A is the whole of the user-visible design. B is what makes it hold. C is the part with the weakest
|
|
756
|
+
demand and it is separable on purpose.
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## 11. Tests and gates
|
|
761
|
+
|
|
762
|
+
Device-free (`vitest`, from the moh root), black-box, real objects:
|
|
763
|
+
|
|
764
|
+
- resolved order for a set of extensions with `after` / `before`; a cycle throws and names both;
|
|
765
|
+
unconstrained pairs resolve by name and the result does not change when registration order does.
|
|
766
|
+
- `get` of an absent record throws; `create` twice throws; `has` agrees with both.
|
|
767
|
+
- publication of a superseded handle fails; publication into a frozen record fails; a handle from a
|
|
768
|
+
previous frame fails. `FrameGraph` and a `SoftwareGPUDevice` are enough for all of these — no real
|
|
769
|
+
device, per the standing rule.
|
|
770
|
+
- **an extension whose output nothing consumes is culled, and that is not an error** — the frame
|
|
771
|
+
renders, no assert fires, and the pass reports `culled` in the JSON export. Pinned as a test
|
|
772
|
+
because it is a property somebody will otherwise "fix" (§5.1).
|
|
773
|
+
- an extension's passes land inside its own DOT cluster, and a graph recorded with no extensions
|
|
774
|
+
exports exactly as it did before. The scope machinery is already specced on the graph; what this
|
|
775
|
+
adds is that the registry opens and closes one per extension, including when `record()` throws.
|
|
776
|
+
- `describe()` output is stable and contains every registered extension.
|
|
777
|
+
- registry survives a simulated renderer replacement with the same extensions in the same order.
|
|
778
|
+
|
|
779
|
+
On a device, extending `probeShadePassInjection`:
|
|
780
|
+
|
|
781
|
+
- a colour-writing extension at each colour-carrying phase, in and out, restoring to within the
|
|
782
|
+
existing 25% bar.
|
|
783
|
+
- **a G-buffer-replacing extension at `AfterGBuffer`** — writes a flat albedo, and the lit frame
|
|
784
|
+
changes. The current gate has no such check, which is why the in/out G-buffer has never been
|
|
785
|
+
exercised outside the decal system.
|
|
786
|
+
- **an order flip** — two extensions publishing the colour, with the `after` edge reversed between
|
|
787
|
+
runs, producing different frames. This is the property the current mechanism cannot state at all.
|
|
788
|
+
- **the upscale boundary, at 60% dynamic resolution scale**: an extension at `AfterTransparency` and
|
|
789
|
+
one at `BeforePresent`, each sizing its output from `frame.resolution`, both producing a full,
|
|
790
|
+
uncropped frame. They must resolve to *different* sizes. The `BeforePresent` half of this fails on
|
|
791
|
+
the current mechanism (§1.3b), and the same check run with `feature_taa_enabled = false` must
|
|
792
|
+
still pass, which is what pins `resolution` as derived rather than declared.
|
|
793
|
+
- **fog of war over particles** — the `AfterTransparency` chain resolved from declarations, with a
|
|
794
|
+
particle emitter inside the concealed region staying concealed. This is §1.3a's bug, as a gate.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## 12. Adversarial review
|
|
799
|
+
|
|
800
|
+
The design above is the post-review version. What follows is what was attacked and what changed.
|
|
801
|
+
|
|
802
|
+
**"A type-keyed container is a registry, and the house rule forbids registries."** The rule forbids
|
|
803
|
+
*string*-keyed factories chosen to save boilerplate. A class is a type; the container is a typed
|
|
804
|
+
map. The alternative — a struct with fixed fields — is defect §1.1 restated. *Survived, with the
|
|
805
|
+
concession that every built-in record is an ordinary class with named fields, so the container is
|
|
806
|
+
the only indirection.*
|
|
807
|
+
|
|
808
|
+
**"You claim the graph orders passes. It executes in insertion order."** Correct, and the first
|
|
809
|
+
draft leaned on the graph for ordering. **Changed:** §4 now states the execution model outright, and
|
|
810
|
+
every ordering claim is attributed to the resolver. The design would have been wrong without this.
|
|
811
|
+
|
|
812
|
+
> *Since.* The graph answered this from its own side rather than by being routed around: `validate()`
|
|
813
|
+
> now proves that the recorded insertion order is a valid topological order and `compile()` asserts
|
|
814
|
+
> on it. The design's position is unchanged — §3.6 still owns ordering — but the failure mode it was
|
|
815
|
+
> protecting against narrowed sharply. A recording order that is *inconsistent* is now the graph's
|
|
816
|
+
> to reject; §3.6 is left owning the orders that are consistent and still wrong, which is exactly
|
|
817
|
+
> the fog-over-particles case (§1.3a) and is the only kind of ordering bug the game actually had.
|
|
818
|
+
|
|
819
|
+
**"`scene.color = handle` is untracked mutation — the same side channel you criticised
|
|
820
|
+
`context.gbuffer` for being."** Half right. The criticism of `context.gbuffer` was that it was the
|
|
821
|
+
*only* extra mutable thing, at *one* phase, invisible in the pass signature. **Changed:** publication
|
|
822
|
+
is now uniform across every record and validated at the setter — freeze state, frame identity, and
|
|
823
|
+
`is_valid_resource` against the graph. The last of those catches a class of silent wrong-picture bug
|
|
824
|
+
that neither the current mechanism nor URP catches at all.
|
|
825
|
+
|
|
826
|
+
**"You deleted `resolution` and told every extension to call `describe()`. That is a worse API for
|
|
827
|
+
the case everyone has."** Fair, and the first draft did exactly that in reaction to defect (b).
|
|
828
|
+
**Changed:** `frame.resolution` came back as a derived getter over the phase's live target handle,
|
|
829
|
+
so the common case is one property access *and* correct on both sides of the upscale, with
|
|
830
|
+
`describe()` left for the genuinely two-sized case. The lesson from (b) is that the number must not
|
|
831
|
+
be a field somebody fills in, not that the number should be hard to get.
|
|
832
|
+
|
|
833
|
+
**"Ambiguity detection needs the frame to know who is recording — hidden global state."** One field,
|
|
834
|
+
set by the loop that calls `record`. Re-entrant recording is an assert failure. *Survived.*
|
|
835
|
+
|
|
836
|
+
**"Alphabetical tie-breaking is arbitrary. Why is arbitrary-by-name better than
|
|
837
|
+
arbitrary-by-registration?"** Because it does not change when a system's `startup` gains an `await`.
|
|
838
|
+
Reproducibility is the property, not correctness. **Changed:** the ambiguity check was added in the
|
|
839
|
+
same pass, so an arbitrary order between two extensions that actually conflict is an error rather
|
|
840
|
+
than a silently-chosen order.
|
|
841
|
+
|
|
842
|
+
**"`FrameProduct` is a closed enum, so §1.1 is only half fixed."** Concede. The frame's own optional
|
|
843
|
+
products are Shade's features and enumerating them is correct; the *open* half is the record
|
|
844
|
+
vocabulary, which needs no engine edit. **Changed:** §3.7 says this, the enum was cut to two members
|
|
845
|
+
with named consumers, and it was moved to the last delivery step.
|
|
846
|
+
|
|
847
|
+
**"An `enabled` flag and an early return from `record` are two ways to do one thing."** Agreed.
|
|
848
|
+
**Changed:** the flag was cut.
|
|
849
|
+
|
|
850
|
+
**"`get_or_create` is friendlier than three methods."** It is, and it destroys the property that
|
|
851
|
+
made `-1` go away: absent has to stay distinguishable from empty. *Rejected.*
|
|
852
|
+
|
|
853
|
+
**"Six phases plus a blackboard plus a topological sort, to replace two methods, for nine
|
|
854
|
+
consumers."** The two methods currently need four paragraphs of documentation about culling, carry a
|
|
855
|
+
wrong resolution at two of five phases, and enforce a documented ordering requirement through the
|
|
856
|
+
placement of an `await`. The sort is thirty lines over at most a dozen nodes. *Survived, and it is
|
|
857
|
+
the reason §10 splits diagnostics out: if the sort and the records are the value, B and C can be
|
|
858
|
+
judged separately.*
|
|
859
|
+
|
|
860
|
+
**"`SceneColor` absent at `Overlay` means an extension that wants the tonemapped image just gets a
|
|
861
|
+
throw."** Yes, with a message naming `BeforePresent`. That is strictly better than the current
|
|
862
|
+
outcome, which is a validation error from inside a binding builder, and it is better than the
|
|
863
|
+
documentation-only fix that shipped, which was discovered by measurement after the fact. *Survived.*
|
|
864
|
+
|
|
865
|
+
**"The migration makes somebody decide the `AfterTransparency` order, and they may get it wrong."**
|
|
866
|
+
It was decided while this document was under review, and the order in the shipped game turned out to
|
|
867
|
+
be wrong (§1.3a). That is the argument for declaring it, not a counter to it — a mechanism where the
|
|
868
|
+
answer is written down is a mechanism where being wrong is a diff. **Changed:** §8 states the
|
|
869
|
+
resolved chain instead of leaving it open.
|
|
870
|
+
|
|
871
|
+
**"`describe()` is a feature nobody asked for."** It is the only thing that makes "the frame graph is
|
|
872
|
+
a first-class construct" mean something testable at this seam — a pipeline you can print without a
|
|
873
|
+
device is a pipeline you can spec without a device. *Survived, in delivery step B.*
|
|
874
|
+
|
|
875
|
+
**"Culling is what a frame graph is for. You made it an error for one class of user."** Sponsor, on
|
|
876
|
+
the revision that asked the graph to report per-scope culling so a fully-culled extension could
|
|
877
|
+
assert. **The objection is correct and the check is gone (§5.1).** Three ways it was wrong, and the
|
|
878
|
+
review that produced the rest of this document did not catch any of them:
|
|
879
|
+
|
|
880
|
+
- It adopted the construct and then treated its central behaviour as a failure.
|
|
881
|
+
- The line it drew — culled built-in pass is optimisation, culled extension pass is a bug — has
|
|
882
|
+
nothing behind it. Who recorded a pass does not change what culling means.
|
|
883
|
+
- Fatally, it outlawed a pattern this document's own §3.3 puts forward as the reason the record
|
|
884
|
+
vocabulary is open: an extension publishing `FogOfWarVisibility` for a consumer that may not be
|
|
885
|
+
registered. Disable the consumer and the producer is correctly elided — and the assert would have
|
|
886
|
+
called that a hard error. A speculative producer is the point, not an accident.
|
|
887
|
+
|
|
888
|
+
Withdrawn with it: `FRAME_GRAPH_RECOMMENDATIONS.md` item 5, which was the last open engine change,
|
|
889
|
+
and the provenance check in §3.4, which fell to the same standard once it was applied consistently
|
|
890
|
+
— it forbade publishing an imported texture in order to catch a typo. What the argument leaves
|
|
891
|
+
standing is a bar the remaining checks are now stated against: **a check may only fire on something
|
|
892
|
+
that cannot be true**, never on an extension behaving correctly.
|
|
893
|
+
|
|
894
|
+
---
|
|
895
|
+
|
|
896
|
+
## 13. Engine changes required
|
|
897
|
+
|
|
898
|
+
Small, and enumerated so the blast radius is visible. **`FrameGraph` needs nothing.** The graph-side
|
|
899
|
+
items this document originally asked for have either landed or been withdrawn — the last of them,
|
|
900
|
+
the culling report, was withdrawn on the reasoning in §5.1. Everything below is renderer and facade
|
|
901
|
+
work.
|
|
902
|
+
|
|
903
|
+
1. `Renderer` publishes records at six points instead of filling one context at five. The record
|
|
904
|
+
objects are built where `#frame_pass_context` is built today.
|
|
905
|
+
2. `Renderer` gains a `FrameStart` call site, and (delivery C) honours the product union when
|
|
906
|
+
deciding whether to build velocity and the colour copy.
|
|
907
|
+
3. `GraphicsEngine` swaps `add_pass`/`remove_pass`/`pass_count` for
|
|
908
|
+
`add_extension`/`remove_extension`/`extension_count`. No net surface change.
|
|
909
|
+
4. `src/shade/renderer/pass/` becomes `src/shade/renderer/extension/`. The folder is MEEP 3's, not
|
|
910
|
+
Shade's, and "pass" collides with `FramePassNode` — the graph's passes and an extension are
|
|
911
|
+
different things and should not share a word.
|
|
912
|
+
|
|
913
|
+
Nothing in Shade's frame structure, no pass reordering, no change to how built-in passes record.
|
|
914
|
+
|
|
915
|
+
**Already done, and nothing further is needed from them** — `FrameGraphState` and its asserts,
|
|
916
|
+
`validate()` with both order checks, `push_scope` / `pop_scope` and DOT clustering,
|
|
917
|
+
`ResourceNode.producer` at record time, and the `exportToJson` fix. `getDescriptor`,
|
|
918
|
+
`is_valid_resource` and `onExecuted` were already there and the design uses them as they stand.
|