From 08ee8c2bf70c9c7bd4ce494bac5a9fe2219b93c5 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Wed, 21 Sep 2022 00:00:00 +0000 Subject: [PATCH] com.unity.entities.graphics@1.0.0-exp.8 --- uid: changelog --- # Changelog ## [1.0.0-exp.8] - 2022-09-21 ### Added * Hybrid assemblies will not be included in DOTS Runtime builds. * Error/loading shader support for Hybrid Renderer * Implement error/loading shader support for Entities Graphics. * Implement Entity picking * Shadow receiver sphere culling, which culls entities that cannot cast a visible shadow in the camera. * Support for skinned motion vectors for High Definition Render Pipeline. ### Changed * HLOD now requires root LOD nodes to have an HLODParent component rather than any GameObject parent. * Removed use of the obsolete AlwaysUpdateSystem attribute. The new RequireMatchingQueriesForUpdate attribute has been added where appropriate. * use the existing mesh buffer to retrieve the shared mesh in bind pose. * use the existing graphics buffer to retrieve the skin weights for skinning. * use the existing graphics buffer to retrieve the vertex deltas for blendshapes. * DeformationsInPresentation and SkinnedMeshRendererConversion are now sealed. * RegisterMaterialMeshSystem is now internal instead of public. ### Removed * PushMeshDataSystem, PushSkinMatrixSystem, PushBlendWeightSystem, InstantiateDeformationSystem, BlendShapeDeformationSystem and SkinningDeformationSystem are no longer part of the public API. Use DeformationsInPresentation instead. * ENABLE_COMPUTE_DEFORMATIONS define, compute deformations are always enabled. ### Fixed * Mesh and material indices not updating correctly if entity was created ad disabled and later enabled. * Improved multi threaded load balancing of the Entities Graphics frustum culling Burst job * Guard against overflow of static readonly int k_MaxSize for the deformation buffers. ## [0.14.0] - 2021-09-17 ### Added * Hybrid Renderer is automatically disabled when required support is not present or -nographics is given on the command line. ### Removed * Hybrid Renderer V1 is now removed. V2 is the default renderer ### Fixed * Fixed memory leak on frames where no data uploading happened. * `HybridRendererSystem` was not correctly tracking all its component read/write dependencies. * Warning about erroneous materials did not have enough information to act on them. ## [0.13.0] - 2021-03-15 ### Removed * Removed an unused internal struct that could cause compiler warnings. ### Fixed * Entities that use the ambient light probe now have no SH components and use much less memory. * Fixed a bug where global ambient probes were not always rendered correctly. ## [0.12.0] - 2021-01-26 ### Added * `CountNewChunksJob`, which is responsible for counting the number of new chunks since last frame, and filling the `newChunks` array. * New #define DISABLE_HYBRID_LIGHT_PROBES to disable light probes globally to save memory. ### Changed * (Root)LodRequirement component split to (Root)LodRange and (Root)LodWorldReferencePoint. The LOD ranges are created during conversion and the world reference point gets recalculated every frame. Splitting them allows performance optimizations. * LODGroupWorldReferencePoint component added to LOD/HLOD groups. Allows us to transform the LOD pivot point once per group and storing it instead of doing repeated work per leaf entity. Total LODRequirementSystem performance increase up to 2x (when combined with the optimization above). * Cache `GetComponentTypeHandle<>()` calls instead of doing them for each jobs. * `UpdateAllHybridChunksJob` does not count the number of new chunks since last frame anymore. * No longer track shader reflection version changes in `HybridRendererSystem`. A new system is now taking care of that in the `StructuralChangePresentationSystemGroup` * No longer add/remove chunks in the `HybridRendererSystem`. A new system is now taking care of that in the `StructuralChangePresentationSystemGroup` * Update minimum editor version to 2020.2.1f1-dots.3 *Loaded the Samples project in a 2020.2.1f1-dots.3 editor without any issues or console errors. *Ran `Full CI [Entities] [project version]` ### Fixed * LOD bitfield becoming stale when entities get removed or LOD components modified using LiveLink incremental update * Fixed edge case bug in instance GPU allocation behavior. * Improved GameObject conversion of light mapped objects during incremental conversion. * Fixed a performance regression related to ambient probe update when probe grid was not available * Do not perform structural changes in the `HybridRendererSystem` anymore. * Static objects now always have per-object motion vectors disabled, regardless of MeshRenderer settings. ## [0.11.0] - 2020-11-13 ### Added * Frame queuing limiting solution to avoid hazards in the GPU uploader * Hybrid V2 should now render objects with errors (e.g. missing or broken material) as bright magenta when the used SRP contains compatible error shaders, and display warnings. * Support for lightmaps in hybrid renderer. You will need to bake with subscenes open, upon closing the lightmaps will be converted into the subscene. (Note: Requires release 10.1.0 of graphics packages). * Support for lightprobes in hybrid renderer. Entities can dynamically look up the the current ambient probe or probe grid. (Note: Requires release 10.1.0 of graphics packages). * Added error message when total used GPU memory is bigger than some backends can handle (1 GiB) * HybridBatchPartition shared component that can force entities into separate batches. * It is now possible to override DOTS instanced material properties using `ISharedComponentData`. * RenderMeshDescription and RenderMeshUtility.AddComponent APIs to efficiently create Hybrid Rendered entities. ### Changed * Log warning instead of error message when shader on SMR does not support DOTS Skinning * Update minimum editor version to 2020.1.2f1 ### Fixed * Fixed float2 and float3 material properties like HDRP emissive color to work correctly. * GPU buffer now grows by doubling, so initial startup lag is reduced. * GPU resources are now cleaned up better in case of internal exceptions, leading to less errors in subsequent frames. * Hybrid Renderer forces entities using URP and HDRP transparent materials into separate batches, so they are rendered in the correct order and produce correct rendering results. * Fixed a bug with motion vector parameters not getting set correctly. * HLOD conversion code now properly handles uninitialized components * Removed internal frame queuing and replace it with frame fencing. Hybrid renderer will now longer wait for GPU buffers to be available, making it easier to see if you are GPU or CPU bound and avoiding some potential deadlocks. * Disable deformation systems when no graphics device is present instead of throwing error. * Fixed a bug with converting ambient light probe settings from GameObjects. ## [0.10.0] - 2020-09-24 ### Added * Error message when trying to convert SkinnedMeshRenderer that is using a shader that does not support skinning. ### Removed * HybridRendererSettings asset was removed since memory management for the hybrid renderer data buffer is now automatic. ### Fixed * Fixed missing mesh breaking subscene conversion * Fixed chunk render bounds getting stale when RenderMesh shared component is changed. * Improved Hybrid V2 memory usage during GPU uploading. * Chunk render bounds getting stale when RenderMesh shared component is changed. * Reduced Hybrid V2 peak memory use when batches are deleted and created at the same time. ## [0.9.0] - 2020-08-26 ### Added * Added: Hybrid component conversion support for: ParticleSystem and Volume+collider pairs (local volumes). * Hybrid component conversion support for: ParticleSystem and Volume+collider pairs (local volumes). ### Fixed * Fixed parallel for checking errors in Hybrid Renderer jobs. * Fixed parallel for checking errors in occlusion jobs. ## [0.8.0] - 2020-08-04 ### Changed * Changed SkinnedMeshRendererConversion to take the RootBone into account. The render entities are now parented to the RootBone entity instead of the SkinnedMeshRenderer GameObject entity. As a result the RenderBounds will update correctly when the root bone is transformed. * Changed SkinnedMeshRendererConversion to compute the SkinMatrices in SkinnedMeshRenderer's root bone space instead of worldspace. ### Fixed * Fixed the Hybrid V2 uploading code not supporting more than 65535 separate upload operations per frame. * Fixed render bounds being offset on converted SkinnedMeshRenderers. * Partially fixed editor picking for Hybrid V2. Picking should now work in simple cases. * Fixed a memory leak in the HeapAllocator class used by Hybrid Renderer. ## [0.7.0] - 2020-07-10 ### Added * Added support for controling persistent GPU buffer sizes through project settings ### Changed * Updated minimum Unity Editor version to 2020.1.0b15 (40d9420e7de8) ### Fixed * Improved hashing of the RenderMesh component. * Fixed blendshapes getting applied with incorrect weights when the blendshapes are sparse. ### Known Issues * This version is not compatible with 2020.2.0a17. Please update to the forthcoming alpha. ## [0.6.0] - 2020-05-27 ### Added * Added support for Mesh Deformations using compute shaders. * Added support for sparse Blendshapes in the compute deformation system. * Added support for Skinning using sparse bone weights with n number of influences in the compute deformation system. * Added support for storing matrices as 3x4 on the GPU side. This will used for SRP 10.x series of packages and up. * Added support for ambient probe environment lighting in URP. ### Changed * Updated minimum Unity Editor version to 2020.1.0b9 (9c0aec301c8d) ### Fixed * Fix floating point precision issue in vertex shader skinning. * Fixed culling of hybrid lights in SceneView when using LiveLink (on 2020.1). ## [0.5.1] - 2020-05-04 ### Changed * Updated dependencies of this package. ## [0.5.0] - 2020-04-24 ### Changed Changes that only affect *Hybrid Renderer V2*: * V2 now computes accurate AABBs for batches. * V2 now longer adds WorldToLocal component to renderable entities. Changes that affect both versions: * Updated dependencies of this package. ### Deprecated * Deprecated `FrozenRenderSceneTagProxy` and `RenderMeshProxy`. Please use the GameObject-to-Entity conversion workflow instead. ### Fixed * Improved precision of camera frustum plane calculation in FrustumPlanes.FromCamera. * Improved upload performance by uploading matrices as 4x3 instead of 4x4 as well as calculating inverses on the GPU * Fixed default color properties being in the wrong color space ## [0.4.2] - 2020-04-15 ### Changes * Updated dependencies of this package. ## [0.4.1] - 2020-04-08 ### Added (Hybrid V2) * DisableRendering tag component for disabling rendering of entities ### Changed * Improved hybrid.renderer landing document. Lots of new information. ### Fixed * Fixed shadow mapping issues, especially when using the built-in renderer. ### Misc * Highlighting additional changes introduced in `0.3.4-preview.24` which were not part of the previous changelogs, see below. ## [0.4.0] - 2020-03-13 ### Added (All Versions) * HeapAllocator: Offset allocator for sub-allocating resources such as NativeArrays or ComputeBuffers. ### Added (Hybrid V2) Hybrid Renderer V2 is a new experimental renderer. It has a significantly higher performance and better feature set compared to the existing hybrid renderer. However, it is not yet confirmed to work on all platforms. To enable Hybrid Renderer V2, use the `ENABLE_HYBRID_RENDERER_V2` define in the Project Settings. * HybridHDRPSamples Project for sample Scenes, unit tests and graphics tests. * HybridURPSamples Project for sample Scenes, unit tests and graphics tests. * MaterialOverride component: User friendly way to configure material overrides for shader properties. * MaterialOverrideAsset: MaterialOverride asset for configuring general material overrides tied to a shader. * SparseUploader: Delta update ECS data on GPU ComputeBuffer. * Support for Unity built-in material properties: See BuiltinMaterialProperties directory for all IComponentData structs. * Support for HDRP material properties: See HDRPMaterialProperties directory for all IComponentData structs. * Support for URP material properties: See URPMaterialProperties directory for all IComponentData structs. * New API (2020.1) to directly write to ComputeBuffer from parallel Burst jobs. * New API (2020.1) to render Hybrid V2 batches though optimized SRP Batcher backend. ### Changes (Hybrid V2) * Full rewrite of RenderMeshSystemV2 and InstancedRenderMeshBatchGroup. New code is located at `HybridV2RenderSystem.cs`. * Partial rewrite of culling. Now all culling code is located at `HybridV2Culling.cs`. * Hybrid Renderer and culling no longer use hash maps or IJobNativeMultiHashMapVisitKeyMutableValue jobs. Chunk components and chunk/forEach jobs are used instead. * Batch setup and update now runs in parallel Burst jobs. Huge performance benefit. * GPU persistent data model. ComputeBuffer to store persistent data on GPU side. Use `chunk.DidChange` to delta update only changed data. Huge performance benefit. * Per-instance shader constants are no longer setup to constant buffers for each viewport. This makes HDRP script main thread cost significantly smaller and saves significant amount of CPU time in render thread. ### Fixed * Fixed culling issues (disappearing entities) 8000+ meters away from origin. * Fixes to solve chunk fragmentation issues with ChunkWorldRenderBounds and other chunk components. Some changes were already included in 0.3.4 package, but not documented. * Removed unnecessary reference to Unity.RenderPipelines.HighDefinition.Runtime from asmdef. * Fixed uninitialized data issues causing flickering on some graphics backends (2020.1). ### Misc * Highlighting `RenderBounds` component change introduced in `0.3.4-preview.24` which was not part of the previous changelogs, see below. ## [0.3.5] - 2020-03-03 ### Changed * Updated dependencies of this package. ## [0.3.4] - 2020-02-17 ### Changed * Updated dependencies of this package. * When creating entities from scratch with code, user now needs to manually add `RenderBounds` component. Instantiating prefab works as before. * Inactive GameObjects and Prefabs with `StaticOptimizeEntity` are now correctly treated as static * `RenderBoundsUpdateSystem` is no longer `public` (breaking) * deleted public `CreateMissingRenderBoundsFromMeshRenderer` system (breaking) ## [0.3.3] - 2020-01-28 ### Changed * Updated dependencies of this package. ## [0.3.2] - 2020-01-16 ### Changed * Updated dependencies of this package. ## [0.3.1] - 2019-12-16 **This version requires Unity 2019.3.0f1+** ### Changes * Updated dependencies of this package. ## [0.3.0] - 2019-12-03 ### Changes * Updated dependencies of this package. ## [0.2.0] - 2019-11-22 **This version requires Unity 2019.3 0b11+** ### New Features * Added support for vertex skinning. ### Fixes * Fixed an issue where disabled UnityEngine Components were not getting ignored when converted via `ConvertToEntity` (it only was working for subscenes). ### Changes * Removed `LightSystem` and light conversion. * Updated dependencies for this package. ### Upgrade guide * `Lightsystem` was not performance by default and the concept of driving a game object from a component turned out to be not performance by default. It was also not maintainable because every property added to lights has to be reflected in this package. * `LightSystem` will be replaced with hybrid entities in the future. This will be a more clean uniform API for graphics related functionalities. ## [0.1.1] - 2019-08-06 ### Fixes * Adding a disabled tag component, now correctly disables the light. ### Changes * Updated dependencies for this package. ## [0.1.0] - 2019-07-30 ### New Features * New `GameObjectConversionSettings` class that we are using to help manage the various and growing settings that can tune a GameObject conversion. * New ability to convert and export Assets, which is initially needed for Tiny. * Assets are discovered via `DeclareReferencedAsset` in the `GameObjectConversionDeclareObjectsGroup` phase and can then be converted by a System during normal conversion phases. * Assets can be marked for export and assigned a guid via `GameObjectConversionSystem.GetGuidForAssetExport`. During the System `GameObjectExportGroup` phase, the converted assets can be exported via `TryCreateAssetExportWriter`. * `GetPrimaryEntity`, `HasPrimaryEntity`, and the new `TryGetPrimaryEntity` all now work on `UnityEngine.Object` instead of `GameObject` so that they can also query against Unity Assets. ### Upgrade guide * Various GameObject conversion-related methods now receive a `GameObjectConversionSettings` object rather than a set of misc config params. * `GameObjectConversionSettings` has implicit constructors for common parameters such as `World`, so much existing code will likely just work. * Otherwise construct a `GameObjectConversionSettings`, configure it with the parameters you used previously, and send it in. * `GameObjectConversionSystem`: `AddLinkedEntityGroup` is now `DeclareLinkedEntityGroup` (should auto-upgrade). * The System group `GameObjectConversionDeclarePrefabsGroup` is now `GameObjectConversionDeclareObjectsGroup`. This cannot auto-upgrade but a global find&replace will fix it. * `GameObjectConversionUtility.ConversionFlags.None` is gone, use 0 instead. ### Changes * Changing `entities` dependency to latest version (`0.1.0-preview`). ## [0.0.1-preview.13] - 2019-05-24 ### Changes * Changing `entities` dependency to latest version (`0.0.12-preview.33`). ## [0.0.1-preview.12] - 2019-05-16 ### Fixes * Adding/fixing `Equals` and `GetHashCode` for proxy components. ## [0.0.1-preview.11] - 2019-05-01 Change tracking started with this version. --- .footignore | 1 + ApiUpdater~/ValidationWhiteList.txt | 0 CHANGELOG.md | 435 +++ CHANGELOG.md.meta | 7 + Documentation~/TableOfContents.md | 21 + Documentation~/batch-renderer-group-api.md | 7 + ...reating-a-new-entities-graphics-project.md | 18 + Documentation~/entities-graphics-features.md | 7 + Documentation~/entities-graphics-versions.md | 31 + Documentation~/filter.yml | 20 + Documentation~/getting-started.md | 8 + Documentation~/hybrid-entities.md | 40 + .../images/DOTSInstancingMasterNode.png | Bin 0 -> 53971 bytes .../images/GPUInstancingMaterial.png | Bin 0 -> 38488 bytes .../images/GPUInstancingProperty.png | Bin 0 -> 25567 bytes .../images/HybridInstancingProperty.png | Bin 0 -> 14152 bytes .../images/HybridInstancingProperty2019-3.png | Bin 0 -> 32008 bytes .../images/HybridInstancingProperty2020-2.png | Bin 0 -> 13644 bytes .../images/HybridRendererSplash.png | Bin 0 -> 99168 bytes .../images/MaterialOverrideAssetSelect.png | Bin 0 -> 69996 bytes .../images/MaterialOverrideMaterialSelect.png | Bin 0 -> 12132 bytes .../images/MaterialOverridePerInstance.png | Bin 0 -> 74690 bytes .../images/MaterialOverridePropertySelect.png | Bin 0 -> 32652 bytes .../images/ProjectSettingsDialog.png | Bin 0 -> 149809 bytes .../batch-renderer-group-api-properties.png | Bin 0 -> 143775 bytes Documentation~/index.md | 18 + Documentation~/material-overrides-asset.md | 25 + Documentation~/material-overrides-code.md | 88 + Documentation~/material-overrides.md | 15 + Documentation~/mesh_deformations.md | 72 + Documentation~/overview.md | 28 + .../requirements-and-compatibility.md | 29 + Documentation~/runtime-entity-creation.md | 132 + Documentation~/runtime-usage.md | 7 + Documentation~/sample-projects.md | 20 + Documentation~/upgrade-guide.md | 23 + Documentation~/whats-new.md | 34 + Editor.meta | 8 + Editor/MaterialOverrideAssetEditor.cs | 285 ++ Editor/MaterialOverrideAssetEditor.cs.meta | 11 + Editor/MaterialOverrideEditor.cs | 156 ++ Editor/MaterialOverrideEditor.cs.meta | 11 + Editor/Unity.Entities.Graphics.Editor.asmdef | 11 + ...Unity.Entities.Graphics.Editor.asmdef.meta | 7 + LICENSE.md | 31 + LICENSE.md.meta | 7 + README.md | 3 + README.md.meta | 7 + Unity.Entities.Graphics.Tests.meta | 8 + .../FrustumPlanesTests.cs | 127 + .../FrustumPlanesTests.cs.meta | 11 + .../HeapAllocatorTests.cs | 281 ++ .../HeapAllocatorTests.cs.meta | 11 + .../SparseUploaderTests.cs | 900 +++++++ .../SparseUploaderTests.cs.meta | 11 + .../Unity.Entities.Graphics.Tests.asmdef | 60 + .../Unity.Entities.Graphics.Tests.asmdef.meta | 7 + Unity.Entities.Graphics.meta | 8 + .../BuiltinMaterialProperties.meta | 8 + .../BuiltinMaterialPropertyUnity_LODFade.cs | 11 + Unity.Entities.Graphics/ComponentTypeCache.cs | 270 ++ .../ComponentTypeCache.cs.meta | 11 + Unity.Entities.Graphics/CullingTypes.cs | 27 + Unity.Entities.Graphics/CullingTypes.cs.meta | 3 + Unity.Entities.Graphics/Deformations.meta | 8 + .../Deformations/BufferManagers.meta | 8 + .../BufferManagers/BlendShapeBufferManager.cs | 69 + .../BlendShapeBufferManager.cs.meta | 11 + .../BufferManagers/ComputeBufferWrapper.cs | 109 + .../ComputeBufferWrapper.cs.meta | 11 + .../BufferManagers/FencedBufferPool.cs | 134 + .../BufferManagers/FencedBufferPool.cs.meta | 11 + .../BufferManagers/MeshBufferManager.cs | 80 + .../BufferManagers/MeshBufferManager.cs.meta | 11 + .../BufferManagers/SkinningBufferManager.cs | 69 + .../SkinningBufferManager.cs.meta | 11 + .../Deformations/Components.meta | 8 + .../Deformations/Components/BlendShape.cs | 12 + .../Components/BlendShape.cs.meta | 11 + .../Deformations/Components/DeformedMesh.cs | 39 + .../Components/DeformedMesh.cs.meta | 11 + .../Deformations/Components/SharedMesh.cs | 42 + .../Components/SharedMesh.cs.meta | 11 + .../Deformations/Components/Skinning.cs | 13 + .../Deformations/Components/Skinning.cs.meta | 11 + .../Deformations/DeformationSystemGroup.cs | 51 + .../DeformationSystemGroup.cs.meta | 11 + .../Deformations/Resources.meta | 8 + .../Resources/BlendShapeComputeShader.compute | 84 + .../BlendShapeComputeShader.compute.meta | 8 + .../InstantiateDeformationData.compute | 58 + .../InstantiateDeformationData.compute.meta | 8 + .../Resources/SkinningComputeShader.compute | 207 ++ .../SkinningComputeShader.compute.meta | 8 + .../Deformations/ShaderLibrary.meta | 8 + .../ShaderLibrary/DotsDeformation.hlsl | 52 + .../ShaderLibrary/DotsDeformation.hlsl.meta | 7 + .../Deformations/Structs.meta | 8 + .../Structs/BlendShapeVertexDelta.cs | 12 + .../Structs/BlendShapeVertexDelta.cs.meta | 11 + .../Deformations/Structs/BoneWeight.cs | 8 + .../Deformations/Structs/BoneWeight.cs.meta | 11 + .../Deformations/Structs/VertexData.cs | 17 + .../Deformations/Structs/VertexData.cs.meta | 11 + .../Deformations/Systems.meta | 8 + .../Systems/BlendShapeDeformationSystem.cs | 105 + .../BlendShapeDeformationSystem.cs.meta | 11 + .../Systems/InstantiateDeformationSystem.cs | 83 + .../InstantiateDeformationSystem.cs.meta | 11 + .../Systems/PushBlendWeightSystem.cs | 91 + .../Systems/PushBlendWeightSystem.cs.meta | 11 + .../Systems/PushMeshDataSystem.cs | 613 +++++ .../Systems/PushMeshDataSystem.cs.meta | 11 + .../Systems/PushSkinMatrixSystem.cs | 93 + .../Systems/PushSkinMatrixSystem.cs.meta | 11 + .../Systems/SkinningDeformationSystem.cs | 121 + .../Systems/SkinningDeformationSystem.cs.meta | 11 + .../DisableRenderingComponent.cs | 10 + .../DisableRenderingComponent.cs.meta | 11 + .../DrawCommandGeneration.cs | 1951 ++++++++++++++ .../DrawCommandGeneration.cs.meta | 11 + .../EntitiesGraphicsChunkUpdate.cs | 349 +++ .../EntitiesGraphicsChunkUpdate.cs.meta | 3 + .../EntitiesGraphicsComponents.cs | 108 + .../EntitiesGraphicsComponents.cs.meta | 11 + .../EntitiesGraphicsConversion.cs | 229 ++ .../EntitiesGraphicsConversion.cs.meta | 11 + .../EntitiesGraphicsCulling.cs | 1037 +++++++ .../EntitiesGraphicsCulling.cs.meta | 11 + .../EntitiesGraphicsEditorTools.cs | 45 + .../EntitiesGraphicsEditorTools.cs.meta | 11 + .../EntitiesGraphicsLightBakingDataSystem.cs | 51 + ...itiesGraphicsLightBakingDataSystem.cs.meta | 3 + .../EntitiesGraphicsStats.cs | 236 ++ .../EntitiesGraphicsStats.cs.meta | 11 + .../EntitiesGraphicsStatsDrawer.cs | 60 + .../EntitiesGraphicsStatsDrawer.cs.meta | 11 + .../EntitiesGraphicsSystem.cs | 2384 +++++++++++++++++ .../EntitiesGraphicsSystem.cs.meta | 11 + .../EntitiesGraphicsUtils.cs | 272 ++ .../EntitiesGraphicsUtils.cs.meta | 11 + .../FreezeStaticLODObjects.cs | 23 + .../FreezeStaticLODObjects.cs.meta | 11 + .../FrozenRenderSceneTagProxy.cs | 56 + .../FrozenRenderSceneTagProxy.cs.meta | 11 + .../FrozenStaticRendererSystem.cs | 41 + .../FrozenStaticRendererSystem.cs.meta | 11 + Unity.Entities.Graphics/FrustumPlanes.cs | 284 ++ Unity.Entities.Graphics/FrustumPlanes.cs.meta | 11 + Unity.Entities.Graphics/GpuUploadOperation.cs | 118 + .../GpuUploadOperation.cs.meta | 3 + Unity.Entities.Graphics/GraphicsArchetype.cs | 209 ++ .../GraphicsArchetype.cs.meta | 3 + .../HDRPMaterialProperties.meta | 8 + ...HDRPMaterialPropertyAORemapMaxAuthoring.cs | 26 + ...HDRPMaterialPropertyAORemapMinAuthoring.cs | 26 + ...DRPMaterialPropertyAlphaCutoffAuthoring.cs | 26 + .../HDRPMaterialPropertyBaseColorAuthoring.cs | 30 + .../HDRPMaterialPropertyMetallicAuthoring.cs | 26 + ...HDRPMaterialPropertySmoothnessAuthoring.cs | 26 + .../HDRPMaterialPropertyThicknessAuthoring.cs | 26 + ...HDRPMaterialPropertyUnlitColorAuthoring.cs | 30 + Unity.Entities.Graphics/HeapAllocator.cs | 686 +++++ Unity.Entities.Graphics/HeapAllocator.cs.meta | 11 + Unity.Entities.Graphics/LODGroupBaking.cs | 38 + .../LODGroupBaking.cs.meta | 11 + Unity.Entities.Graphics/LODGroupExtensions.cs | 246 ++ .../LODGroupExtensions.cs.meta | 11 + .../LODRequirementsUpdateSystem.cs | 367 +++ .../LODRequirementsUpdateSystem.cs.meta | 11 + Unity.Entities.Graphics/LightMaps.cs | 111 + Unity.Entities.Graphics/LightMaps.cs.meta | 3 + Unity.Entities.Graphics/MaterialColor.cs | 52 + Unity.Entities.Graphics/MaterialColor.cs.meta | 11 + Unity.Entities.Graphics/MaterialOverride.cs | 170 ++ .../MaterialOverride.cs.meta | 11 + .../MaterialOverrideAsset.cs | 158 ++ .../MaterialOverrideAsset.cs.meta | 11 + .../MaterialPropertyAttribute.cs | 32 + .../MaterialPropertyAttribute.cs.meta | 11 + .../MatrixPreviousSystem.cs | 72 + .../MatrixPreviousSystem.cs.meta | 11 + Unity.Entities.Graphics/MeshLODComponent.cs | 78 + .../MeshLODComponent.cs.meta | 11 + Unity.Entities.Graphics/MeshRendererBaking.cs | 252 ++ .../MeshRendererBaking.cs.meta | 11 + .../MeshRendererBakingUtility.cs | 224 ++ .../MeshRendererBakingUtility.cs.meta | 3 + Unity.Entities.Graphics/Occlusion.meta | 8 + Unity.Entities.Graphics/Occlusion/Masked.meta | 8 + .../Occlusion/Masked/BufferGroup.cs | 185 ++ .../Occlusion/Masked/BufferGroup.cs.meta | 11 + .../Occlusion/Masked/ClearJob.cs | 24 + .../Occlusion/Masked/ClearJob.cs.meta | 11 + .../Occlusion/Masked/ComputeBoundsJob.cs | 126 + .../Occlusion/Masked/ComputeBoundsJob.cs.meta | 11 + .../Occlusion/Masked/Dots.meta | 8 + .../Occlusion/Masked/Dots/OccludeeBaking.cs | 153 ++ .../Masked/Dots/OccludeeBaking.cs.meta | 11 + .../Occlusion/Masked/Dots/OccluderBaking.cs | 186 ++ .../Masked/Dots/OccluderBaking.cs.meta | 11 + .../Occlusion/Masked/Dots/OccluderMesh.cs | 571 ++++ .../Masked/Dots/OccluderMesh.cs.meta | 11 + .../Occlusion/Masked/Dots/OcclusionTest.cs | 31 + .../Masked/Dots/OcclusionTest.cs.meta | 11 + .../Occlusion/Masked/IntrinsicUtils.cs | 160 ++ .../Occlusion/Masked/IntrinsicUtils.cs.meta | 11 + .../Occlusion/Masked/MergeJob.cs | 180 ++ .../Occlusion/Masked/MergeJob.cs.meta | 11 + .../Occlusion/Masked/MeshTransformJob.cs | 48 + .../Occlusion/Masked/MeshTransformJob.cs.meta | 11 + .../Occlusion/Masked/RasterizeJob.cs | 955 +++++++ .../Occlusion/Masked/RasterizeJob.cs.meta | 11 + .../Occlusion/Masked/TestJob.cs | 322 +++ .../Occlusion/Masked/TestJob.cs.meta | 11 + .../Occlusion/Masked/Types.cs | 27 + .../Occlusion/Masked/Types.cs.meta | 11 + .../Occlusion/Masked/Visualization.meta | 8 + .../Masked/Visualization/DebugSettings.cs | 164 ++ .../Visualization/DebugSettings.cs.meta | 11 + .../Masked/Visualization/DebugView.cs | 369 +++ .../Masked/Visualization/DebugView.cs.meta | 11 + .../Visualization/DecodeMaskedDepthJob.cs | 53 + .../DecodeMaskedDepthJob.cs.meta | 11 + .../Visualization/FilterOccludedTestJob.cs | 52 + .../FilterOccludedTestJob.cs.meta | 11 + .../Visualization/MeshAggregationJob.cs | 72 + .../Visualization/MeshAggregationJob.cs.meta | 11 + .../Masked/Visualization/OccludeeAABBJob.cs | 44 + .../Visualization/OccludeeAABBJob.cs.meta | 11 + .../Visualization/OccludeeOutlineJob.cs | 92 + .../Visualization/OccludeeOutlineJob.cs.meta | 11 + Unity.Entities.Graphics/Occlusion/Occluder.cs | 59 + .../Occlusion/Occluder.cs.meta | 11 + .../Occlusion/OccluderInspector.cs | 66 + .../Occlusion/OccluderInspector.cs.meta | 11 + .../Occlusion/OcclusionBrowseWindow.cs | 113 + .../Occlusion/OcclusionBrowseWindow.cs.meta | 11 + .../Occlusion/OcclusionBrowseWindow.uss | 94 + .../Occlusion/OcclusionBrowseWindow.uss.meta | 11 + .../Occlusion/OcclusionBrowseWindow.uxml | 9 + .../Occlusion/OcclusionBrowseWindow.uxml.meta | 10 + .../Occlusion/OcclusionDebugRenderSystem.cs | 133 + .../OcclusionDebugRenderSystem.cs.meta | 11 + .../Occlusion/OcclusionMenu.cs | 153 ++ .../Occlusion/OcclusionMenu.cs.meta | 11 + .../Occlusion/OcclusionSortJob.cs | 43 + .../Occlusion/OcclusionSortJob.cs.meta | 11 + .../Occlusion/OcclusionWindow.cs | 85 + .../Occlusion/OcclusionWindow.cs.meta | 11 + .../Occlusion/UnityOcclusion.cs | 330 +++ .../Occlusion/UnityOcclusion.cs.meta | 11 + Unity.Entities.Graphics/Probes.meta | 3 + .../Probes/BlendProbeTag.cs | 14 + .../Probes/BlendProbeTag.cs.meta | 3 + .../Probes/CustomProbeTag.cs | 14 + .../Probes/CustomProbeTag.cs.meta | 3 + .../Probes/LightProbeUpdateSystem.cs | 155 ++ .../Probes/LightProbeUpdateSystem.cs.meta | 3 + .../Probes/ManageSHPropertiesSystem.cs | 96 + .../Probes/ManageSHPropertiesSystem.cs.meta | 3 + Unity.Entities.Graphics/RemoveLocalBounds.cs | 23 + .../RemoveLocalBounds.cs.meta | 11 + .../RenderBoundsComponent.cs | 40 + .../RenderBoundsComponent.cs.meta | 11 + .../RenderBoundsUpdateSystem.cs | 247 ++ .../RenderBoundsUpdateSystem.cs.meta | 11 + .../RenderFilterSettings.cs | 130 + .../RenderFilterSettings.cs.meta | 11 + Unity.Entities.Graphics/RenderMeshArray.cs | 453 ++++ .../RenderMeshArray.cs.meta | 11 + .../RenderMeshBakingContext.cs | 317 +++ .../RenderMeshBakingContext.cs.meta | 11 + Unity.Entities.Graphics/RenderMeshProxy.cs | 114 + .../RenderMeshProxy.cs.meta | 12 + Unity.Entities.Graphics/RenderMeshUtility.cs | 417 +++ .../RenderMeshUtility.cs.meta | 3 + Unity.Entities.Graphics/Resources.meta | 8 + .../Resources/Occlusion.meta | 8 + .../Occlusion/OccludeeScreenSpaceAABB.shader | 46 + .../OccludeeScreenSpaceAABB.shader.meta | 10 + .../Occlusion/OcclusionDebugComposite.shader | 80 + .../OcclusionDebugComposite.shader.meta | 13 + .../Occlusion/OcclusionDebugOccluders.shader | 76 + .../OcclusionDebugOccluders.shader.meta | 9 + .../Occlusion/ShowOccluderMesh.shadergraph | 24 + .../ShowOccluderMesh.shadergraph.meta | 10 + .../Resources/SparseUploader.compute | 369 +++ .../Resources/SparseUploader.compute.meta | 8 + .../SkinnedMeshRendererBaking.cs | 114 + .../SkinnedMeshRendererBaking.cs.meta | 3 + Unity.Entities.Graphics/SparseUploader.cs | 781 ++++++ .../SparseUploader.cs.meta | 11 + ...StructuralChangePresentationSystemGroup.cs | 17 + ...turalChangePresentationSystemGroup.cs.meta | 11 + Unity.Entities.Graphics/ThreadLocalAABB.cs | 41 + .../ThreadLocalAABB.cs.meta | 11 + .../URPMaterialProperties.meta | 8 + .../URPMaterialPropertyBaseColorAuthoring.cs | 33 + ...MaterialPropertyBaseColorAuthoring.cs.meta | 11 + .../URPMaterialPropertyBumpScaleAuthoring.cs | 29 + ...MaterialPropertyBumpScaleAuthoring.cs.meta | 11 + .../URPMaterialPropertyCutoffAuthoring.cs | 29 + ...URPMaterialPropertyCutoffAuthoring.cs.meta | 11 + ...PMaterialPropertyEmissionColorAuthoring.cs | 33 + .../URPMaterialPropertyMetallicAuthoring.cs | 29 + ...PMaterialPropertyMetallicAuthoring.cs.meta | 11 + .../URPMaterialPropertySmoothnessAuthoring.cs | 29 + .../URPMaterialPropertySpecColorAuthoring.cs | 33 + ...MaterialPropertySpecColorAuthoring.cs.meta | 11 + .../Unity.Entities.Graphics.asmdef | 79 + .../Unity.Entities.Graphics.asmdef.meta | 7 + .../UpdateDrawCommandFlags.cs | 69 + .../UpdateDrawCommandFlags.cs.meta | 11 + .../UpdateEntitiesGraphicsChunksStructure.cs | 84 + ...ateEntitiesGraphicsChunksStructure.cs.meta | 11 + .../UpdatePresentationSystemGroup.cs | 14 + .../UpdatePresentationSystemGroup.cs.meta | 11 + ValidationExceptions.json | 20 + ValidationExceptions.json.meta | 7 + package.json | 26 + package.json.meta | 7 + 322 files changed, 25951 insertions(+) create mode 100644 .footignore create mode 100644 ApiUpdater~/ValidationWhiteList.txt create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 Documentation~/TableOfContents.md create mode 100644 Documentation~/batch-renderer-group-api.md create mode 100644 Documentation~/creating-a-new-entities-graphics-project.md create mode 100644 Documentation~/entities-graphics-features.md create mode 100644 Documentation~/entities-graphics-versions.md create mode 100644 Documentation~/filter.yml create mode 100644 Documentation~/getting-started.md create mode 100644 Documentation~/hybrid-entities.md create mode 100644 Documentation~/images/DOTSInstancingMasterNode.png create mode 100644 Documentation~/images/GPUInstancingMaterial.png create mode 100644 Documentation~/images/GPUInstancingProperty.png create mode 100644 Documentation~/images/HybridInstancingProperty.png create mode 100644 Documentation~/images/HybridInstancingProperty2019-3.png create mode 100644 Documentation~/images/HybridInstancingProperty2020-2.png create mode 100644 Documentation~/images/HybridRendererSplash.png create mode 100644 Documentation~/images/MaterialOverrideAssetSelect.png create mode 100644 Documentation~/images/MaterialOverrideMaterialSelect.png create mode 100644 Documentation~/images/MaterialOverridePerInstance.png create mode 100644 Documentation~/images/MaterialOverridePropertySelect.png create mode 100644 Documentation~/images/ProjectSettingsDialog.png create mode 100644 Documentation~/images/batch-renderer-group-api-properties.png create mode 100644 Documentation~/index.md create mode 100644 Documentation~/material-overrides-asset.md create mode 100644 Documentation~/material-overrides-code.md create mode 100644 Documentation~/material-overrides.md create mode 100644 Documentation~/mesh_deformations.md create mode 100644 Documentation~/overview.md create mode 100644 Documentation~/requirements-and-compatibility.md create mode 100644 Documentation~/runtime-entity-creation.md create mode 100644 Documentation~/runtime-usage.md create mode 100644 Documentation~/sample-projects.md create mode 100644 Documentation~/upgrade-guide.md create mode 100644 Documentation~/whats-new.md create mode 100644 Editor.meta create mode 100644 Editor/MaterialOverrideAssetEditor.cs create mode 100644 Editor/MaterialOverrideAssetEditor.cs.meta create mode 100644 Editor/MaterialOverrideEditor.cs create mode 100644 Editor/MaterialOverrideEditor.cs.meta create mode 100644 Editor/Unity.Entities.Graphics.Editor.asmdef create mode 100644 Editor/Unity.Entities.Graphics.Editor.asmdef.meta create mode 100644 LICENSE.md create mode 100644 LICENSE.md.meta create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 Unity.Entities.Graphics.Tests.meta create mode 100644 Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs create mode 100644 Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs.meta create mode 100644 Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs create mode 100644 Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs.meta create mode 100644 Unity.Entities.Graphics.Tests/SparseUploaderTests.cs create mode 100644 Unity.Entities.Graphics.Tests/SparseUploaderTests.cs.meta create mode 100644 Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef create mode 100644 Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef.meta create mode 100644 Unity.Entities.Graphics.meta create mode 100644 Unity.Entities.Graphics/BuiltinMaterialProperties.meta create mode 100644 Unity.Entities.Graphics/BuiltinMaterialProperties/BuiltinMaterialPropertyUnity_LODFade.cs create mode 100644 Unity.Entities.Graphics/ComponentTypeCache.cs create mode 100644 Unity.Entities.Graphics/ComponentTypeCache.cs.meta create mode 100644 Unity.Entities.Graphics/CullingTypes.cs create mode 100644 Unity.Entities.Graphics/CullingTypes.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs create mode 100644 Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Components.meta create mode 100644 Unity.Entities.Graphics/Deformations/Components/BlendShape.cs create mode 100644 Unity.Entities.Graphics/Deformations/Components/BlendShape.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs create mode 100644 Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs create mode 100644 Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Components/Skinning.cs create mode 100644 Unity.Entities.Graphics/Deformations/Components/Skinning.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs create mode 100644 Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Resources.meta create mode 100644 Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute create mode 100644 Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute.meta create mode 100644 Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute create mode 100644 Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute.meta create mode 100644 Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute create mode 100644 Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute.meta create mode 100644 Unity.Entities.Graphics/Deformations/ShaderLibrary.meta create mode 100644 Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl create mode 100644 Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl.meta create mode 100644 Unity.Entities.Graphics/Deformations/Structs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs create mode 100644 Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs create mode 100644 Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Structs/VertexData.cs create mode 100644 Unity.Entities.Graphics/Deformations/Structs/VertexData.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs create mode 100644 Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs.meta create mode 100644 Unity.Entities.Graphics/DisableRenderingComponent.cs create mode 100644 Unity.Entities.Graphics/DisableRenderingComponent.cs.meta create mode 100644 Unity.Entities.Graphics/DrawCommandGeneration.cs create mode 100644 Unity.Entities.Graphics/DrawCommandGeneration.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsComponents.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsComponents.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsConversion.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsConversion.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsCulling.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsCulling.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsStats.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsStats.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsSystem.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsSystem.cs.meta create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsUtils.cs create mode 100644 Unity.Entities.Graphics/EntitiesGraphicsUtils.cs.meta create mode 100644 Unity.Entities.Graphics/FreezeStaticLODObjects.cs create mode 100644 Unity.Entities.Graphics/FreezeStaticLODObjects.cs.meta create mode 100644 Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs create mode 100644 Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs.meta create mode 100644 Unity.Entities.Graphics/FrozenStaticRendererSystem.cs create mode 100644 Unity.Entities.Graphics/FrozenStaticRendererSystem.cs.meta create mode 100644 Unity.Entities.Graphics/FrustumPlanes.cs create mode 100644 Unity.Entities.Graphics/FrustumPlanes.cs.meta create mode 100644 Unity.Entities.Graphics/GpuUploadOperation.cs create mode 100644 Unity.Entities.Graphics/GpuUploadOperation.cs.meta create mode 100644 Unity.Entities.Graphics/GraphicsArchetype.cs create mode 100644 Unity.Entities.Graphics/GraphicsArchetype.cs.meta create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties.meta create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMaxAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMinAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAlphaCutoffAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyBaseColorAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyMetallicAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertySmoothnessAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyThicknessAuthoring.cs create mode 100644 Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyUnlitColorAuthoring.cs create mode 100644 Unity.Entities.Graphics/HeapAllocator.cs create mode 100644 Unity.Entities.Graphics/HeapAllocator.cs.meta create mode 100644 Unity.Entities.Graphics/LODGroupBaking.cs create mode 100644 Unity.Entities.Graphics/LODGroupBaking.cs.meta create mode 100644 Unity.Entities.Graphics/LODGroupExtensions.cs create mode 100644 Unity.Entities.Graphics/LODGroupExtensions.cs.meta create mode 100644 Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs create mode 100644 Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs.meta create mode 100644 Unity.Entities.Graphics/LightMaps.cs create mode 100644 Unity.Entities.Graphics/LightMaps.cs.meta create mode 100644 Unity.Entities.Graphics/MaterialColor.cs create mode 100644 Unity.Entities.Graphics/MaterialColor.cs.meta create mode 100644 Unity.Entities.Graphics/MaterialOverride.cs create mode 100644 Unity.Entities.Graphics/MaterialOverride.cs.meta create mode 100644 Unity.Entities.Graphics/MaterialOverrideAsset.cs create mode 100644 Unity.Entities.Graphics/MaterialOverrideAsset.cs.meta create mode 100644 Unity.Entities.Graphics/MaterialPropertyAttribute.cs create mode 100644 Unity.Entities.Graphics/MaterialPropertyAttribute.cs.meta create mode 100644 Unity.Entities.Graphics/MatrixPreviousSystem.cs create mode 100644 Unity.Entities.Graphics/MatrixPreviousSystem.cs.meta create mode 100644 Unity.Entities.Graphics/MeshLODComponent.cs create mode 100644 Unity.Entities.Graphics/MeshLODComponent.cs.meta create mode 100644 Unity.Entities.Graphics/MeshRendererBaking.cs create mode 100644 Unity.Entities.Graphics/MeshRendererBaking.cs.meta create mode 100644 Unity.Entities.Graphics/MeshRendererBakingUtility.cs create mode 100644 Unity.Entities.Graphics/MeshRendererBakingUtility.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Types.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Types.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/Occluder.cs create mode 100644 Unity.Entities.Graphics/Occlusion/Occluder.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OccluderInspector.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OccluderInspector.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs create mode 100644 Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs.meta create mode 100644 Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs create mode 100644 Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs.meta create mode 100644 Unity.Entities.Graphics/Probes.meta create mode 100644 Unity.Entities.Graphics/Probes/BlendProbeTag.cs create mode 100644 Unity.Entities.Graphics/Probes/BlendProbeTag.cs.meta create mode 100644 Unity.Entities.Graphics/Probes/CustomProbeTag.cs create mode 100644 Unity.Entities.Graphics/Probes/CustomProbeTag.cs.meta create mode 100644 Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs create mode 100644 Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs.meta create mode 100644 Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs create mode 100644 Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs.meta create mode 100644 Unity.Entities.Graphics/RemoveLocalBounds.cs create mode 100644 Unity.Entities.Graphics/RemoveLocalBounds.cs.meta create mode 100644 Unity.Entities.Graphics/RenderBoundsComponent.cs create mode 100644 Unity.Entities.Graphics/RenderBoundsComponent.cs.meta create mode 100644 Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs create mode 100644 Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs.meta create mode 100644 Unity.Entities.Graphics/RenderFilterSettings.cs create mode 100644 Unity.Entities.Graphics/RenderFilterSettings.cs.meta create mode 100644 Unity.Entities.Graphics/RenderMeshArray.cs create mode 100644 Unity.Entities.Graphics/RenderMeshArray.cs.meta create mode 100644 Unity.Entities.Graphics/RenderMeshBakingContext.cs create mode 100644 Unity.Entities.Graphics/RenderMeshBakingContext.cs.meta create mode 100644 Unity.Entities.Graphics/RenderMeshProxy.cs create mode 100644 Unity.Entities.Graphics/RenderMeshProxy.cs.meta create mode 100644 Unity.Entities.Graphics/RenderMeshUtility.cs create mode 100644 Unity.Entities.Graphics/RenderMeshUtility.cs.meta create mode 100644 Unity.Entities.Graphics/Resources.meta create mode 100644 Unity.Entities.Graphics/Resources/Occlusion.meta create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader.meta create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader.meta create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader.meta create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph create mode 100644 Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph.meta create mode 100644 Unity.Entities.Graphics/Resources/SparseUploader.compute create mode 100644 Unity.Entities.Graphics/Resources/SparseUploader.compute.meta create mode 100644 Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs create mode 100644 Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs.meta create mode 100644 Unity.Entities.Graphics/SparseUploader.cs create mode 100644 Unity.Entities.Graphics/SparseUploader.cs.meta create mode 100644 Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs create mode 100644 Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs.meta create mode 100644 Unity.Entities.Graphics/ThreadLocalAABB.cs create mode 100644 Unity.Entities.Graphics/ThreadLocalAABB.cs.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyEmissionColorAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs.meta create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySmoothnessAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs create mode 100644 Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs.meta create mode 100644 Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef create mode 100644 Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef.meta create mode 100644 Unity.Entities.Graphics/UpdateDrawCommandFlags.cs create mode 100644 Unity.Entities.Graphics/UpdateDrawCommandFlags.cs.meta create mode 100644 Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs create mode 100644 Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs.meta create mode 100644 Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs create mode 100644 Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs.meta create mode 100644 ValidationExceptions.json create mode 100644 ValidationExceptions.json.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/.footignore b/.footignore new file mode 100644 index 0000000..9cf577b --- /dev/null +++ b/.footignore @@ -0,0 +1 @@ +ValidationExceptions.json diff --git a/ApiUpdater~/ValidationWhiteList.txt b/ApiUpdater~/ValidationWhiteList.txt new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d8dfbc8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,435 @@ +--- +uid: changelog +--- +# Changelog + +## [1.0.0-exp.8] - 2022-09-21 + +### Added + +* Hybrid assemblies will not be included in DOTS Runtime builds. +* Error/loading shader support for Hybrid Renderer +* Implement error/loading shader support for Entities Graphics. +* Implement Entity picking +* Shadow receiver sphere culling, which culls entities that cannot cast a visible shadow in the camera. +* Support for skinned motion vectors for High Definition Render Pipeline. + +### Changed + +* HLOD now requires root LOD nodes to have an HLODParent component rather than any GameObject parent. +* Removed use of the obsolete AlwaysUpdateSystem attribute. The new RequireMatchingQueriesForUpdate attribute has been added where appropriate. +* use the existing mesh buffer to retrieve the shared mesh in bind pose. +* use the existing graphics buffer to retrieve the skin weights for skinning. +* use the existing graphics buffer to retrieve the vertex deltas for blendshapes. +* DeformationsInPresentation and SkinnedMeshRendererConversion are now sealed. +* RegisterMaterialMeshSystem is now internal instead of public. + + +### Removed + +* PushMeshDataSystem, PushSkinMatrixSystem, PushBlendWeightSystem, InstantiateDeformationSystem, BlendShapeDeformationSystem and SkinningDeformationSystem are no longer part of the public API. Use DeformationsInPresentation instead. +* ENABLE_COMPUTE_DEFORMATIONS define, compute deformations are always enabled. + +### Fixed + +* Mesh and material indices not updating correctly if entity was created ad disabled and later enabled. +* Improved multi threaded load balancing of the Entities Graphics frustum culling Burst job +* Guard against overflow of static readonly int k_MaxSize for the deformation buffers. + + + + +## [0.14.0] - 2021-09-17 + +### Added + +* Hybrid Renderer is automatically disabled when required support is not present or -nographics is given on the command line. + +### Removed + +* Hybrid Renderer V1 is now removed. V2 is the default renderer + +### Fixed + +* Fixed memory leak on frames where no data uploading happened. +* `HybridRendererSystem` was not correctly tracking all its component read/write dependencies. +* Warning about erroneous materials did not have enough information to act on them. + +## [0.13.0] - 2021-03-15 + +### Removed + +* Removed an unused internal struct that could cause compiler warnings. + +### Fixed + +* Entities that use the ambient light probe now have no SH components and use much less memory. +* Fixed a bug where global ambient probes were not always rendered correctly. + +## [0.12.0] - 2021-01-26 + +### Added + +* `CountNewChunksJob`, which is responsible for counting the number of new chunks since last frame, and filling the `newChunks` array. +* New #define DISABLE_HYBRID_LIGHT_PROBES to disable light probes globally to save memory. + +### Changed + +* (Root)LodRequirement component split to (Root)LodRange and (Root)LodWorldReferencePoint. The LOD ranges are created during conversion and the world reference point gets recalculated every frame. Splitting them allows performance optimizations. +* LODGroupWorldReferencePoint component added to LOD/HLOD groups. Allows us to transform the LOD pivot point once per group and storing it instead of doing repeated work per leaf entity. Total LODRequirementSystem performance increase up to 2x (when combined with the optimization above). +* Cache `GetComponentTypeHandle<>()` calls instead of doing them for each jobs. +* `UpdateAllHybridChunksJob` does not count the number of new chunks since last frame anymore. +* No longer track shader reflection version changes in `HybridRendererSystem`. A new system is now taking care of that in the `StructuralChangePresentationSystemGroup` +* No longer add/remove chunks in the `HybridRendererSystem`. A new system is now taking care of that in the `StructuralChangePresentationSystemGroup` +* Update minimum editor version to 2020.2.1f1-dots.3 +*Loaded the Samples project in a 2020.2.1f1-dots.3 editor without any issues or console errors. +*Ran `Full CI [Entities] [project version]` + +### Fixed + +* LOD bitfield becoming stale when entities get removed or LOD components modified using LiveLink incremental update +* Fixed edge case bug in instance GPU allocation behavior. +* Improved GameObject conversion of light mapped objects during incremental conversion. +* Fixed a performance regression related to ambient probe update when probe grid was not available +* Do not perform structural changes in the `HybridRendererSystem` anymore. +* Static objects now always have per-object motion vectors disabled, regardless of MeshRenderer settings. + + + +## [0.11.0] - 2020-11-13 + +### Added + +* Frame queuing limiting solution to avoid hazards in the GPU uploader +* Hybrid V2 should now render objects with errors (e.g. missing or broken material) as bright magenta when the used SRP contains compatible error shaders, and display warnings. +* Support for lightmaps in hybrid renderer. You will need to bake with subscenes open, upon closing the lightmaps will be converted into the subscene. (Note: Requires release 10.1.0 of graphics packages). +* Support for lightprobes in hybrid renderer. Entities can dynamically look up the the current ambient probe or probe grid. (Note: Requires release 10.1.0 of graphics packages). +* Added error message when total used GPU memory is bigger than some backends can handle (1 GiB) +* HybridBatchPartition shared component that can force entities into separate batches. +* It is now possible to override DOTS instanced material properties using `ISharedComponentData`. +* RenderMeshDescription and RenderMeshUtility.AddComponent APIs to efficiently create Hybrid Rendered entities. + +### Changed + +* Log warning instead of error message when shader on SMR does not support DOTS Skinning +* Update minimum editor version to 2020.1.2f1 + +### Fixed + +* Fixed float2 and float3 material properties like HDRP emissive color to work correctly. +* GPU buffer now grows by doubling, so initial startup lag is reduced. +* GPU resources are now cleaned up better in case of internal exceptions, leading to less errors in subsequent frames. +* Hybrid Renderer forces entities using URP and HDRP transparent materials into separate batches, so they are rendered in the correct order and produce correct rendering results. +* Fixed a bug with motion vector parameters not getting set correctly. +* HLOD conversion code now properly handles uninitialized components +* Removed internal frame queuing and replace it with frame fencing. Hybrid renderer will now longer wait for GPU buffers to be available, making it easier to see if you are GPU or CPU bound and avoiding some potential deadlocks. +* Disable deformation systems when no graphics device is present instead of throwing error. +* Fixed a bug with converting ambient light probe settings from GameObjects. + +## [0.10.0] - 2020-09-24 + +### Added + +* Error message when trying to convert SkinnedMeshRenderer that is using a shader that does not support skinning. + +### Removed + +* HybridRendererSettings asset was removed since memory management for the hybrid renderer data buffer is now automatic. + +### Fixed + +* Fixed missing mesh breaking subscene conversion +* Fixed chunk render bounds getting stale when RenderMesh shared component is changed. +* Improved Hybrid V2 memory usage during GPU uploading. +* Chunk render bounds getting stale when RenderMesh shared component is changed. +* Reduced Hybrid V2 peak memory use when batches are deleted and created at the same time. + +## [0.9.0] - 2020-08-26 + +### Added + +* Added: Hybrid component conversion support for: ParticleSystem and Volume+collider pairs (local volumes). +* Hybrid component conversion support for: ParticleSystem and Volume+collider pairs (local volumes). + +### Fixed + +* Fixed parallel for checking errors in Hybrid Renderer jobs. +* Fixed parallel for checking errors in occlusion jobs. + + +## [0.8.0] - 2020-08-04 + + +### Changed + +* Changed SkinnedMeshRendererConversion to take the RootBone into account. The render entities are now parented to the RootBone entity instead of the SkinnedMeshRenderer GameObject entity. As a result the RenderBounds will update correctly when the root bone is transformed. +* Changed SkinnedMeshRendererConversion to compute the SkinMatrices in SkinnedMeshRenderer's root bone space instead of worldspace. + +### Fixed + +* Fixed the Hybrid V2 uploading code not supporting more than 65535 separate upload operations per frame. +* Fixed render bounds being offset on converted SkinnedMeshRenderers. +* Partially fixed editor picking for Hybrid V2. Picking should now work in simple cases. +* Fixed a memory leak in the HeapAllocator class used by Hybrid Renderer. + + + +## [0.7.0] - 2020-07-10 + +### Added + +* Added support for controling persistent GPU buffer sizes through project settings + +### Changed + +* Updated minimum Unity Editor version to 2020.1.0b15 (40d9420e7de8) + +### Fixed + +* Improved hashing of the RenderMesh component. +* Fixed blendshapes getting applied with incorrect weights when the blendshapes are sparse. + +### Known Issues + +* This version is not compatible with 2020.2.0a17. Please update to the forthcoming alpha. + + +## [0.6.0] - 2020-05-27 + +### Added + +* Added support for Mesh Deformations using compute shaders. +* Added support for sparse Blendshapes in the compute deformation system. +* Added support for Skinning using sparse bone weights with n number of influences in the compute deformation system. +* Added support for storing matrices as 3x4 on the GPU side. This will used for SRP 10.x series of packages and up. +* Added support for ambient probe environment lighting in URP. + +### Changed + +* Updated minimum Unity Editor version to 2020.1.0b9 (9c0aec301c8d) + +### Fixed + +* Fix floating point precision issue in vertex shader skinning. +* Fixed culling of hybrid lights in SceneView when using LiveLink (on 2020.1). + +## [0.5.1] - 2020-05-04 + +### Changed + +* Updated dependencies of this package. + + +## [0.5.0] - 2020-04-24 + +### Changed + +Changes that only affect *Hybrid Renderer V2*: +* V2 now computes accurate AABBs for batches. +* V2 now longer adds WorldToLocal component to renderable entities. + +Changes that affect both versions: +* Updated dependencies of this package. + +### Deprecated + +* Deprecated `FrozenRenderSceneTagProxy` and `RenderMeshProxy`. Please use the GameObject-to-Entity conversion workflow instead. + +### Fixed + +* Improved precision of camera frustum plane calculation in FrustumPlanes.FromCamera. +* Improved upload performance by uploading matrices as 4x3 instead of 4x4 as well as calculating inverses on the GPU +* Fixed default color properties being in the wrong color space + + +## [0.4.2] - 2020-04-15 + +### Changes + +* Updated dependencies of this package. + + +## [0.4.1] - 2020-04-08 + +### Added (Hybrid V2) + +* DisableRendering tag component for disabling rendering of entities + +### Changed + +* Improved hybrid.renderer landing document. Lots of new information. + +### Fixed + +* Fixed shadow mapping issues, especially when using the built-in renderer. + +### Misc + +* Highlighting additional changes introduced in `0.3.4-preview.24` which were not part of the previous changelogs, see below. + + +## [0.4.0] - 2020-03-13 + +### Added (All Versions) + +* HeapAllocator: Offset allocator for sub-allocating resources such as NativeArrays or ComputeBuffers. + +### Added (Hybrid V2) + +Hybrid Renderer V2 is a new experimental renderer. It has a significantly higher performance and better feature set compared to the existing hybrid renderer. However, it is not yet confirmed to work on all platforms. To enable Hybrid Renderer V2, use the `ENABLE_HYBRID_RENDERER_V2` define in the Project Settings. + +* HybridHDRPSamples Project for sample Scenes, unit tests and graphics tests. +* HybridURPSamples Project for sample Scenes, unit tests and graphics tests. +* MaterialOverride component: User friendly way to configure material overrides for shader properties. +* MaterialOverrideAsset: MaterialOverride asset for configuring general material overrides tied to a shader. +* SparseUploader: Delta update ECS data on GPU ComputeBuffer. +* Support for Unity built-in material properties: See BuiltinMaterialProperties directory for all IComponentData structs. +* Support for HDRP material properties: See HDRPMaterialProperties directory for all IComponentData structs. +* Support for URP material properties: See URPMaterialProperties directory for all IComponentData structs. +* New API (2020.1) to directly write to ComputeBuffer from parallel Burst jobs. +* New API (2020.1) to render Hybrid V2 batches though optimized SRP Batcher backend. + +### Changes (Hybrid V2) + +* Full rewrite of RenderMeshSystemV2 and InstancedRenderMeshBatchGroup. New code is located at `HybridV2RenderSystem.cs`. +* Partial rewrite of culling. Now all culling code is located at `HybridV2Culling.cs`. +* Hybrid Renderer and culling no longer use hash maps or IJobNativeMultiHashMapVisitKeyMutableValue jobs. Chunk components and chunk/forEach jobs are used instead. +* Batch setup and update now runs in parallel Burst jobs. Huge performance benefit. +* GPU persistent data model. ComputeBuffer to store persistent data on GPU side. Use `chunk.DidChange` to delta update only changed data. Huge performance benefit. +* Per-instance shader constants are no longer setup to constant buffers for each viewport. This makes HDRP script main thread cost significantly smaller and saves significant amount of CPU time in render thread. + +### Fixed + +* Fixed culling issues (disappearing entities) 8000+ meters away from origin. +* Fixes to solve chunk fragmentation issues with ChunkWorldRenderBounds and other chunk components. Some changes were already included in 0.3.4 package, but not documented. +* Removed unnecessary reference to Unity.RenderPipelines.HighDefinition.Runtime from asmdef. +* Fixed uninitialized data issues causing flickering on some graphics backends (2020.1). + +### Misc + +* Highlighting `RenderBounds` component change introduced in `0.3.4-preview.24` which was not part of the previous changelogs, see below. + + +## [0.3.5] - 2020-03-03 + +### Changed + +* Updated dependencies of this package. + + +## [0.3.4] - 2020-02-17 + +### Changed + +* Updated dependencies of this package. +* When creating entities from scratch with code, user now needs to manually add `RenderBounds` component. Instantiating prefab works as before. +* Inactive GameObjects and Prefabs with `StaticOptimizeEntity` are now correctly treated as static +* `RenderBoundsUpdateSystem` is no longer `public` (breaking) +* deleted public `CreateMissingRenderBoundsFromMeshRenderer` system (breaking) + + +## [0.3.3] - 2020-01-28 + +### Changed + +* Updated dependencies of this package. + + +## [0.3.2] - 2020-01-16 + +### Changed + +* Updated dependencies of this package. + + +## [0.3.1] - 2019-12-16 + +**This version requires Unity 2019.3.0f1+** + +### Changes + +* Updated dependencies of this package. + + +## [0.3.0] - 2019-12-03 + +### Changes + +* Updated dependencies of this package. + + +## [0.2.0] - 2019-11-22 + +**This version requires Unity 2019.3 0b11+** + +### New Features + +* Added support for vertex skinning. + +### Fixes + +* Fixed an issue where disabled UnityEngine Components were not getting ignored when converted via `ConvertToEntity` (it only was working for subscenes). + +### Changes + +* Removed `LightSystem` and light conversion. +* Updated dependencies for this package. + +### Upgrade guide + + * `Lightsystem` was not performance by default and the concept of driving a game object from a component turned out to be not performance by default. It was also not maintainable because every property added to lights has to be reflected in this package. + * `LightSystem` will be replaced with hybrid entities in the future. This will be a more clean uniform API for graphics related functionalities. + + +## [0.1.1] - 2019-08-06 + +### Fixes + +* Adding a disabled tag component, now correctly disables the light. + +### Changes + +* Updated dependencies for this package. + + +## [0.1.0] - 2019-07-30 + +### New Features + +* New `GameObjectConversionSettings` class that we are using to help manage the various and growing settings that can tune a GameObject conversion. +* New ability to convert and export Assets, which is initially needed for Tiny. + * Assets are discovered via `DeclareReferencedAsset` in the `GameObjectConversionDeclareObjectsGroup` phase and can then be converted by a System during normal conversion phases. + * Assets can be marked for export and assigned a guid via `GameObjectConversionSystem.GetGuidForAssetExport`. During the System `GameObjectExportGroup` phase, the converted assets can be exported via `TryCreateAssetExportWriter`. +* `GetPrimaryEntity`, `HasPrimaryEntity`, and the new `TryGetPrimaryEntity` all now work on `UnityEngine.Object` instead of `GameObject` so that they can also query against Unity Assets. + +### Upgrade guide + +* Various GameObject conversion-related methods now receive a `GameObjectConversionSettings` object rather than a set of misc config params. + * `GameObjectConversionSettings` has implicit constructors for common parameters such as `World`, so much existing code will likely just work. + * Otherwise construct a `GameObjectConversionSettings`, configure it with the parameters you used previously, and send it in. +* `GameObjectConversionSystem`: `AddLinkedEntityGroup` is now `DeclareLinkedEntityGroup` (should auto-upgrade). +* The System group `GameObjectConversionDeclarePrefabsGroup` is now `GameObjectConversionDeclareObjectsGroup`. This cannot auto-upgrade but a global find&replace will fix it. +* `GameObjectConversionUtility.ConversionFlags.None` is gone, use 0 instead. + +### Changes + +* Changing `entities` dependency to latest version (`0.1.0-preview`). + + +## [0.0.1-preview.13] - 2019-05-24 + +### Changes + +* Changing `entities` dependency to latest version (`0.0.12-preview.33`). + + +## [0.0.1-preview.12] - 2019-05-16 + +### Fixes + +* Adding/fixing `Equals` and `GetHashCode` for proxy components. + + +## [0.0.1-preview.11] - 2019-05-01 + +Change tracking started with this version. diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..66c38b4 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c50cf48ef34d88448bdddae8dc5e14ba +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md new file mode 100644 index 0000000..c881427 --- /dev/null +++ b/Documentation~/TableOfContents.md @@ -0,0 +1,21 @@ +* [Entities Graphics](index.md) + * [What's new](whats-new.md) + * [Upgrade guide](upgrade-guide.md) +* [Requirements](requirements-and-compatibility.md) +* [Getting Started](getting-started.md) + * [Entities Graphics Overview](overview.md) + * [Installing Entities Graphics](creating-a-new-entities-graphics-project.md) + * [Entities Graphics Feature Matrix](entities-graphics-versions.md) +* [Entities Graphics Features](entities-graphics-features.md) + * [Material Overrides](material-overrides.md) + * [Material Overrides Using C#](material-overrides-code.md) + * [Material Overrides Using an Asset](material-overrides-asset.md) + * [Hybrid Entities](hybrid-entities.md) + * [The BatchRendererGroup API](batch-renderer-group-api.md) +* Animation + * [Mesh deformations](mesh_deformations.md) +* [Runtime Usage](runtime-usage.md) + * [Runtime Entity Creation](runtime-entity-creation.md) +* Sample Content + * [Sample Projects](sample-projects.md) + diff --git a/Documentation~/batch-renderer-group-api.md b/Documentation~/batch-renderer-group-api.md new file mode 100644 index 0000000..c1f7532 --- /dev/null +++ b/Documentation~/batch-renderer-group-api.md @@ -0,0 +1,7 @@ +# The BatchRendererGroup API + +Entities Graphics is built on top of the Unity Engine [BatchRendererGroup](https://docs.unity3d.com/ScriptReference/Rendering.BatchRendererGroup.html) API. This API connects Entities Graphics to the Unity Engine rendering backend. If you use Entities Graphics, you don't need to interact with this API directly. + +Unity updated the `BatchRendererGroup` API in Unity 2022.1 and replaced the old code paths with a single unified code path. The new API is easier to use, more efficient, more flexible, and has full test coverage. + +For more information about the new BatchRendererGroup API, see the [BatchRendererGroup](https://docs.unity3d.com/ScriptReference/Rendering.BatchRendererGroup.html) documentation. diff --git a/Documentation~/creating-a-new-entities-graphics-project.md b/Documentation~/creating-a-new-entities-graphics-project.md new file mode 100644 index 0000000..49238ea --- /dev/null +++ b/Documentation~/creating-a-new-entities-graphics-project.md @@ -0,0 +1,18 @@ +# Creating a new Entities Graphics project + +1. Create a new project. Depending on which render pipeline you want to use with Entities Graphics, the project should use a specific template: + * For the Universal Render Pipeline (URP), use the **Universal Render Pipeline** template. + * For the High Definition Render Pipeline (HDRP), use the **High Definition RP** template. + * Entities Graphics doesn't support the Built-in Render Pipeline (**3D** template). +2. Install the Entities Graphics package. Since this is an experimental package, it's not visible in the Package Manager window. The most consistent way to install this package for all versions of Unity is to use the [manifest.json](https://docs.unity3d.com/Manual/upm-manifestPrj.html). + 1. In the Project window, go to **Packages** and right-click in an empty space. + 2. Click **Show in Explorer** then, in the File Explorer window, open **Packages > manifest.json**. + 3. Add `"com.unity.entities.graphics": "**"` to the list of dependencies where \ is the version of the Entities Graphics Package you want to install. For example:
`"com.unity.entities.graphics": "0.x.y"` + 4. Installing the Entities Graphics package also installs all of its dependencies including the DOTS packages. +3. Make sure SRP Batcher is enabled in your Project's URP or HDRP Assets. Creating a Project from the URP or HDRP template enables SRP Batcher automatically. + * **URP**: Select the [URP Asset](https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest?subfolder=/manual/universalrp-asset.html) and view it in the Inspector, go to **Advanced** and make sure **SRP Batcher** is enabled. + * **HDRP**: Select the [HDRP Asset](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@latest?subfolder=/manual/index.html) and view it in the Inspector, enter [Debug Mode ](https://docs.unity3d.com/Manual/InspectorOptions.html)for the Inspector, and make sure **SRP Batcher** is enabled. +4. Entities Graphics does not support gamma space. Your Project must use linear color space. To do this: + 1. Go to **Edit > Project Settings > Player > Other Settings** and locate the **Color Space** property. + 2. Select **Linear** from the **Color Space** drop-down. +5. Entities Graphics is now installed and ready to use. diff --git a/Documentation~/entities-graphics-features.md b/Documentation~/entities-graphics-features.md new file mode 100644 index 0000000..fd714c5 --- /dev/null +++ b/Documentation~/entities-graphics-features.md @@ -0,0 +1,7 @@ +# Entities Graphics features + +This section of the documentation contains information on the various features in Entities Graphics. The pages in this section are: + +- [Material Property Overrides](material-overrides.md) +- [Hybrid Entities](hybrid-entities.md) +- [BatchRendererGroup API](batch-renderer-group-api.md) \ No newline at end of file diff --git a/Documentation~/entities-graphics-versions.md b/Documentation~/entities-graphics-versions.md new file mode 100644 index 0000000..96b7097 --- /dev/null +++ b/Documentation~/entities-graphics-versions.md @@ -0,0 +1,31 @@ +# Entities Graphics feature matrix + +Entities Graphics is in active development. The goal is to reach full High Definition Render Pipeline (HDRP) and Universal Render Pipeline (URP) feature coverage. The only exceptions are select old deprecated features in each render pipeline that now have modern replacements. + +The following table compares Entities Graphics feature support between HDRP and URP: + +| **Feature** | **URP** | **HDRP** | +| ------------------------------- | ------------ | ---------------- | +| **Material property overrides** | Yes | Yes | +| **Built-in property overrides** | Yes | Yes | +| **Shader Graph** | Yes | Yes | +| **Lit shader** | Yes | Yes | +| **Unlit shader** | Yes | Yes | +| **Decal shader** | N/A | Yes | +| **LayeredLit shader** | N/A | Yes | +| **RenderLayer** | Yes | Yes | +| **TransformParams** | Yes | Yes | +| **DisableRendering** | Yes | Yes | +| **Motion blur** | Yes | Yes | +| **Temporal AA** | N/A | Yes | +| **Sun light** | Yes | Yes | +| **Point + spot lights** | 2022 | Yes | +| **Ambient probe** | Yes | Yes | +| **Light probes** | Yes | Yes | +| **Reflection probes** | 2022 | Yes | +| **Lightmaps** | Experimental | Experimental | +| **LOD crossfade** | 2021 | 2021 | +| **Custom pass shader override** | Yes | Yes | +| **Sorted Transparencies** | Yes | Yes | +| **Dynamic Occlusion culling** | Experimental | Experimental | +| **Skinning & deformations** | Experimental | Experimental | diff --git a/Documentation~/filter.yml b/Documentation~/filter.yml new file mode 100644 index 0000000..57ff90b --- /dev/null +++ b/Documentation~/filter.yml @@ -0,0 +1,20 @@ +apiRules: + - exclude: + # inherited Object methods + uidRegex: ^System\.Object\..*$ + type: Method + - exclude: + # mentioning types from System.* namespace + uidRegex: ^System\..*$ + type: Type + - exclude: + hasAttribute: + uid: System.ObsoleteAttribute + type: Member + - exclude: + hasAttribute: + uid: System.ObsoleteAttribute + type: Type + - exclude: + uidRegex: Tests$ + type: Namespace diff --git a/Documentation~/getting-started.md b/Documentation~/getting-started.md new file mode 100644 index 0000000..7eb4fe4 --- /dev/null +++ b/Documentation~/getting-started.md @@ -0,0 +1,8 @@ +# Getting started + +Before you use the Entities Graphics package, make sure that its feature and platform coverage match your needs: + +- For information about the graphics features Entities Graphics supports, see [Entities Graphics feature matrix](entities-graphics-versions.md). +- For information about render pipeline and Unity version compatibility, see [Requirements and compatibility](requirements-and-compatibility.md). + +For information on how to create and set up your project, see [Creating a new Entities Graphics project](creating-a-new-entities-graphics-project.md). diff --git a/Documentation~/hybrid-entities.md b/Documentation~/hybrid-entities.md new file mode 100644 index 0000000..2a21f57 --- /dev/null +++ b/Documentation~/hybrid-entities.md @@ -0,0 +1,40 @@ +# Hybrid entities + +Hybrid entities is a new DOTS feature. This feature allows you to attach MonoBehaviour components to DOTS entities, without converting them to IComponentData. A conversion system calls AddHybridComponent to attach a managed component to DOTS entity. + +The following graphics related hybrid components are supported by Entities Graphics: + +- Light + HDAdditionalLightData (HDRP) +- Light + UniversalAdditionalLightData (URP) +- ReflectionProbe + HDAdditionalReflectionData (HDRP) +- TextMesh +- SpriteRenderer +- ParticleSystem +- VisualEffect +- DecalProjector (HDRP) +- DensityVolume (HDRP) +- PlanarReflectionProbe (HDRP) +- Volume +- Volume + Sphere/Box/Capsule/MeshCollider pair (local volumes) + +Note that the conversion of Camera (+HDAdditionalCameraData, UniversalAdditionalCameraData) components is disabled by default, because the scene main camera can't be a hybrid entity. To enable this conversion, add **HYBRID_ENTITIES_CAMERA_CONVERSION** define to your project settings. + +Unity updates the transform of a hybrid entity whenever it updates the DOTS LocalToWorld component. Parenting a hybrid entity to a standard DOTS entity is supported. Hybrid entities can be included in DOTS subscenes. The managed component is serialized in the DOTS subscene. + +You can write DOTS ECS queries including both IComponentData and managed hybrid components. However, these queries cannot be Burst compiled and must run in the main thread, as managed components are not thread safe. Use WithoutBurst(), and call .Run() instead of .Schedule(). + +An example of setting HDRP Light component intensity: + +```C# +class AnimateHDRPIntensitySystem : SystemBase +{ + protected override void OnUpdate() + { + Entities.WithoutBurst().ForEach((HDLightAdditionalData hdLight) => + { + hdLight.intensity = 1.5f; + }) + .Run(); + } +} +``` \ No newline at end of file diff --git a/Documentation~/images/DOTSInstancingMasterNode.png b/Documentation~/images/DOTSInstancingMasterNode.png new file mode 100644 index 0000000000000000000000000000000000000000..f87bc844a4cdf141444f4ac789c8327c73f0cbf6 GIT binary patch literal 53971 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

wMmH+Sxqf# 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 z z2zw?8Z_D5SoCvCji}d@z@;nlxIDpJ*QO(U4?(@j6N941ygOPLNS7^jYZ!%S9Z^ zh;UixkyF=?DcJTvJm)p2#gLL*hL)7q6&BEf4msr~E8);oEhjZtyZJ9E5ACc55}?I_ zwU!ALBjUK_1fDybbszmGCnrhx^A=Dkp&#aB_0nL7BpU$AdezdNk&+{TGm4SSVo|E2 zKc$YmGRDKl=ksXK0F*pp1E1cOSk*#RB5ULg;9?5{*#v*T212iRC2h3F zDojUcc%a^T2?hJS+5J>9sp;B2b|DZtdyQWf%VrTimser^bGZW4!8_qY-YcH`WO3ZORc_yfcQ+ zW#2L4R`NC0#nIVHf-q5+nk4hKF9{iqsN}36_OizTL{f?^?rUyZ|F@=O;P8(o*(N6O^WfVh%O((hGY_@@i>2 z-J*-rL)D@PF}x79l_X((YcWhH(_7ZJMs`VM=DIbmOKmXfb*40LUCdq5Esvi`!;^6jnOT1sXN{a#Kp~01?Q|eEfp1F%QWbY zGCS0x8Ibv?U-%d_8|nf-FYeN|c!QbOAXLEoFsh|piVb23cIpWDvJ%_FXVO%!m_?R& zvF3(+$Hy}*H+MPku&&G*NlLRX2qZpUTxC@|B_-yNn0ujDfeh#ORDWokS<2;jsAP?c zU40%=;9<{*wjFzgt9{>?dN&a56vdnw%Ml`-QMQI~FDr(DFME}Xu48y>mC4;(kR?o7 z*_>8i0FT!592kEwM5LDAXOJ)vqem)>yI+y-X`Xhw?vM4Q zWW5+eq>?`_BlJ{ewm~uQ;{Dv|DrAidMah`Rk09Gl9PoYa$`t=SI(uYWP7{R*}GP z={&?#%U?ASdkRJP^vUkTm3pcI;m8yRAajy|#ZeX~$xV617DCJD@YKeTl%? zKBJ&KhESQf-3U&rV*OXFMI_T4t-ucmF}2?ryKEq{&(fU`GLkF7ZIq_B#smXs_m@Ai z6GgZT@4x4bc+@clSqK!kJ}%qkL=j8h8b4CXeE&;z?tQP9mc6j+EakzQPCAN9Eia@{ z@BRGuh!lkGOA;8zpFP-)H0(ZNEAWqsti>BBWh#tAUl@kV6v7kk1m=EivD3kh0$yso8lRaKK4YdMFJ9yU zVFm~0&VW+dg*1kJFvtND2bDZp6R}L@Wn$gjw0yO{UMo3`m6a*1-qaA53!0@f6$&4G zMMlQu=AiSo?q(g0c#cUO&GSR@aQQeKo6^;r=Q3&Buh9f0NIr8!sj1?)3Vu|ZGhJTE zAPOqV7~!aqoFw3k-KzOngUG9Jy)ONM@bi8j3^JyXYlQt8XHnVU zQ=!iSoV*WhYvNYZ&JjLtrZiWgp`lzE( zuXDFH%+1k7_)S0@H-fX}FB`hG)_?gz$FX6}b$^jS!QvgoFGruWbgOQpeIn|4pWjWv znaTCP!7V_QZlfMs+g1c}Y6YG%sM*)>kBEEkIn7xVc`Bj=-vWO;=~L<4SIO`{#iy1a zo{n$l&64x*OHaxA-Yn#94>R!s`&ye&lyj7G;XD`J-_9$n>NqbkJ0vNjMU&s(&!1Y{ zep_==uh!15fO_2bszxIy=-iFJy z=z%`-->V}$6HT4B8`-yb3e-}R401WysBX!kcSas`gMTj&bUBKrm?Qy697f1vjisw3|LR2h|Wy%Ph!`*ot_@k|UQLS3VHsb#QQkuM4 literal 0 HcmV?d00001 diff --git a/Documentation~/images/GPUInstancingMaterial.png b/Documentation~/images/GPUInstancingMaterial.png new file mode 100644 index 0000000000000000000000000000000000000000..58d6d2201b7907cfc4ed9b766e81bb627c691818 GIT binary patch literal 38488 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 literal 0 HcmV?d00001 diff --git a/Documentation~/images/HybridInstancingProperty.png b/Documentation~/images/HybridInstancingProperty.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8999e287cddd14edb15b144c65528c932ab2b8 GIT binary patch literal 14152 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%0Ay5}x_MWU>yYq7CI!Sqx}w;{;^WamHng8T0sC zTXmW3*!{PYWOKXBf=;|hj_+YQL(;zV{R)-@8Li} zj9ZLHZw3^)F75~oeDhox)bvd$C(;wlm83yz5ohoNEK|g5+=9KF`m;cFw0oh3==a`h z(#Im+3}$j9qP&jrIj8k7y*!~EKbtUWKEO_=M;rd-tIeId$}xS8xLinWekbREO~}%9 zQsh0TyYFeG6%}p2lN?k8kPIak#Bu0&hoKsXZG2C7(a_!_s^*Q5wn?5a;ra#9xaVpc z;vM0o?8rI^HO0&t)&#PMm(frehCB*;Wqy9g( i`^UsnF(Ne2`Js&-QApv-}`@n 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 literal 0 HcmV?d00001 diff --git a/Documentation~/images/HybridRendererSplash.png b/Documentation~/images/HybridRendererSplash.png new file mode 100644 index 0000000000000000000000000000000000000000..65d46ba170b63aa8edc102367cc99f27695a7610 GIT binary patch literal 99168 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
I2q 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 uzXz)R|KSd}9k^ce5kn|A% literal 0 HcmV?d00001 diff --git a/Documentation~/images/MaterialOverridePerInstance.png b/Documentation~/images/MaterialOverridePerInstance.png new file mode 100644 index 0000000000000000000000000000000000000000..01c012fd6cd21af763b33876f176dfc3c4b192b3 GIT binary patch literal 74690 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 AasU7T literal 0 HcmV?d00001 diff --git a/Documentation~/images/MaterialOverridePropertySelect.png b/Documentation~/images/MaterialOverridePropertySelect.png new file mode 100644 index 0000000000000000000000000000000000000000..e8558822e902097023b130dc1a5c469145600688 GIT binary patch literal 32652 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 z -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 literal 0 HcmV?d00001 diff --git a/Documentation~/images/ProjectSettingsDialog.png b/Documentation~/images/ProjectSettingsDialog.png new file mode 100644 index 0000000000000000000000000000000000000000..5374d88ffc10029367ff2a6027608abe739f6822 GIT binary patch literal 149809 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

(KaH4Tj`~kiu#5X5G)d04u&PMnkO|i3Y&HQ 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 literal 0 HcmV?d00001 diff --git a/Documentation~/images/batch-renderer-group-api-properties.png b/Documentation~/images/batch-renderer-group-api-properties.png new file mode 100644 index 0000000000000000000000000000000000000000..bda96e87dd6edd637b1c7eec01829b21b6f0ebc1 GIT binary patch literal 143775 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ð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
IDG~zTW%WiTeeyU`%Nw1i$ZNd!_r`aIH}~NB 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 RiK~*>tWFuQaWfrw9O^k6DGv$g3k3 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 Create > Material**) and select it. +2. If the material uses a Shader Graph, enable **Override Property Declaration** then set **Shader Declaration** to **Hybrid Per Instance** for every custom property you want to override.
![](images/HybridInstancingProperty2020-2.png) +3. Create a new Material Override Asset (menu: **Assets > Create > Shader > Material Override Asset**) and select it. +4. In the Inspector, add your material to the **Material** property.
![](images/MaterialOverrideMaterialSelect.png) +5. Click **Add Property Override** and select the properties you want this asset to override. Note that, if the material uses custom properties from a Shader Graph, you may have to wait for an asset data refresh and import to happen after you select the properties. When you add the property overrides, Unity creates a C# script next to your material in the Project window. Do not touch this file.
![](images/MaterialOverridePropertySelect.png) +6. After you add the property overrides, modify them to be the values you want. +7. Select a GameObject that uses the material (or assign the material to a GameObject in your scene). Then, in the Inspector, click **Add Component** and select **Material Override**. +8. In the Inspector for that component, add your new Material Override Asset to the **Override Asset** field.
![](images/MaterialOverrideAssetSelect.png) +9. You can add this asset to all GameObjects you want to give this set of overrides. Editing the material properties from the Inspector while the Material Override Asset is selected updates all the corresponding GameObjects. +10. You can also edit the properties on a GameObject's **Material Override** component as well to only affect that instance. When you do this, the margin turns blue and the property text becomes bold. This means you have overridden the defaults of the Material Override Asset. +11. You can either push the instance's setting to the asset to update all other GameObjects or reset it to the asset's default by right clicking on the property. This does not affect GameObjects that also override the asset's default values.
![](images/MaterialOverridePerInstance.png) +12. You can create more Material Override Assets for the same material with the same or a different set of properties and then modify those properties per-GameObject as well. It does not interfere with other Material Override Assets. + diff --git a/Documentation~/material-overrides-code.md b/Documentation~/material-overrides-code.md new file mode 100644 index 0000000..910bfce --- /dev/null +++ b/Documentation~/material-overrides-code.md @@ -0,0 +1,88 @@ +# Material overrides using C# + +Entities Graphics supports per-entity overrides of various HDRP and URP material properties as well as overrides for custom Shader Graphs. You can write C#/Burst code to setup and animate material override values at runtime. + +# Built-in Material overrides + +Entities Graphics contains a built-in library of IComponentData components that you can add to your entities to override their material properties. + +**Supported HDRP Material overrides:** + +- AlphaCutoff +- AORemapMax +- AORemapMin +- BaseColor +- DetailAlbedoScale +- DetailNormalScale +- DetailSmoothnessScale +- DiffusionProfileHash +- EmissiveColor +- Metallic +- Smoothness +- SmoothnessRemapMax +- SmoothnessRemapMin +- SpecularColor +- Thickness +- ThicknessRemap +- UnlitColor (HDRP/Unlit) + +**Supported URP Material overrides:** + +- BaseColor +- BumpScale +- Cutoff +- EmissionColor +- Metallic +- OcclusionStrength +- Smoothness +- SpecColor + +If you want to override a built-in HDRP or URP property not listed here, you can do that with custom Shader Graph Material overrides. + +## Custom Shader Graph Material overrides + +You can create your own custom Shader Graph properties, and expose them to DOTS as IComponentData. This allows you to write C#/Burst code to setup and animate your own shader inputs. To do this, see the following steps: + +### Shader Graph Asset + +1. Select your Shader Graph custom property and view it in the **Graph Inspector**. +2. Open the **Node Settings** tab. +3. Enable **Override Property Declaration** then set **Shader Declaration** to **Hybrid Per Instance**.
![](images/HybridInstancingProperty2020-2.png) + +### IComponentData + +For the DOTS IComponentData struct, use the `MaterialProperty` Attribute, passing in the **Reference** and type for the Shader Graph property. For example, the IComponentData for the color (float4) property in the above step would be: + +``` +[MaterialProperty("_Color", MaterialPropertyFormat.Float4)] +public struct MyOwnColor : IComponentData +{ + public float4 Value; +} +``` + +Ensure that the *Reference* name in Shader Graph and the string name in MaterialProperty attribute match exactly. The type declared in the MaterialPropertyFormat should also be compatible with both the Shader Graph and the struct data layout. If the binary size doesn't match, you will see an error message in the console window. + +### Burst C# system + +Now you can write Burst C# system to animate your Material property: + +``` +class AnimateMyOwnColorSystem : SystemBase +{ + protected override void OnUpdate() + { + Entities.ForEach((ref MyOwnColor color, in MyAnimationTime t) => + { + color.Value = new float4( + math.cos(t.Value + 1.0f), + math.cos(t.Value + 2.0f), + math.cos(t.Value + 3.0f), + 1.0f); + }) + .Schedule(); + } +} +``` + +**Important:** You need to create a matching IComponentData struct for every custom Shader Graph property that has **Hybrid Per Instance** enabled. For information on how to do this, see [IComponentData](#icomponentdata). If you don't do this for a custom property, Entities Graphics zero fills the property. diff --git a/Documentation~/material-overrides.md b/Documentation~/material-overrides.md new file mode 100644 index 0000000..c1f1520 --- /dev/null +++ b/Documentation~/material-overrides.md @@ -0,0 +1,15 @@ +# Material overrides + +Entities Graphics enables you to override the property values of various HDRP and URP material properties. It also enables you to override material properties for custom Shader Graphs. There are two ways to do this, either: + +- [Using C#/Burst code](material-overrides-code.md) +- [Using the Material Override Asset](material-overrides-asset.md) + +## Sample scenes + +Entities Graphics provides sample scenes to show you material overrides using both the Universal Render Pipeline (URP) and the High Definition Render Pipeline (HDRP). For information on where to download the samples from, see [Sample Projects](sample-projects.md). + +The paths to the per-render pipeline sample scenes are as follows: + +- **HDRP**: **HybridHDRPSamples > SampleScenes > MaterialOverridesSample** +- **URP**: **HybridURPSamples > SampleScenes > MaterialOverridesSample** diff --git a/Documentation~/mesh_deformations.md b/Documentation~/mesh_deformations.md new file mode 100644 index 0000000..9813844 --- /dev/null +++ b/Documentation~/mesh_deformations.md @@ -0,0 +1,72 @@ +## Mesh deformations +This page describes how to deform meshes using skinning and blendshapes, similar to what the [SkinnedMeshRenderer](https://docs.unity3d.com/Manual/class-SkinnedMeshRenderer.html) does. Generally, you want to use this in combination with DOTS Animation. For samples of setups and usage of the systems, see [DOTS Animation Samples](https://github.com/Unity-Technologies/Unity.Animation.Samples/blob/master/README.md). + + +## Disclaimer +This version is highly experimental. This means that it is not yet ready for production use and parts of the implementation will change. +## Setup + +To use mesh deformations in your Unity Project, you need to correctly set up: + +- Your [Unity Project](#project-setup) +- A [material to use with the deformed mesh](#material-setup) +- A [mesh to apply the material to](#mesh-setup). + +### Project setup + +Follow these steps to set your Unity Project up to support mesh deformation. + +1. Make sure your Unity Project uses [Scriptable Render Pipeline](https://docs.unity3d.com/Manual/ScriptableRenderPipeline.html) (SRP) version 7.x or higher. +2. Make sure your Unity Project uses Entities Graphics +3. If you intend to use Compute Deformation (required for blendshapes), go to Project Settings (menu: **Edit > Project Settings**) and, in the Player section, add the `ENABLE_COMPUTE_DEFORMATIONS` define to **Scripting Define Symbols**. + +### Material setup + +Follow these steps to create a material that Entities Graphics can use to render mesh deformations: + +1. Create a Shader Graph and open it. You can use any Shader Graph from the High Definition Render Pipeline (HDRP) or the Universal Render Pipeline (URP). +2. Add either the [Compute Deformation](https://docs.unity3d.com/Packages/com.unity.shadergraph@latest?subfolder=/manual/Compute-Deformation-Node.html) or [Linear Blend Skinning](https://docs.unity3d.com/Packages/com.unity.shadergraph@latest?subfolder=/manual/Linear-Blend-Skinning-Node.html) node to the Shader Graph. +3. Connect the position, normal, and tangent outputs of the node to the vertex position, normal, and tangent slots in the master node respectively. Save the Shader Graph. + +1. Create a material that uses the new Shader Graph. To do this, right-click on the Shader Graph asset and click **Create > Material**. +2. If you already have a mesh set up, assign the material to all material slots on the SkinnedMeshRenderer. If not, see [Mesh setup](#mesh-setup). +3. Now Entities Graphics is able to fetch the result of the deformation when the Entity renders. However, for the mesh to actually deform, you must set it up correctly. For information on how to do this, see [Mesh setup](#mesh-setup). + +### Mesh setup + +Follow these steps to set up a mesh that Entities Graphics can animate using mesh deformation: + +1. Make sure your GameObject or Prefab is suitable for mesh deformations. This means it uses the SkinnedMeshRenderer component and not the MeshRenderer component. Furthermore, the mesh you assign to a SkinnedMeshRenderer needs to have blendshapes and/or a valid bind pose and skin weights. If it does not, an error appears in the SkinnedMeshRenderer component Inspector. +2. Assign the material you created in [Material setup](#material-setup) to all material slots on the SkinnedMeshRenderer(s). +3. When Unity converts the GameObject or Prefab into an entity, it adds the correct deformation components. Furthermore, the deformation systems dispatch and apply the deformations to the mesh. Note that to create motion you should either use Dots Animation or write to the SkinMatrix and BlendShapeWeights components directly. + +### Vertex shader skinning +Skins the mesh on the GPU in the vertex shader. +#### Features +- Linear blend skinning with four influences per vertex. +- Does not support blendshapes. +#### Requirements +- Unity 2019.3b11 or newer (recommended) +- Entities Graphics 0.5.0 or higher (recommended) +- SRP version 7.x.x or higher (recommended) + + +### Compute shader deformation +Applies mesh deformations on the GPU using compute shaders. +#### Features +- Linear blend skinning, supports up to 255 sparse influences per vertex +- Supports sparse blendshapes +#### Requirements +- Add the `ENABLE_COMPUTE_DEFORMATIONS` define to **Scripting Define Symbols** in your Project Settings (menu: **Edit > Project Settings > Player**) +- Unity 2020.1.0b6 or higher (recommended) +- Entities Graphics 0.5.0 or higher (recommended) +- SRP version 9.x.x or higher (recommended) + +## Known limitations +- Wire frame mode and other debug modes do not display mesh deformations. +- Render bounds are not resized or transformed based on the mesh deformations. +- No frustum or occlusion culling, Unity processes mesh deformation for everything that uses it in the scene. +- Visual glitches may appear on the first frame. +- Live link is still untested with many of the features. +- Deformed meshes can disappear or show in their bind pose when Unity renders them as GameObjects. +- Compute deformation performance varies based on GPU. diff --git a/Documentation~/overview.md b/Documentation~/overview.md new file mode 100644 index 0000000..c041968 --- /dev/null +++ b/Documentation~/overview.md @@ -0,0 +1,28 @@ +# Entities Graphics overview + +Entities Graphics acts as a bridge between DOTS and Unity's existing rendering architecture. This allows you to use ECS entities instead of GameObjects for significantly improved runtime memory layout and performance in large scenes, while maintaining the compatibility and ease of use of Unity's existing workflows. + +## Entities Graphics feature matrix + +For more information about the render pipeline feature compatibility, see [Entities Graphics feature matrix](entities-graphics-versions.md). + +## The GameObject conversion system + +Entities Graphics includes systems that convert GameObjects into equivalent DOTS entities. You can use these systems to convert the GameObjects in the Unity Editor, or at runtime. + +To convert entites in the Unity Editor, put them in a SubScene. The Unity Editor performs the conversion offline, and saves the results to disk. To convert your GameObjects to entities at runtime at scene load, add a ConvertToEntity component to them. Using offline SubScene conversion results in significantly better scene loading performance. + +Unity performs the following steps during conversion: + +- The conversion system converts [MeshRenderer](https://docs.unity3d.com/Manual/class-MeshRenderer.html) and[ MeshFilter](https://docs.unity3d.com/Manual/class-MeshFilter.html) components into a DOTS RenderMesh component on the entity. Depending on the render pipeline your Project uses, the conversion system might also add other rendering-related components. +- The conversion system converts[ LODGroup](https://docs.unity3d.com/Manual/class-LODGroup.html) components in GameObject hierarchies to DOTS MeshLODGroupComponents. Each entity referred by the LODGroup component has a DOTS MeshLODComponent. +- The conversion system converts the Transform of each GameObject into a DOTS LocalToWorld component on the entity. Depending on the Transform's properties, the conversion system might also add DOTS Translation, Rotation, and NonUniformScale components. + +## Runtime functionality + +At runtime, Entities Graphics processes all entities that have LocalToWorld, RenderMesh, and RenderBounds DOTS components. Many HDRP and URP features require their own material property components. These components are added during the MeshRenderer conversion. Processed entities are added to batches. Unity renders the batches using the [SRP Batcher](https://blogs.unity3d.com/2019/02/28/srp-batcher-speed-up-your-rendering/). + +There are two best practice ways to instantiate entities at runtime: Prefabs and the `RenderMeshUtility.AddComponents` API: + +* Unity converts Prefabs to an optimal data layout during DOTS conversion. There are no additional structural changes during instantiation. Use prefabs if you want to instantiate complex objects. +* If you want to build the entity from a C# script, use the [RenderMeshUtility.AddComponents](runtime-entity-creation.md) API. This API automatically adds all the graphics components that the active render pipeline requires. Don't add graphics components manually. This is less efficient (adds structural changes) and is not forward compatible with future Entities Graphics package versions. \ No newline at end of file diff --git a/Documentation~/requirements-and-compatibility.md b/Documentation~/requirements-and-compatibility.md new file mode 100644 index 0000000..70f4cb3 --- /dev/null +++ b/Documentation~/requirements-and-compatibility.md @@ -0,0 +1,29 @@ +# Requirements and compatibility + +This page contains information on system requirements and compatibility of the Entities Graphics package. + +## Render pipeline compatibility + +Entities Graphics requires a Scriptable Render Pipeline (SRP) to function. + +| **Render pipeline** | **Compatibility** | +| ------------------------------------------ | ---------------------------------------------------------- | +| **Built-in Render Pipeline** | Not supported | +| **High Definition Render Pipeline (HDRP)** | HDRP version 13.0.0 and above, with Unity 2022.1 and above | +| **Universal Render Pipeline (URP)** | URP version 13.0.0 and above, with Unity 2022.1 and above | + +For more information about the supported feature set, see [Entities Graphics feature matrix](entities-graphics-versions.md). + +## Unity Player system requirements + +This section describes the Entities Graphics package’s target platform requirements. For platforms or use cases not covered in this section, general system requirements for the Unity Player apply. + +For more information, see [System requirements for Unity](https://docs.unity3d.com/Manual/system-requirements.html). + +Entities Graphics is not yet tested or supported on [XR](https://docs.unity3d.com/Manual/XR.html) devices. XR support is intended in a later version. + +Entities Graphics does not support ray-tracing (DXR). Ray-tracing support is intended in a later version. + +## DOTS feature compatibility + +Entities Graphics does not support multiple DOTS [Worlds](https://docs.unity3d.com/Packages/com.unity.entities@latest?subfolder=/manual/world.html). Limited support for multiple Worlds is intended in a later version. The current plan is to add support for creating multiple rendering systems, one per renderable World, but then only have one World active for rendering at once. diff --git a/Documentation~/runtime-entity-creation.md b/Documentation~/runtime-entity-creation.md new file mode 100644 index 0000000..aba7f29 --- /dev/null +++ b/Documentation~/runtime-entity-creation.md @@ -0,0 +1,132 @@ +# Runtime entity creation + +To render an entity, Entities Graphics requires that the entity contains a specific minimum set of components. The list of components Entities Graphics requires is substantial and may change in the future. To allow you to flexibly create entities at runtime in a way that is consistent between package versions, Entities Graphics provides the `RenderMeshUtility.AddComponents` API. + +## RenderMeshUtility - AddComponents + +This API takes an entity and adds the components Entities Graphics requires based on the given mesh and material, and a `RenderMeshDescription`, which is a struct that describes additional rendering settings. There are two versions of the API: +- One version accepts a `RenderMesh`. For information on the structure of a `RenderMesh`, see [RenderMesh](#rendermesh). Entities Graphics only uses this version during the GameObject conversion process; using this version at runtime doesn't produce rendering entities. +- A second version accepts a `RenderMeshArray`. For information on the structure of a `RenderMeshArray`, see [RenderMeshArray](#rendermesharray). Use this version of `AddComponents` at runtime. + +### RenderMeshDescription and RenderFilterSettings + +A `RenderMeshDescription` struct describes when and how to draw an entity. It contains a `RenderFilterSettings` which specifies layering, shadowing and motion vector settings, and light probe usage settings. + +### RenderMesh + +A `RenderMesh` describes which mesh and material an entity should use. Entities Graphics uses the `RenderMesh` component during GameObject conversion before transforming the entity into a more efficient format. + +**Note**: Entities Graphics no longer uses this component at runtime. It only uses this component to simplify the GameObject conversion process. + +### RenderMeshArray + +A `RenderMeshArray` contains a list of meshes and materials that a collection of entities share. Each entity can efficiently select from any mesh and material inside this array using a `MaterialMeshInfo` component created using the `MaterialMeshInfo.FromRenderMeshArrayIndices` method. During GameObject conversion, Entities Graphics attempts to pack all meshes and materials in a single subscene into a single shared `RenderMeshArray` to minimize chunk fragmentation. + +### MaterialMeshInfo + +The `MaterialMeshInfo` is a Burst-compatible plain data component that you can use to efficiently select or change an entity's mesh and material. This component supports two methods of selecting or changing an entity's mesh or material: + +* Referring to array indices inside a `RenderMeshArray` shared component on the same entity. +* Referring directly to mesh and material IDs that you registered with the Entities Graphics beforehand. + +### Usage instructions + +This API tries to be as efficient as possible, but it is still a main-thread only API and therefore not suitable for creating a large number of entities. Instead, it is best practice to use `Instantiate` to efficiently clone existing entities then set their components (e.g. `Translation` or `LocalToWorld`) to new values afterward. This workflow has several advantages: + +- You can convert the base entity from a Prefab, or create it at runtime using `RenderMeshUtility.AddComponents`. Instantiation performance does not depend on which approach you use. +- `Instantiate` and `SetComponent` / `SetComponentData` don't cause resource-intensive structural changes. +- You can use `Instantiate` and `SetComponent` from Burst jobs using `EntityCommandBuffer.ParallelWriter`, which efficiently scales to multiple cores. +- Internal Entities Graphics components are pre-created for the entities, which means that Entities Graphics does not need to create those components at runtime. + +### Example usage + +The following code example shows how to use `RenderMeshUtility.AddComponents` to create a base entity and then instantiate that entity many times in a Burst job: + +```c# +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Rendering; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Rendering; + +public class AddComponentsExample : MonoBehaviour +{ + public Mesh Mesh; + public Material Material; + public int EntityCount; + + // Example Burst job that creates many entities + [GenerateTestsForBurstCompatibility] + public struct SpawnJob : IJobParallelFor + { + public Entity Prototype; + public int EntityCount; + public EntityCommandBuffer.ParallelWriter Ecb; + + public void Execute(int index) + { + // Clone the Prototype entity to create a new entity. + var e = Ecb.Instantiate(index, Prototype); + // Prototype has all correct components up front, can use SetComponent to + // set values unique to the newly created entity, such as the transform. + Ecb.SetComponent(index, e, new LocalToWorld {Value = ComputeTransform(index)}); + } + + public float4x4 ComputeTransform(int index) + { + return float4x4.Translate(new float3(index, 0, 0)); + } + } + + void Start() + { + var world = World.DefaultGameObjectInjectionWorld; + var entityManager = world.EntityManager; + + EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob); + + // Create a RenderMeshDescription using the convenience constructor + // with named parameters. + var desc = new RenderMeshDescription( + shadowCastingMode: ShadowCastingMode.Off, + receiveShadows: false); + + // Create an array of mesh and material required for runtime rendering. + var renderMeshArray = new RenderMeshArray(new List { Material }, new List { Mesh }); + + // Create empty base entity + var prototype = entityManager.CreateEntity(); + + // Call AddComponents to populate base entity with the components required + // by Entities Graphics + RenderMeshUtility.AddComponents( + prototype, + entityManager, + desc, + renderMeshArray, + MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0)); + entityManager.AddComponentData(prototype, new LocalToWorld()); + + // Spawn most of the entities in a Burst job by cloning a pre-created prototype entity, + // which can be either a Prefab or an entity created at run time like in this sample. + // This is the fastest and most efficient way to create entities at run time. + var spawnJob = new SpawnJob + { + Prototype = prototype, + Ecb = ecb.AsParallelWriter(), + EntityCount = EntityCount, + }; + + var spawnHandle = spawnJob.Schedule(EntityCount, 128); + spawnHandle.Complete(); + + ecb.Playback(entityManager); + ecb.Dispose(); + entityManager.DestroyEntity(prototype); + } +} +``` + diff --git a/Documentation~/runtime-usage.md b/Documentation~/runtime-usage.md new file mode 100644 index 0000000..99d5a9a --- /dev/null +++ b/Documentation~/runtime-usage.md @@ -0,0 +1,7 @@ +# Runtime usage + +This section contains information on how to use Entities Graphics at runtime. + +This section contains the following articles: + +* [Runtime Entity Creation](runtime-entity-creation.md): This article explains how to create entities at runtime and add the required components they need to render correctly. \ No newline at end of file diff --git a/Documentation~/sample-projects.md b/Documentation~/sample-projects.md new file mode 100644 index 0000000..da6da20 --- /dev/null +++ b/Documentation~/sample-projects.md @@ -0,0 +1,20 @@ +# Sample projects + +To explore more use cases of the Hybrid Render, the **EntityComponentSystemSamples** repository contains various samples. + +This repository is hosted [here](https://github.com/Unity-Technologies/EntityComponentSystemSamples) on Unity Technology's GitHub. + +When you clone the Project's repository, make sure to use git or a git client. This is because if you use the Green button on the GitHub website, it does not download all the Assets. + +The Entities Graphics sample projects can be found at: + +- **HDRP**: [HybridHDRPSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/HybridHDRPSamples) +- **URP**: [HybridURPSamples](https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/HybridURPSamples) + +Project folder structure: + +- **SampleScenes**: Contains all sample scenes, showcasing the supported features. +- **StressTestScenes**: Contains stress test scenes for benchmarking. +- **Tests**: Graphics tests (for image comparisons). + +Sample projects use Entities Graphics and require Unity 2022.1 or later and version 13.0.0 or later of the HDRP and URP packages. diff --git a/Documentation~/upgrade-guide.md b/Documentation~/upgrade-guide.md new file mode 100644 index 0000000..1fb512c --- /dev/null +++ b/Documentation~/upgrade-guide.md @@ -0,0 +1,23 @@ +# Upgrade to Entities Graphics version 1.0 + +To upgrade to Entities Graphics package version 1.0, you need to do the following: + +* Remove usage of HLOD. +* Replace runtime usage of `RenderMesh` with `RenderMeshArray`. +* Replace usage of rendering settings in `RenderMesh` with rendering settings in `RenderFilterSettings`. + +## Remove HLOD + +HLOD is a feature created specifically for the MegaCity demo and has been removed from the 1.0 release of the Entities Graphics package. To upgrade Entities Graphics to 1.0, you must remove any usage of HLOD in your project. + +## Replace runtime usage of RenderMesh with RenderMeshArray + +Previously, Entities Graphics used the RenderMesh shared component at runtime to create batches for rendering. Entities Graphics 1.0 replaces this with the RenderMeshArray shared component and the MaterialMeshInfo component. + +The `RenderMesh` component still exists as a convenient intermediate step during entity baking, but Entities Graphics ignores it at runtime. To upgrade Entities Graphics to 1.0, you must update any code that uses `RenderMesh` or the `RenderMeshUtility.AddComponents` APIs to use the new `RenderMeshArray` versions. For more information, refer to [Runtime entity creation](runtime-entity-creation.md). + +## Replace usage of rendering settings in RenderMesh with rendering settings in RenderFilterSettings + +Entities Graphics 1.0 moves many rendering settings from the `RenderMesh` shared component to the `RenderFilterSettings` shared component. This includes settings such as rendering layer settings, motion vector rendering settings, and shadow rendering settings. To upgrade Entities Graphics to 1.0, you must update any code that uses the settings in `RenderMesh` to use the settings in `RenderFilterSettings`. + +For a full list of changes and updates in this version, refer to the Entities Graphics package [changelog](xref:changelog). \ No newline at end of file diff --git a/Documentation~/whats-new.md b/Documentation~/whats-new.md new file mode 100644 index 0000000..30f6cef --- /dev/null +++ b/Documentation~/whats-new.md @@ -0,0 +1,34 @@ +# What's new in version 1.0 + +Summary of changes in Entities Graphics package version 1.0. + +The main updates in this release include: + +### Added + +* Added support for OpenGL ES 3.1 on Android. +* New `RegisterMesh` and `RegisterMaterial` APIs. +* Added efficient Mesh and Material switching at runtime using `MaterialMeshInfo`. + +#### New RegisterMesh and RegisterMaterial APIs + +In Entities Graphics 1.0, you can directly register your own meshes and materials to use for rendering. To do this, call the new `EntitiesGraphicsSystem.RegisterMaterial` and `EntitiesGraphicsSystem.RegisterMesh` APIs to get Burst-compatible IDs that can be placed in a `MaterialMeshInfo` component. + +#### Efficient Mesh and Material switching at runtime using MaterialMeshInfo + +Entities Graphics 1.0 uses the new `MaterialMeshInfo` component to specify the mesh and material for entities. `MaterialMeshInfo` is a normal IComponentData, and you can change its value efficiently, unlike a shared component. This makes it possible to efficiently change the mesh and material that Unity uses to render an entity. + +To use the new `MaterialMeshInfo` component, you either need to manually register materials and meshes using the new `RegisterMaterial` and `RegisterMesh` APIs, or you can also use array indices to select the corresponding mesh and material from the `RenderMeshArray` shared component, if the entity has one. + +Entities that use manually registered IDs don't need to have a `RenderMeshArray` component, while entities that use array indices must have one. Entities Graphics 1.0 entity baking uses `RenderMeshArray`. + +### Updated + +* Updated the name of the package from Hybrid Renderer to Entities Graphics. +* Updated Scene view entity picking to use [BatchRendererGroup](https://docs.unity3d.com/2022.1/Documentation/Manual/batch-renderer-group.html). + +### Removed + +* Removed the HLOD component. + +For a full list of changes and updates in this version, refer to the Entities Graphics package [changelog](xref:changelog). diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..b205c3a --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dec5bb0babeb99944a1cc3fea1121d4e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/MaterialOverrideAssetEditor.cs b/Editor/MaterialOverrideAssetEditor.cs new file mode 100644 index 0000000..7673cc9 --- /dev/null +++ b/Editor/MaterialOverrideAssetEditor.cs @@ -0,0 +1,285 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +internal class MaterialPropPopup : PopupWindowContent +{ + public bool propertyListChanged; + + private Vector2 _scrollViewVector; + private MaterialOverrideAsset _overrideAsset; + private SerializedObject _serializedObject; + private List _generatedScriptPaths; + + //TODO(andrew.theisen): we need a way to support float2 and float3. can't do this until we can get the DOTS property type or byte size + private readonly ShaderPropertyType[] _supportedTypes = + { + ShaderPropertyType.Color, + ShaderPropertyType.Vector, + ShaderPropertyType.Float, + ShaderPropertyType.Range, + }; + + public MaterialPropPopup(MaterialOverrideAsset overrideAsset, SerializedObject serializedObject) + { + _overrideAsset = overrideAsset; + _serializedObject = serializedObject; + _generatedScriptPaths = new List(); + propertyListChanged = false; + } + + public override void OnGUI(Rect rect) + { + _scrollViewVector = GUILayout.BeginScrollView(_scrollViewVector); + if (_overrideAsset.material != null) + { + Shader shader = _overrideAsset.material.shader; + for (int i = 0; i < shader.GetPropertyCount(); i++) + { + ShaderPropertyType propertyType = shader.GetPropertyType(i); + if (_supportedTypes.Any(item => item == propertyType)) + { + string propertyName = shader.GetPropertyName(i); + string displayName = propertyName; + + //TODO(andrew.theisen): review if this UI code is too coupled with behavior? + int index = _overrideAsset.overrideList.FindIndex(d => d.name == displayName); + bool overriden = index != -1; + bool toggle = GUILayout.Toggle(overriden, displayName); + if (overriden != toggle) + { + _serializedObject.Update(); + SerializedProperty overrideListProp = _serializedObject.FindProperty("overrideList"); + int arraySize = overrideListProp.arraySize; + + string shaderName = AssetDatabase.GetAssetPath(shader); + if (toggle) + { + overrideListProp.InsertArrayElementAtIndex(arraySize); + SerializedProperty overrideProp = overrideListProp.GetArrayElementAtIndex(arraySize); + overrideProp.FindPropertyRelative("name").stringValue = propertyName; + overrideProp.FindPropertyRelative("displayName").stringValue = displayName; + + overrideProp.FindPropertyRelative("shaderName").stringValue = shaderName; + overrideProp.FindPropertyRelative("materialName").stringValue = _overrideAsset.material.name; + overrideProp.FindPropertyRelative("type").intValue = (int)propertyType; + overrideProp.FindPropertyRelative("instanceOverride").boolValue = false; + if (propertyType == ShaderPropertyType.Vector || propertyType == ShaderPropertyType.Color) + { + overrideProp.FindPropertyRelative("value").vector4Value = _overrideAsset.material.GetVector(propertyName); + } + else if (propertyType == ShaderPropertyType.Float || propertyType == ShaderPropertyType.Range) + { + Vector4 vec4 = new Vector4(_overrideAsset.material.GetFloat(propertyName), 0.0f, 0.0f, 0.0f); + overrideProp.FindPropertyRelative("value").vector4Value = vec4; + } + } + else + { + overrideListProp.DeleteArrayElementAtIndex(index); + } + _serializedObject.ApplyModifiedProperties(); + + string scriptPath = GenerateIComponentData(); + if (scriptPath != null) + { + _generatedScriptPaths.Add(scriptPath); + } + + propertyListChanged = true; + } + } + } + } + + GUILayout.EndScrollView(); + } + + private string GenerateIComponentData() + { + string filepath = null; + string preamble = +@"using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ +"; + for (int i = 0; i < _overrideAsset.overrideList.Count; i++) + { + MaterialOverrideAsset.OverrideData overrideData = _overrideAsset.overrideList[i]; + + + if (_overrideAsset.GetTypeFromAttrs(overrideData) != null) + { + continue; + } + + string generatedStruct = ""; + + string @fieldName = overrideData.name.Replace("_", ""); //TODO(andrew.theisen): properly sanitize type names to follow c# class name rules + string @typeName = ""; + + if (overrideData.type == ShaderPropertyType.Color || overrideData.type == ShaderPropertyType.Vector) + { + @typeName = "Vector4Override"; + generatedStruct = + $@" [MaterialProperty(""{@overrideData.name}"")] + struct {@fieldName}{@typeName} : IComponentData + {{ + public float4 Value; + }} +}} +"; + } + else if (overrideData.type == ShaderPropertyType.Float || overrideData.type == ShaderPropertyType.Range) + { + @typeName = "FloatOverride"; + generatedStruct = + $@" [MaterialProperty(""{@overrideData.name}"")] + struct {@fieldName}{@typeName} : IComponentData + {{ + public float Value; + }} +}} +"; + } + + if (generatedStruct != "") + { + //TODO(andrew.theisen): writeall text 260 char limit for filepath + filepath = Path.Combine(Path.GetDirectoryName(AssetDatabase.GetAssetPath(_overrideAsset.material)), + $@"{@fieldName}{@typeName}OverrideGenerated.cs"); + File.WriteAllText(filepath, preamble + generatedStruct); + + _overrideAsset.overrideList[i] = overrideData; + } + } + + return filepath; + } + + public override void OnClose() + { + if (_generatedScriptPaths.Count != 0) + { + AssetDatabase.StartAssetEditing(); + foreach (var scriptPath in _generatedScriptPaths) + { + AssetDatabase.ImportAsset(scriptPath); + } + AssetDatabase.StopAssetEditing(); + AssetDatabase.Refresh(); + //Type overrideType = GetTypeFromAttrs(overrideData); //TODO(andrew.theisen): how do we get type manager to update so we can get the type right away? + } + } +} + +///

+/// Represents the Inspector UI for a material override asset. +/// +[CustomEditor(typeof(MaterialOverrideAsset))] +public class MaterialOverrideAssetEditor : Editor +{ + private Rect _buttonRect; + private MaterialPropPopup _popupWindow; + + /// + /// Draws the custom inspector for the material override asset. + /// + public override void OnInspectorGUI() + { + serializedObject.Update(); + + MaterialOverrideAsset overrideAsset = (target as MaterialOverrideAsset); + Material currentMat = overrideAsset.material; + + bool dirtyGameObjects = false; + EditorGUI.BeginChangeCheck(); + + EditorGUILayout.PropertyField(serializedObject.FindProperty("material"), new GUIContent("Material")); + + SerializedProperty overrideListProp = serializedObject.FindProperty("overrideList"); + for (int i = 0; i < overrideListProp.arraySize; i++) + { + SerializedProperty overrideProp = overrideListProp.GetArrayElementAtIndex(i); + string displayName = overrideProp.FindPropertyRelative("displayName").stringValue; + ShaderPropertyType type = (ShaderPropertyType)overrideProp.FindPropertyRelative("type").intValue; + + if (type == ShaderPropertyType.Color) + { + SerializedProperty colorProp = overrideProp.FindPropertyRelative("value"); + Color color = new Color(colorProp.vector4Value.x, colorProp.vector4Value.y, colorProp.vector4Value.z, colorProp.vector4Value.w); + Color newColor = EditorGUILayout.ColorField(new GUIContent(displayName), color); + Vector4 vec4 = new Vector4(newColor.r, newColor.g, newColor.b, newColor.a); + colorProp.vector4Value = vec4; + } + else if (type == ShaderPropertyType.Vector) + { + SerializedProperty vector4Prop = overrideProp.FindPropertyRelative("value"); + EditorGUILayout.PropertyField(vector4Prop, new GUIContent(displayName)); + } + else if (type == ShaderPropertyType.Float || type == ShaderPropertyType.Range) + { + SerializedProperty floatProp = overrideProp.FindPropertyRelative("value"); + float f = floatProp.vector4Value.x; + float newF = EditorGUILayout.FloatField(new GUIContent(displayName), f); + floatProp.vector4Value = new Vector4(newF, 0.0f, 0.0f, 0.0f); + } + else + { + Debug.Log("Property " + displayName + " is of unsupported type " + type + " for material override."); + } + } + + if (EditorGUI.EndChangeCheck()) + { + dirtyGameObjects = true; + } + + string buttonTxt = overrideAsset.material == null ? "Select a Material" : "Add Property Overrride"; + + if (overrideAsset.material == null) + { + GUI.enabled = false; + } + + + if (GUILayout.Button(buttonTxt)) + { + if (Event.current.type == EventType.Repaint) + { + _buttonRect = GUILayoutUtility.GetLastRect(); + } + _buttonRect.x = Event.current.mousePosition.x; + _buttonRect.y = Event.current.mousePosition.y; + _popupWindow = new MaterialPropPopup(overrideAsset, serializedObject); + PopupWindow.Show(_buttonRect, _popupWindow); + } + GUI.enabled = true; + + if (_popupWindow != null) + { + dirtyGameObjects = _popupWindow.propertyListChanged ? true : dirtyGameObjects; + } + if (dirtyGameObjects) + { + foreach (var overrideComponent in FindObjectsOfType()) + { + if (overrideComponent.overrideAsset == target) + { + EditorUtility.SetDirty(overrideComponent); + } + } + } + + serializedObject.ApplyModifiedProperties(); + if (currentMat != overrideAsset.material) + { + overrideAsset.overrideList = new List(); + } + } +} diff --git a/Editor/MaterialOverrideAssetEditor.cs.meta b/Editor/MaterialOverrideAssetEditor.cs.meta new file mode 100644 index 0000000..b5220c2 --- /dev/null +++ b/Editor/MaterialOverrideAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5b5f542dd7bba24e8a45c01fff42195 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/MaterialOverrideEditor.cs b/Editor/MaterialOverrideEditor.cs new file mode 100644 index 0000000..0b0c417 --- /dev/null +++ b/Editor/MaterialOverrideEditor.cs @@ -0,0 +1,156 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +/// +/// Represents the Inspector UI for a material override. +/// +[CustomEditor(typeof(MaterialOverride))] +public class MaterialOverrideEditor : Editor +{ + /// + /// Draws the custom inspector for the material override asset. + /// + public override void OnInspectorGUI() + { + Font defaultFont = EditorStyles.label.font; + + serializedObject.Update(); + SerializedProperty assetProp = serializedObject.FindProperty("overrideAsset"); + EditorGUILayout.PropertyField(assetProp, new GUIContent("Override Asset")); + + MaterialOverride overrideComponent = (target as MaterialOverride); + if (overrideComponent != null) + { + MaterialOverrideAsset overrideAsset = overrideComponent.overrideAsset; + if (overrideAsset != null) + { + SerializedProperty overrideListProp = serializedObject.FindProperty("overrideList"); + for (int i = 0; i < overrideListProp.arraySize; i++) + { + SerializedProperty overrideProp = overrideListProp.GetArrayElementAtIndex(i); + string displayName = overrideProp.FindPropertyRelative("displayName").stringValue; + ShaderPropertyType type = (ShaderPropertyType)overrideProp.FindPropertyRelative("type").intValue; + SerializedProperty instanceProp = overrideProp.FindPropertyRelative("instanceOverride"); + + Rect fieldRect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); + GUI.skin.font = defaultFont; + if (instanceProp.boolValue) + { + DrawOverrideMargin(fieldRect); + GUI.skin.font = EditorStyles.boldFont; + } + + EditorGUI.BeginChangeCheck(); + if (type == ShaderPropertyType.Color) + { + SerializedProperty colorProp = overrideProp.FindPropertyRelative("value"); + + Color color = new Color(colorProp.vector4Value.x, colorProp.vector4Value.y, colorProp.vector4Value.z, colorProp.vector4Value.w); + Color newColor = EditorGUI.ColorField(fieldRect, new GUIContent(displayName), color); + Vector4 vec4 = new Vector4(newColor.r, newColor.g, newColor.b, newColor.a); + colorProp.vector4Value = vec4; + } + else if (type == ShaderPropertyType.Vector) + { + SerializedProperty vector4Prop = overrideProp.FindPropertyRelative("value"); + + Vector4 vec4 = vector4Prop.vector4Value; + Vector4 newVec4 = EditorGUI.Vector4Field(fieldRect, new GUIContent(displayName), vec4); + vector4Prop.vector4Value = newVec4; + } + else if (type == ShaderPropertyType.Float || type == ShaderPropertyType.Range) + { + SerializedProperty floatProp = overrideProp.FindPropertyRelative("value"); + + float f = floatProp.vector4Value.x; + float newF = EditorGUI.FloatField(fieldRect, new GUIContent(displayName), f); + floatProp.vector4Value = new Vector4(newF, 0.0f, 0.0f, 0.0f); + } + else + { + Debug.Log("Property " + displayName + " is of unsupported type " + type + " for material override."); + } + if (EditorGUI.EndChangeCheck()) + { + instanceProp.boolValue = true; + } + + + if (instanceProp.boolValue) + { + if (fieldRect.Contains(Event.current.mousePosition) && Event.current.type == EventType.ContextClick) + { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Apply to MaterialOverride '" + overrideComponent.overrideAsset.name + "'"), + false, ApplyToOverrideAsset, i); + menu.AddItem(new GUIContent("Revert"), false, RevertGameobjectOverride, i); + menu.ShowAsContext(); + Event.current.Use(); + } + } + } + } + } + serializedObject.ApplyModifiedProperties(); + } + + //TODO(andrew.theisen): can we avoid this SO update? + private void ApplyToOverrideAsset(object index) + { + serializedObject.Update(); + + int intIndex = (int)index; + SerializedProperty overrideListProp = serializedObject.FindProperty("overrideList"); + SerializedProperty overrideProp = overrideListProp.GetArrayElementAtIndex(intIndex); + SerializedProperty value = overrideProp.FindPropertyRelative("value"); + SerializedProperty instanceProp = overrideProp.FindPropertyRelative("instanceOverride"); + instanceProp.boolValue = false; + + serializedObject.ApplyModifiedProperties(); + + MaterialOverride overrideComponent = (target as MaterialOverride); + SerializedObject assetSerializedObj = new SerializedObject(overrideComponent.overrideAsset); + assetSerializedObj.Update(); + SerializedProperty assetOverrideProp = assetSerializedObj.FindProperty("overrideList").GetArrayElementAtIndex(intIndex); + assetOverrideProp.FindPropertyRelative("value").vector4Value = value.vector4Value; + assetOverrideProp.FindPropertyRelative("instanceOverride").boolValue = false; + assetSerializedObj.ApplyModifiedProperties(); + } + + //TODO(andrew.theisen): can we avoid this SO update? + private void RevertGameobjectOverride(object index) + { + serializedObject.Update(); + + SerializedProperty overrideListProp = serializedObject.FindProperty("overrideList"); + SerializedProperty overrideProp = overrideListProp.GetArrayElementAtIndex((int)index); + SerializedProperty instanceProp = overrideProp.FindPropertyRelative("instanceOverride"); + instanceProp.boolValue = false; + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawOverrideMargin(Rect controlRect) + { + controlRect.yMin += 2; + controlRect.yMax += 1; + + if (Event.current.type == EventType.Repaint) + { + Color oldColor = GUI.backgroundColor; + bool oldEnabled = GUI.enabled; + GUI.enabled = true; + + Color k_OverrideMarginColor = new Color(1f / 255f, 153f / 255f, 235f / 255f, 0.75f); + + GUI.backgroundColor = k_OverrideMarginColor; + controlRect.x = 0; + controlRect.width = 2; + GUI.skin.GetStyle("OverrideMargin").Draw(controlRect, false, false, false, false); + + GUI.enabled = oldEnabled; + GUI.backgroundColor = oldColor; + } + } +} diff --git a/Editor/MaterialOverrideEditor.cs.meta b/Editor/MaterialOverrideEditor.cs.meta new file mode 100644 index 0000000..832b8f9 --- /dev/null +++ b/Editor/MaterialOverrideEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a2320b7419446d341a9459934d63cde1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Entities.Graphics.Editor.asmdef b/Editor/Unity.Entities.Graphics.Editor.asmdef new file mode 100644 index 0000000..e42f6f5 --- /dev/null +++ b/Editor/Unity.Entities.Graphics.Editor.asmdef @@ -0,0 +1,11 @@ +{ + "name": "Unity.Entities.Graphics.Editor", + "references": [ + "Unity.Entities.Graphics", + "Unity.Entities" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [] +} diff --git a/Editor/Unity.Entities.Graphics.Editor.asmdef.meta b/Editor/Unity.Entities.Graphics.Editor.asmdef.meta new file mode 100644 index 0000000..71829cb --- /dev/null +++ b/Editor/Unity.Entities.Graphics.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ecaf56f8789abe34ea6993fba292940f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d524abc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,31 @@ +com.unity.entities.graphics copyright © 2020 Unity Technologies ApS + +Unity Companion License (“License”) + +Unity Technologies ApS (“Unity”) grants to you a worldwide, non-exclusive, no-charge, and royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the software that is made available under this License (“Software”), subject to the following terms and conditions: + +1. Unity Companion Use Only. Exercise of the license granted herein is limited to exercise for the creation, use, and/or distribution of applications, software, or other content pursuant to a valid Unity content authoring and rendering engine software license (“Engine License”). That means while use of the Software is not limited to use in the software licensed under the Engine License, the Software may not be used for any purpose other than the creation, use, and/or distribution of Engine License-dependent applications, software, or other content. No other exercise of the license granted herein is permitted, and in no event may the Software be used for competitive analysis or to develop a competing product or service. + +2. No Modification of Engine License. Neither this License nor any exercise of the license granted herein modifies the Engine License in any way. + +3. Ownership & Grant Back to You. + +3.1 You own your content. In this License, “derivative works” means derivatives of the Software itself--works derived only from the Software by you under this License (for example, modifying the code of the Software itself to improve its efficacy); “derivative works” of the Software do not include, for example, games, apps, or content that you create using the Software. You keep all right, title, and interest to your own content. + +3.2 Unity owns its content. While you keep all right, title, and interest to your own content per the above, as between Unity and you, Unity will own all right, title, and interest to all intellectual property rights (including patent, trademark, and copyright) in the Software and derivative works of the Software, and you hereby assign and agree to assign all such rights in those derivative works to Unity. + +3.3 You have a license to those derivative works. Subject to this License, Unity grants to you the same worldwide, non-exclusive, no-charge, and royalty-free copyright license to derivative works of the Software you create as is granted to you for the Software under this License. + +4. Trademarks. You are not granted any right or license under this License to use any trademarks, service marks, trade names, products names, or branding of Unity or its affiliates (“Trademarks”). Descriptive uses of Trademarks are permitted; see, for example, Unity’s Branding Usage Guidelines at https://unity3d.com/public-relations/brand. + +5. Notices & Third-Party Rights. This License, including the copyright notice associated with the Software, must be provided in all substantial portions of the Software and derivative works thereof (or, if that is impracticable, in any other location where such notices are customarily placed). Further, if the Software is accompanied by a Unity “third-party notices” or similar file, you acknowledge and agree that software identified in that file is governed by those separate license terms. + +6. DISCLAIMER, LIMITATION OF LIABILITY. THE SOFTWARE AND ANY DERIVATIVE WORKS THEREOF IS PROVIDED ON AN "AS IS" BASIS, AND IS PROVIDED WITHOUT WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND/OR NONINFRINGEMENT. IN NO EVENT SHALL ANY COPYRIGHT HOLDER OR AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES (WHETHER DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL, INCLUDING PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, DATA, OR PROFITS, AND BUSINESS INTERRUPTION), OR OTHER LIABILITY WHATSOEVER, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM OR OUT OF, OR IN CONNECTION WITH, THE SOFTWARE OR ANY DERIVATIVE WORKS THEREOF OR THE USE OF OR OTHER DEALINGS IN SAME, EVEN WHERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +7. USE IS ACCEPTANCE and License Versions. Your receipt and use of the Software constitutes your acceptance of this License and its terms and conditions. Software released by Unity under this License may be modified or updated and the License with it; upon any such modification or update, you will comply with the terms of the updated License for any use of any of the Software under the updated License. + +8. Use in Compliance with Law and Termination. Your exercise of the license granted herein will at all times be in compliance with applicable law and will not infringe any proprietary rights (including intellectual property rights); this License will terminate immediately on any breach by you of this License. + +9. Severability. If any provision of this License is held to be unenforceable or invalid, that provision will be enforced to the maximum extent possible and the other provisions will remain in full force and effect. + +10. Governing Law and Venue. This License is governed by and construed in accordance with the laws of Denmark, except for its conflict of laws rules; the United Nations Convention on Contracts for the International Sale of Goods will not apply. If you reside (or your principal place of business is) within the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the state and federal courts located in San Francisco County, California concerning any dispute arising out of this License (“Dispute”). If you reside (or your principal place of business is) outside the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the courts located in Copenhagen, Denmark concerning any Dispute. diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..bfd974f --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 37fe57d2cffc4fb4180acf6ed317048a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..09da71c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Entities Graphics + +Entities Graphics provides systems and components for rendering [ECS](https://docs.unity3d.com/Packages/com.unity.entities@latest) entities. Entities Graphics is not a render pipeline: it is a system that collects the data necessary for rendering ECS entities, and sends this data to Unity's existing rendering architecture. \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..a08fca7 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 051ac405b5d3a4441b9cb188b8c32051 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.Tests.meta b/Unity.Entities.Graphics.Tests.meta new file mode 100644 index 0000000..82c6738 --- /dev/null +++ b/Unity.Entities.Graphics.Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2be1444527de3d64196d3a346fdc1aa8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs b/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs new file mode 100644 index 0000000..45702ca --- /dev/null +++ b/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs @@ -0,0 +1,127 @@ +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; +using Unity.Rendering; +using Plane = UnityEngine.Plane; +using GameObject = UnityEngine.GameObject; +using Camera = UnityEngine.Camera; +using Vector3 = UnityEngine.Vector3; +using static Unity.Mathematics.math; +using System; + +namespace Unity.Entities.Tests +{ + public class FrustumPlanesTests + { + static readonly Plane[] Planes = + { + new Plane(new Vector3(1.0f, 0.0f, 0.0f), -1.0f), + new Plane(new Vector3(-1.0f, 0.0f, 0.0f), 1.0f), + new Plane(new Vector3(0.0f, 1.0f, 0.0f), -1.0f), + new Plane(new Vector3(0.0f, -1.0f, 0.0f), 1.0f), + new Plane(new Vector3(0.0f, -1.0f, 1.0f), 1.0f), + new Plane(new Vector3(0.0f, -2.0f, 0.0f), 12.0f), + new Plane(new Vector3(1.0f, -1.0f, -7.0f), -12.0f), + new Plane(new Vector3(0.0f, 183.0f, -7.0f), -12.0f), + new Plane(new Vector3(0.9933293f, 0.01911314f, 0.113717f), -409.9551f), + }; + + static readonly AABB[] boxes = + { + new AABB { Center = new float3(0.0f, 0.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(-1.0f, 0.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(-2.0f, 0.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, -2.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, -1.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 1.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 2.0f, 0.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 0.0f, -2.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 0.0f, -1.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 0.0f, 1.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 0.0f, 2.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(1.0f, -1.0f, 1.0f), Extents = new float3(0.5f, 0.5f, 0.5f) }, + new AABB { Center = new float3(0.0f, 0.0f, 0.0f), Extents = new float3(16384.0f, 16384.0f, 16384.0f) }, + new AABB { Center = new float3(-325.303f, 391.993f, 1053.86f), Extents = new float3(22.32453f, 18.56214f, 23.49754f) }, + }; + + static NativeArray CreatePlanes(int n) + { + var result = new NativeArray(n, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < n; ++i) + { + result[i] = Planes[i]; + } + return result; + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + [TestCase(7)] + [TestCase(8)] + [TestCase(9)] + public void MultiPlaneTest(int planeCount) + { + using (var par = CreatePlanes(planeCount)) + using (var soap = FrustumPlanes.BuildSOAPlanePackets(par, Allocator.Temp)) + { + foreach (var box in boxes) + { + Assert.AreEqual(ReferenceTest(par, box), FrustumPlanes.Intersect2(soap, box)); + } + } + } + + private FrustumPlanes.IntersectResult ReferenceTest(NativeArray par, AABB box) + { + FrustumPlanes.IntersectResult result; + var temp = new NativeArray(par.Length, Allocator.Temp); + + for (int i = 0; i < par.Length; ++i) + { + temp[i] = new float4(par[i].normal, par[i].distance); + } + + result = FrustumPlanes.Intersect(temp, box); + + temp.Dispose(); + return result; + } + + [Test] + [TestCase(0f)] + [TestCase(1e5f)] + [TestCase(-1e5f)] + public void FrustumFromCamera_PlaneDistance(float zPosition) + { + var gameObject = new GameObject(); + var camera = gameObject.AddComponent(); + + var nearClipPlane = 0.3f; + var farClipPlane = 1000f; + + camera.nearClipPlane = nearClipPlane; + camera.farClipPlane = farClipPlane; + + gameObject.transform.position = new float3(0, 0, zPosition); + + using (var planes = new NativeArray(6, Allocator.Temp)) + { + FrustumPlanes.FromCamera(camera, planes); + + var nearPlane = planes[4]; + var farPlane = planes[5]; + + Assert.That(nearPlane.w, Is.EqualTo(-nearClipPlane - zPosition).Within(1e-3f)); + Assert.That(farPlane.w, Is.EqualTo(farClipPlane + zPosition).Within(1e-3f)); + } + + UnityEngine.Object.DestroyImmediate(gameObject); + } + } +} diff --git a/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs.meta b/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs.meta new file mode 100644 index 0000000..515971b --- /dev/null +++ b/Unity.Entities.Graphics.Tests/FrustumPlanesTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9638d8d24217aa43b3644dd063b5b7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs b/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs new file mode 100644 index 0000000..2949315 --- /dev/null +++ b/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Rendering; + +namespace Unity.Rendering.Tests +{ + public class HeapAllocatorTests + { + [Test] + public void BasicTests() + { + var allocator = new HeapAllocator(1000); + + allocator.DebugValidateInternalState(); + + Assert.AreEqual(allocator.FreeSpace, 1000); + + HeapBlock b10 = allocator.Allocate(10); + allocator.DebugValidateInternalState(); + HeapBlock b100 = allocator.Allocate(100); + allocator.DebugValidateInternalState(); + + // Check that the allocations have sufficient size. + Assert.GreaterOrEqual(b10.Length, 10); + Assert.GreaterOrEqual(b100.Length, 100); + + // Check that the amount of free space has decreased accordingly. + Assert.LessOrEqual(allocator.FreeSpace, 1000 - 100 - 10); + + allocator.Release(b10); + allocator.DebugValidateInternalState(); + allocator.Release(b100); + allocator.DebugValidateInternalState(); + + // Everything should now be freed. + Assert.AreEqual(allocator.FreeSpace, 1000); + allocator.Dispose(); + } + + [Test] + public void AllocateEntireHeap() + { + var allocator = new HeapAllocator(100); + // Check that it's possible to allocate the entire heap. + Assert.AreEqual(allocator.Allocate(100).Length, 100); + allocator.Dispose(); + } + + [Test] + public void Coalescing() + { + var allocator = new HeapAllocator(100); + allocator.DebugValidateInternalState(); + + // Try to allocate ten blocks. These should succeed, because the heap should not be fragmented yet. + var blocks10 = Enumerable.Range(0, 10).Select(x => allocator.Allocate(10)).ToArray(); + allocator.DebugValidateInternalState(); + + Assert.IsTrue(blocks10.All(b => b.Length == 10)); + Assert.IsTrue(allocator.Full); + + // Release all of them. + foreach (var b in blocks10) + { + allocator.Release(b); + allocator.DebugValidateInternalState(); + } + + // Now try to allocate the entire heap. It should succeed, because everything has been freed. + Assert.AreEqual(allocator.Allocate(100).Length, 100); + allocator.DebugValidateInternalState(); + allocator.Dispose(); + } + + [Test] + public void RandomStressTest() + { + const int HeapSize = 1_000_000; + const int NumBlocks = 1_000; + const int NumRounds = 20; + const int MaxAlloc = 10_000; + const int OperationsPerRound = 4_000; + + int numAllocs = 0; + int numReleases = 0; + int numFailed = 0; + + var rnd = new System.Random(293875); + var allocator = new HeapAllocator(HeapSize); + var blocks = Enumerable.Range(0, NumBlocks).Select(x => new HeapBlock()).ToArray(); + var blockEnds = new SortedSet(); + + // Stress test the allocator by doing a bunch of random allocs and deallocs and + // try to verify that allocator internal asserts don't fire, and free space behaves + // as expected. + + for (int i = 0; i < NumRounds; ++i) + { + Assert.IsTrue(allocator.Empty); + + // Perform random alloc/dealloc operations + for (int j = 0; j < OperationsPerRound; ++j) + { + ulong before = allocator.FreeSpace; + + int b = rnd.Next(NumBlocks); + + int size = 0; + if (blocks[b].Empty) + { + size = rnd.Next(1, MaxAlloc); + blocks[b] = allocator.Allocate((ulong)size); + allocator.DebugValidateInternalState(); + + if (blocks[b].Empty) + { + size = 0; + ++numFailed; + } + else + { + size = (int)blocks[b].Length; + bool added = blockEnds.Add(blocks[b].end); + Assert.IsTrue(added); + } + + ++numAllocs; + } + else + { + size = -(int)blocks[b].Length; + allocator.Release(blocks[b]); + allocator.DebugValidateInternalState(); + bool removed = blockEnds.Remove(blocks[b].end); + Assert.IsTrue(removed); + blocks[b] = new HeapBlock(); + + ++numReleases; + } + + ulong after = allocator.FreeSpace; + ulong highest = allocator.OnePastHighestUsedAddress; + + Assert.AreEqual((long)after, (long)before - size); + if (blockEnds.Count > 0) + Assert.AreEqual(blockEnds.Max, highest); + else + Assert.AreEqual(0, highest); + } + + for (int b = 0; b < NumBlocks; ++b) + { + if (!blocks[b].Empty) + { + allocator.Release(blocks[b]); + blocks[b] = new HeapBlock(); + blockEnds.Clear(); + } + } + Assert.IsTrue(allocator.Empty); + } + + Debug.Log($"Allocs: {numAllocs}, Releases: {numReleases}, Failed: {numFailed}"); + allocator.Dispose(); + } + + [Test] + public void AllocationsDontOverlap() + { + // Make sure that allocations given by the allocator are disjoint (i.e. don't alias). + + const int HeapSize = 1_000_000; + const int NumBlocks = 1_000; + const int MaxAlloc = 10_000; + const int OperationsPerRound = 10_000; + + var rnd = new System.Random(9283572); + var allocator = new HeapAllocator(HeapSize); + var blocks = Enumerable.Range(0, NumBlocks).Select(x => new HeapBlock()).ToArray(); + var inUse = new ulong[HeapSize / 8 + 1]; + + Func qword = (ulong i) => (i / 64, (int)(i % 64)); + + // Perform random alloc/dealloc operations + for (int i = 0; i < OperationsPerRound; ++i) + { + int b = rnd.Next(NumBlocks); + const ulong kAllOnes = ~0UL; + + int size = 0; + if (blocks[b].Empty) + { + size = rnd.Next(1, MaxAlloc); + blocks[b] = allocator.Allocate((ulong)size); + + // Mark the block as allocated, and check that it wasn't allocated. + + // Do tests and sets entire qwords at a time so it's fast + var begin = qword(blocks[b].begin); + var end = qword(blocks[b].end); + + if (begin.Item1 == end.Item1) + { + ulong qw = begin.Item1; + ulong mask = kAllOnes << begin.Item2; + mask &= ~(kAllOnes << end.Item2); + Assert.IsTrue((inUse[qw] & mask) == 0, "Elements were already allocated"); + inUse[qw] |= mask; + } + else + { + ulong qw = begin.Item1; + ulong mask = kAllOnes << begin.Item2; + + Assert.IsTrue((inUse[qw] & mask) == 0, "Elements were already allocated"); + inUse[qw] |= mask; + + for (qw = begin.Item1 + 1; qw < end.Item1; ++qw) + { + mask = kAllOnes; + Assert.IsTrue((inUse[qw] & mask) == 0, "Elements were already allocated"); + inUse[qw] |= mask; + } + + qw = end.Item1; + mask = ~(kAllOnes << end.Item2); + Assert.IsTrue((inUse[qw] & mask) == 0, "Elements were already allocated"); + inUse[qw] |= mask; + } + } + else + { + allocator.Release(blocks[b]); + + // Mark the block as not allocated, and check that it was allocated. + + var begin = qword(blocks[b].begin); + var end = qword(blocks[b].end); + + if (begin.Item1 == end.Item1) + { + ulong qw = begin.Item1; + ulong mask = kAllOnes << begin.Item2; + mask &= ~(kAllOnes << end.Item2); + Assert.IsTrue((inUse[qw] & mask) == mask, "Elements were not allocated"); + inUse[qw] &= ~mask; + } + else + { + ulong qw = begin.Item1; + ulong mask = kAllOnes << begin.Item2; + + Assert.IsTrue((inUse[qw] & mask) == mask, "Elements were not allocated"); + inUse[qw] &= ~mask; + + for (qw = begin.Item1 + 1; qw < end.Item1; ++qw) + { + mask = kAllOnes; + Assert.IsTrue((inUse[qw] & mask) == mask, "Elements were not allocated"); + inUse[qw] &= ~mask; + } + + qw = end.Item1; + mask = ~(kAllOnes << end.Item2); + Assert.IsTrue((inUse[qw] & mask) == mask, "Elements were not allocated"); + inUse[qw] &= ~mask; + } + + blocks[b] = new HeapBlock(); + } + } + + allocator.Dispose(); + } + } +} diff --git a/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs.meta b/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs.meta new file mode 100644 index 0000000..0c54a06 --- /dev/null +++ b/Unity.Entities.Graphics.Tests/HeapAllocatorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d7d33b72145f4e46af2d3dd92a500e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs b/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs new file mode 100644 index 0000000..0d45ca2 --- /dev/null +++ b/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs @@ -0,0 +1,900 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using NUnit.Framework; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering.Tests +{ + public class SparseUploaderTests + { + struct ExampleStruct + { + public int someData; + } + + + private GraphicsBuffer buffer; + private SparseUploader uploader; + + private void Setup(int count) where T : struct + { + buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, GraphicsBuffer.UsageFlags.None, count, + UnsafeUtility.SizeOf()); + uploader = new SparseUploader(buffer); + } + + private void Setup(T[] initialData) where T : struct + { + buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, GraphicsBuffer.UsageFlags.None, + initialData.Length, UnsafeUtility.SizeOf()); + buffer.SetData(initialData); + uploader = new SparseUploader(buffer); + } + + private void Teardown() + { + uploader.Dispose(); + buffer.Dispose(); + } + + private float4x4 GenerateTestMatrix(int i) + { + var trans = float4x4.Translate(new float3(i * 0.2f, -i * 0.4f, math.cos(i * math.PI * 0.02f))); + var rot = float4x4.EulerXYZ(i * 0.1f, math.PI * 0.5f, -i * 0.3f); + return math.mul(trans, rot); + } + + static float4x4 ExpandMatrix(float3x4 mat) + { + return new float4x4( + new float4(mat.c0.x, mat.c0.y, mat.c0.z, 0.0f), + new float4(mat.c1.x, mat.c1.y, mat.c1.z, 0.0f), + new float4(mat.c2.x, mat.c2.y, mat.c2.z, 0.0f), + new float4(mat.c3.x, mat.c3.y, mat.c3.z, 1.0f)); + } + + static float3x4 PackMatrix(float4x4 mat) + { + return new float3x4( + mat.c0.xyz, + mat.c1.xyz, + mat.c2.xyz, + mat.c3.xyz); + } + + internal unsafe class TestSparseUploader + { + public ThreadedSparseUploader ThreadedSparseUploader; + private NativeArray m_ExpectedData; + + public TestSparseUploader(ThreadedSparseUploader sparseUploader, NativeArray expectedData = default) + { + ThreadedSparseUploader = sparseUploader; + m_ExpectedData = expectedData; + } + + public static implicit operator ThreadedSparseUploader(TestSparseUploader tsu) => tsu.ThreadedSparseUploader; + + // Generics in constructors are not supported, so use this instead. + public TestSparseUploader WithInitialData(T[] initialData) where T : unmanaged + { + int totalSize = UnsafeUtility.SizeOf() * initialData.Length; + Assert.AreEqual(0, totalSize % sizeof(int)); + + int totalInts = totalSize / sizeof(int); + m_ExpectedData = new NativeArray(totalInts, Allocator.Temp, NativeArrayOptions.ClearMemory); + + UnsafeUtility.MemCpy( + m_ExpectedData.GetUnsafePtr(), + UnsafeUtility.AddressOf(ref initialData[0]), + totalSize); + + return this; + } + + private byte* DstPointer(int offset = 0) => (byte*)m_ExpectedData.GetUnsafePtr() + offset; + + public void AddUpload(void* src, int size, int offsetInBytes, int repeatCount = 1) + { + Assert.AreEqual(0, size % sizeof(int)); + Assert.AreEqual(0, offsetInBytes % sizeof(int)); + + byte* dst = DstPointer(offsetInBytes); + + for (int i = 0; i < repeatCount; ++i) + { + UnsafeUtility.MemCpy(dst, src, size); + dst += size; + } + + ThreadedSparseUploader.AddUpload(src, size, offsetInBytes, repeatCount); + } + + public void AddUpload(T val, int offsetInBytes, int repeatCount = 1) where T : unmanaged + { + var size = UnsafeUtility.SizeOf(); + AddUpload(&val, size, offsetInBytes, repeatCount); + ThreadedSparseUploader.AddUpload(val, offsetInBytes, repeatCount); + } + + public void AddUpload(NativeArray array, int offsetInBytes, int repeatCount = 1) where T : unmanaged + { + var size = UnsafeUtility.SizeOf() * array.Length; + AddUpload(array.GetUnsafeReadOnlyPtr(), size, offsetInBytes, repeatCount); + ThreadedSparseUploader.AddUpload(array, offsetInBytes, repeatCount); + } + + public void AddMatrixUpload(void* src, int numMatrices, int offset, int offsetInverse, + ThreadedSparseUploader.MatrixType srcType, ThreadedSparseUploader.MatrixType dstType) + { + float* srcFloats = (float*)src; + byte* dstBytes = DstPointer(); + + float* dst = (float*)(dstBytes + offset); + float* dstInverse = (offsetInverse < 0) ? null : (float*)(dstBytes + offsetInverse); + + Assert.Less(offset, m_ExpectedData.Length * sizeof(int)); + Assert.Less(offsetInverse, m_ExpectedData.Length * sizeof(int)); + + bool srcIs4x4 = srcType == ThreadedSparseUploader.MatrixType.MatrixType4x4; + bool dstIs4x4 = dstType == ThreadedSparseUploader.MatrixType.MatrixType4x4; + + // 3 supported cases: + // - 4x4 to 4x4 + // - 3x4 to 3x4 + // - 4x4 to 3x4 + Assert.False(!srcIs4x4 && dstIs4x4, "MatrixUpload from 3x4 to 4x4 is not supported"); + + int srcStride = srcIs4x4 ? 16 : 12; + int dstStride = dstIs4x4 ? 16 : 12; + + int srcSize = srcStride * sizeof(float); + int dstSize = dstStride * sizeof(float); + + for (int i = 0; i < numMatrices; ++i) + { + float* srcMatrix = srcFloats + i * srcStride; + + if (srcType == dstType) + { + UnsafeUtility.MemCpy( + dst + i * dstStride, + srcMatrix, + dstSize); + } + else if (srcIs4x4) + { + float4x4 m; + UnsafeUtility.MemCpy(&m, srcMatrix, srcSize); + float3x4 m3 = PackMatrix(m); + UnsafeUtility.MemCpy( + dst + i * dstStride, + &m3, + dstSize); + } + + if (dstInverse != null) + { + float4x4 m; + + if (srcIs4x4) + { + UnsafeUtility.MemCpy(&m, srcMatrix, srcSize); + } + else + { + float3x4 m3; + UnsafeUtility.MemCpy(&m3, srcMatrix, srcSize); + m = ExpandMatrix(m3); + } + + float4x4 mi = math.fastinverse(m); + + if (dstIs4x4) + { + UnsafeUtility.MemCpy( + dstInverse + i * dstStride, + &mi, + dstSize); + } + else + { + float3x4 m3 = PackMatrix(mi); + UnsafeUtility.MemCpy( + dstInverse + i * dstStride, + &m3, + dstSize); + } + } + } + + if (offsetInverse < 0) + { + ThreadedSparseUploader.AddMatrixUpload(src, numMatrices, offset, srcType, dstType); + } + else + { + ThreadedSparseUploader.AddMatrixUploadAndInverse(src, numMatrices, offset, offsetInverse, srcType, dstType); + } + } + + public void AddStridedUpload(void* src, uint elemSize, uint srcStride, uint count, uint dstOffset, int dstStride) + { + Assert.Greater(elemSize, 0); + Assert.LessOrEqual(elemSize, 64 * sizeof(int)); + Assert.Greater(count, 0); + Assert.NotZero(dstStride); + Assert.Zero(elemSize % sizeof(float)); + Assert.Zero(srcStride % sizeof(float)); + Assert.Zero(dstOffset % sizeof(float)); + Assert.Zero(dstStride % sizeof(float)); + + byte* srcBytes = (byte*)src; + byte* dstBytes = DstPointer((int)dstOffset); + + for (int i = 0; i < count; ++i) + { + UnsafeUtility.MemCpy(dstBytes, srcBytes, elemSize); + + srcBytes += srcStride; + dstBytes += dstStride; + } + + ThreadedSparseUploader.AddStridedUpload(src, elemSize, srcStride, count, dstOffset, dstStride); + } + + public void ValidateBitExact(void* actualResult) + { + int totalSize = m_ExpectedData.Length * sizeof(int); + + int* actualInts = (int*)actualResult; + + for (int i = 0; i < m_ExpectedData.Length; ++i) + { + int expected = m_ExpectedData[i]; + int actual = actualInts[i]; + + if (expected != actual) + { + float fExpected = BitConverter.ToSingle(BitConverter.GetBytes(expected)); + float fActual = BitConverter.ToSingle(BitConverter.GetBytes(actual)); + + Assert.AreEqual(expected, actual, $"Value mismatch at index {i}: {actual} ({fActual}) != {expected} ({fExpected})"); + } + } + } + + public void ValidateApproximateFloat(void* actualResult, float delta = 0.001f) + { + int totalFloats = m_ExpectedData.Length; + + float* expectedFloats = (float*)m_ExpectedData.GetUnsafePtr(); + float* actualFloats = (float*)actualResult; + + for (int i = 0; i < totalFloats; ++i) + { + CompareFloats(expectedFloats[i], actualFloats[i], delta); + } + } + + public void ValidateBitExact(GraphicsBuffer buffer) + { + int[] actualData = new int[m_ExpectedData.Length]; + buffer.GetData(actualData); + ValidateBitExact(UnsafeUtility.AddressOf(ref actualData[0])); + } + + public void ValidateApproximateFloat(GraphicsBuffer buffer, float delta = 0.001f) + { + float[] actualData = new float[m_ExpectedData.Length]; + buffer.GetData(actualData); + ValidateApproximateFloat(UnsafeUtility.AddressOf(ref actualData[0]), delta); + } + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void ReplaceBuffer() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + + var initialData = new ExampleStruct[64]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = i }; + + Setup(initialData); + + var newBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, GraphicsBuffer.UsageFlags.None, + initialData.Length * 2, UnsafeUtility.SizeOf()); + + uploader.ReplaceBuffer(newBuffer, true); + buffer.Dispose(); + buffer = newBuffer; + + var resultingData = new ExampleStruct[initialData.Length]; + buffer.GetData(resultingData); + + for (int i = 0; i < resultingData.Length; ++i) + Assert.AreEqual(i, resultingData[i].someData); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void NoUploads() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + Setup(1); + + var tsu = uploader.Begin(0, 0, 0); + uploader.EndAndCommit(tsu); + + tsu = uploader.Begin(1024, 1024, 1); + uploader.EndAndCommit(tsu); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void SmallUpload() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[64]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = 0 }; + + Setup(initialData); + + var structSize = UnsafeUtility.SizeOf(); + var totalSize = structSize * initialData.Length; + + var tsu = new TestSparseUploader(uploader.Begin(totalSize, structSize, initialData.Length)) + .WithInitialData(initialData); + + for (int i = 0; i < initialData.Length; ++i) + tsu.AddUpload(new ExampleStruct { someData = i }, i * 4); + + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void BasicUploads() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[1024]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = i }; + + Setup(initialData); + + var structSize = UnsafeUtility.SizeOf(); + var totalSize = structSize * initialData.Length; + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, initialData.Length)) + .WithInitialData(initialData); + + tsu.AddUpload(new ExampleStruct { someData = 7 }, 4); + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + tsu.ThreadedSparseUploader = uploader.Begin(structSize, structSize, 1); + tsu.AddUpload(new ExampleStruct { someData = 13 }, 8); + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void BigUploads() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[4 * 1024]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = i }; + + Setup(initialData); + + var newData = new ExampleStruct[312]; + for (int i = 0; i < newData.Length; ++i) + newData[i] = new ExampleStruct { someData = i + 3000 }; + + var newData2 = new ExampleStruct[316]; + for (int i = 0; i < newData2.Length; ++i) + newData2[i] = new ExampleStruct { someData = i + 4000 }; + + var structSize = UnsafeUtility.SizeOf(); + var totalSize = structSize * (newData.Length + newData2.Length); + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, initialData.Length)) + .WithInitialData(initialData); + + fixed (void* ptr = newData) + tsu.AddUpload(ptr, newData.Length * 4, 512 * 4); + fixed (void* ptr2 = newData2) + tsu.AddUpload(ptr2, newData2.Length * 4, 1136 * 4); + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void SplatUpload() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[64]; + + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = 0 }; + + Setup(initialData); + + var structSize = UnsafeUtility.SizeOf(); + + var tsu = + new TestSparseUploader(uploader.Begin(structSize, structSize, 1)) + .WithInitialData(initialData); + + tsu.AddUpload(new ExampleStruct { someData = 1 }, 0, 64); + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + + struct UploadJob : IJobParallelFor + { + public ThreadedSparseUploader uploader; + + public void Execute(int index) + { + uploader.AddUpload(new ExampleStruct { someData = index }, index * 4); + } + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void UploadFromJobs() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[4 * 1024]; + var stride = UnsafeUtility.SizeOf(); + + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = new ExampleStruct { someData = 0 }; + + Setup(initialData); + + var job = new UploadJob(); + var totalSize = initialData.Length * stride; + job.uploader = uploader.Begin(totalSize, stride, initialData.Length); + job.Schedule(initialData.Length, 64).Complete(); + uploader.EndAndCommit(job.uploader); + + var resultingData = new ExampleStruct[initialData.Length]; + buffer.GetData(resultingData); + + for (int i = 0; i < resultingData.Length; ++i) + Assert.AreEqual(i, resultingData[i].someData); + + Teardown(); + } + + static void CompareFloats(float expected, float actual, float delta = 0.00001f) + { + Assert.LessOrEqual(math.abs(expected - actual), delta); + } + + static void CompareMatrices(float4x4 expected, float4x4 actual, float delta = 0.00001f) + { + for (int i = 0; i < 4; ++i) + { + for (int j = 0; j < 4; ++j) + { + CompareFloats(expected[i][j], actual[i][j], delta); + } + } + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void MatrixUploads4x4() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var numMatrices = 1025; + var initialData = new float4x4[numMatrices]; + + for (int i = 0; i < numMatrices; ++i) + initialData[i] = float4x4.zero; + + Setup(initialData); + + var matSize = UnsafeUtility.SizeOf(); + var totalSize = numMatrices * matSize; + + var tsu = new TestSparseUploader(uploader.Begin(totalSize, totalSize, 1)) + .WithInitialData(initialData); + + var deltaData = new NativeArray(numMatrices, Allocator.Temp); + for (int i = 0; i < numMatrices; ++i) + deltaData[i] = GenerateTestMatrix(i); + tsu.AddMatrixUpload(deltaData.GetUnsafeReadOnlyPtr(), numMatrices, 0, -1, + ThreadedSparseUploader.MatrixType.MatrixType4x4, + ThreadedSparseUploader.MatrixType.MatrixType4x4); + uploader.EndAndCommit(tsu); + deltaData.Dispose(); + + tsu.ValidateApproximateFloat(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void MatrixUploads4x4To3x4() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var numMatrices = 1025; + var initialData = new float3x4[numMatrices]; + + for (int i = 0; i < numMatrices; ++i) + initialData[i] = float3x4.zero; + + Setup(initialData); + + var matSize = UnsafeUtility.SizeOf(); + var totalSize = numMatrices * matSize; + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, 1)) + .WithInitialData(initialData); + + var deltaData = new NativeArray(numMatrices, Allocator.Temp); + for (int i = 0; i < numMatrices; ++i) + deltaData[i] = GenerateTestMatrix(i); + tsu.AddMatrixUpload(deltaData.GetUnsafeReadOnlyPtr(), numMatrices, 0, -1, + ThreadedSparseUploader.MatrixType.MatrixType4x4, + ThreadedSparseUploader.MatrixType.MatrixType3x4); + uploader.EndAndCommit(tsu); + deltaData.Dispose(); + + tsu.ValidateApproximateFloat(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void InverseMatrixUploads4x4() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var numMatrices = 1025; + var initialData = new float4x4[numMatrices * 2]; + + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = float4x4.zero; + + Setup(initialData); + + var matSize = UnsafeUtility.SizeOf(); + var totalSize = numMatrices * matSize; + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, 1)) + .WithInitialData(initialData); + + var deltaData = new NativeArray(numMatrices, Allocator.Temp); + for (int i = 0; i < numMatrices; ++i) + deltaData[i] = GenerateTestMatrix(i); + tsu.AddMatrixUpload(deltaData.GetUnsafeReadOnlyPtr(), numMatrices, 0, numMatrices * 64, + ThreadedSparseUploader.MatrixType.MatrixType4x4, + ThreadedSparseUploader.MatrixType.MatrixType4x4); + uploader.EndAndCommit(tsu); + + deltaData.Dispose(); + + tsu.ValidateApproximateFloat(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void InverseMatrixUploads4x4To3x4() + { + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var numMatrices = 1025; + var initialData = new float3x4[numMatrices * 2]; + + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = float3x4.zero; + + Setup(initialData); + + var matSize = UnsafeUtility.SizeOf(); + var totalSize = numMatrices * matSize; + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, 1)) + .WithInitialData(initialData); + + var deltaData = new NativeArray(numMatrices, Allocator.Temp); + for (int i = 0; i < numMatrices; ++i) + deltaData[i] = GenerateTestMatrix(i); + tsu.AddMatrixUpload(deltaData.GetUnsafeReadOnlyPtr(), numMatrices, 0, numMatrices * 48, + ThreadedSparseUploader.MatrixType.MatrixType4x4, + ThreadedSparseUploader.MatrixType.MatrixType3x4); + uploader.EndAndCommit(tsu); + + deltaData.Dispose(); + + tsu.ValidateApproximateFloat(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public void HugeUploadCount() + { + const int HugeCount = 100000; + + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new ExampleStruct[HugeCount]; + + Setup(initialData); + + var structSize = UnsafeUtility.SizeOf(); + var tsu = + new TestSparseUploader(uploader.Begin(structSize * HugeCount, structSize, HugeCount)) + .WithInitialData(initialData); + + for (int i = 0; i < initialData.Length; ++i) + tsu.AddUpload(new ExampleStruct { someData = i }, 4 * i); + + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void StridedUploadBasic() + { + const int Count = 100; + const int BufferSize = Count * 4; + + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new int[BufferSize]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = 0; + + Setup(initialData); + + uint structSize = sizeof(int); + int totalSize = (int)(structSize * Count); + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, Count)) + .WithInitialData(initialData); + + var src = new NativeArray(Count, Allocator.Temp); + for (int i = 0; i < src.Length; ++i) + src[i] = i; + + tsu.AddStridedUpload(src.GetUnsafeReadOnlyPtr(), + structSize, structSize, Count, + 0, (int)(structSize * 2)); + + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void StridedUploadFloat3() + { + const int Count = 2; + const int BufferSize = Count * 4; + + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new float[BufferSize]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = 0; + + Setup(initialData); + + uint structSize = (uint)sizeof(float3); + int totalSize = (int)(structSize * Count); + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, Count)) + .WithInitialData(initialData); + + var src = new NativeArray(Count, Allocator.Temp); + for (int i = 0; i < src.Length; ++i) + src[i] = new float3(i, i * 2, i * 3); + + + // Upload float3s as float4s + tsu.AddStridedUpload(src.GetUnsafeReadOnlyPtr(), + structSize, structSize, Count, + 0, sizeof(float4)); + + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + + internal unsafe struct TestFloat5 + { + public fixed float fs[5]; + + public TestFloat5(float f) + { + fs[0] = f + 1; + fs[1] = 2*f + 1; + fs[2] = 3*f + 1; + fs[3] = 4*f + 1; + fs[4] = 5*f + 1; + } + } + +#if UNITY_2020_1_OR_NEWER + [Test] +#endif + public unsafe void StridedUploadWeirdStrides() + { + const int Count = 100; + const int BufferSize = Count * 16; + + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) + { + Assert.Ignore("Skipped due to platform/computer not supporting compute shaders"); + return; + } + + var initialData = new float[BufferSize]; + for (int i = 0; i < initialData.Length; ++i) + initialData[i] = 0; + + Setup(initialData); + + uint structSize = (uint)UnsafeUtility.SizeOf(); + + uint srcStride = structSize * 2; + int totalSize = (int)(srcStride * Count); + + var tsu = + new TestSparseUploader(uploader.Begin(totalSize, totalSize, Count)) + .WithInitialData(initialData); + + var src = new NativeArray(Count * 2, Allocator.Temp); + for (int i = 0; i < src.Length; ++i) + src[i] = new TestFloat5(i); + + uint dstOffset = BufferSize * sizeof(float) - 2 * structSize; + int dstStride = -7 * 4; + + tsu.AddStridedUpload(src.GetUnsafeReadOnlyPtr(), + structSize, srcStride, Count, + dstOffset, dstStride); + + uploader.EndAndCommit(tsu); + + tsu.ValidateBitExact(buffer); + + Teardown(); + } + } +} diff --git a/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs.meta b/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs.meta new file mode 100644 index 0000000..cb572ec --- /dev/null +++ b/Unity.Entities.Graphics.Tests/SparseUploaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d65a783cf4e65cc48b30923238ef4d2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef b/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef new file mode 100644 index 0000000..5a52cfd --- /dev/null +++ b/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef @@ -0,0 +1,60 @@ +{ + "name": "Unity.Entities.Graphics.Tests", + "rootNamespace": "", + "references": [ + "Unity.Rendering", + "Unity.Entities.Graphics", + "Unity.Transforms", + "Unity.Transforms.Hybrid", + "Unity.Mathematics", + "Unity.Mathematics.Extensions", + "Unity.Mathematics.Extensions.Hybrid", + "Unity.Scenes", + "Unity.Scenes.Editor", + "Unity.Entities.Tests", + "Unity.Entities", + "Unity.Entities.Hybrid", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Unity.RenderPipelines.HighDefinition.Runtime", + "Unity.RenderPipelines.Universal.Runtime", + "Unity.RenderPipelines.Core.Runtime", + "Unity.Collections" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [ + { + "name": "com.unity.render-pipelines.high-definition", + "expression": "9.9.9", + "define": "HDRP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.render-pipelines.universal", + "expression": "9.9.9", + "define": "URP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.render-pipelines.core", + "expression": "9.9.9", + "define": "SRP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.tiny", + "expression": "0.21.9", + "define": "TINY_0_22_0_OR_NEWER" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef.meta b/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef.meta new file mode 100644 index 0000000..b7e626a --- /dev/null +++ b/Unity.Entities.Graphics.Tests/Unity.Entities.Graphics.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ac35034034affa945a0672efca427990 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics.meta b/Unity.Entities.Graphics.meta new file mode 100644 index 0000000..2081dc3 --- /dev/null +++ b/Unity.Entities.Graphics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c9a1d4acd904842dfa5413e35662577f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/BuiltinMaterialProperties.meta b/Unity.Entities.Graphics/BuiltinMaterialProperties.meta new file mode 100644 index 0000000..da36701 --- /dev/null +++ b/Unity.Entities.Graphics/BuiltinMaterialProperties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 663ffa67814b22143a278134d9ee6109 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/BuiltinMaterialProperties/BuiltinMaterialPropertyUnity_LODFade.cs b/Unity.Entities.Graphics/BuiltinMaterialProperties/BuiltinMaterialPropertyUnity_LODFade.cs new file mode 100644 index 0000000..8813c8a --- /dev/null +++ b/Unity.Entities.Graphics/BuiltinMaterialProperties/BuiltinMaterialPropertyUnity_LODFade.cs @@ -0,0 +1,11 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("unity_LODFade")] + internal struct BuiltinMaterialPropertyUnity_LODFade : IComponentData + { + public float4 Value; + } +} diff --git a/Unity.Entities.Graphics/ComponentTypeCache.cs b/Unity.Entities.Graphics/ComponentTypeCache.cs new file mode 100644 index 0000000..d38e63e --- /dev/null +++ b/Unity.Entities.Graphics/ComponentTypeCache.cs @@ -0,0 +1,270 @@ +using System.Runtime.InteropServices; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + // Helper to only call GetDynamicComponentTypeHandle once per type per frame + internal struct ComponentTypeCache + { + internal NativeParallelHashMap UsedTypes; + + // Re-populated each frame with fresh objects for each used type. + // Use C# array so we can hold SafetyHandles without problems. + internal DynamicComponentTypeHandle[] TypeDynamics; + internal int MaxIndex; + + public ComponentTypeCache(int initialCapacity) : this() + { + Reset(initialCapacity); + } + + public void Reset(int capacity = 0) + { + Dispose(); + UsedTypes = new NativeParallelHashMap(capacity, Allocator.Persistent); + MaxIndex = 0; + } + + public void Dispose() + { + if (UsedTypes.IsCreated) UsedTypes.Dispose(); + TypeDynamics = null; + } + + public int UsedTypeCount => UsedTypes.Count(); + + public void UseType(int typeIndex) + { + // Use indices without flags so we have a nice compact range + int i = GetArrayIndex(typeIndex); + Debug.Assert(!UsedTypes.ContainsKey(i) || UsedTypes[i] == typeIndex, + "typeIndex is not consistent with its stored array index"); + UsedTypes[i] = typeIndex; + MaxIndex = math.max(i, MaxIndex); + } + + public void FetchTypeHandles(ComponentSystemBase componentSystem) + { + var types = UsedTypes.GetKeyValueArrays(Allocator.Temp); + + if (TypeDynamics == null || TypeDynamics.Length < MaxIndex + 1) + // Allocate according to Capacity so we grow with the same geometric formula as NativeList + TypeDynamics = new DynamicComponentTypeHandle[MaxIndex + 1]; + + ref var keys = ref types.Keys; + ref var values = ref types.Values; + int numTypes = keys.Length; + for (int i = 0; i < numTypes; ++i) + { + int arrayIndex = keys[i]; + int typeIndex = values[i]; + TypeDynamics[arrayIndex] = componentSystem.GetDynamicComponentTypeHandle( + ComponentType.ReadOnly(typeIndex)); + } + + types.Dispose(); + } + + public static int GetArrayIndex(int typeIndex) => typeIndex & TypeManager.ClearFlagsMask; + + public DynamicComponentTypeHandle Type(int typeIndex) + { + return TypeDynamics[GetArrayIndex(typeIndex)]; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BurstCompatibleTypeArray + { + public const int kMaxTypes = 128; + + [NativeDisableParallelForRestriction] public NativeArray TypeIndexToArrayIndex; + + [ReadOnly] public DynamicComponentTypeHandle t0; + [ReadOnly] public DynamicComponentTypeHandle t1; + [ReadOnly] public DynamicComponentTypeHandle t2; + [ReadOnly] public DynamicComponentTypeHandle t3; + [ReadOnly] public DynamicComponentTypeHandle t4; + [ReadOnly] public DynamicComponentTypeHandle t5; + [ReadOnly] public DynamicComponentTypeHandle t6; + [ReadOnly] public DynamicComponentTypeHandle t7; + [ReadOnly] public DynamicComponentTypeHandle t8; + [ReadOnly] public DynamicComponentTypeHandle t9; + [ReadOnly] public DynamicComponentTypeHandle t10; + [ReadOnly] public DynamicComponentTypeHandle t11; + [ReadOnly] public DynamicComponentTypeHandle t12; + [ReadOnly] public DynamicComponentTypeHandle t13; + [ReadOnly] public DynamicComponentTypeHandle t14; + [ReadOnly] public DynamicComponentTypeHandle t15; + [ReadOnly] public DynamicComponentTypeHandle t16; + [ReadOnly] public DynamicComponentTypeHandle t17; + [ReadOnly] public DynamicComponentTypeHandle t18; + [ReadOnly] public DynamicComponentTypeHandle t19; + [ReadOnly] public DynamicComponentTypeHandle t20; + [ReadOnly] public DynamicComponentTypeHandle t21; + [ReadOnly] public DynamicComponentTypeHandle t22; + [ReadOnly] public DynamicComponentTypeHandle t23; + [ReadOnly] public DynamicComponentTypeHandle t24; + [ReadOnly] public DynamicComponentTypeHandle t25; + [ReadOnly] public DynamicComponentTypeHandle t26; + [ReadOnly] public DynamicComponentTypeHandle t27; + [ReadOnly] public DynamicComponentTypeHandle t28; + [ReadOnly] public DynamicComponentTypeHandle t29; + [ReadOnly] public DynamicComponentTypeHandle t30; + [ReadOnly] public DynamicComponentTypeHandle t31; + [ReadOnly] public DynamicComponentTypeHandle t32; + [ReadOnly] public DynamicComponentTypeHandle t33; + [ReadOnly] public DynamicComponentTypeHandle t34; + [ReadOnly] public DynamicComponentTypeHandle t35; + [ReadOnly] public DynamicComponentTypeHandle t36; + [ReadOnly] public DynamicComponentTypeHandle t37; + [ReadOnly] public DynamicComponentTypeHandle t38; + [ReadOnly] public DynamicComponentTypeHandle t39; + [ReadOnly] public DynamicComponentTypeHandle t40; + [ReadOnly] public DynamicComponentTypeHandle t41; + [ReadOnly] public DynamicComponentTypeHandle t42; + [ReadOnly] public DynamicComponentTypeHandle t43; + [ReadOnly] public DynamicComponentTypeHandle t44; + [ReadOnly] public DynamicComponentTypeHandle t45; + [ReadOnly] public DynamicComponentTypeHandle t46; + [ReadOnly] public DynamicComponentTypeHandle t47; + [ReadOnly] public DynamicComponentTypeHandle t48; + [ReadOnly] public DynamicComponentTypeHandle t49; + [ReadOnly] public DynamicComponentTypeHandle t50; + [ReadOnly] public DynamicComponentTypeHandle t51; + [ReadOnly] public DynamicComponentTypeHandle t52; + [ReadOnly] public DynamicComponentTypeHandle t53; + [ReadOnly] public DynamicComponentTypeHandle t54; + [ReadOnly] public DynamicComponentTypeHandle t55; + [ReadOnly] public DynamicComponentTypeHandle t56; + [ReadOnly] public DynamicComponentTypeHandle t57; + [ReadOnly] public DynamicComponentTypeHandle t58; + [ReadOnly] public DynamicComponentTypeHandle t59; + [ReadOnly] public DynamicComponentTypeHandle t60; + [ReadOnly] public DynamicComponentTypeHandle t61; + [ReadOnly] public DynamicComponentTypeHandle t62; + [ReadOnly] public DynamicComponentTypeHandle t63; + [ReadOnly] public DynamicComponentTypeHandle t64; + [ReadOnly] public DynamicComponentTypeHandle t65; + [ReadOnly] public DynamicComponentTypeHandle t66; + [ReadOnly] public DynamicComponentTypeHandle t67; + [ReadOnly] public DynamicComponentTypeHandle t68; + [ReadOnly] public DynamicComponentTypeHandle t69; + [ReadOnly] public DynamicComponentTypeHandle t70; + [ReadOnly] public DynamicComponentTypeHandle t71; + [ReadOnly] public DynamicComponentTypeHandle t72; + [ReadOnly] public DynamicComponentTypeHandle t73; + [ReadOnly] public DynamicComponentTypeHandle t74; + [ReadOnly] public DynamicComponentTypeHandle t75; + [ReadOnly] public DynamicComponentTypeHandle t76; + [ReadOnly] public DynamicComponentTypeHandle t77; + [ReadOnly] public DynamicComponentTypeHandle t78; + [ReadOnly] public DynamicComponentTypeHandle t79; + [ReadOnly] public DynamicComponentTypeHandle t80; + [ReadOnly] public DynamicComponentTypeHandle t81; + [ReadOnly] public DynamicComponentTypeHandle t82; + [ReadOnly] public DynamicComponentTypeHandle t83; + [ReadOnly] public DynamicComponentTypeHandle t84; + [ReadOnly] public DynamicComponentTypeHandle t85; + [ReadOnly] public DynamicComponentTypeHandle t86; + [ReadOnly] public DynamicComponentTypeHandle t87; + [ReadOnly] public DynamicComponentTypeHandle t88; + [ReadOnly] public DynamicComponentTypeHandle t89; + [ReadOnly] public DynamicComponentTypeHandle t90; + [ReadOnly] public DynamicComponentTypeHandle t91; + [ReadOnly] public DynamicComponentTypeHandle t92; + [ReadOnly] public DynamicComponentTypeHandle t93; + [ReadOnly] public DynamicComponentTypeHandle t94; + [ReadOnly] public DynamicComponentTypeHandle t95; + [ReadOnly] public DynamicComponentTypeHandle t96; + [ReadOnly] public DynamicComponentTypeHandle t97; + [ReadOnly] public DynamicComponentTypeHandle t98; + [ReadOnly] public DynamicComponentTypeHandle t99; + [ReadOnly] public DynamicComponentTypeHandle t100; + [ReadOnly] public DynamicComponentTypeHandle t101; + [ReadOnly] public DynamicComponentTypeHandle t102; + [ReadOnly] public DynamicComponentTypeHandle t103; + [ReadOnly] public DynamicComponentTypeHandle t104; + [ReadOnly] public DynamicComponentTypeHandle t105; + [ReadOnly] public DynamicComponentTypeHandle t106; + [ReadOnly] public DynamicComponentTypeHandle t107; + [ReadOnly] public DynamicComponentTypeHandle t108; + [ReadOnly] public DynamicComponentTypeHandle t109; + [ReadOnly] public DynamicComponentTypeHandle t110; + [ReadOnly] public DynamicComponentTypeHandle t111; + [ReadOnly] public DynamicComponentTypeHandle t112; + [ReadOnly] public DynamicComponentTypeHandle t113; + [ReadOnly] public DynamicComponentTypeHandle t114; + [ReadOnly] public DynamicComponentTypeHandle t115; + [ReadOnly] public DynamicComponentTypeHandle t116; + [ReadOnly] public DynamicComponentTypeHandle t117; + [ReadOnly] public DynamicComponentTypeHandle t118; + [ReadOnly] public DynamicComponentTypeHandle t119; + [ReadOnly] public DynamicComponentTypeHandle t120; + [ReadOnly] public DynamicComponentTypeHandle t121; + [ReadOnly] public DynamicComponentTypeHandle t122; + [ReadOnly] public DynamicComponentTypeHandle t123; + [ReadOnly] public DynamicComponentTypeHandle t124; + [ReadOnly] public DynamicComponentTypeHandle t125; + [ReadOnly] public DynamicComponentTypeHandle t126; + [ReadOnly] public DynamicComponentTypeHandle t127; + + // Need to accept &t0 as input, because 'fixed' must be in the callsite. + public unsafe DynamicComponentTypeHandle Type(DynamicComponentTypeHandle* fixedT0, + int typeIndex) + { + return fixedT0[TypeIndexToArrayIndex[GetArrayIndex(typeIndex)]]; + } + + public void Dispose(JobHandle disposeDeps) + { + if (TypeIndexToArrayIndex.IsCreated) TypeIndexToArrayIndex.Dispose(disposeDeps); + } + } + + public unsafe BurstCompatibleTypeArray ToBurstCompatible(Allocator allocator) + { + BurstCompatibleTypeArray typeArray = default; + + Debug.Assert(UsedTypeCount > 0, "No types have been registered"); + Debug.Assert(UsedTypeCount <= BurstCompatibleTypeArray.kMaxTypes, "Maximum supported amount of types exceeded"); + + typeArray.TypeIndexToArrayIndex = new NativeArray( + MaxIndex + 1, + allocator, + NativeArrayOptions.UninitializedMemory); + ref var toArrayIndex = ref typeArray.TypeIndexToArrayIndex; + + // Use an index guaranteed to cause a crash on invalid indices + uint GuaranteedCrashOffset = 0x80000000; + for (int i = 0; i < toArrayIndex.Length; ++i) + toArrayIndex[i] = (int)GuaranteedCrashOffset; + + var typeIndices = UsedTypes.GetValueArray(Allocator.Temp); + int numTypes = math.min(typeIndices.Length, BurstCompatibleTypeArray.kMaxTypes); + var fixedT0 = &typeArray.t0; + + for (int i = 0; i < numTypes; ++i) + { + int typeIndex = typeIndices[i]; + fixedT0[i] = Type(typeIndex); + toArrayIndex[GetArrayIndex(typeIndex)] = i; + } + + // TODO: Is there a way to avoid this? + // We need valid type objects in each field. + { + var someType = Type(typeIndices[0]); + for (int i = numTypes; i < BurstCompatibleTypeArray.kMaxTypes; ++i) + fixedT0[i] = someType; + } + + typeIndices.Dispose(); + + return typeArray; + } + } +} diff --git a/Unity.Entities.Graphics/ComponentTypeCache.cs.meta b/Unity.Entities.Graphics/ComponentTypeCache.cs.meta new file mode 100644 index 0000000..4a8f6fa --- /dev/null +++ b/Unity.Entities.Graphics/ComponentTypeCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aaef83246923491cb1ac526ff170e1f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/CullingTypes.cs b/Unity.Entities.Graphics/CullingTypes.cs new file mode 100644 index 0000000..20d2ee5 --- /dev/null +++ b/Unity.Entities.Graphics/CullingTypes.cs @@ -0,0 +1,27 @@ +using Unity.Mathematics; + +namespace Unity.Rendering +{ + internal struct Fixed16CamDistance + { + public const float kRes = 100.0f; + + public static ushort FromFloatCeil(float f) + { + return (ushort)math.clamp((int)math.ceil(f * kRes), 0, 0xffff); + } + + public static ushort FromFloatFloor(float f) + { + return (ushort)math.clamp((int)math.floor(f * kRes), 0, 0xffff); + } + } + + /// + /// Tag to enable instance LOD + /// + internal unsafe struct ChunkInstanceLodEnabled + { + public fixed ulong Enabled[2]; + } +} diff --git a/Unity.Entities.Graphics/CullingTypes.cs.meta b/Unity.Entities.Graphics/CullingTypes.cs.meta new file mode 100644 index 0000000..359bd10 --- /dev/null +++ b/Unity.Entities.Graphics/CullingTypes.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7f5d544ee40e476dbd8689b00c317560 +timeCreated: 1585229592 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Deformations.meta b/Unity.Entities.Graphics/Deformations.meta new file mode 100644 index 0000000..cb0f851 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 752de8d84651ea04a8fa961228189249 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers.meta b/Unity.Entities.Graphics/Deformations/BufferManagers.meta new file mode 100644 index 0000000..2fcde87 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 00eb07c9867593f459cbb54248f18ca7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs b/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs new file mode 100644 index 0000000..b28dbf4 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs @@ -0,0 +1,69 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + internal class BlendShapeBufferManager + { + const int k_ChunkSize = 2048; + + static readonly int k_BlendShapeWeightsBuffer = Shader.PropertyToID("_BlendShapeWeights"); + static readonly int k_MaxSize = (int)math.min(SystemInfo.maxGraphicsBufferSize / UnsafeUtility.SizeOf(), int.MaxValue); + + FencedBufferPool m_BufferPool; + + public BlendShapeBufferManager() + { + m_BufferPool = new FencedBufferPool(); + } + + public void Dispose() + { + m_BufferPool.Dispose(); + } + + public bool ResizePassBufferIfRequired(int requiredSize) + { + var size = m_BufferPool.BufferSize; + if (size <= requiredSize || size - requiredSize > k_ChunkSize) + { + var newSize = ((requiredSize / k_ChunkSize) + 1) * k_ChunkSize; + + if (newSize > k_MaxSize) + { + // Only inform users if the content requires a buffer that is too big. + if (requiredSize > k_MaxSize) + UnityEngine.Debug.LogWarning("The world contains too many blend shapes to fit into a single GraphicsBuffer. Not all deformed meshes are guaranteed to render correctly. Reduce the number of active deformed meshes."); + + // Do not actually resize the buffer if we are already at max capacity. + if (size == k_MaxSize) + return false; + + newSize = k_MaxSize; + } + + m_BufferPool.ResizeBuffer(newSize, UnsafeUtility.SizeOf()); + return true; + } + + return false; + } + + public NativeArray LockBlendWeightBufferForWrite(int count) + { + m_BufferPool.BeginFrame(); + var buffer = m_BufferPool.GetCurrentFrameBuffer(); + return buffer.LockBufferForWrite(0, count); + } + + public void UnlockBlendWeightBufferForWrite(int count) + { + var buffer = m_BufferPool.GetCurrentFrameBuffer(); + buffer.UnlockBufferAfterWrite(count); + Shader.SetGlobalBuffer(k_BlendShapeWeightsBuffer, buffer); + m_BufferPool.EndFrame(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs.meta b/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs.meta new file mode 100644 index 0000000..1137e15 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/BlendShapeBufferManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58e8b1a992931fe4daf9fc6045c812a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs b/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs new file mode 100644 index 0000000..f7c4bd4 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs @@ -0,0 +1,109 @@ +using Unity.Assertions; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace Unity.Rendering +{ + internal class ComputeBufferWrapper where DataType : struct + { + ComputeBuffer m_Buffer; + ComputeShader m_Shader; + public int PropertyID; + + public int BufferSize { get; private set; } + + public ComputeBufferWrapper(int namePropertyId, int size) + { + BufferSize = size; + PropertyID = namePropertyId; + m_Buffer = new ComputeBuffer(size, UnsafeUtility.SizeOf(), ComputeBufferType.Default); + } + + public ComputeBufferWrapper(int namePropertyId, int size, ComputeShader shader) : this(namePropertyId, size) + { + Debug.Assert(shader != null); + m_Shader = shader; + } + + public void Resize(int newSize) + { + BufferSize = newSize; + m_Buffer.Dispose(); + m_Buffer = new ComputeBuffer(newSize, UnsafeUtility.SizeOf(), ComputeBufferType.Default); + } + + public void SetData(NativeArray data, int nativeBufferStartIndex, int computeBufferStartIndex, int count) + { + m_Buffer.SetData(data, nativeBufferStartIndex, computeBufferStartIndex, count); + } + + public void PushDataToGlobal() + { + Debug.Assert(m_Buffer.count > 0); + Debug.Assert(m_Buffer.IsValid()); + Shader.SetGlobalBuffer(PropertyID, m_Buffer); + } + + public void PushDataToKernel(int kernelIndex) + { + Debug.Assert(m_Buffer.count > 0 && m_Shader != null); + Debug.Assert(m_Buffer.IsValid()); + m_Shader.SetBuffer(kernelIndex, PropertyID, m_Buffer); + } + + public void Destroy() + { + BufferSize = -1; + PropertyID = -1; + m_Buffer.Dispose(); + m_Shader = null; + } + } + + internal class ComputeDoubleBufferWrapper where DataType : struct + { + ComputeBufferWrapper[] m_Buffers = new ComputeBufferWrapper[2]; + readonly int m_PrimaryBufferPropertyID; + readonly int m_SecondaryBufferPropertyID; + + public ComputeBufferWrapper ActiveBuffer => m_Buffers[ActiveBufferIndex]; + public ComputeBufferWrapper BackBuffer => m_Buffers[ActiveBufferIndex ^ 1]; + public int ActiveBufferIndex { get; private set; } + + public ComputeDoubleBufferWrapper(int primaryPropertyID, int secondaryPropertyID, int size) + { + m_PrimaryBufferPropertyID = primaryPropertyID; + m_SecondaryBufferPropertyID = secondaryPropertyID; + + m_Buffers[0] = new ComputeBufferWrapper(m_PrimaryBufferPropertyID, size); + m_Buffers[1] = new ComputeBufferWrapper(m_SecondaryBufferPropertyID, size); + } + + public void PushDataToGlobal() + { + m_Buffers[0].PushDataToGlobal(); + m_Buffers[1].PushDataToGlobal(); + } + + public void SetActiveBuffer(int bufferIndex) + { + Assert.IsTrue(bufferIndex == 0 || bufferIndex == 1, "Invalid index for Mesh Deform buffers"); + + // Assumes buffer index is changed correctly externally + ActiveBufferIndex = bufferIndex; + + m_Buffers[bufferIndex].PropertyID = m_PrimaryBufferPropertyID; + m_Buffers[bufferIndex ^ 1].PropertyID = m_SecondaryBufferPropertyID; + + m_Buffers[0].PushDataToGlobal(); + m_Buffers[1].PushDataToGlobal(); + } + + public void Destroy() + { + m_Buffers[0].Destroy(); + m_Buffers[1].Destroy(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs.meta b/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs.meta new file mode 100644 index 0000000..5310f5a --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/ComputeBufferWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0424d4fb0cc119640ba622635f0e2681 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs b/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs new file mode 100644 index 0000000..4ee1534 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs @@ -0,0 +1,134 @@ +using System; +using Unity.Assertions; +using Unity.Collections; + +using GraphicsBuffer = UnityEngine.GraphicsBuffer; + +namespace Unity.Rendering +{ + internal class FencedBufferPool : IDisposable + { + struct FrameData + { + public int DataBufferID; + public int FenceBufferID; + public UnityEngine.Rendering.AsyncGPUReadbackRequest Fence; + } + + public int BufferSize { get; private set; } + + NativeQueue m_FrameData; + + BufferPool m_FenceBufferPool; + BufferPool m_DataBufferPool; + + int m_CurrentFrameBufferID; + + public FencedBufferPool() + { + m_FrameData = new NativeQueue(Allocator.Persistent); + m_CurrentFrameBufferID = -1; + } + + public void Dispose() + { + if (m_FrameData.IsCreated) + m_FrameData.Dispose(); + + m_FenceBufferPool?.Dispose(); + m_DataBufferPool?.Dispose(); + + m_CurrentFrameBufferID = -1; + } + + public void BeginFrame() + { + Assert.IsTrue(m_CurrentFrameBufferID == -1); + + RecoverBuffers(); + m_CurrentFrameBufferID = m_DataBufferPool.GetBufferId(); + } + + public void EndFrame() + { + Assert.IsFalse(m_CurrentFrameBufferID == -1); + + var fenceBufferID = m_FenceBufferPool.GetBufferId(); + var frameData = new FrameData + { + DataBufferID = m_CurrentFrameBufferID, + FenceBufferID = fenceBufferID, + }; + + if (UnityEngine.SystemInfo.supportsAsyncGPUReadback) + { + frameData.Fence = UnityEngine.Rendering.AsyncGPUReadback.Request(m_FenceBufferPool.GetBufferFromId(fenceBufferID)); + } + + m_FrameData.Enqueue(frameData); + + m_CurrentFrameBufferID = -1; + } + + public GraphicsBuffer GetCurrentFrameBuffer() + { + Assert.IsFalse(m_CurrentFrameBufferID == -1); + return m_DataBufferPool.GetBufferFromId(m_CurrentFrameBufferID); + } + + // todo: improve the behavior here (GFXMESH-62). + public void ResizeBuffer(int size, int stride) + { + m_FrameData.Clear(); + + m_FenceBufferPool?.Dispose(); + m_DataBufferPool?.Dispose(); + + m_FenceBufferPool = new BufferPool(1, 4, GraphicsBuffer.Target.Raw, GraphicsBuffer.UsageFlags.None); + m_DataBufferPool = new BufferPool(size, stride, GraphicsBuffer.Target.Raw, GraphicsBuffer.UsageFlags.LockBufferForWrite); + + BufferSize = size; + } + + void RecoverBuffers() + { + while (CanFreeNextBuffer()) + { + var data = m_FrameData.Dequeue(); + + Assert.IsFalse(data.FenceBufferID == -1); + + m_FenceBufferPool.PutBufferId(data.FenceBufferID); + m_DataBufferPool.PutBufferId(data.DataBufferID); + } + + // Something is probably leaking if any of these fail. + Assert.IsFalse(m_FrameData.Count > 15); + Assert.IsFalse(m_DataBufferPool.TotalBufferCount > 15); + Assert.IsFalse(m_FenceBufferPool.TotalBufferCount > 15); + + bool CanFreeNextBuffer() + { + // Assume 3 frames in flight if the platform does not support async readbacks. + if (UnityEngine.SystemInfo.supportsAsyncGPUReadback) + { + // Keep buffers around for another frame on Metal (GFXMESH-65). + // hasError is set to true when the Fence is disposed. + if (UnityEngine.SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Metal) + { + return !m_FrameData.IsEmpty() && m_FrameData.Peek().Fence.hasError; + } + else + { + return !m_FrameData.IsEmpty() && m_FrameData.Peek().Fence.done; + } + } + else + { + return m_FrameData.Count > 3; + } + } + } + } +} + diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs.meta b/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs.meta new file mode 100644 index 0000000..d35539c --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/FencedBufferPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 602e5e393d603004eb5c54fa0514962b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs b/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs new file mode 100644 index 0000000..02fc1ca --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs @@ -0,0 +1,80 @@ +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + internal class MeshBufferManager + { + const int k_ChunkSize = 2048; + + static readonly int k_MaxSize = (int)math.min(SystemInfo.maxGraphicsBufferSize / UnsafeUtility.SizeOf(), int.MaxValue); + +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + ComputeDoubleBufferWrapper m_DeformedMeshData; + static readonly int k_CurrentFrameBufferProperty = Shader.PropertyToID("_DeformedMeshData"); + static readonly int k_PreviousFrameBufferProperty = Shader.PropertyToID("_PreviousFrameDeformedMeshData"); + + public int ActiveDeformedMeshBufferIndex => (m_DeformedMeshData != null) ? m_DeformedMeshData.ActiveBufferIndex : 0; +#else + ComputeBufferWrapper m_DeformedMeshData; +#endif + + public MeshBufferManager() + { +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + m_DeformedMeshData = new ComputeDoubleBufferWrapper(k_CurrentFrameBufferProperty, k_PreviousFrameBufferProperty, k_ChunkSize); +#else + m_DeformedMeshData = new ComputeBufferWrapper(Shader.PropertyToID("_DeformedMeshData"), k_ChunkSize); +#endif + m_DeformedMeshData.PushDataToGlobal(); + } + + public void Dispose() + { + m_DeformedMeshData.Destroy(); + } + + public bool ResizeAndPushDeformMeshBuffersIfRequired(int requiredSize) + { +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + var buffer = m_DeformedMeshData.ActiveBuffer; +#else + var buffer = m_DeformedMeshData; +#endif + var size = buffer.BufferSize; + + if (size <= requiredSize || size - requiredSize > k_ChunkSize) + { + var newSize = ((requiredSize / k_ChunkSize) + 1) * k_ChunkSize; + + if (newSize > k_MaxSize) + { + // Only inform users if the content requires a buffer that is too big. + if (requiredSize > k_MaxSize) + UnityEngine.Debug.LogWarning("The world contains too many deformed meshes to fit into a single GraphicsBuffer. Not all deformed meshes are guaranteed to render correctly. Reduce the number of active deformed meshes."); + + // Do not actually resize the buffer if we are already at max capacity. + if (size == k_MaxSize) + return false; + + newSize = k_MaxSize; + } + + buffer.Resize(newSize); + buffer.PushDataToGlobal(); + + return true; + } + + return false; + } + +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + public void FlipDeformedMeshBuffer() + { + m_DeformedMeshData.SetActiveBuffer(m_DeformedMeshData.ActiveBufferIndex ^ 1); + } +#endif + } +} diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs.meta b/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs.meta new file mode 100644 index 0000000..3c56d3c --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/MeshBufferManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e4cd00beaf5782458e9913246afa3d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs b/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs new file mode 100644 index 0000000..d9ffae4 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs @@ -0,0 +1,69 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + internal class SkinningBufferManager + { + const int k_ChunkSize = 2048; + + static readonly int k_SkinMatricesBuffer = Shader.PropertyToID("_SkinMatrices"); + static readonly int k_MaxSize = (int)math.min(SystemInfo.maxGraphicsBufferSize / UnsafeUtility.SizeOf(), int.MaxValue); + + FencedBufferPool m_BufferPool; + + public SkinningBufferManager() + { + m_BufferPool = new FencedBufferPool(); + } + + public void Dispose() + { + m_BufferPool.Dispose(); + } + + public bool ResizePassBufferIfRequired(int requiredSize) + { + var size = m_BufferPool.BufferSize; + if (size <= requiredSize || size - requiredSize > k_ChunkSize) + { + var newSize = ((requiredSize / k_ChunkSize) + 1) * k_ChunkSize; + + if (newSize > k_MaxSize) + { + // Only inform users if the content requires a buffer that is too big. + if(requiredSize > k_MaxSize) + UnityEngine.Debug.LogWarning("The world contains too many skin matrices to fit into a single GraphicsBuffer. Not all skinned meshes are guaranteed to render correctly. Reduce the number of active deformed meshes."); + + // Do not actually resize the buffer if we are already at max capacity. + if (size == k_MaxSize) + return false; + + newSize = k_MaxSize; + } + + m_BufferPool.ResizeBuffer(newSize, UnsafeUtility.SizeOf()); + return true; + } + + return false; + } + + public NativeArray LockSkinMatrixBufferForWrite(int count) + { + m_BufferPool.BeginFrame(); + var buffer = m_BufferPool.GetCurrentFrameBuffer(); + return buffer.LockBufferForWrite(0, count); + } + + public void UnlockSkinMatrixBufferForWrite(int count) + { + var buffer = m_BufferPool.GetCurrentFrameBuffer(); + buffer.UnlockBufferAfterWrite(count); + Shader.SetGlobalBuffer(k_SkinMatricesBuffer, buffer); + m_BufferPool.EndFrame(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs.meta b/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs.meta new file mode 100644 index 0000000..ea5efda --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/BufferManagers/SkinningBufferManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 454d6288399c0be4aaef270a847393d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Components.meta b/Unity.Entities.Graphics/Deformations/Components.meta new file mode 100644 index 0000000..ea8dff8 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 821048d13e64db642867ec08c44534da +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs b/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs new file mode 100644 index 0000000..50e9d4f --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs @@ -0,0 +1,12 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + internal struct BlendWeightBufferIndex : IComponentData + { + // Keep index 0 reserved as Invalid. + public const int Null = 0; + + public int Value; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs.meta b/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs.meta new file mode 100644 index 0000000..2be2fe5 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/BlendShape.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ee11a2e5c4431348a0d7bec13603c9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs b/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs new file mode 100644 index 0000000..6450dae --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs @@ -0,0 +1,39 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + /// + /// Material property that contains the index where the mesh data (pos, nrm, tan) + /// of a deformed mesh starts in the deformed mesh instance buffer. + /// The deformed mesh buffer is double buffered to keep previous vertex positions around for motion vectors. + /// Use MeshBufferManager.ActiveDeformedMeshBufferIndex to access the DeformedMeshIndex for the current frame. + /// x,y = position in current and previous frame buffers + /// z = the current frame index (used as index into x and y properties) + /// w = unused + /// + /// This should be split into two separate components (GFXMESH-79) + [MaterialProperty("_DotsDeformationParams")] + internal struct DeformedMeshIndex : IComponentData + { + public uint4 Value; + } +#else + [MaterialProperty("_ComputeMeshIndex")] + internal struct DeformedMeshIndex : IComponentData + { + public uint Value; + } +#endif + + /// + /// Used by render entities to retrieve the deformed entity which + /// holds the animated data that controls the mesh deformation, + /// such as skin matrices or blend shape weights. + /// + internal struct DeformedEntity : IComponentData + { + public Entity Value; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs.meta b/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs.meta new file mode 100644 index 0000000..df02e68 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/DeformedMesh.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d58f45078a7125641b84c3dd30aca570 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs b/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs new file mode 100644 index 0000000..6bd31d2 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs @@ -0,0 +1,42 @@ +using Unity.Core; +using Unity.Entities; + +namespace Unity.Rendering +{ + internal struct SharedMeshTracker : ICleanupComponentData + { + public int VersionHash; + } + + internal struct SharedMeshData + { + public UnityEngine.Rendering.BatchMeshID MeshID; + + public int VertexCount; + public int BlendShapeCount; + public int BoneCount; + public int RefCount; + + public bool HasSkinning => BoneCount > 0; + public bool HasBlendShapes => BlendShapeCount > 0; + + public readonly int StateHash() + { + int hash = 0; + unsafe + { + var buffer = stackalloc int[] + { + (int)MeshID.value, + VertexCount, + BlendShapeCount, + BoneCount, + }; + + hash = (int)XXHash.Hash32((byte*)buffer, 4 * 4); + } + + return hash; + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs.meta b/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs.meta new file mode 100644 index 0000000..c7490f7 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/SharedMesh.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b0ff971063caa6a4983ffc85b959da4f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Components/Skinning.cs b/Unity.Entities.Graphics/Deformations/Components/Skinning.cs new file mode 100644 index 0000000..bce558a --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/Skinning.cs @@ -0,0 +1,13 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_SkinMatrixIndex")] + internal struct SkinMatrixBufferIndex : IComponentData + { + // Keep index 0 reserved as Invalid. + public const int Null = 0; + + public int Value; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Components/Skinning.cs.meta b/Unity.Entities.Graphics/Deformations/Components/Skinning.cs.meta new file mode 100644 index 0000000..d1ddc34 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Components/Skinning.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ed691a7fe872894194007d4e8bd7981 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs b/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs new file mode 100644 index 0000000..f55bf3a --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs @@ -0,0 +1,51 @@ +using Unity.Entities; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + /// + /// Represents a system group that contains all systems that handle and execute mesh deformations such as skinning and blend shapes. + /// + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup)), UpdateAfter(typeof(RegisterMaterialsAndMeshesSystem)), UpdateBefore(typeof(EntitiesGraphicsSystem))] + public sealed class DeformationsInPresentation : ComponentSystemGroup + { + /// + protected override void OnCreate() + { + if (UnityEngine.SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null) + { + UnityEngine.Debug.LogWarning("Warning: No Graphics Device found. Deformation systems will not run."); + Enabled = false; + } + + base.OnCreate(); + } + } + + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation))] + sealed partial class PushMeshDataSystem : SystemBase { } + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation)), UpdateAfter(typeof(PushMeshDataSystem)), UpdateBefore(typeof(SkinningDeformationSystem))] + sealed partial class PushSkinMatrixSystem : SystemBase { } + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation)), UpdateAfter(typeof(PushMeshDataSystem)), UpdateBefore(typeof(BlendShapeDeformationSystem))] + sealed partial class PushBlendWeightSystem : SystemBase { } + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation)), UpdateAfter(typeof(PushMeshDataSystem))] + sealed partial class InstantiateDeformationSystem : SystemBase { } + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation)), UpdateAfter(typeof(InstantiateDeformationSystem))] + sealed partial class BlendShapeDeformationSystem : SystemBase { } + + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(DeformationsInPresentation))] + [UpdateAfter(typeof(BlendShapeDeformationSystem))] + sealed partial class SkinningDeformationSystem : SystemBase { } +} diff --git a/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs.meta b/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs.meta new file mode 100644 index 0000000..aeed45b --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/DeformationSystemGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8418ea98f94962b428f9a4f380f24e1f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Resources.meta b/Unity.Entities.Graphics/Deformations/Resources.meta new file mode 100644 index 0000000..c00d698 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f28196f844168b44e8cc3640e6a8bd83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute b/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute new file mode 100644 index 0000000..bc896ce --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute @@ -0,0 +1,84 @@ +#pragma kernel BlendShapeComputeKernel + +#define NBR_THREAD_GROUPS 1024 + +#define NBR_THREADS_X 128 +#define NBR_THREADS_Y 1 +#define NBR_THREADS_Z 1 + +#define THREAD_COUNT NBR_THREADS_Y * NBR_THREADS_X * NBR_THREADS_Z +#define STEP_SIZE THREAD_COUNT * NBR_THREAD_GROUPS + +struct VertexData +{ + float3 Position; + float3 Normal; + float3 Tangent; +}; + +struct BlendShapeVertexDelta +{ + int BlendShapeIndex; + float3 Position; + float3 Normal; + float3 Tangent; +}; +#define SIZE_OF_VERTEX_DELTA 10 + +uniform StructuredBuffer _BlendShapeVertexData; +uniform ByteAddressBuffer _BlendShapeWeights; +uniform RWStructuredBuffer _DeformedMeshData : register(u1); + +uint g_VertexCount; +uint g_BlendShapeWeightStartIndex; +uint g_DeformedMeshStartIndex; +uint g_InstanceCount; +uint g_BlendShapeCount; + +BlendShapeVertexDelta LoadBlendShapeVertex(int index) +{ + BlendShapeVertexDelta data; + data.BlendShapeIndex = asint(_BlendShapeVertexData[index]); + data.Position = float3(_BlendShapeVertexData[index + 1], _BlendShapeVertexData[index + 2], _BlendShapeVertexData[index + 3]); + data.Normal = float3(_BlendShapeVertexData[index + 4], _BlendShapeVertexData[index + 5], _BlendShapeVertexData[index + 6]); + data.Tangent = float3(_BlendShapeVertexData[index + 7], _BlendShapeVertexData[index + 8], _BlendShapeVertexData[index + 9]); + return data; +} + +int2 LoadBlendShapeRange(uint index) +{ + return int2(asint(_BlendShapeVertexData[index]), asint(_BlendShapeVertexData[index + 1])); +} + +float LoadBlendWeight(uint index) +{ + return asfloat(_BlendShapeWeights.Load(index * 4)); +} + +[numthreads(NBR_THREADS_X, NBR_THREADS_Y, NBR_THREADS_Z)] +void BlendShapeComputeKernel(uint id : SV_GroupIndex, uint3 groupId : SV_GroupID) +{ + const uint totalNumVertices = g_VertexCount * g_InstanceCount; + const uint start = id + groupId[0] * THREAD_COUNT; + + for (uint i = start; i < totalNumVertices; i += STEP_SIZE) + { + const uint sharedMeshVertexIndex = i % g_VertexCount; + const uint deformedMeshVertexIndex = g_DeformedMeshStartIndex + i; + const uint blendShapeWeightOffset = g_BlendShapeWeightStartIndex + (i / g_VertexCount) * g_BlendShapeCount; + + VertexData vertex = _DeformedMeshData[deformedMeshVertexIndex]; + + const int2 range = LoadBlendShapeRange(sharedMeshVertexIndex); + for (int j = range[0]; j < range[1]; j += SIZE_OF_VERTEX_DELTA) + { + const BlendShapeVertexDelta vertexDelta = LoadBlendShapeVertex(j); + const float weight = LoadBlendWeight(blendShapeWeightOffset + vertexDelta.BlendShapeIndex) * 0.01f; + vertex.Position += weight * vertexDelta.Position; + vertex.Normal += weight * vertexDelta.Normal; + vertex.Tangent += weight * vertexDelta.Tangent; + } + + _DeformedMeshData[deformedMeshVertexIndex] = vertex; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute.meta b/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute.meta new file mode 100644 index 0000000..5a68b5d --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/BlendShapeComputeShader.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4386b5f0b09b2e64b92c0cea7c25e3aa +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute b/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute new file mode 100644 index 0000000..2288aa1 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute @@ -0,0 +1,58 @@ +#pragma kernel InstantiateDeformationDataKernel + +#define NBR_THREAD_GROUPS 1024 + +#define NBR_THREADS_X 128 +#define NBR_THREADS_Y 1 +#define NBR_THREADS_Z 1 + +#define THREAD_COUNT NBR_THREADS_Y * NBR_THREADS_X * NBR_THREADS_Z +#define STEP_SIZE THREAD_COUNT * NBR_THREAD_GROUPS + +struct VertexData +{ + float3 Position; + float3 Normal; + float3 Tangent; +}; + +uniform ByteAddressBuffer _SharedMeshVertexBuffer : register(t1); +uniform RWStructuredBuffer _DeformedMeshData : register(u1); + +uint g_VertexCount; +uint g_DeformedMeshStartIndex; +uint g_InstanceCount; + +VertexData LoadVertex(uint index) +{ + // Vertex attribute is assumed to be position, normal & tangent. + // These are float3, float3 and float4 respectively, thus the stride is 40. + uint offset = index * 40; + + // Note that VertexData uses float3 for tangent. + float3 pos = asfloat(_SharedMeshVertexBuffer.Load3(offset + 0 * 12)); + float3 nor = asfloat(_SharedMeshVertexBuffer.Load3(offset + 1 * 12)); + float3 tan = asfloat(_SharedMeshVertexBuffer.Load3(offset + 2 * 12)); + + VertexData data = (VertexData)0; + data.Position = pos; + data.Normal = nor; + data.Tangent = tan; + + return data; +} + +[numthreads(NBR_THREADS_X, NBR_THREADS_Y, NBR_THREADS_Z)] +void InstantiateDeformationDataKernel(uint id : SV_GroupIndex, uint3 groupId : SV_GroupID) +{ + const uint totalNumVertices = g_VertexCount * g_InstanceCount; + const uint start = id + groupId[0] * THREAD_COUNT; + + for (uint i = start; i < totalNumVertices; i += STEP_SIZE) + { + const uint sharedMeshVertexIndex = i % g_VertexCount; + const uint deformedMeshVertexIndex = g_DeformedMeshStartIndex + i; + + _DeformedMeshData[deformedMeshVertexIndex] = LoadVertex(sharedMeshVertexIndex); + } +} diff --git a/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute.meta b/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute.meta new file mode 100644 index 0000000..fd4c328 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/InstantiateDeformationData.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 69ff4543fb2ddcf4194cceec021b67b3 +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute b/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute new file mode 100644 index 0000000..527d81a --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute @@ -0,0 +1,207 @@ +#pragma kernel SkinningDense1ComputeKernel +#pragma kernel SkinningDense2ComputeKernel +#pragma kernel SkinningDense4ComputeKernel +#pragma kernel SkinningSparseComputeKernel + +#define NBR_THREAD_GROUPS 1024 + +#define NBR_THREADS_X 128 +#define NBR_THREADS_Y 1 +#define NBR_THREADS_Z 1 + +#define THREAD_COUNT NBR_THREADS_Y * NBR_THREADS_X * NBR_THREADS_Z +#define STEP_SIZE THREAD_COUNT * NBR_THREAD_GROUPS + +struct VertexData +{ + float3 Position; + float3 Normal; + float3 Tangent; +}; + +struct SkinInfluence1 +{ + uint index; +}; + +#define DENSE_SKIN_INFLUENCE(influencesPerVertex) \ +struct SkinInfluence##influencesPerVertex \ +{ \ + float##influencesPerVertex weights; \ + uint##influencesPerVertex indices; \ +}; + +DENSE_SKIN_INFLUENCE(2) +DENSE_SKIN_INFLUENCE(4) + +struct SparseSkinInfluence +{ + float weight; + uint index; +}; + +uniform ByteAddressBuffer _SharedMeshBoneWeights; +uniform ByteAddressBuffer _SkinMatrices; +uniform RWStructuredBuffer _DeformedMeshData : register(u1); + +uint g_VertexCount; +uint g_SharedMeshBoneCount; +uint g_InstanceCount; +uint g_DeformedMeshStartIndex; +uint g_SkinMatricesStartIndex; + +SkinInfluence1 LoadDenseSkinInfluence1(uint index) +{ + const uint offset = index * 4; + + SkinInfluence1 data; + data.index = asuint(_SharedMeshBoneWeights.Load(offset)); + return data; +} + +SkinInfluence2 LoadDenseSkinInfluence2(uint index) +{ + const uint offset = index * 16; + + SkinInfluence2 data; + uint4 values = _SharedMeshBoneWeights.Load4(offset); + data.weights = asfloat(values.xy); + data.indices = values.zw; + return data; +} + +SkinInfluence4 LoadDenseSkinInfluence4(uint index) +{ + const uint offset = index * 32; + + SkinInfluence4 data; + data.weights = asfloat(_SharedMeshBoneWeights.Load4(offset + 0 * 16)); + data.indices = asuint(_SharedMeshBoneWeights.Load4(offset + 1 * 16)); + return data; +} + +SparseSkinInfluence LoadSparseSkinInfluence(uint index) +{ + const uint offset = index * 4; + const uint weightAndIndex = _SharedMeshBoneWeights.Load(offset); + + SparseSkinInfluence data; + data.weight = float(weightAndIndex >> 16) * (1.0f / 65535.0f); + data.index = weightAndIndex & 0xFFFF; + return data; +} + +// Returns the buffer range where the blend shape deltas for a given vertex can be found. +// The first element contains the start index. The second element contains the end index. +uint2 LoadSkinInfluenceRange(uint index) +{ + const uint offset = index * 4; + const uint2 range = asuint(_SharedMeshBoneWeights.Load2(offset)); + return range; +} + +float3x4 LoadSkinMatrix(uint index) +{ + uint offset = index * 48; + + // Read in 4 columns of float3 data each. + // Done in 3 load4 and then repacking into final 3x4 matrix + float4 p1 = asfloat(_SkinMatrices.Load4(offset + 0 * 16)); + float4 p2 = asfloat(_SkinMatrices.Load4(offset + 1 * 16)); + float4 p3 = asfloat(_SkinMatrices.Load4(offset + 2 * 16)); + + return float3x4( + p1.x, p1.w, p2.z, p3.y, + p1.y, p2.x, p2.w, p3.z, + p1.z, p2.y, p3.x, p3.w + ); +} + +VertexData SkinVertexDense1(in uint sharedVertexIndex, in uint vertexIndex, in uint boneOffset) +{ + VertexData vertex = _DeformedMeshData[vertexIndex]; + const float4 basePos = float4(vertex.Position, 1); + const float4 baseNrm = float4(vertex.Normal, 0); + const float4 baseTan = float4(vertex.Tangent, 0); + + const SkinInfluence1 influence = LoadDenseSkinInfluence1(sharedVertexIndex); + const float3x4 skinMatrix = LoadSkinMatrix(boneOffset + influence.index); + + vertex.Position = mul(skinMatrix, basePos); + vertex.Normal = mul(skinMatrix, baseNrm); + vertex.Tangent = mul(skinMatrix, baseTan); + + return vertex; +} + +#define SKIN_VERTEX_DENSE(countPerVertex) \ +VertexData SkinVertexDense##countPerVertex(in uint sharedVertexIndex, in uint vertexIndex, in uint boneOffset) \ +{ \ + VertexData vertex = _DeformedMeshData[vertexIndex]; \ + const float4 basePos = float4(vertex.Position, 1); \ + const float4 baseNor = float4(vertex.Normal, 0); \ + const float4 baseTan = float4(vertex.Tangent, 0); \ + vertex = (VertexData)0; \ + \ + const SkinInfluence##countPerVertex influence = LoadDenseSkinInfluence##countPerVertex(sharedVertexIndex); \ + \ + for (uint i = 0; i < countPerVertex; ++i) \ + { \ + const float3x4 skinMatrix = LoadSkinMatrix(boneOffset + influence.indices[i]); \ + \ + vertex.Position += mul(skinMatrix, basePos) * influence.weights[i]; \ + vertex.Normal += mul(skinMatrix, baseNor) * influence.weights[i]; \ + vertex.Tangent += mul(skinMatrix, baseTan) * influence.weights[i]; \ + } \ + \ + return vertex; \ +} + +SKIN_VERTEX_DENSE(2) +SKIN_VERTEX_DENSE(4) + +VertexData SkinVertexSparse(in uint sharedVertexIndex, in uint vertexIndex, in uint boneOffset) +{ + VertexData vertex = _DeformedMeshData[vertexIndex]; + const float4 basePos = float4(vertex.Position, 1); + const float4 baseNor = float4(vertex.Normal, 0); + const float4 baseTan = float4(vertex.Tangent, 0); + vertex = (VertexData)0; + + const uint2 range = LoadSkinInfluenceRange(sharedVertexIndex); + for (uint i = range[0]; i < range[1]; ++i) + { + const SparseSkinInfluence influence = LoadSparseSkinInfluence(i); + const float3x4 skinMatrix = LoadSkinMatrix(boneOffset + influence.index); + + vertex.Position += mul(skinMatrix, basePos) * influence.weight; + vertex.Normal += mul(skinMatrix, baseNor) * influence.weight; + vertex.Tangent += mul(skinMatrix, baseTan) * influence.weight; + } + + return vertex; +} + +#define SKINNING_KERNEL(function) \ +[numthreads(NBR_THREADS_X, NBR_THREADS_Y, NBR_THREADS_Z)] \ +void Skinning##function##ComputeKernel(uint id : SV_GroupIndex, uint3 groupId : SV_GroupID) \ +{ \ + const uint totalNumVertices = g_VertexCount * g_InstanceCount; \ + const uint start = id + groupId[0] * THREAD_COUNT; \ + \ + for (uint i = start; i < totalNumVertices; i += STEP_SIZE) \ + { \ + const uint sharedMeshVertexIndex = i % g_VertexCount; \ + const uint deformedMeshVertexIndex = g_DeformedMeshStartIndex + i; \ + const uint boneOffset = g_SkinMatricesStartIndex + ((i / g_VertexCount) * g_SharedMeshBoneCount); \ + \ + VertexData vertex = SkinVertex##function(sharedMeshVertexIndex, deformedMeshVertexIndex, boneOffset); \ + \ + _DeformedMeshData[deformedMeshVertexIndex] = vertex; \ + } \ +} + +SKINNING_KERNEL(Dense1) +SKINNING_KERNEL(Dense2) +SKINNING_KERNEL(Dense4) +SKINNING_KERNEL(Sparse) diff --git a/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute.meta b/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute.meta new file mode 100644 index 0000000..89246c8 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Resources/SkinningComputeShader.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 709c3a8bb7a9d82418815cdaaf6453f0 +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/ShaderLibrary.meta b/Unity.Entities.Graphics/Deformations/ShaderLibrary.meta new file mode 100644 index 0000000..b342b27 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/ShaderLibrary.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7be034303155adb4b82b45d27646074c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl b/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl new file mode 100644 index 0000000..5a26183 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl @@ -0,0 +1,52 @@ +#ifndef DOTS_DEFORMATIONS_INCLUDED +#define DOTS_DEFORMATIONS_INCLUDED + +#if defined(UNITY_DOTS_INSTANCING_ENABLED) + +struct DeformedVertexData +{ + float3 Position; + float3 Normal; + float3 Tangent; +}; +uniform StructuredBuffer _DeformedMeshData : register(t1); +uniform StructuredBuffer _PreviousFrameDeformedMeshData; + + +void ApplyDeformedVertexData(uint vertexID, out float3 positionOut, out float3 normalOut, out float3 tangentOut) +{ + const uint4 materialProperty = asuint(UNITY_ACCESS_HYBRID_INSTANCED_PROP(_DotsDeformationParams, float4)); + const uint currentFrameIndex = materialProperty[2]; + const uint meshStartIndex = materialProperty[currentFrameIndex]; + + const DeformedVertexData vertexData = _DeformedMeshData[meshStartIndex + vertexID]; + + positionOut = vertexData.Position; + normalOut = vertexData.Normal; + tangentOut = vertexData.Tangent; +} + +void ApplyPreviousFrameDeformedVertexPosition(in uint vertexID, out float3 positionOS) +{ + const uint4 materialProperty = asuint(UNITY_ACCESS_HYBRID_INSTANCED_PROP(_DotsDeformationParams, float4)); + const uint prevFrameIndex = materialProperty[2] ^ 1; + const uint meshStartIndex = materialProperty[prevFrameIndex]; + + // If we have a valid index, fetch the previous frame position + // Index zero is reserved as 'uninitialized'. + if (meshStartIndex > 0) + { + positionOS = _PreviousFrameDeformedMeshData[meshStartIndex + vertexID].Position; + } + // Else grab the current frame position + else + { + const uint currentFrameIndex = materialProperty[2]; + const uint currentFrameMeshStartIndex = materialProperty[currentFrameIndex]; + + positionOS = _DeformedMeshData[currentFrameMeshStartIndex + vertexID].Position; + } +} +#endif //UNITY_DOTS_INSTANCING_ENABLED + +#endif //DOTS_DEFORMATIONS_INCLUDED diff --git a/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl.meta b/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl.meta new file mode 100644 index 0000000..b8ee9cc --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/ShaderLibrary/DotsDeformation.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 063dca9964f34ce46afc6f5fdf616c72 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Structs.meta b/Unity.Entities.Graphics/Deformations/Structs.meta new file mode 100644 index 0000000..1c272dd --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cbbc78f7a735c794994c1484912b35eb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs b/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs new file mode 100644 index 0000000..727c882 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs @@ -0,0 +1,12 @@ +using Unity.Mathematics; + +namespace Unity.Rendering +{ + internal struct BlendShapeVertexDelta + { + public int BlendShapeIndex; + public float3 Position; + public float3 Normal; + public float3 Tangent; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs.meta b/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs.meta new file mode 100644 index 0000000..dc946c2 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/BlendShapeVertexDelta.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e77459b27749414e8b882958628a741 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs b/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs new file mode 100644 index 0000000..631a75e --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs @@ -0,0 +1,8 @@ +namespace Unity.Rendering +{ + internal struct BoneWeight + { + public float Weight; + public uint Index; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs.meta b/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs.meta new file mode 100644 index 0000000..0d7427e --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/BoneWeight.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 649d57203139db24b99cfa4da7bd756e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs b/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs new file mode 100644 index 0000000..87a29f1 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs @@ -0,0 +1,17 @@ +using Unity.Mathematics; + +namespace Unity.Rendering +{ + /// + /// Represent vertex data for a SharedMesh buffer + /// + /// + /// This must map between compute shaders and CPU data. + /// + internal struct VertexData + { + public float3 Position; + public float3 Normal; + public float3 Tangent; + } +} diff --git a/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs.meta b/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs.meta new file mode 100644 index 0000000..cfbe02c --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Structs/VertexData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1aff5c88804bfa4097140e05656a00d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems.meta b/Unity.Entities.Graphics/Deformations/Systems.meta new file mode 100644 index 0000000..c8dba28 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7ab70994f3df2c84b8a12e22bb1085d7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs new file mode 100644 index 0000000..7d79663 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs @@ -0,0 +1,105 @@ +using Unity.Assertions; +using Unity.Deformations; +using Unity.Entities; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + partial class BlendShapeDeformationSystem : SystemBase + { + static readonly ProfilerMarker k_FinalizePushBlendShapeWeight = new ProfilerMarker("FinalizeBlendWeightForGPU"); + static readonly ProfilerMarker k_BlendShapeDeformationMarker = new ProfilerMarker("BlendShapeDeformationDispatch"); + + static readonly int k_VertexCount = Shader.PropertyToID("g_VertexCount"); + static readonly int k_DeformedMeshStartIndex = Shader.PropertyToID("g_DeformedMeshStartIndex"); + static readonly int k_InstanceCount = Shader.PropertyToID("g_InstanceCount"); + static readonly int k_BlendShapeCount = Shader.PropertyToID("g_BlendShapeCount"); + static readonly int k_BlendShapeWeightStartIndex = Shader.PropertyToID("g_BlendShapeWeightStartIndex"); + static readonly int k_BlendShapeVerticesBuffer = Shader.PropertyToID("_BlendShapeVertexData"); + + ComputeShader m_ComputeShader; + PushMeshDataSystem m_PushMeshDataSystem; + EntitiesGraphicsSystem m_RendererSystem; + + int m_Kernel; + + EntityQuery m_BlendWeightQuery; + + protected override void OnCreate() + { +#if !HYBRID_RENDERER_DISABLED + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) +#endif + { + Enabled = false; + UnityEngine.Debug.Log("No SRP present, no compute shader support, or running with -nographics. Mesh Deformation Systems disabled."); + return; + } + + m_PushMeshDataSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_PushMeshDataSystem, $"{nameof(PushMeshDataSystem)} was not found!"); + + m_ComputeShader = Resources.Load("BlendShapeComputeShader"); + Assert.IsNotNull(m_ComputeShader, $"Compute shader for {typeof(BlendShapeDeformationSystem)} was not found!"); + + m_RendererSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_RendererSystem, $"{nameof(EntitiesGraphicsSystem)} was not found!"); + + m_Kernel = m_ComputeShader.FindKernel("BlendShapeComputeKernel"); + + m_BlendWeightQuery = GetEntityQuery( + ComponentType.ReadWrite() + ); + } + + protected override void OnUpdate() + { + if (m_PushMeshDataSystem.BlendShapeWeightCount == 0) + return; + + k_FinalizePushBlendShapeWeight.Begin(); + + // Complete the Read/Write dependency on BlendShapeWeight. + // This guarantees that the data has been written to GPU + // Assuming that PushBlendWeightSystem has executed before this system. + m_BlendWeightQuery.CompleteDependency(); + m_PushMeshDataSystem.BlendShapeBufferManager.UnlockBlendWeightBufferForWrite(m_PushMeshDataSystem.BlendShapeWeightCount); + + k_FinalizePushBlendShapeWeight.End(); + k_BlendShapeDeformationMarker.Begin(); + + foreach (var deformationBatch in m_PushMeshDataSystem.DeformationBatches) + { + var id = deformationBatch.Key; + var batchData = deformationBatch.Value; + + var hasMeshData = m_PushMeshDataSystem.TryGetSharedMeshData(id, out var meshData); + + Assert.IsTrue(hasMeshData); + + if (!meshData.HasBlendShapes) + continue; + + m_ComputeShader.SetInt(k_VertexCount, meshData.VertexCount); + m_ComputeShader.SetInt(k_BlendShapeCount, meshData.BlendShapeCount); + m_ComputeShader.SetInt(k_DeformedMeshStartIndex, batchData.MeshVertexIndex); + m_ComputeShader.SetInt(k_BlendShapeWeightStartIndex, batchData.BlendShapeIndex); + m_ComputeShader.SetInt(k_InstanceCount, batchData.InstanceCount); + + var mesh = m_RendererSystem.GetMesh(meshData.MeshID); + var blendShapeBuffer = mesh.GetBlendShapeBuffer(BlendShapeBufferLayout.PerVertex); + Assert.IsNotNull(blendShapeBuffer); + + m_ComputeShader.SetBuffer(m_Kernel, k_BlendShapeVerticesBuffer, blendShapeBuffer); + m_ComputeShader.Dispatch(m_Kernel, 1024, 1, 1); + + blendShapeBuffer.Dispose(); + } + + k_BlendShapeDeformationMarker.End(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs.meta new file mode 100644 index 0000000..0ee810f --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/BlendShapeDeformationSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7376b5e65b4ef55409ff6cd3e0ba43f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs new file mode 100644 index 0000000..0348149 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs @@ -0,0 +1,83 @@ +using Unity.Assertions; +using Unity.Entities; +using Unity.Profiling; +using UnityEngine; + +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + partial class InstantiateDeformationSystem : SystemBase + { + static readonly ProfilerMarker k_InstantiateDeformationMarker = new ProfilerMarker("InstantiateDeformationSystem"); + + static readonly int k_VertexCount = Shader.PropertyToID("g_VertexCount"); + static readonly int k_DeformedMeshStartIndex = Shader.PropertyToID("g_DeformedMeshStartIndex"); + static readonly int k_InstancesCount = Shader.PropertyToID("g_InstanceCount"); + static readonly int k_SharedMeshVertexBuffer = Shader.PropertyToID("_SharedMeshVertexBuffer"); + + ComputeShader m_ComputeShader; + PushMeshDataSystem m_PushMeshDataSystem; + EntitiesGraphicsSystem m_RendererSystem; + + int m_kernel; + + EntityQuery m_Query; + + protected override void OnCreate() + { +#if !HYBRID_RENDERER_DISABLED + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) +#endif + { + Enabled = false; + UnityEngine.Debug.Log("No SRP present, no compute shader support, or running with -nographics. Mesh Deformation Systems disabled."); + return; + } + + m_ComputeShader = Resources.Load("InstantiateDeformationData"); + Assert.IsNotNull(m_ComputeShader, $"Compute shader for {typeof(InstantiateDeformationSystem)} was not found!"); + + m_PushMeshDataSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_PushMeshDataSystem, $"{nameof(PushMeshDataSystem)} was not found!"); + + m_RendererSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_RendererSystem, $"{nameof(EntitiesGraphicsSystem)} was not found!"); + + m_kernel = m_ComputeShader.FindKernel("InstantiateDeformationDataKernel"); + + m_Query = GetEntityQuery( + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + } + + protected override void OnUpdate() + { + k_InstantiateDeformationMarker.Begin(); + + foreach (var deformationBatch in m_PushMeshDataSystem.DeformationBatches) + { + var id = deformationBatch.Key; + var batchData = deformationBatch.Value; + + var hasMeshData = m_PushMeshDataSystem.TryGetSharedMeshData(id, out var meshData); + + Assert.IsTrue(hasMeshData); + + m_ComputeShader.SetInt(k_VertexCount, meshData.VertexCount); + m_ComputeShader.SetInt(k_DeformedMeshStartIndex, batchData.MeshVertexIndex); + m_ComputeShader.SetInt(k_InstancesCount, batchData.InstanceCount); + + var mesh = m_RendererSystem.GetMesh(meshData.MeshID); + var vertexBuffer = mesh.GetVertexBuffer(0); + Assert.IsNotNull(vertexBuffer); + + m_ComputeShader.SetBuffer(m_kernel, k_SharedMeshVertexBuffer, vertexBuffer); + m_ComputeShader.Dispatch(m_kernel, 1024, 1, 1); + vertexBuffer.Dispose(); + } + + k_InstantiateDeformationMarker.End(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs.meta new file mode 100644 index 0000000..b86e9ee --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/InstantiateDeformationSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f2465af0d2111242ae00589ef1c8297 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs new file mode 100644 index 0000000..e26c288 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs @@ -0,0 +1,91 @@ +using Unity.Assertions; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Deformations; +using Unity.Entities; +using Unity.Jobs; +using Unity.Profiling; + +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + partial class PushBlendWeightSystem : SystemBase + { + static readonly ProfilerMarker k_Marker = new ProfilerMarker("PrepareBlendWeightForGPU"); + + EntityQuery m_BlendShapedEntityQuery; + + PushMeshDataSystem m_PushMeshDataSystem; + + protected override void OnCreate() + { + if (!UnityEngine.SystemInfo.supportsComputeShaders) + { + Enabled = false; + return; + } + + m_PushMeshDataSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_PushMeshDataSystem, $"{nameof(PushMeshDataSystem)} system was not found in the world!"); + + m_BlendShapedEntityQuery = GetEntityQuery( + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + } + + protected override void OnUpdate() + { + if (m_PushMeshDataSystem.BlendShapeWeightCount == 0) + return; + + k_Marker.Begin(); + + var deformedEntityToComputeIndex = new NativeMultiHashMap(m_BlendShapedEntityQuery.CalculateEntityCount(), Allocator.TempJob); + var deformedEntityToComputeIndexParallel = deformedEntityToComputeIndex.AsParallelWriter(); + Dependency = Entities + .WithName("ConstructHashMap") + .WithAll() + .ForEach((in BlendWeightBufferIndex index, in DeformedEntity deformedEntity) => + { + // Skip if we have an invalid index. + if (index.Value == BlendWeightBufferIndex.Null) + return; + + deformedEntityToComputeIndexParallel.Add(deformedEntity.Value, index.Value); + }).ScheduleParallel(Dependency); + + var blendShapeWeightsBuffer = m_PushMeshDataSystem.BlendShapeBufferManager.LockBlendWeightBufferForWrite(m_PushMeshDataSystem.BlendShapeWeightCount); + Dependency = Entities + .WithName("CopyBlendShapeWeightsToGPU") + .WithNativeDisableContainerSafetyRestriction(blendShapeWeightsBuffer) + .WithReadOnly(deformedEntityToComputeIndex) + .ForEach((ref DynamicBuffer weights, in Entity entity) => + { + // Not all deformed entities in the world will have a renderer attached to them. + if (!deformedEntityToComputeIndex.ContainsKey(entity)) + return; + + var length = weights.Length * UnsafeUtility.SizeOf(); + var indices = deformedEntityToComputeIndex.GetValuesForKey(entity); + + foreach (var index in indices) + { + unsafe + { + UnsafeUtility.MemCpy( + (float*)blendShapeWeightsBuffer.GetUnsafePtr() + index, + weights.GetUnsafePtr(), + length + ); + } + } + }).ScheduleParallel(Dependency); + + Dependency = deformedEntityToComputeIndex.Dispose(Dependency); + + k_Marker.End(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs.meta new file mode 100644 index 0000000..648964f --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushBlendWeightSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13bdfdec1debd744d92d98ca1bb772b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs new file mode 100644 index 0000000..5a7303f --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs @@ -0,0 +1,613 @@ +using System.Threading; +using Unity.Assertions; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Profiling; + +using BatchMeshID = UnityEngine.Rendering.BatchMeshID; +using VertexAttribute = UnityEngine.Rendering.VertexAttribute; +using VertexAttributeFormat = UnityEngine.Rendering.VertexAttributeFormat; + +namespace Unity.Rendering +{ + struct MeshDeformationBatch + { + public int BatchIndex; + public int MeshVertexIndex; + public int BlendShapeIndex; + public int SkinMatrixIndex; + public int InstanceCount; + } + + [RequireMatchingQueriesForUpdate] + partial class PushMeshDataSystem : SystemBase + { + static readonly ProfilerMarker k_ChangesMarker = new ProfilerMarker("Detect Entity Changes"); + static readonly ProfilerMarker k_OutputCountBuffer = new ProfilerMarker("Counting Deformed Mesh Buffer"); + static readonly ProfilerMarker k_OutputResizeBuffer = new ProfilerMarker("Resize Deformed Mesh Buffer"); + static readonly ProfilerMarker k_CollectActiveMeshes = new ProfilerMarker("Collect Active Deformations"); + + // Reserve index 0 for 'uninitialized' + const int k_DeformBufferStartIndex = 1; + + internal int SkinMatrixCount => m_SkinMatrixCount.Value; + internal int BlendShapeWeightCount => m_BlendShapeWeightCount.Value; + + // Active deformation batches for this frame. + internal NativeParallelHashMap DeformationBatches; + + // The MeshID and data of shared meshes are copied into two parallel arrays. + // For each ID-data pair, the MeshID is stored in `m_MeshIDs[i]` + // while the data is stored in `m_SharedMeshData[i]` (for the same `i`). + NativeList m_MeshIDs; + NativeList m_SharedMeshData; + + NativeReference m_MeshVertexCount; + NativeReference m_BlendShapeWeightCount; + NativeReference m_SkinMatrixCount; + + internal SkinningBufferManager SkinningBufferManager { get; private set; } + + internal BlendShapeBufferManager BlendShapeBufferManager { get; private set; } + + MeshBufferManager m_MeshBufferManager; + EntitiesGraphicsSystem m_RendererSystem; + + JobHandle m_BatchConstructionHandle; + + EntityQuery m_LayoutDeformedMeshesQuery; + + protected override void OnCreate() + { +#if !HYBRID_RENDERER_DISABLED + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) +#endif + { + Enabled = false; + UnityEngine.Debug.Log("No SRP present, no compute shader support, or running with -nographics. Mesh Deformation Systems disabled."); + return; + } + + // Setup data structures + DeformationBatches = new NativeParallelHashMap(64, Allocator.Persistent); + m_MeshIDs = new NativeList(64, Allocator.Persistent); + m_SharedMeshData = new NativeList(64, Allocator.Persistent); + + m_MeshVertexCount = new NativeReference(Allocator.Persistent); + m_BlendShapeWeightCount = new NativeReference(Allocator.Persistent); + m_SkinMatrixCount = new NativeReference(Allocator.Persistent); + + // Create GPU Buffers + m_MeshBufferManager = new MeshBufferManager(); + BlendShapeBufferManager = new BlendShapeBufferManager(); + SkinningBufferManager = new SkinningBufferManager(); + + // Gather references + m_RendererSystem = World.GetOrCreateSystemManaged(); + + // Queries + m_LayoutDeformedMeshesQuery = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadWrite(), + }, + Any = new[] + { + ComponentType.ReadWrite(), + ComponentType.ReadWrite(), + }, + }); + } + + protected override void OnDestroy() + { + if (m_SharedMeshData.IsCreated) + m_SharedMeshData.Dispose(); + if (m_MeshIDs.IsCreated) + m_MeshIDs.Dispose(); + if (DeformationBatches.IsCreated) + DeformationBatches.Dispose(); + if (m_MeshVertexCount.IsCreated) + m_MeshVertexCount.Dispose(); + if (m_BlendShapeWeightCount.IsCreated) + m_BlendShapeWeightCount.Dispose(); + if(m_SkinMatrixCount.IsCreated) + m_SkinMatrixCount.Dispose(); + + m_MeshBufferManager?.Dispose(); + BlendShapeBufferManager?.Dispose(); + SkinningBufferManager?.Dispose(); + } + + /// + protected override void OnUpdate() + { +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + m_MeshBufferManager.FlipDeformedMeshBuffer(); +#endif + + // Handle newly spawned and removed entities. + AddRemoveTrackedMeshes(); + + // Find active deformed meshes for this frame, + // construct deformation batches and allocate + // them in the graphics buffers. + LayoutOutputBuffer(); + + // Complete the constructions of batches + // So that we can resize the output buffers + // and know the final buffer sizes required + m_BatchConstructionHandle.Complete(); + FitOutputBuffer(); + } + + // Support more layouts: GFXMESH-66. + private bool HasSupportedVertexLayout(UnityEngine.Mesh mesh) + { + // There need to be at least 3 vertex attributes + if (mesh.vertexAttributeCount < 3) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). Expecting Position, Normal and Tangent attributes but only found {mesh.vertexAttributeCount} attributes. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + + var position = mesh.GetVertexAttribute(0); + var normal = mesh.GetVertexAttribute(1); + var tangent = mesh.GetVertexAttribute(2); + + // The attributes are Position, Normal & Tangent. + if (position.attribute != VertexAttribute.Position || normal.attribute != VertexAttribute.Normal || tangent.attribute != VertexAttribute.Tangent) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). Expecting Position, Normal and Tangent attributes but found {position.attribute}, {normal.attribute} and {tangent.attribute}. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + + // The format for these attributes is float32. + if (position.format != VertexAttributeFormat.Float32 || normal.format != VertexAttributeFormat.Float32 || tangent.format != VertexAttributeFormat.Float32) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). The Position, Normal and Tangent attributes need to use Float32 format. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + + // The dimension needs to be 3 for position and normal and 4 for tangent. + if (position.dimension != 3 || normal.dimension != 3 || tangent.dimension != 4) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). Expecting dimensions 3, 3 and 4 for Position, Normal and Tangent respectively but got {position.dimension}, {normal.dimension} and {tangent.dimension}. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + + // All three attributes need to be in stream 0 + if (position.stream != 0 || normal.stream != 0 || tangent.stream != 0) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). The Position, Normal and Tangent attributes need to be present in stream 0. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + + // There cannot be any other attributes in stream 0 + for (int i = 3; i < mesh.vertexAttributeCount; i++) + { + var attribute = mesh.GetVertexAttribute(i); + if (attribute.stream == 0) + { + UnityEngine.Debug.LogWarning($"Unsupported vertex layout for deformations in mesh ({mesh.name}). The vertex attribute {attribute.attribute} should not be present in stream 0. The mesh deformations for this mesh will be disabled and rendered as a regular mesh."); + return false; + } + } + + return true; + } + + private SharedMeshData AddSharedMeshData(in BatchMeshID meshID, UnityEngine.Mesh mesh, NativeList meshIDs, NativeList meshData) + { + Assert.IsFalse(meshID == BatchMeshID.Null); + Assert.IsNotNull(mesh); + + mesh.vertexBufferTarget |= UnityEngine.GraphicsBuffer.Target.Raw; + + var data = new SharedMeshData + { + VertexCount = mesh.vertexCount, + BlendShapeCount = mesh.blendShapeCount, + BoneCount = mesh.bindposeCount, + MeshID = meshID, + RefCount = 1, + }; + + // Insert the MeshData sorted by the Hash + var hash = data.StateHash(); + var index = 0; + + while (index < meshData.Length && hash > meshData[index].StateHash()) + index++; + + if (meshIDs.Length > 0 && index != meshIDs.Length) + { + meshIDs.InsertRangeWithBeginEnd(index, index + 1); + meshData.InsertRangeWithBeginEnd(index, index + 1); + + meshIDs[index] = data.MeshID; + meshData[index] = data; + } + else + { + meshIDs.Add(data.MeshID); + meshData.Add(data); + } + + return data; + } + + internal bool TryGetSharedMeshData(BatchMeshID id, out SharedMeshData data) + { + Assert.IsTrue(m_MeshIDs.IsCreated && m_SharedMeshData.IsCreated); + Assert.IsTrue(m_MeshIDs.Length == m_SharedMeshData.Length); + Assert.IsFalse(id == BatchMeshID.Null); + + var index = m_MeshIDs.IndexOf(id); + + if (index < 0) + { + data = default; + return false; + } + + data = m_SharedMeshData[index]; + return true; + } + + private void AddRemoveTrackedMeshes() + { + k_ChangesMarker.Begin(); + + var rmvMeshes = new NativeList(Allocator.TempJob); + var addMeshes = new NativeList(Allocator.TempJob); + + var meshData = m_SharedMeshData; + var meshIDs = m_MeshIDs; + + Entities + .WithName("DisabledComponents") + .WithAll() + .WithStructuralChanges() + .ForEach((Entity e, in SharedMeshTracker tracked) => + { + EntityManager.RemoveComponent(e); + EntityManager.RemoveComponent(e); + + // These components may or may not exist. + EntityManager.RemoveComponent(e); + EntityManager.RemoveComponent(e); + + rmvMeshes.Add(tracked.VersionHash); + }).Run(); + + Entities + .WithName("RemoveComponents") + .WithNone() + .WithStructuralChanges() + .WithEntityQueryOptions(EntityQueryOptions.IncludeDisabledEntities) + .ForEach((Entity e, in SharedMeshTracker tracked) => + { + EntityManager.RemoveComponent(e); + EntityManager.RemoveComponent(e); + + // These components may or may not exist. + EntityManager.RemoveComponent(e); + EntityManager.RemoveComponent(e); + + rmvMeshes.Add(tracked.VersionHash); + }).Run(); + + Entities + .WithName("AddComponents") + .WithAll() + .WithNone() + .WithStructuralChanges() + .ForEach((Entity e, in MaterialMeshInfo renderData) => + { + var meshID = renderData.MeshID; + + Assert.IsFalse(meshID == BatchMeshID.Null); + + // Mesh is already registered. + if (TryGetSharedMeshData(meshID, out var data)) + { + addMeshes.Add(data.StateHash()); + } + // Mesh is not yet registered, register it. + else + { + UnityEngine.Mesh mesh = m_RendererSystem.GetMesh(meshID); + Assert.IsNotNull(mesh); + + // Check the layout of the mesh if it is not valid remove the + // deformed mesh component so it will be treated as a regular mesh. + if (!HasSupportedVertexLayout(mesh)) + { + EntityManager.RemoveComponent(e); + return; + } + + // Note that this do not use 'addMeshes' instead the + // shared mesh is initialized with a ref count of 1 + data = AddSharedMeshData(in meshID, mesh, meshIDs, meshData); + } + + EntityManager.AddComponentData(e, new SharedMeshTracker { VersionHash = data.StateHash() }); + EntityManager.AddComponent(e); + + if (data.HasBlendShapes) + EntityManager.AddComponent(e); + + if (data.HasSkinning) + EntityManager.AddComponent(e); + }).Run(); + + // Update reference counting if a change has occured + if (rmvMeshes.Length > 0 || addMeshes.Length > 0) + { + Job.WithName("UpdateSharedMeshRefCounts") + .WithCode(() => + { + rmvMeshes.Sort(); + addMeshes.Sort(); + + // Single pass O(n) in reverse. Both arrays are guaranteed to be sorted. + for (int i = meshData.Length - 1, j = rmvMeshes.Length - 1, k = addMeshes.Length - 1; i >= 0 && (j >= 0 || k >= 0); i--) + { + var hash = meshData[i].StateHash(); + + // - Removal - + // 1. Update indexer for current array + while (j >= 0 && rmvMeshes[j] > hash) + j--; + + // 2. If this mesh was removed, count how many + int rmvCnt = 0; + if (j >= 0 && rmvMeshes[j] == hash) + { + int start = j; + + while (j > 0 && rmvMeshes[j - 1] == hash) + j--; + + rmvCnt = start - j + 1; + } + + // - Addition - + // 1. Update indexer for current array + while (k >= 0 && addMeshes[k] > hash) + k--; + + // 2. If this mesh was added, count how many + int addCnt = 0; + if (k >= 0 && addMeshes[k] == hash) + { + int start = k; + + while (k > 0 && addMeshes[k - 1] == hash) + k--; + + addCnt = start - k + 1; + } + + int delta = addCnt - rmvCnt; + + // We can skip updating the ref count if the total number won't change. + // Note: we do not need to check for eviction here either because the first + // occurance of a mesh does not go through 'addMeshes'. In other words, + // the delta will be -1 when all instances are added and removed in the same frame. + if (delta != 0) + { + ref var data = ref meshData.ElementAt(i); + data.RefCount += delta; + + Assert.IsTrue(data.RefCount >= 0); + + // Evict Mesh if no one references it + if (data.RefCount == 0) + { + meshData.RemoveAt(i); + meshIDs.RemoveAt(i); + } + } + } + }).Run(); + } + + rmvMeshes.Dispose(); + addMeshes.Dispose(); + + k_ChangesMarker.End(); + } + + private void LayoutOutputBuffer() + { + k_CollectActiveMeshes.Begin(); + + var meshes = m_MeshIDs; + var count = new NativeArray(meshes.Length, Allocator.TempJob); + + Dependency = Entities + .WithName("CountActiveMeshes") + .WithAll().WithAll() + .WithReadOnly(meshes) + .WithNativeDisableParallelForRestriction(count) + .ForEach((in MaterialMeshInfo id) => + { + if (id.MeshID == BatchMeshID.Null) + return; + + var meshID = id.MeshID; + var index = meshes.IndexOf(meshID); + + // For now assume every mesh is visible & active. + + unsafe + { + Interlocked.Increment(ref UnsafeUtility.ArrayElementAsRef(count.GetUnsafePtr(), index)); + } + }).ScheduleParallel(Dependency); + + k_CollectActiveMeshes.End(); + k_OutputCountBuffer.Begin(); + + var vertexCountRef = m_MeshVertexCount; + var shapeWeightCountRef = m_BlendShapeWeightCount; + var skinMatrixCountRef = m_SkinMatrixCount; + var sharedMeshes = m_SharedMeshData; + var batches = DeformationBatches; + batches.Clear(); + + Dependency = Job + .WithName("ConstructDeformationBatches") + .WithReadOnly(meshes).WithReadOnly(sharedMeshes) + .WithCode(() => + { + int vertexCount, shapeWeightCount, skinMatrixCount; + vertexCount = shapeWeightCount = skinMatrixCount = k_DeformBufferStartIndex; + + for (int i = 0; i < meshes.Length; i++) + { + var instanceCount = count[i]; + + // Skip this mesh if we do not have any active instances. + if (instanceCount == 0) + continue; + + var id = meshes[i]; + var batch = new MeshDeformationBatch + { + BatchIndex = batches.Count(), + MeshVertexIndex = vertexCount, + BlendShapeIndex = shapeWeightCount, + SkinMatrixIndex = skinMatrixCount, + InstanceCount = instanceCount, + }; + batches.Add(id, batch); + + var meshData = sharedMeshes[meshes.IndexOf(id)]; + + vertexCount += instanceCount * meshData.VertexCount; + + if (meshData.HasBlendShapes) + shapeWeightCount += instanceCount * meshData.BlendShapeCount; + + if (meshData.HasSkinning) + skinMatrixCount += instanceCount * meshData.BoneCount; + } + + // Assign the total counts + vertexCountRef.Value = vertexCount; + shapeWeightCountRef.Value = shapeWeightCount; + skinMatrixCountRef.Value = skinMatrixCount; + }).Schedule(Dependency); + + m_BatchConstructionHandle = Dependency; + + Dependency = new LayoutDeformedMeshJob + { + DeformedMeshIndexHandle = GetComponentTypeHandle(), + BlendWeightBufferIndexHandle = GetComponentTypeHandle(), + SkinMatrixBufferIndexHandle = GetComponentTypeHandle(), + MaterialMeshInfoHandle = GetComponentTypeHandle(), + BatchData = DeformationBatches, + SharedMeshData = m_SharedMeshData.AsArray(), + MeshIDs = m_MeshIDs.AsArray(), + MeshCounts = count, +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + DeformedMeshBufferIndex = m_MeshBufferManager.ActiveDeformedMeshBufferIndex, +#endif + }.ScheduleParallel(m_LayoutDeformedMeshesQuery, Dependency); + + Dependency = count.Dispose(Dependency); + + k_OutputCountBuffer.End(); + } + + private void FitOutputBuffer() + { + k_OutputResizeBuffer.Begin(); + SkinningBufferManager.ResizePassBufferIfRequired(m_SkinMatrixCount.Value); + BlendShapeBufferManager.ResizePassBufferIfRequired(m_BlendShapeWeightCount.Value); + m_MeshBufferManager.ResizeAndPushDeformMeshBuffersIfRequired(m_MeshVertexCount.Value); + k_OutputResizeBuffer.End(); + } + + [BurstCompile] + struct LayoutDeformedMeshJob : IJobChunk + { + public ComponentTypeHandle DeformedMeshIndexHandle; + public ComponentTypeHandle BlendWeightBufferIndexHandle; + public ComponentTypeHandle SkinMatrixBufferIndexHandle; + + [ReadOnly] public ComponentTypeHandle MaterialMeshInfoHandle; + + [NativeDisableContainerSafetyRestriction] public NativeArray MeshCounts; + + [ReadOnly] public NativeParallelHashMap BatchData; + [ReadOnly] public NativeArray SharedMeshData; + [ReadOnly] public NativeArray MeshIDs; +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + [ReadOnly] public int DeformedMeshBufferIndex; +#endif + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in Unity.Burst.Intrinsics.v128 chunkEnabledMask) + { + // This job was converted from IJobChunk; it is not written to support enabled bits. + Assert.IsFalse(useEnabledMask); + + var meshIndices = chunk.GetNativeArray(DeformedMeshIndexHandle); + var blendWeightIndices = chunk.GetNativeArray(BlendWeightBufferIndexHandle); + var skinMatrixIndices = chunk.GetNativeArray(SkinMatrixBufferIndexHandle); + var meshInfos = chunk.GetNativeArray(MaterialMeshInfoHandle); + + for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) + { + var meshID = meshInfos[i].MeshID; + + Assert.IsFalse(meshID == BatchMeshID.Null); + + var batchRange = BatchData[meshID]; + var meshIndex = MeshIDs.IndexOf(meshID); + var meshData = SharedMeshData[meshIndex]; + + unsafe + { + var instanceIndex = Interlocked.Decrement(ref UnsafeUtility.ArrayElementAsRef(MeshCounts.GetUnsafePtr(), meshIndex)); + + uint deformedMeshIndex = (uint)(batchRange.MeshVertexIndex + instanceIndex * meshData.VertexCount); + +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + ref var component = ref UnsafeUtility.ArrayElementAsRef(meshIndices.GetUnsafePtr(), i); + // Set index into deformed mesh buffer for the current frame + component.Value[DeformedMeshBufferIndex] = deformedMeshIndex; + // Set current frame buffer index (0 or 1) + component.Value[2] = (uint)DeformedMeshBufferIndex; +#else + meshIndices[i] = new DeformedMeshIndex { Value = deformedMeshIndex }; +#endif + + if (meshData.HasBlendShapes) + { + Assert.IsTrue(chunk.Has(BlendWeightBufferIndexHandle)); + blendWeightIndices[i] = new BlendWeightBufferIndex { Value = batchRange.BlendShapeIndex + instanceIndex * meshData.BlendShapeCount }; + } + + if (meshData.HasSkinning) + { + Assert.IsTrue(chunk.Has(SkinMatrixBufferIndexHandle)); + skinMatrixIndices[i] = new SkinMatrixBufferIndex { Value = batchRange.SkinMatrixIndex + instanceIndex * meshData.BoneCount }; + } + } + } + } + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs.meta new file mode 100644 index 0000000..879e0fb --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushMeshDataSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0e6a82e70f2f944fb4a0328ef680644 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs new file mode 100644 index 0000000..e2c6491 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs @@ -0,0 +1,93 @@ +using Unity.Assertions; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Deformations; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Profiling; + +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + partial class PushSkinMatrixSystem : SystemBase + { + static readonly ProfilerMarker k_Marker = new ProfilerMarker("PrepareSkinMatrixForGPU"); + + EntityQuery m_SkinningEntityQuery; + + PushMeshDataSystem m_PushMeshDataSystem; + + protected override void OnCreate() + { + if (!UnityEngine.SystemInfo.supportsComputeShaders) + { + Enabled = false; + return; + } + + m_PushMeshDataSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_PushMeshDataSystem, $"{typeof(PushMeshDataSystem)} system was not found in the world!"); + + m_SkinningEntityQuery = GetEntityQuery( + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + } + + protected override void OnUpdate() + { + if (m_PushMeshDataSystem.SkinMatrixCount == 0) + return; + + k_Marker.Begin(); + + var deformedEntityToComputeIndex = new NativeMultiHashMap(m_SkinningEntityQuery.CalculateEntityCount(), Allocator.TempJob); + var deformedEntityToComputeIndexParallel = deformedEntityToComputeIndex.AsParallelWriter(); + + Dependency = Entities + .WithName("ConstructHashMap") + .WithAll() + .ForEach((in SkinMatrixBufferIndex index, in DeformedEntity deformedEntity) => + { + // Skip if we have an invalid index. + if (index.Value == SkinMatrixBufferIndex.Null) + return; + + deformedEntityToComputeIndexParallel.Add(deformedEntity.Value, index.Value); + }).ScheduleParallel(Dependency); + + var skinMatricesBuffer = m_PushMeshDataSystem.SkinningBufferManager.LockSkinMatrixBufferForWrite(m_PushMeshDataSystem.SkinMatrixCount); + Dependency = Entities + .WithName("CopySkinMatricesToGPU") + .WithNativeDisableContainerSafetyRestriction(skinMatricesBuffer) + .WithReadOnly(deformedEntityToComputeIndex) + .ForEach((ref DynamicBuffer skinMatrices, in Entity entity) => + { + // Not all deformed entities in the world will have a renderer attached to them. + if (!deformedEntityToComputeIndex.ContainsKey(entity)) + return; + + long length = (long)skinMatrices.Length * UnsafeUtility.SizeOf(); + var indices = deformedEntityToComputeIndex.GetValuesForKey(entity); + + foreach (var index in indices) + { + unsafe + { + UnsafeUtility.MemCpy( + (float3x4*)skinMatricesBuffer.GetUnsafePtr() + index, + skinMatrices.GetUnsafePtr(), + length + ); + } + } + }).ScheduleParallel(Dependency); + + Dependency = deformedEntityToComputeIndex.Dispose(Dependency); + + k_Marker.End(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs.meta new file mode 100644 index 0000000..f101d6d --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/PushSkinMatrixSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9e8e325d8e8d44438ec432ed612e91b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs b/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs new file mode 100644 index 0000000..3a53dcf --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs @@ -0,0 +1,121 @@ +using Unity.Assertions; +using Unity.Deformations; +using Unity.Entities; +using Unity.Profiling; +using UnityEngine; + +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + partial class SkinningDeformationSystem : SystemBase + { + static readonly ProfilerMarker k_FinalizePushSkinMatrix = new ProfilerMarker("FinalizeSkinMatrixForGPU"); + static readonly ProfilerMarker k_SkinningDeformationMarker = new ProfilerMarker("SkinningDeformationDispatch"); + + static readonly int k_VertexCount = Shader.PropertyToID("g_VertexCount"); + static readonly int k_DeformedMeshStartIndex = Shader.PropertyToID("g_DeformedMeshStartIndex"); + static readonly int k_InstancesCount = Shader.PropertyToID("g_InstanceCount"); + static readonly int k_SharedMeshBoneCount = Shader.PropertyToID("g_SharedMeshBoneCount"); + static readonly int k_SkinMatricesStartIndex = Shader.PropertyToID("g_SkinMatricesStartIndex"); + static readonly int k_SharedMeshBoneWeightsBuffer = Shader.PropertyToID("_SharedMeshBoneWeights"); + + ComputeShader m_ComputeShader; + PushMeshDataSystem m_PushMeshDataSystem; + EntitiesGraphicsSystem m_RendererSystem; + + int m_KernelDense1; + int m_KernelDense2; + int m_KernelDense4; + int m_KernelSparse; + + EntityQuery m_SkinMatrixQuery; + + protected override void OnCreate() + { +#if !HYBRID_RENDERER_DISABLED + if (!EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem()) +#endif + { + Enabled = false; + UnityEngine.Debug.Log("No SRP present, no compute shader support, or running with -nographics. Mesh Deformation Systems disabled."); + return; + } + + m_PushMeshDataSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_PushMeshDataSystem, $"{nameof(PushMeshDataSystem)} was not found!"); + + m_ComputeShader = Resources.Load("SkinningComputeShader"); + Assert.IsNotNull(m_ComputeShader, $"Compute shader for {typeof(SkinningDeformationSystem)} was not found!"); + + m_RendererSystem = World.GetOrCreateSystemManaged(); + Assert.IsNotNull(m_RendererSystem, $"{nameof(EntitiesGraphicsSystem)} was not found!"); + + m_KernelDense1 = m_ComputeShader.FindKernel("SkinningDense1ComputeKernel"); + m_KernelDense2 = m_ComputeShader.FindKernel("SkinningDense2ComputeKernel"); + m_KernelDense4 = m_ComputeShader.FindKernel("SkinningDense4ComputeKernel"); + m_KernelSparse = m_ComputeShader.FindKernel("SkinningSparseComputeKernel"); + + m_SkinMatrixQuery = GetEntityQuery( + ComponentType.ReadWrite() + ); + } + + protected override void OnUpdate() + { + if (m_PushMeshDataSystem.SkinMatrixCount == 0) + return; + + k_FinalizePushSkinMatrix.Begin(); + + // Complete the Read/Write dependency on SkinMatrix. + // This guarantees that the data has been written to GPU + // Assuming that PushSkinMatrixSystem has executed before this system. + m_SkinMatrixQuery.CompleteDependency(); + m_PushMeshDataSystem.SkinningBufferManager.UnlockSkinMatrixBufferForWrite(m_PushMeshDataSystem.SkinMatrixCount); + + k_FinalizePushSkinMatrix.End(); + k_SkinningDeformationMarker.Begin(); + + foreach (var deformationBatch in m_PushMeshDataSystem.DeformationBatches) + { + var id = deformationBatch.Key; + var batchData = deformationBatch.Value; + + var hasMeshData = m_PushMeshDataSystem.TryGetSharedMeshData(id, out var meshData); + + Assert.IsTrue(hasMeshData); + + if (!meshData.HasSkinning) + continue; + + m_ComputeShader.SetInt(k_VertexCount, meshData.VertexCount); + m_ComputeShader.SetInt(k_SharedMeshBoneCount, meshData.BoneCount); + m_ComputeShader.SetInt(k_DeformedMeshStartIndex, batchData.MeshVertexIndex); + m_ComputeShader.SetInt(k_SkinMatricesStartIndex, batchData.SkinMatrixIndex); + m_ComputeShader.SetInt(k_InstancesCount, batchData.InstanceCount); + + var mesh = m_RendererSystem.GetMesh(meshData.MeshID); + var skinWeightLayout = mesh.skinWeightBufferLayout; + Assert.IsFalse(skinWeightLayout == SkinWeights.None); + var skinWeightBuffer = mesh.GetBoneWeightBuffer(skinWeightLayout); + Assert.IsNotNull(skinWeightBuffer); + + var kernel = skinWeightLayout switch + { + SkinWeights.OneBone => m_KernelDense1, + SkinWeights.TwoBones => m_KernelDense2, + SkinWeights.FourBones => m_KernelDense4, + SkinWeights.Unlimited => m_KernelSparse, + _ => m_KernelDense1, + }; + + m_ComputeShader.SetBuffer(kernel, k_SharedMeshBoneWeightsBuffer, skinWeightBuffer); + m_ComputeShader.Dispatch(kernel, 1024, 1, 1); + + skinWeightBuffer.Dispose(); + } + + k_SkinningDeformationMarker.End(); + } + } +} diff --git a/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs.meta b/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs.meta new file mode 100644 index 0000000..e73ebd4 --- /dev/null +++ b/Unity.Entities.Graphics/Deformations/Systems/SkinningDeformationSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bac97609ee198b447812868eb640f5a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/DisableRenderingComponent.cs b/Unity.Entities.Graphics/DisableRenderingComponent.cs new file mode 100644 index 0000000..5f0a9a9 --- /dev/null +++ b/Unity.Entities.Graphics/DisableRenderingComponent.cs @@ -0,0 +1,10 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// A tag component that disables the rendering of an entity. + /// + public struct DisableRendering : IComponentData + {} +} diff --git a/Unity.Entities.Graphics/DisableRenderingComponent.cs.meta b/Unity.Entities.Graphics/DisableRenderingComponent.cs.meta new file mode 100644 index 0000000..4499421 --- /dev/null +++ b/Unity.Entities.Graphics/DisableRenderingComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4fe0afa641eb48141a9079002a7f0add +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/DrawCommandGeneration.cs b/Unity.Entities.Graphics/DrawCommandGeneration.cs new file mode 100644 index 0000000..8065ed0 --- /dev/null +++ b/Unity.Entities.Graphics/DrawCommandGeneration.cs @@ -0,0 +1,1951 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Jobs.LowLevel.Unsafe; +using Unity.Mathematics; +using Unity.Profiling; +using Unity.Entities.Graphics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; +#if UNITY_EDITOR +using UnityEditor.SceneManagement; +#endif + +namespace Unity.Rendering +{ + // Chunk-agnostic parts of draw + internal unsafe struct DrawCommandSettings : IEquatable + { + // TODO: This could be thinned to fit in 128 bits? + + public int FilterIndex; + public BatchDrawCommandFlags Flags; + public BatchMaterialID MaterialID; + public BatchMeshID MeshID; + public ushort SplitMask; + public ushort SubmeshIndex; + public BatchID BatchID; + private int m_CachedHash; + + public bool Equals(DrawCommandSettings other) + { + // Use temp variables so CPU can co-issue all comparisons + bool eq_batch = BatchID == other.BatchID; + bool eq_rest = math.all(PackedUint4 == other.PackedUint4); + + return eq_batch && eq_rest; + } + + private uint4 PackedUint4 + { + get + { + Debug.Assert(MeshID.value < (1 << 24)); + Debug.Assert(SubmeshIndex < (1 << 8)); + Debug.Assert((uint)Flags < (1 << 24)); + Debug.Assert(SplitMask < (1 << 8)); + + return new uint4( + (uint)FilterIndex, + (((uint)SplitMask & 0xff) << 24) | ((uint)Flags & 0x00ffffffff), + MaterialID.value, + ((MeshID.value & 0x00ffffff) << 8) | ((uint)SubmeshIndex & 0xff) + ); + } + } + + public int CompareTo(DrawCommandSettings other) + { + uint4 a = PackedUint4; + uint4 b = other.PackedUint4; + int cmp_batchID = BatchID.CompareTo(other.BatchID); + + int4 lt = math.select(int4.zero, new int4(-1), a < b); + int4 gt = math.select(int4.zero, new int4( 1), a > b); + int4 neq = lt | gt; + + int* firstNonZero = stackalloc int[4]; + + bool4 nz = neq != int4.zero; + bool anyNz = math.any(nz); + math.compress(firstNonZero, 0, neq, nz); + + return anyNz ? firstNonZero[0] : cmp_batchID; + } + + // Used to verify correctness of fast CompareTo + public int CompareToReference(DrawCommandSettings other) + { + int cmpFilterIndex = FilterIndex.CompareTo(other.FilterIndex); + int cmpFlags = ((int)Flags).CompareTo((int)other.Flags); + int cmpMaterialID = MaterialID.CompareTo(other.MaterialID); + int cmpMeshID = MeshID.CompareTo(other.MeshID); + int cmpSplitMask = SplitMask.CompareTo(other.SubmeshIndex); + int cmpSubmeshIndex = SubmeshIndex.CompareTo(other.SubmeshIndex); + int cmpBatchID = BatchID.CompareTo(other.BatchID); + + if (cmpFilterIndex != 0) return cmpFilterIndex; + if (cmpFlags != 0) return cmpFlags; + if (cmpMaterialID != 0) return cmpMaterialID; + if (cmpMeshID != 0) return cmpMeshID; + if (cmpSubmeshIndex != 0) return cmpSubmeshIndex; + if (cmpSplitMask != 0) return cmpSplitMask; + + return cmpBatchID; + } + + public override int GetHashCode() => m_CachedHash; + + public void ComputeHashCode() + { + m_CachedHash = ChunkDrawCommandOutput.FastHash(this); + } + + public bool HasSortingPosition => (int) (Flags & BatchDrawCommandFlags.HasSortingPosition) != 0; + + public override string ToString() + { + return $"DrawCommandSettings(batchID: {BatchID.value}, materialID: {MaterialID.value}, meshID: {MeshID.value}, submesh: {SubmeshIndex}, filter: {FilterIndex}, flags: {Flags:x}, splitMask: {SplitMask:x})"; + } + } + + internal unsafe struct ThreadLocalAllocator + { + public const int kInitialSize = 1024 * 1024; + public const Allocator kAllocator = Allocator.Persistent; + public const int NumThreads = ChunkDrawCommandOutput.NumThreads; + + [StructLayout(LayoutKind.Explicit, Size = JobsUtility.CacheLineSize)] + public unsafe struct PaddedAllocator + { + [FieldOffset(0)] + public AllocatorHelper Allocator; + [FieldOffset(16)] + public bool UsedSinceRewind; + + public void Initialize(int initialSize) + { + Allocator = new AllocatorHelper(AllocatorManager.Persistent); + Allocator.Allocator.Initialize(initialSize); + } + } + + public UnsafeList Allocators; + + public ThreadLocalAllocator(int expectedUsedCount = -1, int initialSize = kInitialSize) + { + // Note, the comparison is <= as on 32-bit builds this size will be smaller, which is fine. + Debug.Assert(sizeof(AllocatorHelper) <= 16, $"PaddedAllocator's Allocator size has changed. The type layout needs adjusting."); + Debug.Assert(sizeof(PaddedAllocator) >= JobsUtility.CacheLineSize, + $"Thread local allocators should be on different cache lines. Size: {sizeof(PaddedAllocator)}, Cache Line: {JobsUtility.CacheLineSize}"); + + if (expectedUsedCount < 0) + expectedUsedCount = math.max(0, JobsUtility.JobWorkerCount + 1); + + Allocators = new UnsafeList( + NumThreads, + kAllocator, + NativeArrayOptions.ClearMemory); + Allocators.Resize(NumThreads); + + for (int i = 0; i < NumThreads; ++i) + { + if (i < expectedUsedCount) + Allocators.ElementAt(i).Initialize(initialSize); + else + Allocators.ElementAt(i).Initialize(1); + } + } + + public void Rewind() + { + Profiler.BeginSample("RewindAllocators"); + for (int i = 0; i < NumThreads; ++i) + { + ref var allocator = ref Allocators.ElementAt(i); + if (allocator.UsedSinceRewind) + { + Profiler.BeginSample("Rewind"); + Allocators.ElementAt(i).Allocator.Allocator.Rewind(); + Profiler.EndSample(); + } + allocator.UsedSinceRewind = false; + } + Profiler.EndSample(); + + } + + public void Dispose() + { + for (int i = 0; i < NumThreads; ++i) + { + Allocators.ElementAt(i).Allocator.Allocator.Dispose(); + Allocators.ElementAt(i).Allocator.Dispose(); + } + + Allocators.Dispose(); + } + + public RewindableAllocator* ThreadAllocator(int threadIndex) + { + ref var allocator = ref Allocators.ElementAt(threadIndex); + allocator.UsedSinceRewind = true; + return (RewindableAllocator*)UnsafeUtility.AddressOf(ref allocator.Allocator.Allocator); + } + + public RewindableAllocator* GeneralAllocator => ThreadAllocator(Allocators.Length - 1); + } + + internal struct DepthSortedDrawCommand + { + public DrawCommandSettings Settings; + public int InstanceIndex; + public float3 SortingWorldPosition; + } + + internal struct DrawCommandBin + { + public const int MaxInstancesPerCommand = EntitiesGraphicsTuningConstants.kMaxInstancesPerDrawCommand; + public const int kNoSortingPosition = -1; + + public int NumInstances; + public int InstanceOffset; + public int DrawCommandOffset; + public int PositionOffset; + + // Use a -1 value to signal "no sorting position" here. That way, + // when the offset is rewritten from a placeholder to a real offset, + // the semantics are still correct, because -1 is never a valid offset. + public bool HasSortingPosition => PositionOffset != kNoSortingPosition; + + public int NumDrawCommands => HasSortingPosition ? NumDrawCommandsHasPositions : NumDrawCommandsNoPositions; + public int NumDrawCommandsHasPositions => NumInstances; + // Round up to always have enough commands + public int NumDrawCommandsNoPositions => + (MaxInstancesPerCommand - 1 + NumInstances) / + MaxInstancesPerCommand; + } + + internal unsafe struct DrawCommandWorkItem + { + public DrawStream.Header* Arrays; + public DrawStream.Header* TransformArrays; + public int BinIndex; + public int PrefixSumNumInstances; + } + + internal unsafe struct DrawCommandVisibility + { + public int ChunkStartIndex; + public fixed ulong VisibleInstances[2]; + + public DrawCommandVisibility(int startIndex) + { + ChunkStartIndex = startIndex; + VisibleInstances[0] = 0; + VisibleInstances[1] = 0; + } + + public int VisibleInstanceCount => math.countbits(VisibleInstances[0]) + math.countbits(VisibleInstances[1]); + + public override string ToString() + { + return $"Visibility({ChunkStartIndex}, {VisibleInstances[1]:x16}, {VisibleInstances[0]:x16})"; + } + } + + internal struct ChunkDrawCommand : IComparable + { + public DrawCommandSettings Settings; + public DrawCommandVisibility Visibility; + + public int CompareTo(ChunkDrawCommand other) => Settings.CompareTo(other.Settings); + } + + [BurstCompile] + [NoAlias] + internal unsafe struct DrawStream where T : unmanaged + { + public const int kArraySizeElements = 16; + public static int ElementsPerHeader => (sizeof(Header) + sizeof(T) - 1) / sizeof(T); + public const int ElementsPerArray = kArraySizeElements; + + public Header* Head; + private T* m_Begin; + private int m_Count; + private int m_TotalInstances; + + public DrawStream(RewindableAllocator* allocator) + { + Head = null; + m_Begin = null; + m_Count = 0; + m_TotalInstances = 0; + + Init(allocator); + } + + public void Init(RewindableAllocator* allocator) + { + AllocateNewBuffer(allocator); + } + + public bool IsCreated => Head != null; + + // No need to dispose anything with RewindableAllocator + // public void Dispose() + // { + // Header* h = Head; + // + // while (h != null) + // { + // Header* next = h->Next; + // DisposeArray(h, kAllocator); + // h = next; + // } + // } + + private void AllocateNewBuffer(RewindableAllocator* allocator) + { + LinkHead(AllocateArray(allocator)); + m_Begin = Head->Element(0); + m_Count = 0; + Debug.Assert(Head->NumElements == 0); + Debug.Assert(Head->NumInstances == 0); + } + + public void LinkHead(Header* newHead) + { + newHead->Next = Head; + Head = newHead; + } + + [BurstCompile] + [NoAlias] + internal unsafe struct Header + { + // Next array in the chain of arrays + public Header* Next; + // Number of structs in this array + public int NumElements; + // Number of instances in this array + public int NumInstances; + + public T* Element(int i) + { + fixed (Header* self = &this) + return (T*)self + i + ElementsPerHeader; + } + } + + public int TotalInstanceCount => m_TotalInstances; + + public static Header* AllocateArray(RewindableAllocator* allocator) + { + int alignment = math.max( + UnsafeUtility.AlignOf
(), + UnsafeUtility.AlignOf()); + + // Make sure we always have space for ElementsPerArray elements, + // so several streams can be kept in lockstep + int allocCount = ElementsPerHeader + ElementsPerArray; + + Header* buffer = (Header*) allocator->Allocate(sizeof(T), alignment, allocCount); + + // Zero clear the header area (first struct) + UnsafeUtility.MemSet(buffer, 0, sizeof(Header)); + + // Buffer allocation pointer, to be used for Free() + return buffer; + } + + // Assume that the given header is part of an array allocated with AllocateArray, + // and release the array. + // public static void DisposeArray(Header* header, Allocator allocator) + // { + // UnsafeUtility.Free(header, allocator); + // } + + [return: NoAlias] + public T* AppendElement(RewindableAllocator* allocator) + { + if (m_Count >= ElementsPerArray) + AllocateNewBuffer(allocator); + + T* elem = m_Begin + m_Count; + ++m_Count; + Head->NumElements += 1; + return elem; + } + + public void AddInstances(int numInstances) + { + Head->NumInstances += numInstances; + m_TotalInstances += numInstances; + } + } + + [BurstCompile] + [NoAlias] + internal unsafe struct DrawCommandStream + { + private DrawStream m_Stream; + private DrawStream m_ChunkTransformsStream; + private int m_PrevChunkStartIndex; + [NoAlias] + private DrawCommandVisibility* m_PrevVisibility; + + public DrawCommandStream(RewindableAllocator* allocator) + { + m_Stream = new DrawStream(allocator); + m_ChunkTransformsStream = default; // Don't allocate here, only on demand + m_PrevChunkStartIndex = -1; + m_PrevVisibility = null; + } + + public void Dispose() + { + // m_Stream.Dispose(); + } + + public void Emit(RewindableAllocator* allocator, int qwordIndex, int bitIndex, int chunkStartIndex) + { + DrawCommandVisibility* visibility; + + if (chunkStartIndex == m_PrevChunkStartIndex) + { + visibility = m_PrevVisibility; + } + else + { + visibility = m_Stream.AppendElement(allocator); + *visibility = new DrawCommandVisibility(chunkStartIndex); + } + + visibility->VisibleInstances[qwordIndex] |= 1ul << bitIndex; + + m_PrevChunkStartIndex = chunkStartIndex; + m_PrevVisibility = visibility; + m_Stream.AddInstances(1); + } + + public void EmitDepthSorted(RewindableAllocator* allocator, + int qwordIndex, int bitIndex, int chunkStartIndex, + float4x4* chunkTransforms) + { + DrawCommandVisibility* visibility; + + if (chunkStartIndex == m_PrevChunkStartIndex) + { + visibility = m_PrevVisibility; + + // Transforms have already been written when the element was added + } + else + { + visibility = m_Stream.AppendElement(allocator); + *visibility = new DrawCommandVisibility(chunkStartIndex); + + // Store a pointer to the chunk transform array, which + // instance expansion can use to get the positions. + + if (!m_ChunkTransformsStream.IsCreated) + m_ChunkTransformsStream.Init(allocator); + + var transforms = m_ChunkTransformsStream.AppendElement(allocator); + *transforms = (IntPtr) chunkTransforms; + } + + visibility->VisibleInstances[qwordIndex] |= 1ul << bitIndex; + + m_PrevChunkStartIndex = chunkStartIndex; + m_PrevVisibility = visibility; + m_Stream.AddInstances(1); + } + + public DrawStream Stream => m_Stream; + public DrawStream TransformsStream => m_ChunkTransformsStream; + } + + [BurstCompile] + internal unsafe struct ThreadLocalDrawCommands + { + public const Allocator kAllocator = Allocator.TempJob; + + // Store the actual streams in a separate array so we can mutate them in place, + // the hash map only supports a get/set API. + public UnsafeParallelHashMap DrawCommandStreamIndices; + public UnsafeList DrawCommands; + public ThreadLocalAllocator ThreadLocalAllocator; + + private fixed int m_CacheLinePadding[8]; // The padding here assumes some internal sizes + + public ThreadLocalDrawCommands(int capacity, ThreadLocalAllocator tlAllocator) + { + // Make sure we don't get false sharing by placing the thread locals on different cache lines. + Debug.Assert(sizeof(ThreadLocalDrawCommands) >= JobsUtility.CacheLineSize); + DrawCommandStreamIndices = new UnsafeParallelHashMap(capacity, kAllocator); + DrawCommands = new UnsafeList(capacity, kAllocator); + ThreadLocalAllocator = tlAllocator; + } + + public bool IsCreated => DrawCommandStreamIndices.IsCreated; + + public void Dispose() + { + if (!IsCreated) + return; + + for (int i = 0; i < DrawCommands.Length; ++i) + DrawCommands[i].Dispose(); + + if (DrawCommandStreamIndices.IsCreated) + DrawCommandStreamIndices.Dispose(); + if (DrawCommands.IsCreated) + DrawCommands.Dispose(); + } + + public bool Emit(DrawCommandSettings settings, int qwordIndex, int bitIndex, int chunkStartIndex, int threadIndex) + { + var allocator = ThreadLocalAllocator.ThreadAllocator(threadIndex); + + if (DrawCommandStreamIndices.TryGetValue(settings, out int streamIndex)) + { + DrawCommandStream* stream = DrawCommands.Ptr + streamIndex; + stream->Emit(allocator, qwordIndex, bitIndex, chunkStartIndex); + return false; + } + else + { + + streamIndex = DrawCommands.Length; + DrawCommands.Add(new DrawCommandStream(allocator)); + DrawCommandStreamIndices.Add(settings, streamIndex); + + DrawCommandStream* stream = DrawCommands.Ptr + streamIndex; + stream->Emit(allocator, qwordIndex, bitIndex, chunkStartIndex); + + return true; + } + } + + public bool EmitDepthSorted( + DrawCommandSettings settings, int qwordIndex, int bitIndex, int chunkStartIndex, + float4x4* chunkTransforms, + int threadIndex) + { + var allocator = ThreadLocalAllocator.ThreadAllocator(threadIndex); + + if (DrawCommandStreamIndices.TryGetValue(settings, out int streamIndex)) + { + DrawCommandStream* stream = DrawCommands.Ptr + streamIndex; + stream->EmitDepthSorted(allocator, qwordIndex, bitIndex, chunkStartIndex, chunkTransforms); + return false; + } + else + { + + streamIndex = DrawCommands.Length; + DrawCommands.Add(new DrawCommandStream(allocator)); + DrawCommandStreamIndices.Add(settings, streamIndex); + + DrawCommandStream* stream = DrawCommands.Ptr + streamIndex; + stream->EmitDepthSorted(allocator, qwordIndex, bitIndex, chunkStartIndex, chunkTransforms); + + return true; + } + } + } + + [BurstCompile] + internal unsafe struct ThreadLocalCollectBuffer + { + public const Allocator kAllocator = Allocator.TempJob; + public const int kCollectBufferSize = ChunkDrawCommandOutput.NumThreads; + + public UnsafeList WorkItems; + private fixed int m_CacheLinePadding[12]; // The padding here assumes some internal sizes + + public void EnsureCapacity(UnsafeList.ParallelWriter dst, int count) + { + Debug.Assert(sizeof(ThreadLocalCollectBuffer) >= JobsUtility.CacheLineSize); + Debug.Assert(count <= kCollectBufferSize); + + if (!WorkItems.IsCreated) + WorkItems = new UnsafeList( + kCollectBufferSize, + kAllocator, + NativeArrayOptions.UninitializedMemory); + + if (WorkItems.Length + count > WorkItems.Capacity) + Flush(dst); + } + + public void Flush(UnsafeList.ParallelWriter dst) + { + dst.AddRangeNoResize(WorkItems.Ptr, WorkItems.Length); + WorkItems.Clear(); + } + + public void Add(DrawCommandWorkItem workItem) => WorkItems.Add(workItem); + + public void Dispose() + { + if (WorkItems.IsCreated) + WorkItems.Dispose(); + } + } + + [BurstCompile] + internal unsafe struct DrawBinCollector + { + public const Allocator kAllocator = Allocator.TempJob; + public const int NumThreads = ChunkDrawCommandOutput.NumThreads; + + public IndirectList Bins; + private UnsafeParallelHashSet m_BinSet; + private UnsafeList m_ThreadLocalDrawCommands; + + public DrawBinCollector(UnsafeList tlDrawCommands, RewindableAllocator* allocator) + { + Bins = new IndirectList(0, allocator); + m_BinSet = new UnsafeParallelHashSet(0, kAllocator); + m_ThreadLocalDrawCommands = tlDrawCommands; + } + + public bool Add(DrawCommandSettings settings) + { + return true; + } + + [BurstCompile] + internal struct AllocateBinsJob : IJob + { + public IndirectList Bins; + public UnsafeParallelHashSet BinSet; + public UnsafeList ThreadLocalDrawCommands; + + public void Execute() + { + int numBinsUpperBound = 0; + + for (int i = 0; i < NumThreads; ++i) + numBinsUpperBound += ThreadLocalDrawCommands.ElementAt(i).DrawCommands.Length; + + Bins.SetCapacity(numBinsUpperBound); + BinSet.Capacity = numBinsUpperBound; + } + } + + [BurstCompile] + internal struct CollectBinsJob : IJobParallelFor + { + public const int ThreadLocalArraySize = 256; + + public IndirectList Bins; + public UnsafeParallelHashSet.ParallelWriter BinSet; + public UnsafeList ThreadLocalDrawCommands; + + private UnsafeList.ParallelWriter m_BinsParallel; + + public void Execute(int index) + { + ref var drawCommands = ref ThreadLocalDrawCommands.ElementAt(index); + if (!drawCommands.IsCreated) + return; + + m_BinsParallel = Bins.List->AsParallelWriter(); + + var uniqueSettings = new NativeArray( + ThreadLocalArraySize, + Allocator.Temp, + NativeArrayOptions.UninitializedMemory); + int numSettings = 0; + + var keys = drawCommands.DrawCommandStreamIndices.GetEnumerator(); + while (keys.MoveNext()) + { + var settings = keys.Current.Key; + if (BinSet.Add(settings)) + AddBin(uniqueSettings, ref numSettings, settings); + } + keys.Dispose(); + + Flush(uniqueSettings, numSettings); + } + + private void AddBin( + NativeArray uniqueSettings, + ref int numSettings, + DrawCommandSettings settings) + { + if (numSettings >= ThreadLocalArraySize) + { + Flush(uniqueSettings, numSettings); + numSettings = 0; + } + + uniqueSettings[numSettings] = settings; + ++numSettings; + } + + private void Flush( + NativeArray uniqueSettings, + int numSettings) + { + if (numSettings <= 0) + return; + + m_BinsParallel.AddRangeNoResize( + uniqueSettings.GetUnsafeReadOnlyPtr(), + numSettings); + } + } + + public JobHandle ScheduleFinalize(JobHandle dependency) + { + var allocateDependency = new AllocateBinsJob + { + Bins = Bins, + BinSet = m_BinSet, + ThreadLocalDrawCommands = m_ThreadLocalDrawCommands, + }.Schedule(dependency); + + return new CollectBinsJob + { + Bins = Bins, + BinSet = m_BinSet.AsParallelWriter(), + ThreadLocalDrawCommands = m_ThreadLocalDrawCommands, + }.Schedule(NumThreads, 1, allocateDependency); + } + + public JobHandle Dispose(JobHandle dependency) + { + return JobHandle.CombineDependencies( + Bins.Dispose(dependency), + m_BinSet.Dispose(dependency)); + } + } + + internal unsafe struct IndirectList where T : unmanaged + { + [NativeDisableUnsafePtrRestriction] + public UnsafeList* List; + + public IndirectList(int capacity, RewindableAllocator* allocator) + { + List = AllocIndirectList(capacity, allocator); + } + + public int Length => List->Length; + public void Resize(int length, NativeArrayOptions options) => List->Resize(length, options); + public void SetCapacity(int capacity) => List->SetCapacity(capacity); + public ref T ElementAt(int i) => ref List->ElementAt(i); + public void Add(T value) => List->Add(value); + + private static UnsafeList* AllocIndirectList(int capacity, RewindableAllocator* allocator) + { + AllocatorManager.AllocatorHandle allocatorHandle = allocator->Handle; + var indirectList = allocatorHandle.Allocate(default(UnsafeList), 1); + *indirectList = new UnsafeList(capacity, allocatorHandle); + return indirectList; + } + + // No need to dispose anything with RewindableAllocator + + public JobHandle Dispose(JobHandle dependency) + { + return default; + } + + public void Dispose() + { + } + } + + internal static class IndirectListExtensions + { + public static unsafe JobHandle ScheduleWithIndirectList( + this T jobData, + IndirectList list, + int innerLoopBatchCount = 1, + JobHandle dependencies = default) + where T : struct, IJobParallelForDefer + where U : unmanaged + { + return jobData.Schedule(&list.List->m_length, innerLoopBatchCount, dependencies); + } + } + + internal struct SortedBin + { + public int UnsortedIndex; + } + + [BurstCompile] + [NoAlias] + internal unsafe struct ChunkDrawCommandOutput + { + public const Allocator kAllocator = Allocator.TempJob; + + public const int NumThreads = JobsUtility.MaxJobThreadCount; + + public const int kNumReleaseThreads = 4; + public const int kNumThreadsBitfieldLength = (NumThreads + 63) / 64; + public const int kBinPresentFilterSize = 1 << 10; + + public UnsafeList ThreadLocalDrawCommands; + public UnsafeList ThreadLocalCollectBuffers; + + public UnsafeList BinPresentFilter; + + public DrawBinCollector BinCollector; + public IndirectList UnsortedBins => BinCollector.Bins; + + [NativeDisableUnsafePtrRestriction] + public IndirectList SortedBins; + + [NativeDisableUnsafePtrRestriction] + public IndirectList BinIndices; + + [NativeDisableUnsafePtrRestriction] + public IndirectList WorkItems; + + [NativeDisableParallelForRestriction] + [NativeDisableContainerSafetyRestriction] + public NativeArray CullingOutput; + + public int BinCapacity; + + public ThreadLocalAllocator ThreadLocalAllocator; + + public ProfilerMarker ProfilerEmit; + +#pragma warning disable 649 + [NativeSetThreadIndex] public int ThreadIndex; +#pragma warning restore 649 + + public ChunkDrawCommandOutput( + int initialBinCapacity, + ThreadLocalAllocator tlAllocator, + BatchCullingOutput cullingOutput) + { + BinCapacity = initialBinCapacity; + CullingOutput = cullingOutput.drawCommands; + + ThreadLocalAllocator = tlAllocator; + var generalAllocator = ThreadLocalAllocator.GeneralAllocator; + + ThreadLocalDrawCommands = new UnsafeList( + NumThreads, + generalAllocator->Handle, + NativeArrayOptions.ClearMemory); + ThreadLocalDrawCommands.Resize(ThreadLocalDrawCommands.Capacity); + ThreadLocalCollectBuffers = new UnsafeList( + NumThreads, + generalAllocator->Handle, + NativeArrayOptions.ClearMemory); + ThreadLocalCollectBuffers.Resize(ThreadLocalCollectBuffers.Capacity); + BinPresentFilter = new UnsafeList( + kBinPresentFilterSize * kNumThreadsBitfieldLength, + generalAllocator->Handle, + NativeArrayOptions.ClearMemory); + BinPresentFilter.Resize(BinPresentFilter.Capacity); + + BinCollector = new DrawBinCollector(ThreadLocalDrawCommands, generalAllocator); + SortedBins = new IndirectList(0, generalAllocator); + BinIndices = new IndirectList(0, generalAllocator); + WorkItems = new IndirectList(0, generalAllocator); + + + // Initialized by job system + ThreadIndex = 0; + + ProfilerEmit = new ProfilerMarker("Emit"); + } + + public void InitializeForEmitThread() + { + // First to use the thread local initializes is, but don't double init + if (!ThreadLocalDrawCommands[ThreadIndex].IsCreated) + ThreadLocalDrawCommands[ThreadIndex] = new ThreadLocalDrawCommands(BinCapacity, ThreadLocalAllocator); + } + + public BatchCullingOutputDrawCommands* CullingOutputDrawCommands => + (BatchCullingOutputDrawCommands*) CullingOutput.GetUnsafePtr(); + + public static T* Malloc(int count) where T : unmanaged + { + return (T*)UnsafeUtility.Malloc( + UnsafeUtility.SizeOf() * count, + UnsafeUtility.AlignOf(), + kAllocator); + } + + private ThreadLocalDrawCommands* DrawCommands + { + [return: NoAlias] get => ThreadLocalDrawCommands.Ptr + ThreadIndex; + } + + public ThreadLocalCollectBuffer* CollectBuffer + { + [return: NoAlias] get => ThreadLocalCollectBuffers.Ptr + ThreadIndex; + } + + public void Emit(DrawCommandSettings settings, int entityQword, int entityBit, int chunkStartIndex) + { + // Update the cached hash code here, so all processing after this can just use the cached value + // without recomputing the hash each time. + settings.ComputeHashCode(); + + bool newBinAdded = DrawCommands->Emit(settings, entityQword, entityBit, chunkStartIndex, ThreadIndex); + if (newBinAdded) + { + BinCollector.Add(settings); + MarkBinPresentInThread(settings, ThreadIndex); + } + } + + public void EmitDepthSorted( + DrawCommandSettings settings, int entityQword, int entityBit, int chunkStartIndex, + float4x4* chunkTransforms) + { + // Update the cached hash code here, so all processing after this can just use the cached value + // without recomputing the hash each time. + settings.ComputeHashCode(); + + bool newBinAdded = DrawCommands->EmitDepthSorted(settings, entityQword, entityBit, chunkStartIndex, chunkTransforms, ThreadIndex); + if (newBinAdded) + { + BinCollector.Add(settings); + MarkBinPresentInThread(settings, ThreadIndex); + } + } + + [return: NoAlias] + public long* BinPresentFilterForSettings(DrawCommandSettings settings) + { + uint hash = (uint) settings.GetHashCode(); + uint index = hash % (uint)kBinPresentFilterSize; + return BinPresentFilter.Ptr + index * kNumThreadsBitfieldLength; + } + + private void MarkBinPresentInThread(DrawCommandSettings settings, int threadIndex) + { + long* settingsFilter = BinPresentFilterForSettings(settings); + + uint threadQword = (uint) threadIndex / 64; + uint threadBit = (uint) threadIndex % 64; + + AtomicHelpers.AtomicOr( + settingsFilter, + (int)threadQword, + 1L << (int) threadBit); + } + + public static int FastHash(T value) where T : struct + { + // TODO: Replace with hardware CRC32? + return (int)xxHash3.Hash64(UnsafeUtility.AddressOf(ref value), UnsafeUtility.SizeOf()).x; + } + + public JobHandle Dispose(JobHandle dependencies) + { + // First schedule a job to release all the thread local arrays, which requires + // that the data structures are still in place so we can find them. + var releaseChunkDrawCommandsDependency = new ReleaseChunkDrawCommandsJob + { + DrawCommandOutput = this, + NumThreads = kNumReleaseThreads, + }.Schedule(kNumReleaseThreads, 1, dependencies); + + // When those have been released, release the data structures. + var disposeDone = new JobHandle(); + disposeDone = JobHandle.CombineDependencies(disposeDone, + ThreadLocalDrawCommands.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + ThreadLocalCollectBuffers.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + BinPresentFilter.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + BinCollector.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + SortedBins.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + BinIndices.Dispose(releaseChunkDrawCommandsDependency)); + disposeDone = JobHandle.CombineDependencies(disposeDone, + WorkItems.Dispose(releaseChunkDrawCommandsDependency)); + + return disposeDone; + } + + [BurstCompile] + private struct ReleaseChunkDrawCommandsJob : IJobParallelFor + { + public ChunkDrawCommandOutput DrawCommandOutput; + public int NumThreads; + + public void Execute(int index) + { + for (int i = index; i < ChunkDrawCommandOutput.NumThreads; i += NumThreads) + { + DrawCommandOutput.ThreadLocalDrawCommands[i].Dispose(); + DrawCommandOutput.ThreadLocalCollectBuffers[i].Dispose(); + } + } + } + } + + [BurstCompile] + internal unsafe struct EmitDrawCommandsJob : IJobParallelForDefer + { + [ReadOnly] public IndirectList VisibilityItems; + [ReadOnly] public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public ComponentTypeHandle MaterialMeshInfo; + [ReadOnly] public ComponentTypeHandle LocalToWorld; + [ReadOnly] public ComponentTypeHandle DepthSorted; + [ReadOnly] public ComponentTypeHandle DeformedMeshIndex; + [ReadOnly] public SharedComponentTypeHandle RenderFilterSettings; + [ReadOnly] public SharedComponentTypeHandle LightMaps; + [ReadOnly] public NativeParallelHashMap FilterSettings; + + public ChunkDrawCommandOutput DrawCommandOutput; + + public ulong SceneCullingMask; + public float3 CameraPosition; + public uint LastSystemVersion; + public uint CullingLayerMask; + + public ProfilerMarker ProfilerEmitChunk; + +#if UNITY_EDITOR + [ReadOnly] public SharedComponentTypeHandle EditorDataComponentHandle; + [ReadOnly] public NativeParallelHashMap BatchEditorData; +#endif + + public void Execute(int index) + { + var visibilityItem = VisibilityItems.ElementAt(index); + + + var chunk = visibilityItem.Chunk; + var chunkVisibility = visibilityItem.Visibility; + + int filterIndex = chunk.GetSharedComponentIndex(RenderFilterSettings); + BatchFilterSettings filterSettings = FilterSettings[filterIndex]; + + if (((1 << filterSettings.layer) & CullingLayerMask) == 0) return; + + DrawCommandOutput.InitializeForEmitThread(); + + { + //using var prof = ProfilerEmitChunk.Auto(); + + var entitiesGraphicsChunkInfo = chunk.GetChunkComponentData(EntitiesGraphicsChunkInfo); + + if (!entitiesGraphicsChunkInfo.Valid) + return; + + ref var chunkCullingData = ref entitiesGraphicsChunkInfo.CullingData; + + // If nothing is visible in the chunk, avoid all unnecessary work + bool noVisibleEntities = !chunkVisibility->AnyVisible; + if (noVisibleEntities) + return; + + int batchIndex = entitiesGraphicsChunkInfo.BatchIndex; + +#if UNITY_EDITOR + if (!TestSceneCullingMask(chunk)) + return; +#endif + + var materialMeshInfos = chunk.GetNativeArray(MaterialMeshInfo); + var localToWorlds = chunk.GetNativeArray(LocalToWorld); + bool isDepthSorted = chunk.Has(DepthSorted); + bool isLightMapped = chunk.GetSharedComponentIndex(LightMaps) >= 0; + + // Check if the chunk has statically disabled motion (i.e. never in motion pass) + // or enabled motion (i.e. in motion pass if there was actual motion or force-to-zero). + // We make sure to never set the motion flag if motion is statically disabled to improve batching + // in cases where the transform is changed. + bool hasMotion = (chunkCullingData.Flags & EntitiesGraphicsChunkCullingData.kFlagPerObjectMotion) != 0; + + if (hasMotion) + { + bool orderChanged = chunk.DidOrderChange(LastSystemVersion); + bool transformChanged = chunk.DidChange(LocalToWorld, LastSystemVersion); +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + bool isDeformed = chunk.Has(DeformedMeshIndex); +#else + bool isDeformed = false; +#endif + hasMotion = orderChanged || transformChanged || isDeformed; + } + + int chunkStartIndex = entitiesGraphicsChunkInfo.CullingData.ChunkOffsetInBatch; + + for (int j = 0; j < 2; j++) + { + ulong visibleWord = chunkVisibility->VisibleEntities[j]; + + while (visibleWord != 0) + { + int bitIndex = math.tzcnt(visibleWord); + int entityIndex = (j << 6) + bitIndex; + ulong entityMask = 1ul << bitIndex; + + // Clear the bit first in case we early out from the loop + visibleWord ^= entityMask; + + var materialMeshInfo = materialMeshInfos[entityIndex]; + + // Null materials are handled internally by Unity using the error material if available. + // Invalid meshes at this point will be skipped. + if (materialMeshInfo.Mesh <= 0) + continue; + + bool flipWinding = (chunkCullingData.FlippedWinding[j] & entityMask) != 0; + + var settings = new DrawCommandSettings + { + FilterIndex = filterIndex, + BatchID = new BatchID { value = (uint)batchIndex }, + MaterialID = materialMeshInfo.MaterialID, + MeshID = materialMeshInfo.MeshID, + SplitMask = chunkVisibility->SplitMasks[entityIndex], + SubmeshIndex = (ushort)materialMeshInfo.Submesh, + Flags = 0 + }; + + if (flipWinding) + settings.Flags |= BatchDrawCommandFlags.FlipWinding; + + if (hasMotion) + settings.Flags |= BatchDrawCommandFlags.HasMotion; + + if (isLightMapped) + settings.Flags |= BatchDrawCommandFlags.IsLightMapped; + + // Depth sorted draws are emitted with access to entity transforms, + // so they can also be written out for sorting + if (isDepthSorted) + { + settings.Flags |= BatchDrawCommandFlags.HasSortingPosition; + DrawCommandOutput.EmitDepthSorted(settings, j, bitIndex, chunkStartIndex, + (float4x4*)localToWorlds.GetUnsafeReadOnlyPtr()); + } + else + { + DrawCommandOutput.Emit(settings, j, bitIndex, chunkStartIndex); + } + } + } + } + } + + private bool TestSceneCullingMask(ArchetypeChunk chunk) + { +#if UNITY_EDITOR + int editorRenderDataIndex = chunk.GetSharedComponentIndex(EditorDataComponentHandle); + + // If we can't find a culling mask, use the default + ulong chunkSceneCullingMask = EditorSceneManager.DefaultSceneCullingMask; + + if (editorRenderDataIndex >= 0) + { + BatchEditorRenderData data; + if (BatchEditorData.TryGetValue(editorRenderDataIndex, out data)) + { + chunkSceneCullingMask = data.SceneCullingMask; + } + } + + // Cull the chunk if the scene mask intersection is empty. + return (SceneCullingMask & chunkSceneCullingMask) != 0; +#else + return true; +#endif + } + } + + [BurstCompile] + internal unsafe struct AllocateWorkItemsJob : IJob + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute() + { + int numBins = DrawCommandOutput.UnsortedBins.Length; + + DrawCommandOutput.BinIndices.Resize(numBins, NativeArrayOptions.UninitializedMemory); + + // Each thread can have one item per bin, but likely not all threads will. + int workItemsUpperBound = ChunkDrawCommandOutput.NumThreads * numBins; + DrawCommandOutput.WorkItems.SetCapacity(workItemsUpperBound); + } + } + + [BurstCompile] + internal unsafe struct DrawBinSort + { + public const int kNumSlices = 4; + public const Allocator kAllocator = Allocator.TempJob; + + [BurstCompile] + internal unsafe struct SortArrays + { + public IndirectList SortedBins; + public IndirectList SortTemp; + + public int ValuesPerIndex => (SortedBins.Length + kNumSlices - 1) / kNumSlices; + + [return: NoAlias] public int* ValuesTemp(int i = 0) => SortTemp.List->Ptr + i; + [return: NoAlias] public int* ValuesDst(int i = 0) => SortedBins.List->Ptr + i; + + public void GetBeginEnd(int index, out int begin, out int end) + { + begin = index * ValuesPerIndex; + end = math.min(begin + ValuesPerIndex, SortedBins.Length); + } + } + + internal unsafe struct BinSortComparer : IComparer + { + [NoAlias] + public DrawCommandSettings* Bins; + + public BinSortComparer(IndirectList bins) + { + Bins = bins.List->Ptr; + } + + public int Compare(int x, int y) => Key(x).CompareTo(Key(y)); + + private DrawCommandSettings Key(int bin) => Bins[bin]; + } + + [BurstCompile] + internal unsafe struct AllocateForSortJob : IJob + { + public IndirectList UnsortedBins; + public SortArrays Arrays; + + public void Execute() + { + int numBins = UnsortedBins.Length; + Arrays.SortedBins.Resize(numBins, NativeArrayOptions.UninitializedMemory); + Arrays.SortTemp.Resize(numBins, NativeArrayOptions.UninitializedMemory); + } + } + + [BurstCompile] + internal unsafe struct SortSlicesJob : IJobParallelFor + { + public SortArrays Arrays; + public IndirectList UnsortedBins; + + public void Execute(int index) + { + Arrays.GetBeginEnd(index, out int begin, out int end); + + var valuesFromZero = Arrays.ValuesTemp(); + int N = end - begin; + + for (int i = begin; i < end; ++i) + valuesFromZero[i] = i; + + NativeSortExtension.Sort(Arrays.ValuesTemp(begin), N, new BinSortComparer(UnsortedBins)); + } + } + + [BurstCompile] + internal unsafe struct MergeSlicesJob : IJob + { + public SortArrays Arrays; + public IndirectList UnsortedBins; + public int NumSlices => kNumSlices; + + public void Execute() + { + var sliceRead = stackalloc int[NumSlices]; + var sliceEnd = stackalloc int[NumSlices]; + + int sliceMask = 0; + + for (int i = 0; i < NumSlices; ++i) + { + Arrays.GetBeginEnd(i, out sliceRead[i], out sliceEnd[i]); + if (sliceRead[i] < sliceEnd[i]) + sliceMask |= 1 << i; + } + + int N = Arrays.SortedBins.Length; + var dst = Arrays.ValuesDst(); + var src = Arrays.ValuesTemp(); + var comparer = new BinSortComparer(UnsortedBins); + + for (int i = 0; i < N; ++i) + { + int iterMask = sliceMask; + int firstNonEmptySlice = math.tzcnt(iterMask); + + int bestSlice = firstNonEmptySlice; + int bestValue = src[sliceRead[firstNonEmptySlice]]; + iterMask ^= 1 << firstNonEmptySlice; + + while (iterMask != 0) + { + int slice = math.tzcnt(iterMask); + int value = src[sliceRead[slice]]; + + if (comparer.Compare(value, bestValue) < 0) + { + bestSlice = slice; + bestValue = value; + } + + iterMask ^= 1 << slice; + } + + dst[i] = bestValue; + + int nextValue = sliceRead[bestSlice] + 1; + bool sliceExhausted = nextValue >= sliceEnd[bestSlice]; + sliceRead[bestSlice] = nextValue; + + int mask = 1 << bestSlice; + mask = sliceExhausted ? mask : 0; + sliceMask ^= mask; + } + + Arrays.SortTemp.Dispose(); + } + } + + public static JobHandle ScheduleBinSort( + RewindableAllocator* allocator, + IndirectList sortedBins, + IndirectList unsortedBins, + JobHandle dependency = default) + { + var sortArrays = new SortArrays + { + SortedBins = sortedBins, + SortTemp = new IndirectList(0, allocator), + }; + + var alloc = new AllocateForSortJob + { + Arrays = sortArrays, + UnsortedBins = unsortedBins, + }.Schedule(dependency); + + var sortSlices = new SortSlicesJob + { + Arrays = sortArrays, + UnsortedBins = unsortedBins, + }.Schedule(kNumSlices, 1, alloc); + + var mergeSlices = new MergeSlicesJob + { + Arrays = sortArrays, + UnsortedBins = unsortedBins, + }.Schedule(sortSlices); + + return mergeSlices; + } + } + + + [BurstCompile] + internal unsafe struct CollectWorkItemsJob : IJobParallelForDefer + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public ProfilerMarker ProfileCollect; + public ProfilerMarker ProfileWrite; + + public void Execute(int index) + { + var settings = DrawCommandOutput.UnsortedBins.ElementAt(index); + bool hasSortingPosition = settings.HasSortingPosition; + + long* binPresentFilter = DrawCommandOutput.BinPresentFilterForSettings(settings); + + int maxWorkItems = 0; + for (int qwIndex = 0; qwIndex < ChunkDrawCommandOutput.kNumThreadsBitfieldLength; ++qwIndex) + maxWorkItems += math.countbits(binPresentFilter[qwIndex]); + + // Since we collect at most one item per thread, we will have N = thread count at most + var workItems = DrawCommandOutput.WorkItems.List->AsParallelWriter(); + var collectBuffer = DrawCommandOutput.CollectBuffer; + collectBuffer->EnsureCapacity(workItems, maxWorkItems); + + int numInstancesPrefixSum = 0; + + // ProfileCollect.Begin(); + + for (int qwIndex = 0; qwIndex < ChunkDrawCommandOutput.kNumThreadsBitfieldLength; ++qwIndex) + { + // Load a filter bitfield which has a 1 bit for every thread index that might contain + // draws for a given DrawCommandSettings. The filter is exact if there are no hash + // collisions, but might contain false positives if hash collisions happened. + ulong qword = (ulong) binPresentFilter[qwIndex]; + + while (qword != 0) + { + int bitIndex = math.tzcnt(qword); + ulong mask = 1ul << bitIndex; + qword ^= mask; + + int i = (qwIndex << 6) + bitIndex; + + var threadDraws = DrawCommandOutput.ThreadLocalDrawCommands[i]; + + if (!threadDraws.DrawCommandStreamIndices.IsCreated) + continue; + + if (threadDraws.DrawCommandStreamIndices.TryGetValue(settings, out int streamIndex)) + { + var stream = threadDraws.DrawCommands[streamIndex].Stream; + + if (hasSortingPosition) + { + var transformStream = threadDraws.DrawCommands[streamIndex].TransformsStream; + collectBuffer->Add(new DrawCommandWorkItem + { + Arrays = stream.Head, + TransformArrays = transformStream.Head, + BinIndex = index, + PrefixSumNumInstances = numInstancesPrefixSum, + }); + } + else + { + collectBuffer->Add(new DrawCommandWorkItem + { + Arrays = stream.Head, + TransformArrays = null, + BinIndex = index, + PrefixSumNumInstances = numInstancesPrefixSum, + }); + } + + numInstancesPrefixSum += stream.TotalInstanceCount; + } + } + } + // ProfileCollect.End(); + // ProfileWrite.Begin(); + + DrawCommandOutput.BinIndices.ElementAt(index) = new DrawCommandBin + { + NumInstances = numInstancesPrefixSum, + InstanceOffset = 0, + PositionOffset = hasSortingPosition ? 0 : DrawCommandBin.kNoSortingPosition, + }; + + // ProfileWrite.End(); + } + } + + [BurstCompile] + internal unsafe struct FlushWorkItemsJob : IJobParallelFor + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute(int index) + { + var dst = DrawCommandOutput.WorkItems.List->AsParallelWriter(); + DrawCommandOutput.ThreadLocalCollectBuffers[index].Flush(dst); + } + } + + [BurstCompile] + internal unsafe struct AllocateInstancesJob : IJob + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute() + { + int numBins = DrawCommandOutput.BinIndices.Length; + + int instancePrefixSum = 0; + int sortingPositionPrefixSum = 0; + + for (int i = 0; i < numBins; ++i) + { + ref var bin = ref DrawCommandOutput.BinIndices.ElementAt(i); + bool hasSortingPosition = bin.HasSortingPosition; + + bin.InstanceOffset = instancePrefixSum; + + // Keep kNoSortingPosition in the PositionOffset if no sorting + // positions, so draw command jobs can reliably check it to + // to know whether there are positions without needing access to flags + bin.PositionOffset = hasSortingPosition + ? sortingPositionPrefixSum + : DrawCommandBin.kNoSortingPosition; + + int numInstances = bin.NumInstances; + int numPositions = hasSortingPosition ? numInstances : 0; + + instancePrefixSum += numInstances; + sortingPositionPrefixSum += numPositions; + } + + var output = DrawCommandOutput.CullingOutputDrawCommands; + output->visibleInstanceCount = instancePrefixSum; + output->visibleInstances = ChunkDrawCommandOutput.Malloc(instancePrefixSum); + + int numSortingPositionFloats = sortingPositionPrefixSum * 3; + output->instanceSortingPositionFloatCount = numSortingPositionFloats; + output->instanceSortingPositions = (sortingPositionPrefixSum == 0) + ? null + : ChunkDrawCommandOutput.Malloc(numSortingPositionFloats); + } + } + + [BurstCompile] + internal unsafe struct AllocateDrawCommandsJob : IJob + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute() + { + int numBins = DrawCommandOutput.SortedBins.Length; + + int drawCommandPrefixSum = 0; + + for (int i = 0; i < numBins; ++i) + { + var sortedBin = DrawCommandOutput.SortedBins.ElementAt(i); + ref var bin = ref DrawCommandOutput.BinIndices.ElementAt(sortedBin); + bin.DrawCommandOffset = drawCommandPrefixSum; + + // Bins with sorting positions will be expanded to one draw command + // per instance, whereas other bins will be expanded to contain + // many instances per command. + int numDrawCommands = bin.NumDrawCommands; + drawCommandPrefixSum += numDrawCommands; + } + + var output = DrawCommandOutput.CullingOutputDrawCommands; + + // Draw command count is exact at this point, we can set it up front + int drawCommandCount = drawCommandPrefixSum; + + output->drawCommandCount = drawCommandCount; + output->drawCommands = ChunkDrawCommandOutput.Malloc(drawCommandCount); + output->drawCommandPickingInstanceIDs = null; + + // Worst case is one range per draw command, so this is an upper bound estimate. + // The real count could be less. + output->drawRangeCount = 0; + output->drawRanges = ChunkDrawCommandOutput.Malloc(drawCommandCount); + } + } + + [BurstCompile] + internal unsafe struct ExpandVisibleInstancesJob : IJobParallelForDefer + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute(int index) + { + var workItem = DrawCommandOutput.WorkItems.ElementAt(index); + var header = workItem.Arrays; + var transformHeader = workItem.TransformArrays; + int binIndex = workItem.BinIndex; + + var bin = DrawCommandOutput.BinIndices.ElementAt(binIndex); + int binInstanceOffset = bin.InstanceOffset; + int binPositionOffset = bin.PositionOffset; + int workItemInstanceOffset = workItem.PrefixSumNumInstances; + int headerInstanceOffset = 0; + + int* visibleInstances = DrawCommandOutput.CullingOutputDrawCommands->visibleInstances; + float3* sortingPositions = (float3*)DrawCommandOutput.CullingOutputDrawCommands->instanceSortingPositions; + + if (transformHeader == null) + { + while (header != null) + { + ExpandArray( + visibleInstances, + header, + binInstanceOffset + workItemInstanceOffset + headerInstanceOffset); + + headerInstanceOffset += header->NumInstances; + header = header->Next; + } + } + else + { + while (header != null) + { + Debug.Assert(transformHeader != null); + + int instanceOffset = binInstanceOffset + workItemInstanceOffset + headerInstanceOffset; + int positionOffset = binPositionOffset + workItemInstanceOffset + headerInstanceOffset; + + ExpandArrayWithPositions( + visibleInstances, + sortingPositions, + header, + transformHeader, + instanceOffset, + positionOffset); + + headerInstanceOffset += header->NumInstances; + header = header->Next; + transformHeader = transformHeader->Next; + } + } + } + + private int ExpandArray( + int* visibleInstances, + DrawStream.Header* header, + int instanceOffset) + { + int numStructs = header->NumElements; + + for (int i = 0; i < numStructs; ++i) + { + var visibility = *header->Element(i); + int numInstances = ExpandVisibility(visibleInstances + instanceOffset, visibility); + Debug.Assert(numInstances > 0); + instanceOffset += numInstances; + } + + return instanceOffset; + } + + private int ExpandArrayWithPositions( + int* visibleInstances, + float3* sortingPositions, + DrawStream.Header* header, + DrawStream.Header* transformHeader, + int instanceOffset, + int positionOffset) + { + int numStructs = header->NumElements; + + for (int i = 0; i < numStructs; ++i) + { + var visibility = *header->Element(i); + var transforms = (float4x4*) (*transformHeader->Element(i)); + int numInstances = ExpandVisibilityWithPositions( + visibleInstances + instanceOffset, + sortingPositions + positionOffset, + visibility, + transforms); + Debug.Assert(numInstances > 0); + instanceOffset += numInstances; + positionOffset += numInstances; + } + + return instanceOffset; + } + + + private int ExpandVisibility(int* outputInstances, DrawCommandVisibility visibility) + { + int numInstances = 0; + int startIndex = visibility.ChunkStartIndex; + + for (int i = 0; i < 2; ++i) + { + ulong qword = visibility.VisibleInstances[i]; + while (qword != 0) + { + int bitIndex = math.tzcnt(qword); + ulong mask = 1ul << bitIndex; + qword ^= mask; + int instanceIndex = (i << 6) + bitIndex; + int visibilityIndex = startIndex + instanceIndex; + outputInstances[numInstances] = visibilityIndex; + ++numInstances; + } + } + + return numInstances; + } + + private int ExpandVisibilityWithPositions( + int* outputInstances, + float3* outputSortingPosition, + DrawCommandVisibility visibility, + float4x4* transforms) + { + int numInstances = 0; + int startIndex = visibility.ChunkStartIndex; + + for (int i = 0; i < 2; ++i) + { + ulong qword = visibility.VisibleInstances[i]; + while (qword != 0) + { + int bitIndex = math.tzcnt(qword); + ulong mask = 1ul << bitIndex; + qword ^= mask; + int instanceIndex = (i << 6) + bitIndex; + + var instanceTransform = new LocalToWorld + { + Value = transforms[instanceIndex], + }; + + int visibilityIndex = startIndex + instanceIndex; + outputInstances[numInstances] = visibilityIndex; + outputSortingPosition[numInstances] = instanceTransform.Position; + + ++numInstances; + } + } + + return numInstances; + } + } + + [BurstCompile] + internal unsafe struct GenerateDrawCommandsJob : IJobParallelForDefer + { + public ChunkDrawCommandOutput DrawCommandOutput; + +#if UNITY_EDITOR + [NativeDisableUnsafePtrRestriction] + public EntitiesGraphicsPerThreadStats* Stats; + +#pragma warning disable 649 + [NativeSetThreadIndex] + public int ThreadIndex; +#pragma warning restore 649 +#endif + + public void Execute(int index) + { +#if UNITY_EDITOR + ref var stats = ref Stats[ThreadIndex]; +#endif + var sortedBin = DrawCommandOutput.SortedBins.ElementAt(index); + var settings = DrawCommandOutput.UnsortedBins.ElementAt(sortedBin); + var bin = DrawCommandOutput.BinIndices.ElementAt(sortedBin); + + bool hasSortingPosition = settings.HasSortingPosition; + uint maxPerCommand = hasSortingPosition + ? 1u + : EntitiesGraphicsTuningConstants.kMaxInstancesPerDrawCommand; + uint numInstances = (uint)bin.NumInstances; + int numDrawCommands = bin.NumDrawCommands; + + uint drawInstanceOffset = (uint)bin.InstanceOffset; + uint drawPositionFloatOffset = (uint)bin.PositionOffset * 3; // 3 floats per position + + var cullingOutput = DrawCommandOutput.CullingOutputDrawCommands; + var draws = cullingOutput->drawCommands; + + for (int i = 0; i < numDrawCommands; ++i) + { + var draw = new BatchDrawCommand + { + visibleOffset = drawInstanceOffset, + visibleCount = math.min(maxPerCommand, numInstances), + batchID = settings.BatchID, + materialID = settings.MaterialID, + meshID = settings.MeshID, + submeshIndex = (ushort)settings.SubmeshIndex, + splitVisibilityMask = settings.SplitMask, + flags = settings.Flags, + sortingPosition = hasSortingPosition + ? (int)drawPositionFloatOffset + : 0, + }; + + int drawCommandIndex = bin.DrawCommandOffset + i; + draws[drawCommandIndex] = draw; + + drawInstanceOffset += draw.visibleCount; + drawPositionFloatOffset += draw.visibleCount * 3; + numInstances -= draw.visibleCount; + +#if UNITY_EDITOR + stats.RenderedEntityCount += (int)draw.visibleCount; +#endif + } +#if UNITY_EDITOR + stats.DrawCommandCount += numDrawCommands; +#endif + } + } + + [BurstCompile] + internal unsafe struct GenerateDrawRangesJob : IJob + { + public ChunkDrawCommandOutput DrawCommandOutput; + + [ReadOnly] public NativeParallelHashMap FilterSettings; + + private const int MaxInstances = EntitiesGraphicsTuningConstants.kMaxInstancesPerDrawRange; + private const int MaxCommands = EntitiesGraphicsTuningConstants.kMaxDrawCommandsPerDrawRange; + + private int m_PrevFilterIndex; + private int m_CommandsInRange; + private int m_InstancesInRange; + +#if UNITY_EDITOR + [NativeDisableUnsafePtrRestriction] + public EntitiesGraphicsPerThreadStats* Stats; + +#pragma warning disable 649 + [NativeSetThreadIndex] + public int ThreadIndex; +#pragma warning restore 649 +#endif + + public void Execute() + { +#if UNITY_EDITOR + ref var stats = ref Stats[ThreadIndex]; +#endif + int numBins = DrawCommandOutput.SortedBins.Length; + var output = DrawCommandOutput.CullingOutputDrawCommands; + + ref int rangeCount = ref output->drawRangeCount; + var ranges = output->drawRanges; + + rangeCount = 0; + m_PrevFilterIndex = -1; + m_CommandsInRange = 0; + m_InstancesInRange = 0; + + for (int i = 0; i < numBins; ++i) + { + var sortedBin = DrawCommandOutput.SortedBins.ElementAt(i); + var settings = DrawCommandOutput.UnsortedBins.ElementAt(sortedBin); + var bin = DrawCommandOutput.BinIndices.ElementAt(sortedBin); + + int numInstances = bin.NumInstances; + int drawCommandOffset = bin.DrawCommandOffset; + int numDrawCommands = bin.NumDrawCommands; + int filterIndex = settings.FilterIndex; + bool hasSortingPosition = settings.HasSortingPosition; + + for (int j = 0; j < numDrawCommands; ++j) + { + int instancesInCommand = math.min(numInstances, DrawCommandBin.MaxInstancesPerCommand); + + AccumulateDrawRange( + ref rangeCount, + ranges, + drawCommandOffset, + instancesInCommand, + filterIndex, + hasSortingPosition); + + ++drawCommandOffset; + numInstances -= instancesInCommand; + } + } +#if UNITY_EDITOR + stats.DrawRangeCount += rangeCount; +#endif + + Debug.Assert(rangeCount <= output->drawCommandCount); + } + + private void AccumulateDrawRange( + ref int rangeCount, + BatchDrawRange* ranges, + int drawCommandOffset, + int numInstances, + int filterIndex, + bool hasSortingPosition) + { + bool isFirst = rangeCount == 0; + + bool addNewCommand; + + if (isFirst) + { + addNewCommand = true; + } + else + { + int newInstanceCount = m_InstancesInRange + numInstances; + int newCommandCount = m_CommandsInRange + 1; + + bool sameFilter = filterIndex == m_PrevFilterIndex; + bool tooManyInstances = newInstanceCount > MaxInstances; + bool tooManyCommands = newCommandCount > MaxCommands; + + addNewCommand = !sameFilter || tooManyInstances || tooManyCommands; + } + + if (addNewCommand) + { + ranges[rangeCount] = new BatchDrawRange + { + filterSettings = FilterSettings[filterIndex], + drawCommandsBegin = (uint)drawCommandOffset, + drawCommandsCount = 1, + }; + + ranges[rangeCount].filterSettings.allDepthSorted = hasSortingPosition; + + m_PrevFilterIndex = filterIndex; + m_CommandsInRange = 1; + m_InstancesInRange = numInstances; + + ++rangeCount; + } + else + { + ref var range = ref ranges[rangeCount - 1]; + + ++range.drawCommandsCount; + range.filterSettings.allDepthSorted &= hasSortingPosition; + + ++m_CommandsInRange; + m_InstancesInRange += numInstances; + } + } + } + + + internal unsafe struct DebugValidateSortJob : IJob + { + public ChunkDrawCommandOutput DrawCommandOutput; + + public void Execute() + { + int N = DrawCommandOutput.UnsortedBins.Length; + + for (int i = 0; i < N; ++i) + { + int sorted = DrawCommandOutput.SortedBins.ElementAt(i); + var settings = DrawCommandOutput.UnsortedBins.ElementAt(sorted); + + int next = math.min(i + 1, N - 1); + int sortedNext = DrawCommandOutput.SortedBins.ElementAt(next); + var settingsNext = DrawCommandOutput.UnsortedBins.ElementAt(sortedNext); + + int cmp = settings.CompareTo(settingsNext); + int cmpRef = settings.CompareToReference(settingsNext); + + Debug.Assert(cmpRef <= 0, $"Draw commands not in order. CompareTo: {cmp}, CompareToReference: {cmpRef}, A: {settings}, B: {settingsNext}"); + Debug.Assert(cmpRef == cmp, $"CompareTo() does not match CompareToReference(). CompareTo: {cmp}, CompareToReference: {cmpRef}, A: {settings}, B: {settingsNext}"); + } + } + } +} diff --git a/Unity.Entities.Graphics/DrawCommandGeneration.cs.meta b/Unity.Entities.Graphics/DrawCommandGeneration.cs.meta new file mode 100644 index 0000000..d184954 --- /dev/null +++ b/Unity.Entities.Graphics/DrawCommandGeneration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a2c66a54063f4cc8a9504361249df5c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs b/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs new file mode 100644 index 0000000..181d2b2 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs @@ -0,0 +1,349 @@ +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; + +namespace Unity.Rendering +{ + [BurstCompile] + internal struct EntitiesGraphicsChunkUpdater + { + public ComponentTypeCache.BurstCompatibleTypeArray ComponentTypes; + + [NativeDisableParallelForRestriction] + public NativeArray UnreferencedBatchIndices; + + [NativeDisableParallelForRestriction] + [ReadOnly] + public NativeArray ChunkProperties; + + [NativeDisableParallelForRestriction] + public NativeArray GpuUploadOperations; + public NativeArray NumGpuUploadOperations; + + [NativeDisableParallelForRestriction] + public NativeArray ThreadLocalAABBs; + + public uint LastSystemVersion; + +#pragma warning disable 649 + [NativeSetThreadIndex] public int ThreadIndex; +#pragma warning restore 649 + + public int LocalToWorldType; + public int WorldToLocalType; + public int PrevLocalToWorldType; + public int PrevWorldToLocalType; + +#if PROFILE_BURST_JOB_INTERNALS + public ProfilerMarker ProfileAddUpload; + public ProfilerMarker ProfilePickingMatrices; +#endif + + unsafe void MarkBatchAsReferenced(int batchIndex) + { + // If the batch is referenced, remove it from the unreferenced bitfield + + AtomicHelpers.IndexToQwIndexAndMask(batchIndex, out int qw, out long mask); + + Debug.Assert(qw < UnreferencedBatchIndices.Length, "Batch index out of bounds"); + + AtomicHelpers.AtomicAnd( + (long*)UnreferencedBatchIndices.GetUnsafePtr(), + qw, + ~mask); + } + + public void ProcessChunk(in EntitiesGraphicsChunkInfo chunkInfo, ArchetypeChunk chunk, ChunkWorldRenderBounds chunkBounds) + { +#if DEBUG_LOG_CHUNKS + Debug.Log($"HybridChunkUpdater.ProcessChunk(internalBatchIndex: {chunkInfo.BatchIndex}, valid: {chunkInfo.Valid}, count: {chunk.Count}, chunk: {chunk.GetHashCode()})"); +#endif + + if (chunkInfo.Valid) + ProcessValidChunk(chunkInfo, chunk, chunkBounds.Value, false); + } + + public unsafe void ProcessValidChunk(in EntitiesGraphicsChunkInfo chunkInfo, ArchetypeChunk chunk, + MinMaxAABB chunkAABB, bool isNewChunk) + { + if (!isNewChunk) + MarkBatchAsReferenced(chunkInfo.BatchIndex); + + bool structuralChanges = chunk.DidOrderChange(LastSystemVersion); + + var dstOffsetWorldToLocal = -1; + var dstOffsetPrevWorldToLocal = -1; + + fixed(DynamicComponentTypeHandle* fixedT0 = &ComponentTypes.t0) + { + for (int i = chunkInfo.ChunkTypesBegin; i < chunkInfo.ChunkTypesEnd; ++i) + { + var chunkProperty = ChunkProperties[i]; + var type = chunkProperty.ComponentTypeIndex; + if (type == WorldToLocalType) + dstOffsetWorldToLocal = chunkProperty.GPUDataBegin; + else if (type == PrevWorldToLocalType) + dstOffsetPrevWorldToLocal = chunkProperty.GPUDataBegin; + } + + for (int i = chunkInfo.ChunkTypesBegin; i < chunkInfo.ChunkTypesEnd; ++i) + { + var chunkProperty = ChunkProperties[i]; + var type = ComponentTypes.Type(fixedT0, chunkProperty.ComponentTypeIndex); + + var chunkType = chunkProperty.ComponentTypeIndex; + var isLocalToWorld = chunkType == LocalToWorldType; + var isWorldToLocal = chunkType == WorldToLocalType; + var isPrevLocalToWorld = chunkType == PrevLocalToWorldType; + var isPrevWorldToLocal = chunkType == PrevWorldToLocalType; + + var skipComponent = (isWorldToLocal || isPrevWorldToLocal); + + bool componentChanged = chunk.DidChange(type, LastSystemVersion); + bool copyComponentData = (isNewChunk || structuralChanges || componentChanged) && !skipComponent; + + if (copyComponentData) + { +#if DEBUG_LOG_PROPERTY_UPDATES + Debug.Log($"UpdateChunkProperty(internalBatchIndex: {chunkInfo.BatchIndex}, property: {i}, elementSize: {chunkProperty.ValueSizeBytesCPU})"); +#endif + + var src = chunk.GetDynamicComponentDataArrayReinterpret(type, + chunkProperty.ValueSizeBytesCPU); + +#if PROFILE_BURST_JOB_INTERNALS + ProfileAddUpload.Begin(); +#endif + + int sizeBytes = (int)((uint)chunk.Count * (uint)chunkProperty.ValueSizeBytesCPU); + var srcPtr = src.GetUnsafeReadOnlyPtr(); + var dstOffset = chunkProperty.GPUDataBegin; + if (isLocalToWorld || isPrevLocalToWorld) + { + var numMatrices = sizeBytes / sizeof(float4x4); + AddMatrixUpload( + srcPtr, + numMatrices, + dstOffset, + isLocalToWorld ? dstOffsetWorldToLocal : dstOffsetPrevWorldToLocal, + (chunkProperty.ValueSizeBytesCPU == 4 * 4 * 3) + ? ThreadedSparseUploader.MatrixType.MatrixType3x4 + : ThreadedSparseUploader.MatrixType.MatrixType4x4, + (chunkProperty.ValueSizeBytesGPU == 4 * 4 * 3) + ? ThreadedSparseUploader.MatrixType.MatrixType3x4 + : ThreadedSparseUploader.MatrixType.MatrixType4x4); + } + else + { + AddUpload( + srcPtr, + sizeBytes, + dstOffset); + } +#if PROFILE_BURST_JOB_INTERNALS + ProfileAddUpload.End(); +#endif + } + } + } + + UpdateAABB(chunkAABB); + } + + private unsafe void UpdateAABB(MinMaxAABB chunkAABB) + { + var threadLocalAABB = ((ThreadLocalAABB*) ThreadLocalAABBs.GetUnsafePtr()) + ThreadIndex; + ref var aabb = ref threadLocalAABB->AABB; + aabb.Encapsulate(chunkAABB); + } + + private unsafe void AddUpload(void* srcPtr, int sizeBytes, int dstOffset) + { + int* numGpuUploadOperations = (int*) NumGpuUploadOperations.GetUnsafePtr(); + int index = System.Threading.Interlocked.Add(ref numGpuUploadOperations[0], 1) - 1; + + if (index < GpuUploadOperations.Length) + { + GpuUploadOperations[index] = new GpuUploadOperation + { + Kind = GpuUploadOperation.UploadOperationKind.Memcpy, + Src = srcPtr, + DstOffset = dstOffset, + DstOffsetInverse = -1, + Size = sizeBytes, + }; + } + else + { + // Debug.Assert(false, "Maximum amount of GPU upload operations exceeded"); + } + } + + private unsafe void AddMatrixUpload( + void* srcPtr, + int numMatrices, + int dstOffset, + int dstOffsetInverse, + ThreadedSparseUploader.MatrixType matrixTypeCpu, + ThreadedSparseUploader.MatrixType matrixTypeGpu) + { + int* numGpuUploadOperations = (int*) NumGpuUploadOperations.GetUnsafePtr(); + int index = System.Threading.Interlocked.Add(ref numGpuUploadOperations[0], 1) - 1; + + if (index < GpuUploadOperations.Length) + { + GpuUploadOperations[index] = new GpuUploadOperation + { + Kind = (matrixTypeGpu == ThreadedSparseUploader.MatrixType.MatrixType3x4) + ? GpuUploadOperation.UploadOperationKind.SOAMatrixUpload3x4 + : GpuUploadOperation.UploadOperationKind.SOAMatrixUpload4x4, + SrcMatrixType = matrixTypeCpu, + Src = srcPtr, + DstOffset = dstOffset, + DstOffsetInverse = dstOffsetInverse, + Size = numMatrices, + }; + } + else + { + // Debug.Assert(false, "Maximum amount of GPU upload operations exceeded"); + } + } + } + + [BurstCompile] + internal struct ClassifyNewChunksJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle ChunkHeader; + [ReadOnly] public ComponentTypeHandle EntitiesGraphicsChunkInfo; + + [NativeDisableParallelForRestriction] + public NativeArray NewChunks; + [NativeDisableParallelForRestriction] + public NativeArray NumNewChunks; + + public void Execute(in ArchetypeChunk metaChunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var chunkHeaders = metaChunk.GetNativeArray(ChunkHeader); + var entitiesGraphicsChunkInfos = metaChunk.GetNativeArray(EntitiesGraphicsChunkInfo); + + for (int i = 0, chunkEntityCount = metaChunk.Count; i < chunkEntityCount; i++) + { + var chunkInfo = entitiesGraphicsChunkInfos[i]; + var chunkHeader = chunkHeaders[i]; + + if (ShouldCountAsNewChunk(chunkInfo, chunkHeader.ArchetypeChunk)) + { + ClassifyNewChunk(chunkHeader.ArchetypeChunk); + } + } + } + + bool ShouldCountAsNewChunk(in EntitiesGraphicsChunkInfo chunkInfo, in ArchetypeChunk chunk) + { + return !chunkInfo.Valid && !chunk.Archetype.Prefab && !chunk.Archetype.Disabled; + } + + public unsafe void ClassifyNewChunk(ArchetypeChunk chunk) + { + int* numNewChunks = (int*)NumNewChunks.GetUnsafePtr(); + int iPlus1 = System.Threading.Interlocked.Add(ref numNewChunks[0], 1); + int i = iPlus1 - 1; // C# Interlocked semantics are weird + Debug.Assert(i < NewChunks.Length, "Out of space in the NewChunks buffer"); + NewChunks[i] = chunk; + } + } + + [BurstCompile] + internal struct UpdateOldEntitiesGraphicsChunksJob : IJobChunk + { + public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public ComponentTypeHandle ChunkWorldRenderBounds; + [ReadOnly] public ComponentTypeHandle ChunkHeader; + [ReadOnly] public ComponentTypeHandle LocalToWorld; + [ReadOnly] public ComponentTypeHandle LodRange; + [ReadOnly] public ComponentTypeHandle RootLodRange; + [ReadOnly] public ComponentTypeHandle MaterialMeshInfo; + [ReadOnly] public SharedComponentTypeHandle RenderMeshArray; + public EntitiesGraphicsChunkUpdater EntitiesGraphicsChunkUpdater; + + public void Execute(in ArchetypeChunk metaChunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + // metaChunk is the chunk which contains the meta entities (= entities holding the chunk components) for the actual chunks + + var entitiesGraphicsChunkInfos = metaChunk.GetNativeArray(EntitiesGraphicsChunkInfo); + var chunkHeaders = metaChunk.GetNativeArray(ChunkHeader); + var chunkBoundsArray = metaChunk.GetNativeArray(ChunkWorldRenderBounds); + + for (int i = 0, chunkEntityCount = metaChunk.Count; i < chunkEntityCount; i++) + { + var chunkInfo = entitiesGraphicsChunkInfos[i]; + var chunkHeader = chunkHeaders[i]; + var chunk = chunkHeader.ArchetypeChunk; + + // Skip chunks that for some reason have EntitiesGraphicsChunkInfo, but don't have the + // other required components. This should normally not happen, but can happen + // if the user manually deletes some components after the fact. + bool hasRenderMeshArray = chunk.Has(RenderMeshArray); + bool hasMaterialMeshInfo = chunk.Has(MaterialMeshInfo); + bool hasLocalToWorld = chunk.Has(LocalToWorld); + + if (!math.all(new bool3(hasRenderMeshArray, hasMaterialMeshInfo, hasLocalToWorld))) + continue; + + ChunkWorldRenderBounds chunkBounds = chunkBoundsArray[i]; + + bool localToWorldChange = chunkHeader.ArchetypeChunk.DidChange(LocalToWorld, EntitiesGraphicsChunkUpdater.LastSystemVersion); + + // When LOD ranges change, we must reset the movement grace to avoid using stale data + bool lodRangeChange = + chunkHeader.ArchetypeChunk.DidOrderChange(EntitiesGraphicsChunkUpdater.LastSystemVersion) | + chunkHeader.ArchetypeChunk.DidChange(LodRange, EntitiesGraphicsChunkUpdater.LastSystemVersion) | + chunkHeader.ArchetypeChunk.DidChange(RootLodRange, EntitiesGraphicsChunkUpdater.LastSystemVersion); + + if (lodRangeChange) + { + chunkInfo.CullingData.MovementGraceFixed16 = 0; + entitiesGraphicsChunkInfos[i] = chunkInfo; + } + + EntitiesGraphicsChunkUpdater.ProcessChunk(chunkInfo, chunkHeader.ArchetypeChunk, chunkBounds); + } + } + } + + [BurstCompile] + internal struct UpdateNewEntitiesGraphicsChunksJob : IJobParallelFor + { + [ReadOnly] public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public ComponentTypeHandle ChunkWorldRenderBounds; + + public NativeArray NewChunks; + public EntitiesGraphicsChunkUpdater EntitiesGraphicsChunkUpdater; + + public void Execute(int index) + { + var chunk = NewChunks[index]; + var chunkInfo = chunk.GetChunkComponentData(EntitiesGraphicsChunkInfo); + + ChunkWorldRenderBounds chunkBounds = chunk.GetChunkComponentData(ChunkWorldRenderBounds); + + Debug.Assert(chunkInfo.Valid, "Attempted to process a chunk with uninitialized Hybrid chunk info"); + EntitiesGraphicsChunkUpdater.ProcessValidChunk(chunkInfo, chunk, chunkBounds.Value, true); + } + } + +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs.meta new file mode 100644 index 0000000..7a97808 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsChunkUpdate.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bc790fc1d2db48a4bad97a26c3cb387d +timeCreated: 1619539794 \ No newline at end of file diff --git a/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs b/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs new file mode 100644 index 0000000..8f77aef --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs @@ -0,0 +1,108 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// A chunk component that contains rendering information about a chunk. + /// + /// + /// Entities Graphics adds this chunk component to each chunk that it considers valid for rendering. + /// + // TODO: Try to get this struct to be 64 bytes or less so it fits in a cache line? + public struct EntitiesGraphicsChunkInfo : IComponentData + { + /// + /// The index of the batch. + /// + internal int BatchIndex; + + /// + /// Begin index for component type metadata in external arrays. + /// + internal int ChunkTypesBegin; + + /// + /// End index for component type metadata in external arrays. + /// + internal int ChunkTypesEnd; + + /// + /// Culling data of the chunk. + /// + internal EntitiesGraphicsChunkCullingData CullingData; + + /// + /// Chunk is valid for processing. + /// + internal bool Valid; + } + + /// + /// Culling data of the chunk. + /// + internal unsafe struct EntitiesGraphicsChunkCullingData + { + /// + /// This flag is set if the chunk has LOD data. + /// + public const int kFlagHasLodData = 1 << 0; + + /// + /// This flag is set if the chunk shall be culled. + /// + public const int kFlagInstanceCulling = 1 << 1; + + /// + /// This flag is set is the chunk has per object motion. + /// + public const int kFlagPerObjectMotion = 1 << 2; + + /// + /// Chunk offset in the batch. + /// + public int ChunkOffsetInBatch; + + /// + /// Movement grace distance. + /// + public ushort MovementGraceFixed16; + + /// + /// Per chunk flags. + /// + public byte Flags; + public byte ForceLowLODPrevious; + // TODO: Remove InstanceLodEnableds, replace by just initializing VisibleInstances. + public ChunkInstanceLodEnabled InstanceLodEnableds; + public fixed ulong FlippedWinding[2]; + } + + /// + /// An unmanaged component that separates entities into different batches. + /// + /// + /// Entities with different PartitionValues are never in the same Entities Graphics batch. + /// This allows you to force entities into separate batches which can be useful for things like draw call sorting. + /// Entities Graphics treats entities that have no PartitionValue as if they have a PartitionValue of 0. + /// + public struct EntitiesGraphicsBatchPartition : ISharedComponentData + { + /// + /// The partition ID that Entities Graphics uses to sort entities into batches. + /// + public ulong PartitionValue; + } + + /// + /// A tag component that enables the unity_WorldToObject material property. + /// + /// + /// unity_WorldToObject contains the world to local conversion matrix. + /// + public struct WorldToLocal_Tag : IComponentData {} + + /// + /// A tag component that enables depth sorting for the entity. + /// + public struct DepthSorted_Tag : IComponentData {} +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs.meta new file mode 100644 index 0000000..7277b72 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsComponents.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b5f5c12f8894b329c716eb6b0e49273 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs b/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs new file mode 100644 index 0000000..f3b8cd0 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs @@ -0,0 +1,229 @@ +using System.Collections.Generic; +using Unity.Transforms; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.VFX; +#if HDRP_10_0_0_OR_NEWER +using UnityEngine.Rendering.HighDefinition; +#endif +#if URP_10_0_0_OR_NEWER +using UnityEngine.Rendering.Universal; +#endif + +namespace Unity.Rendering +{ +#if !UNITY_DISABLE_MANAGED_COMPONENTS +#if !TINY_0_22_0_OR_NEWER + class LightCompanionBaker : Baker + { + public override void Bake(Light authoring) + { + AddComponentObject(authoring); + +#if UNITY_EDITOR + // Explicitly store the LightBakingOutput using a component, so we can restore it + // at runtime. + var bakingOutput = authoring.bakingOutput; + AddComponent(new LightBakingOutputData {Value = bakingOutput}); +#endif + } + } + + class LightProbeProxyVolumeCompanionBaker : Baker + { + public override void Bake(LightProbeProxyVolume authoring) + { + AddComponentObject(authoring); + } + } + + class ReflectionProbeCompanionBaker : Baker + { + public override void Bake(ReflectionProbe authoring) + { + AddComponentObject(authoring); + } + } + + class TextMeshCompanionBaker : Baker + { + public override void Bake(TextMesh authoring) + { + var meshRenderer = GetComponent(); + AddComponentObject(authoring); + AddComponentObject(meshRenderer); + } + } + + class SpriteRendererCompanionBaker : Baker + { + public override void Bake(SpriteRenderer authoring) + { + AddComponentObject(authoring); + } + } + + class VisualEffectCompanionBaker : Baker + { + public override void Bake(VisualEffect authoring) + { + AddComponentObject(authoring); + } + } + + class ParticleSystemCompanionBaker : Baker + { + public override void Bake(ParticleSystem authoring) + { + var particleSystemRenderer = GetComponent(); + AddComponentObject(authoring); + AddComponentObject(particleSystemRenderer); + } + } + + class AudioSourceCompanionBaker : Baker + { + public override void Bake(AudioSource authoring) + { + AddComponentObject(authoring); + } + } + +#if SRP_10_0_0_OR_NEWER + class VolumeCompanionBaker : Baker + { + public override void Bake(Volume authoring) + { + var sphereCollider = GetComponent(); + var boxCollider = GetComponent(); + var capsuleCollider = GetComponent(); + var meshCollider = GetComponent(); + + AddComponentObject(authoring); + + if(sphereCollider != null) + AddComponentObject(sphereCollider); + + if(boxCollider != null) + AddComponentObject(boxCollider); + + if(capsuleCollider != null) + AddComponentObject(capsuleCollider); + + if(meshCollider != null) + AddComponentObject(meshCollider); + } + } +#endif + +#if HDRP_10_0_0_OR_NEWER + class HDAdditionalLightDataCompanionBaker : Baker + { + public override void Bake(HDAdditionalLightData authoring) + { + var light = GetComponent(); +#if UNITY_EDITOR + var isBaking = light.lightmapBakeType == LightmapBakeType.Baked; + if(!isBaking) + AddComponentObject(authoring); +#else + AddComponentObject(authoring); +#endif + } + } + + class HDAdditionalReflectionDataCompanionBaker : Baker + { + public override void Bake(HDAdditionalReflectionData authoring) + { + AddComponentObject(authoring); + } + } + + class DecalProjectorCompanionBaker : Baker + { + public override void Bake(DecalProjector authoring) + { + AddComponentObject(authoring); + } + } + + class LocalVolumetricFogCompanionBaker : Baker + { + public override void Bake(LocalVolumetricFog authoring) + { + AddComponentObject(authoring); + } + } + + class PlanarReflectionProbeCompanionBaker : Baker + { + public override void Bake(PlanarReflectionProbe authoring) + { + AddComponentObject(authoring); + } + } +#if PROBEVOLUME_CONVERSION + class ProbeVolumeCompanionBaker : Baker + { + public override void Bake(ProbeVolume authoring) + { + AddComponentObject(authoring); + } + } +#endif +#endif + +#if URP_10_0_0_OR_NEWER + class UniversalAdditionalLightDataCompanionBaker : Baker + { + public override void Bake(UniversalAdditionalLightData authoring) + { + var light = GetComponent(); +#if UNITY_EDITOR + var isBaking = light.lightmapBakeType == LightmapBakeType.Baked; + if(!isBaking) + AddComponentObject(authoring); +#else + AddComponentObject(authoring); +#endif + } + } +#endif + +#if HYBRID_ENTITIES_CAMERA_CONVERSION + class CameraCompanionBaker : Baker + { + public override void Bake(Camera authoring) + { + AddComponentObject(authoring); + } + } + +#if HDRP_10_0_0_OR_NEWER + class HDAdditionalCameraDataCompanionBaker : Baker + { + public override void Bake(HDAdditionalCameraData authoring) + { + AddComponentObject(authoring); + } + } +#endif + +#if URP_10_0_0_OR_NEWER + class UniversalAdditionalCameraDataCompanionBaker : Baker + { + public override void Bake(UniversalAdditionalCameraData authoring) + { + AddComponentObject(authoring); + } + } +#endif +#endif +#endif + +#endif +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs.meta new file mode 100644 index 0000000..31613ac --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsConversion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7adf227aab336643ae816c1d23d792f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs b/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs new file mode 100644 index 0000000..73dca76 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs @@ -0,0 +1,1037 @@ +// #define DISABLE_HYBRID_SPHERE_CULLING +// #define DISABLE_HYBRID_RECEIVER_CULLING +// #define DISABLE_INCLUDE_EXCLUDE_LIST_FILTERING +// #define DEBUG_VALIDATE_VISIBLE_COUNTS +// #define DEBUG_VALIDATE_COMBINED_SPLIT_RECEIVER_CULLING +// #define DEBUG_VALIDATE_SOA_SPHERE_TEST +// #define DEBUG_VALIDATE_EXTRA_SPLITS + +#if UNITY_EDITOR +using UnityEditor; +#endif + +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; + +/* + * Batch-oriented culling. + * + * This culling approach oriented from Megacity and works well for relatively + * slow-moving cameras in a large, dense environment. + * + * The primary CPU costs involved in culling all the chunks of mesh instances + * in megacity is touching the chunks of memory. A naive culling approach would + * look like this: + * + * for each chunk: + * select what instances should be enabled based on camera position (lod selection) + * + * for each frustum: + * for each chunk: + * if the chunk is completely out of the frustum: + * discard + * else: + * for each instance in the chunk: + * if the instance is inside the frustum: + * write index of instance to output index buffer + * + * The approach implemented here does essentially this, but has been optimized + * so that chunks need to be accessed as infrequently as possible: + * + * - Because the chunks are static, we can cache bounds information outside the chunks + * + * - Because the camera moves relatively slowly, we can compute a grace + * distance which the camera has to move (in any direction) before the LOD + * selection would compute a different result + * + * - Because only a some chunks straddle the frustum boundaries, we can treat + * them as "in" rather than "partial" to save touching their chunk memory + */ + +namespace Unity.Rendering +{ + [BurstCompile] + internal unsafe struct SelectLodEnabled : IJobChunk + { + [ReadOnly] public LODGroupExtensions.LODParams LODParams; + [ReadOnly] public NativeList ForceLowLOD; + [ReadOnly] public ComponentTypeHandle RootLODRanges; + [ReadOnly] public ComponentTypeHandle RootLODReferencePoints; + [ReadOnly] public ComponentTypeHandle LODRanges; + [ReadOnly] public ComponentTypeHandle LODReferencePoints; + public ushort CameraMoveDistanceFixed16; + public float DistanceScale; + public bool DistanceScaleChanged; + + public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public ComponentTypeHandle ChunkHeader; + +#if UNITY_EDITOR + [NativeDisableUnsafePtrRestriction] public EntitiesGraphicsPerThreadStats* Stats; + +#pragma warning disable 649 + [NativeSetThreadIndex] public int ThreadIndex; +#pragma warning restore 649 + +#endif + + public void Execute(in ArchetypeChunk archetypeChunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var entitiesGraphicsChunkInfoArray = archetypeChunk.GetNativeArray(EntitiesGraphicsChunkInfo); + var chunkHeaderArray = archetypeChunk.GetNativeArray(ChunkHeader); + +#if UNITY_EDITOR + ref var stats = ref Stats[ThreadIndex]; +#endif + + for (int entityIndex = 0, chunkEntityCount = archetypeChunk.Count; entityIndex < chunkEntityCount; entityIndex++) + { + var entitiesGraphicsChunkInfo = entitiesGraphicsChunkInfoArray[entityIndex]; + if (!entitiesGraphicsChunkInfo.Valid) + continue; + + var chunkHeader = chunkHeaderArray[entityIndex]; + +#if UNITY_EDITOR + stats.LodTotal++; +#endif + var batchIndex = entitiesGraphicsChunkInfo.BatchIndex; + var chunkInstanceCount = chunkHeader.ArchetypeChunk.Count; + var isOrtho = LODParams.isOrtho; + + ref var chunkCullingData = ref entitiesGraphicsChunkInfo.CullingData; + ChunkInstanceLodEnabled chunkEntityLodEnabled = chunkCullingData.InstanceLodEnableds; + +#if UNITY_EDITOR + ChunkInstanceLodEnabled oldEntityLodEnabled = chunkEntityLodEnabled; +#endif + var forceLowLOD = ForceLowLOD[batchIndex]; + + if (0 == (chunkCullingData.Flags & EntitiesGraphicsChunkCullingData.kFlagHasLodData)) + { +#if UNITY_EDITOR + stats.LodNoRequirements++; +#endif + chunkEntityLodEnabled.Enabled[0] = 0; + chunkEntityLodEnabled.Enabled[1] = 0; + chunkCullingData.ForceLowLODPrevious = forceLowLOD; + + for (int i = 0; i < chunkInstanceCount; ++i) + { + int wordIndex = i >> 6; + int bitIndex = i & 63; + chunkEntityLodEnabled.Enabled[wordIndex] |= 1ul << bitIndex; + } + } + else + { + int diff = (int)chunkCullingData.MovementGraceFixed16 - CameraMoveDistanceFixed16; + chunkCullingData.MovementGraceFixed16 = (ushort)math.max(0, diff); + + var graceExpired = chunkCullingData.MovementGraceFixed16 == 0; + var forceLodChanged = forceLowLOD != chunkCullingData.ForceLowLODPrevious; + + if (graceExpired || forceLodChanged || DistanceScaleChanged) + { + chunkEntityLodEnabled.Enabled[0] = 0; + chunkEntityLodEnabled.Enabled[1] = 0; + +#if UNITY_EDITOR + stats.LodChunksTested++; +#endif + var chunk = chunkHeader.ArchetypeChunk; + + var rootLODRanges = chunk.GetNativeArray(RootLODRanges); + var rootLODReferencePoints = chunk.GetNativeArray(RootLODReferencePoints); + var lodRanges = chunk.GetNativeArray(LODRanges); + var lodReferencePoints = chunk.GetNativeArray(LODReferencePoints); + + float graceDistance = float.MaxValue; + + for (int i = 0; i < chunkInstanceCount; i++) + { + var rootLODRange = rootLODRanges[i]; + var rootLODReferencePoint = rootLODReferencePoints[i]; + + var rootLodDistance = + math.select( + DistanceScale * + math.length(LODParams.cameraPos - rootLODReferencePoint.Value), + DistanceScale, isOrtho); + + float rootMinDist = math.select(rootLODRange.LOD.MinDist, 0.0f, forceLowLOD == 1); + float rootMaxDist = rootLODRange.LOD.MaxDist; + + graceDistance = math.min(math.abs(rootLodDistance - rootMinDist), graceDistance); + graceDistance = math.min(math.abs(rootLodDistance - rootMaxDist), graceDistance); + + var rootLodIntersect = (rootLodDistance < rootMaxDist) && (rootLodDistance >= rootMinDist); + + if (rootLodIntersect) + { + var lodRange = lodRanges[i]; + var lodReferencePoint = lodReferencePoints[i]; + + var instanceDistance = + math.select( + DistanceScale * + math.length(LODParams.cameraPos - + lodReferencePoint.Value), DistanceScale, + isOrtho); + + var instanceLodIntersect = + (instanceDistance < lodRange.MaxDist) && + (instanceDistance >= lodRange.MinDist); + + graceDistance = math.min(math.abs(instanceDistance - lodRange.MinDist), + graceDistance); + graceDistance = math.min(math.abs(instanceDistance - lodRange.MaxDist), + graceDistance); + + if (instanceLodIntersect) + { + var index = i; + var wordIndex = index >> 6; + var bitIndex = index & 0x3f; + var lodWord = chunkEntityLodEnabled.Enabled[wordIndex]; + + lodWord |= 1UL << bitIndex; + chunkEntityLodEnabled.Enabled[wordIndex] = lodWord; + } + } + } + + chunkCullingData.MovementGraceFixed16 = Fixed16CamDistance.FromFloatFloor(graceDistance); + chunkCullingData.ForceLowLODPrevious = forceLowLOD; + } + } + + +#if UNITY_EDITOR + if (oldEntityLodEnabled.Enabled[0] != chunkEntityLodEnabled.Enabled[0] || + oldEntityLodEnabled.Enabled[1] != chunkEntityLodEnabled.Enabled[1]) + { + stats.LodChanged++; + } +#endif + + chunkCullingData.InstanceLodEnableds = chunkEntityLodEnabled; + entitiesGraphicsChunkInfoArray[entityIndex] = entitiesGraphicsChunkInfo; + } + } + } + + internal unsafe struct ChunkVisibility + { + public fixed ulong VisibleEntities[2]; + public fixed byte SplitMasks[128]; + + public bool AnyVisible => (VisibleEntities[0] | VisibleEntities[1]) != 0; + } + + internal unsafe struct ChunkVisibilityItem + { + public ArchetypeChunk Chunk; + public ChunkVisibility* Visibility; + } + + internal static class CullingExtensions + { + // We want to use UnsafeList to use RewindableAllocator, but PlanePacket APIs want NativeArrays + internal static unsafe NativeArray AsNativeArray(this UnsafeList list) where T : unmanaged + { + var array = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(list.Ptr, list.Length, Allocator.None); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref array, AtomicSafetyHandle.GetTempMemoryHandle()); +#endif + return array; + } + + internal static NativeArray GetSubNativeArray(this UnsafeList list, int start, int length) + where T : unmanaged => + list.AsNativeArray().GetSubArray(start, length); + } + + internal unsafe struct CullingSplits + { + [ReadOnly] public UnsafeList BackfacingReceiverPlanes; + [ReadOnly] public UnsafeList SplitPlanePackets; + [ReadOnly] public UnsafeList ReceiverPlanePackets; + [ReadOnly] public UnsafeList CombinedSplitAndReceiverPlanePackets; + [ReadOnly] public UnsafeList Splits; + [ReadOnly] public SOASphereTest SplitSOASphereTest; + + public float3 LightAxisX; + public float3 LightAxisY; + public bool SphereTestEnabled; + + public CullingSplits( + ref BatchCullingContext cullingContext, + ShadowProjection shadowProjection, + RewindableAllocator* allocator) + { + BackfacingReceiverPlanes = default; + SplitPlanePackets = default; + ReceiverPlanePackets = default; + CombinedSplitAndReceiverPlanePackets = default; + Splits = default; + SplitSOASphereTest = default; + + LightAxisX = default; + LightAxisY = default; + SphereTestEnabled = false; + + var allocatorHandle = allocator->Handle; + + // Initialize receiver planes first, so they are ready to be combined in + // InitializeSplits + InitializeReceiverPlanes(ref cullingContext, allocatorHandle); + InitializeSplits(ref cullingContext, allocatorHandle); + InitializeSphereTest(ref cullingContext, shadowProjection, allocatorHandle); + } + + private void InitializeReceiverPlanes(ref BatchCullingContext cullingContext, AllocatorManager.AllocatorHandle allocator) + { +#if DISABLE_HYBRID_RECEIVER_CULLING + bool disableReceiverCulling = true; +#else + bool disableReceiverCulling = false; +#endif + // Receiver culling is only used for shadow maps + if ((cullingContext.viewType != BatchCullingViewType.Light) || + (cullingContext.receiverPlaneCount == 0) || + disableReceiverCulling) + { + // Make an empty array so job system doesn't complain. + ReceiverPlanePackets = new UnsafeList(0, allocator); + return; + } + + bool isOrthographic = cullingContext.projectionType == BatchCullingProjectionType.Orthographic; + int numPlanes = 0; + + var planes = cullingContext.cullingPlanes.GetSubArray( + cullingContext.receiverPlaneOffset, + cullingContext.receiverPlaneCount); + BackfacingReceiverPlanes = new UnsafeList(planes.Length, allocator); + BackfacingReceiverPlanes.Resize(planes.Length); + + float3 lightDir = ((float4)cullingContext.localToWorldMatrix.GetColumn(2)).xyz; + Vector3 lightPos = cullingContext.localToWorldMatrix.GetPosition(); + + for (int i = 0; i < planes.Length; ++i) + { + var p = planes[i]; + float3 n = p.normal; + + const float kEpsilon = (float)1e-12; + + // Compare with epsilon so that perpendicular planes are not counted + // as back facing + bool isBackfacing = isOrthographic + ? math.dot(n, lightDir) < -kEpsilon + : p.GetSide(lightPos); + + if (isBackfacing) + { + BackfacingReceiverPlanes[numPlanes] = p; + ++numPlanes; + } + } + + ReceiverPlanePackets = FrustumPlanes.BuildSOAPlanePackets( + BackfacingReceiverPlanes.GetSubNativeArray(0, numPlanes), + allocator); + BackfacingReceiverPlanes.Resize(numPlanes); + } + +#if DEBUG_VALIDATE_EXTRA_SPLITS + private static int s_DebugExtraSplitsCounter = 0; +#endif + + private void InitializeSplits(ref BatchCullingContext cullingContext, AllocatorManager.AllocatorHandle allocator) + { + var cullingPlanes = cullingContext.cullingPlanes; + var cullingSplits = cullingContext.cullingSplits; + + int numSplits = cullingSplits.Length; + +#if DEBUG_VALIDATE_EXTRA_SPLITS + // If extra splits validation is enabled, pad the split number so it's between 5 and 8 by copying existing + // splits, to ensure that the code functions correctly with higher split counts. + if (numSplits > 1 && numSplits < 5) + { + numSplits = 5 + s_DebugExtraSplitsCounter; + s_DebugExtraSplitsCounter = (s_DebugExtraSplitsCounter + 1) % 4; + } +#endif + + Debug.Assert(numSplits > 0, "No culling splits provided, expected at least 1"); + Debug.Assert(numSplits <= 8, "Split count too high, only up to 8 splits supported"); + + int planePacketCount = 0; + int combinedPlanePacketCount = 0; + for (int i = 0; i < numSplits; ++i) + { + int splitIndex = i; +#if DEBUG_VALIDATE_EXTRA_SPLITS + splitIndex %= cullingSplits.Length; +#endif + + planePacketCount += (cullingSplits[splitIndex].cullingPlaneCount + 3) / 4; + combinedPlanePacketCount += + ((cullingSplits[splitIndex].cullingPlaneCount + BackfacingReceiverPlanes.Length) + 3) / 4; + } + + SplitPlanePackets = new UnsafeList(planePacketCount, allocator); + CombinedSplitAndReceiverPlanePackets = new UnsafeList(combinedPlanePacketCount, allocator); + Splits = new UnsafeList(numSplits, allocator); + + var combinedPlanes = new UnsafeList(combinedPlanePacketCount * 4, allocator); + + int planeIndex = 0; + int combinedPlaneIndex = 0; + + // In the special case where there is only a single split with zero planes, compute culling planes + // based on the culling matrix. + bool noSplitPlanes = numSplits == 1 && cullingSplits[0].cullingPlaneCount == 0; + + for (int i = 0; i < numSplits; ++i) + { + int splitIndex = i; +#if DEBUG_VALIDATE_EXTRA_SPLITS + splitIndex %= cullingSplits.Length; +#endif + + var s = cullingSplits[splitIndex]; + float3 p = s.sphereCenter; + float r = s.sphereRadius; + + if (s.sphereRadius <= 0) + r = 0; + + var splitCullingPlanes = cullingPlanes.GetSubArray(s.cullingPlaneOffset, s.cullingPlaneCount); + + if (noSplitPlanes) + splitCullingPlanes = CullingPlanesFromMatrix(s.cullingMatrix); + + var planePackets = FrustumPlanes.BuildSOAPlanePackets( + splitCullingPlanes, + allocator); + + foreach (var pp in planePackets) + SplitPlanePackets.Add(pp); + + combinedPlanes.Resize(splitCullingPlanes.Length + BackfacingReceiverPlanes.Length); + + // Make combined packets that have both the split planes and the receiver planes so + // they can be tested simultaneously + UnsafeUtility.MemCpy( + combinedPlanes.Ptr, + splitCullingPlanes.GetUnsafeReadOnlyPtr(), + splitCullingPlanes.Length * UnsafeUtility.SizeOf()); + UnsafeUtility.MemCpy( + combinedPlanes.Ptr + splitCullingPlanes.Length, + BackfacingReceiverPlanes.Ptr, + BackfacingReceiverPlanes.Length * UnsafeUtility.SizeOf()); + + var combined = FrustumPlanes.BuildSOAPlanePackets( + combinedPlanes.AsNativeArray(), + allocator); + + foreach (var pp in combined) + CombinedSplitAndReceiverPlanePackets.Add(pp); + + Splits.Add(new CullingSplitData + { + CullingSphereCenter = p, + CullingSphereRadius = r, + PlanePacketOffset = planeIndex, + PlanePacketCount = planePackets.Length, + CombinedPlanePacketOffset = combinedPlaneIndex, + CombinedPlanePacketCount = combined.Length, + }); + + planeIndex += planePackets.Length; + combinedPlaneIndex += combined.Length; + } + } + + private static Plane[] s_CullingPlanes = new Plane[6]; + private static NativeArray CullingPlanesFromMatrix(Matrix4x4 cullingMatrix) + { + GeometryUtility.CalculateFrustumPlanes(cullingMatrix, s_CullingPlanes); + var planeArray = new NativeArray(s_CullingPlanes, Allocator.Temp); + return planeArray; + } + + private void InitializeSphereTest(ref BatchCullingContext cullingContext, ShadowProjection shadowProjection, AllocatorManager.AllocatorHandle allocator) + { + // Receiver sphere testing is only enabled if the cascade projection is stable + bool projectionIsStable = shadowProjection == ShadowProjection.StableFit; + bool allSplitsHaveValidReceiverSpheres = true; + for (int i = 0; i < Splits.Length; ++i) + { + // This should also catch NaNs, which return false + // for every comparison. + if (!(Splits[i].CullingSphereRadius > 0)) + { + allSplitsHaveValidReceiverSpheres = false; + break; + } + } + + if (projectionIsStable && allSplitsHaveValidReceiverSpheres) + { + LightAxisX = new float4(cullingContext.localToWorldMatrix.GetColumn(0)).xyz; + LightAxisY = new float4(cullingContext.localToWorldMatrix.GetColumn(1)).xyz; + + SplitSOASphereTest = new SOASphereTest(ref this, allocator); + + SphereTestEnabled = true; + } + } + + public float2 TransformToLightSpaceXY(float3 positionWS) => new float2( + math.dot(positionWS, LightAxisX), + math.dot(positionWS, LightAxisY)); + } + + internal unsafe struct CullingSplitData + { + public float3 CullingSphereCenter; + public float CullingSphereRadius; + public int PlanePacketOffset; + public int PlanePacketCount; + public int CombinedPlanePacketOffset; + public int CombinedPlanePacketCount; + } + + [BurstCompile] + internal unsafe struct IncludeExcludeListFilter + { +#if !DISABLE_INCLUDE_EXCLUDE_LIST_FILTERING + public NativeParallelHashSet IncludeEntityIndices; + public NativeParallelHashSet ExcludeEntityIndices; + public bool IsIncludeEnabled; + public bool IsExcludeEnabled; + + public bool IsEnabled => IsIncludeEnabled || IsExcludeEnabled; + public bool IsIncludeEmpty => IncludeEntityIndices.IsEmpty; + public bool IsExcludeEmpty => ExcludeEntityIndices.IsEmpty; + + public IncludeExcludeListFilter( + EntityManager entityManager, + NativeArray includeEntityIndices, + NativeArray excludeEntityIndices, + Allocator allocator) + { + IncludeEntityIndices = default; + ExcludeEntityIndices = default; + + // Null NativeArray means that the list shoudln't be used for filtering + IsIncludeEnabled = includeEntityIndices.IsCreated; + IsExcludeEnabled = excludeEntityIndices.IsCreated; + + if (IsIncludeEnabled) + { + IncludeEntityIndices = new NativeParallelHashSet(includeEntityIndices.Length, allocator); + for (int i = 0; i < includeEntityIndices.Length; ++i) + IncludeEntityIndices.Add(includeEntityIndices[i]); + } + else + { + // NativeParallelHashSet must be non-null even if empty to be passed to jobs. Otherwise errors happen. + IncludeEntityIndices = new NativeParallelHashSet(0, allocator); + } + + if (IsExcludeEnabled) + { + ExcludeEntityIndices = new NativeParallelHashSet(excludeEntityIndices.Length, allocator); + for (int i = 0; i < excludeEntityIndices.Length; ++i) + ExcludeEntityIndices.Add(excludeEntityIndices[i]); + } + else + { + // NativeParallelHashSet must be non-null even if empty to be passed to jobs. Otherwise errors happen. + ExcludeEntityIndices = new NativeParallelHashSet(0, allocator); + } + } + + public void Dispose() + { + if (IncludeEntityIndices.IsCreated) + IncludeEntityIndices.Dispose(); + + if (ExcludeEntityIndices.IsCreated) + ExcludeEntityIndices.Dispose(); + } + + public JobHandle Dispose(JobHandle dependencies) + { + JobHandle disposeInclude = IncludeEntityIndices.IsCreated ? IncludeEntityIndices.Dispose(dependencies) : default; + JobHandle disposeExclude = ExcludeEntityIndices.IsCreated ? ExcludeEntityIndices.Dispose(dependencies) : default; + return JobHandle.CombineDependencies(disposeInclude, disposeExclude); + } + + public bool EntityPassesFilter(int entityIndex) + { + if (IsIncludeEnabled) + { + if (!IncludeEntityIndices.Contains(entityIndex)) + return false; + } + + if (IsExcludeEnabled) + { + if (ExcludeEntityIndices.Contains(entityIndex)) + return false; + } + + return true; + } +#else + public bool IsIncludeEnabled => false; + public bool IsExcludeEnabled => false; + public bool IsEnabled => false; + public bool IsIncludeEmpty => true; + public bool IsExcludeEmpty => true; + public bool EntityPassesFilter(int entityIndex) => true; + public void Dispose() { } + public JobHandle Dispose(JobHandle dependencies) => new JobHandle(); +#endif + } + + [BurstCompile] + internal unsafe struct FrustumCullingJob : IJobChunk + { + public IndirectList VisibilityItems; + public ThreadLocalAllocator ThreadLocalAllocator; + + [ReadOnly] public CullingSplits Splits; + + [ReadOnly] public ComponentTypeHandle BoundsComponent; + [ReadOnly] public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public ComponentTypeHandle ChunkWorldRenderBounds; + + [ReadOnly] public IncludeExcludeListFilter IncludeExcludeListFilter; + [ReadOnly] public EntityTypeHandle EntityHandle; + + public BatchCullingViewType CullingViewType; + +#pragma warning disable 649 + [NativeSetThreadIndex] public int ThreadIndex; +#pragma warning restore 649 + +#if UNITY_EDITOR + [NativeDisableUnsafePtrRestriction] public EntitiesGraphicsPerThreadStats* Stats; +#endif + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var allocator = ThreadLocalAllocator.ThreadAllocator(ThreadIndex); + var visibilityItemWriter = VisibilityItems.List->AsParallelWriter(); + ChunkVisibility chunkVisibility; + + bool isLightCulling = CullingViewType == BatchCullingViewType.Light; + + var entitiesGraphicsChunkInfo = chunk.GetChunkComponentData(EntitiesGraphicsChunkInfo); + if (!entitiesGraphicsChunkInfo.Valid) + return; + + var chunkBounds = chunk.GetChunkComponentData(ChunkWorldRenderBounds); + +#if UNITY_EDITOR + ref var stats = ref Stats[ThreadIndex]; + stats.ChunkTotal++; +#else + var stats = new EntitiesGraphicsPerThreadStats{}; +#endif + + ref var chunkCullingData = ref entitiesGraphicsChunkInfo.CullingData; + var chunkEntityLodEnabled = chunkCullingData.InstanceLodEnableds; + var anyLodEnabled = (chunkEntityLodEnabled.Enabled[0] | chunkEntityLodEnabled.Enabled[1]) != 0; + + chunkVisibility.VisibleEntities[0] = 0; + chunkVisibility.VisibleEntities[1] = 0; + + var perInstanceCull = 0 != (chunkCullingData.Flags & EntitiesGraphicsChunkCullingData.kFlagInstanceCulling); + + if (anyLodEnabled) + { + stats.ChunkCountAnyLod++; + if (isLightCulling) + { + bool useSphereTest = Splits.SphereTestEnabled; +#if DISABLE_HYBRID_SPHERE_CULLING + useSphereTest = false; +#endif + FrustumCullWithReceiverAndSphereCulling(chunkBounds, chunk, chunkEntityLodEnabled, + perInstanceCull, &chunkVisibility, ref stats, + useSphereTest: useSphereTest); + } + else + FrustumCull(chunkBounds, chunk, chunkEntityLodEnabled, perInstanceCull, &chunkVisibility, ref stats); + + if (chunkVisibility.AnyVisible) + { + var visibilityItem = new ChunkVisibilityItem + { + Chunk = chunk, + Visibility = allocator->Allocate(chunkVisibility, 1), + }; + UnsafeUtility.MemCpy(visibilityItem.Visibility, &chunkVisibility, UnsafeUtility.SizeOf()); + visibilityItemWriter.AddNoResize(visibilityItem); + } + } + } + + private void FrustumCull(ChunkWorldRenderBounds chunkBounds, + ArchetypeChunk chunk, + ChunkInstanceLodEnabled chunkEntityLodEnabled, + bool perInstanceCull, + ChunkVisibility* chunkVisibility, + ref EntitiesGraphicsPerThreadStats stats) + { + Debug.Assert(Splits.Splits.Length == 1); + + var chunkIn = perInstanceCull + ? FrustumPlanes.Intersect2(Splits.SplitPlanePackets.AsNativeArray(), chunkBounds.Value) + : FrustumPlanes.Intersect2NoPartial(Splits.SplitPlanePackets.AsNativeArray(), chunkBounds.Value); + + // Have to filter all entities separately if the filter is enabled + if (IncludeExcludeListFilter.IsEnabled && chunkIn == FrustumPlanes.IntersectResult.In) + chunkIn = FrustumPlanes.IntersectResult.Partial; + + if (chunkIn == FrustumPlanes.IntersectResult.Partial) + { +#if UNITY_EDITOR + int instanceTestCount = 0; +#endif + var chunkInstanceBounds = chunk.GetNativeArray(BoundsComponent); + var chunkEntities = chunk.GetNativeArray(EntityHandle); + + for (int j = 0; j < 2; j++) + { + var lodWord = chunkEntityLodEnabled.Enabled[j]; + ulong visibleWord = 0; + + while (lodWord != 0) + { + var bitIndex = math.tzcnt(lodWord); + var finalIndex = (j << 6) + bitIndex; + + int visible = FrustumPlanes.Intersect2NoPartial(Splits.SplitPlanePackets.AsNativeArray(), chunkInstanceBounds[finalIndex].Value) != + FrustumPlanes.IntersectResult.Out + ? 1 + : 0; + + if (IncludeExcludeListFilter.IsEnabled && visible != 0) + { + if (!IncludeExcludeListFilter.EntityPassesFilter(chunkEntities[finalIndex].Index)) + visible = 0; + } + + lodWord ^= 1ul << bitIndex; + visibleWord |= ((ulong)visible) << bitIndex; + +#if UNITY_EDITOR + instanceTestCount++; +#endif + } + + chunkVisibility->VisibleEntities[j] = visibleWord; + } + +#if UNITY_EDITOR + stats.ChunkCountInstancesProcessed++; + stats.InstanceTests += instanceTestCount; +#endif + } + else if (chunkIn == FrustumPlanes.IntersectResult.In) + { +#if UNITY_EDITOR + stats.ChunkCountFullyIn++; +#endif + for (int j = 0; j < 2; j++) + { + var lodWord = chunkEntityLodEnabled.Enabled[j]; + chunkVisibility->VisibleEntities[j] = lodWord; + } + } + else if (chunkIn == FrustumPlanes.IntersectResult.Out) + { + // No need to do anything + } + } + + private void FrustumCullWithReceiverAndSphereCulling( + ChunkWorldRenderBounds chunkBounds, + ArchetypeChunk chunk, + ChunkInstanceLodEnabled chunkEntityLodEnabled, + bool perInstanceCull, + ChunkVisibility* chunkVisibility, + ref EntitiesGraphicsPerThreadStats stats, + bool useSphereTest) + { + int numEntities = chunk.Count; + + ref var receiverPlanes = ref Splits.ReceiverPlanePackets; + + bool haveReceiverPlanes = Splits.ReceiverPlanePackets.Length > 0; + // Do chunk receiver test first, since it doesn't consider splits + if (haveReceiverPlanes) + { + if (FrustumPlanes.Intersect2NoPartial(receiverPlanes.AsNativeArray(), chunkBounds.Value) == + FrustumPlanes.IntersectResult.Out) + return; + } + + // Initially set zero split mask for every entity in the chunk + UnsafeUtility.MemSet(chunkVisibility->SplitMasks, 0, numEntities); + + ref var splits = ref Splits.Splits; + + var worldRenderBounds = chunk.GetNativeArray(BoundsComponent); + + // First, perform frustum and receiver plane culling for all splits + for (int splitIndex = 0; splitIndex < splits.Length; ++splitIndex) + { + var s = splits[splitIndex]; + + byte splitMask = (byte)(1 << splitIndex); + + var splitPlanes = Splits.SplitPlanePackets.GetSubNativeArray( + s.PlanePacketOffset, + s.PlanePacketCount); + var combinedSplitPlanes = Splits.CombinedSplitAndReceiverPlanePackets.GetSubNativeArray( + s.CombinedPlanePacketOffset, + s.CombinedPlanePacketCount); + + float2 receiverSphereLightSpace = Splits.TransformToLightSpaceXY(s.CullingSphereCenter); + + // If the entire chunk fails the sphere test, no need to consider further + if (useSphereTest && SphereTest(s, chunkBounds.Value, receiverSphereLightSpace) == SphereTestResult.CannotCastShadow) + continue; + + var chunkIn = perInstanceCull + ? FrustumPlanes.Intersect2(splitPlanes, chunkBounds.Value) + : FrustumPlanes.Intersect2NoPartial(splitPlanes, chunkBounds.Value); + + if (chunkIn == FrustumPlanes.IntersectResult.Partial) + { + for (int j = 0; j < 2; j++) + { + ulong lodWord = chunkEntityLodEnabled.Enabled[j]; + ulong visibleWord = 0; + + while (lodWord != 0) + { + var bitIndex = math.tzcnt(lodWord); + var entityIndex = (j << 6) + bitIndex; + ulong mask = 1ul << bitIndex; + + var bounds = worldRenderBounds[entityIndex].Value; + + int visible = + FrustumPlanes.Intersect2NoPartial(combinedSplitPlanes, bounds) != FrustumPlanes.IntersectResult.Out + ? 1 + : 0; + +#if DEBUG_VALIDATE_COMBINED_SPLIT_RECEIVER_CULLING + bool visibleFrustum = FrustumPlanes.Intersect2NoPartial(splitPlanes, bounds) != FrustumPlanes.IntersectResult.Out; + bool visibleReceiver = FrustumPlanes.Intersect2NoPartial(receiverPlanes.AsNativeArray(), bounds) != FrustumPlanes.IntersectResult.Out; + int visibleReference = (visibleFrustum && visibleReceiver) ? 1 : 0; + // Use Debug.Log instead of Debug.Assert so that Burst does not remove it + if (visible != visibleReference) + Debug.Log($"Combined Split+Receiver ({visible}) plane culling mismatch with separate Split ({visibleFrustum}) and Receiver ({visibleReceiver})"); +#endif + + lodWord ^= mask; + visibleWord |= ((ulong)visible) << bitIndex; + + if (visible != 0) + chunkVisibility->SplitMasks[entityIndex] |= splitMask; + } + + chunkVisibility->VisibleEntities[j] |= visibleWord; + } + } + else if (chunkIn == FrustumPlanes.IntersectResult.In) + { + // VisibleEntities contains the union of all splits, so enable bits + // for this split + chunkVisibility->VisibleEntities[0] |= chunkEntityLodEnabled.Enabled[0]; + chunkVisibility->VisibleEntities[1] |= chunkEntityLodEnabled.Enabled[1]; + + for (int i = 0; i < numEntities; ++i) + chunkVisibility->SplitMasks[i] |= splitMask; + } + else if (chunkIn == FrustumPlanes.IntersectResult.Out) + { + // No need to do anything. Split mask bits for this split should already + // be cleared since they were initialized to zero. + } + } + + // If anything survived the culling, perform sphere testing for each split + if (useSphereTest && chunkVisibility->AnyVisible) + { + for (int j = 0; j < 2; j++) + { + ulong visibleWord = chunkVisibility->VisibleEntities[j]; + + while (visibleWord != 0) + { + int bitIndex = math.tzcnt(visibleWord); + int entityIndex = (j << 6) + bitIndex; + ulong mask = 1ul << bitIndex; + + var bounds = worldRenderBounds[entityIndex].Value; + + int planeSplitMask = chunkVisibility->SplitMasks[entityIndex]; + int sphereSplitMask = Splits.SplitSOASphereTest.SOASphereTestSplitMask(ref Splits, bounds); + +#if DEBUG_VALIDATE_SOA_SPHERE_TEST + int referenceSphereSplitMask = 0; + for (int splitIndex = 0; splitIndex < splits.Length; ++splitIndex) + { + var s = splits[splitIndex]; + byte splitMask = (byte)(1 << splitIndex); + float2 receiverSphereLightSpace = Splits.TransformToLightSpaceXY(s.CullingSphereCenter); + if (SphereTest(s, bounds, receiverSphereLightSpace) == SphereTestResult.MightCastShadow) + referenceSphereSplitMask |= splitMask; + } + // Use Debug.Log instead of Debug.Assert so that Burst does not remove it + if (sphereSplitMask != referenceSphereSplitMask) + Debug.Log($"SoA sphere test ({sphereSplitMask:x2}) disagrees with reference sphere tests ({referenceSphereSplitMask:x2})"); +#endif + + byte newSplitMask = (byte)(planeSplitMask & sphereSplitMask); + chunkVisibility->SplitMasks[entityIndex] = newSplitMask; + + if (newSplitMask == 0) + chunkVisibility->VisibleEntities[j] ^= mask; + + visibleWord ^= mask; + } + } + } + } + + private enum SphereTestResult + { + // The caster is guaranteed to not cast a visible shadow in the tested cascade + CannotCastShadow, + // The caster might cast a shadow in the tested cascade, and has to be rendered in the shadow map + MightCastShadow, + } + + private SphereTestResult SphereTest(CullingSplitData split, AABB aabb, float2 receiverSphereLightSpace) + { + // This test has been ported from the corresponding test done by Unity's + // built in shadow culling. + + float casterRadius = math.length(aabb.Extents); + float2 casterCenterLightSpaceXY = Splits.TransformToLightSpaceXY(aabb.Center); + + // A spherical caster casts a cylindrical shadow volume. In XY in light space this ends up being a circle/circle intersection test. + // Thus we first check if the caster bounding circle is at least partially inside the cascade circle. + float sqrDistBetweenCasterAndCascadeCenter = math.lengthsq(casterCenterLightSpaceXY - receiverSphereLightSpace); + float combinedRadius = casterRadius + split.CullingSphereRadius; + float sqrCombinedRadius = combinedRadius * combinedRadius; + + // If the 2D circles intersect, then the caster is potentially visible in the cascade. + // If they don't intersect, then there is no way for the caster to cast a shadow that is + // visible inside the circle. + // Casters that intersect the circle but are behind the receiver sphere also don't cast shadows. + // We don't consider that here, since those casters should be culled out by the receiver + // plane culling. + if (sqrDistBetweenCasterAndCascadeCenter <= sqrCombinedRadius) + return SphereTestResult.MightCastShadow; + else + return SphereTestResult.CannotCastShadow; + } + } + + internal struct SOASphereTest + { + [NoAlias] public UnsafeList ReceiverCenterX; + [NoAlias] public UnsafeList ReceiverCenterY; + [NoAlias] public UnsafeList ReceiverRadius; + + public SOASphereTest(ref CullingSplits splits, AllocatorManager.AllocatorHandle allocator) + { + int numSplits = splits.Splits.Length; + int numPackets = (numSplits + 3) / 4; + + Debug.Assert(numSplits > 0, "No valid culling splits for sphere testing"); + + ReceiverCenterX = new UnsafeList(numPackets, allocator); + ReceiverCenterY = new UnsafeList(numPackets, allocator); + ReceiverRadius = new UnsafeList(numPackets, allocator); + ReceiverCenterX.Resize(numPackets); + ReceiverCenterY.Resize(numPackets); + ReceiverRadius.Resize(numPackets); + + // Initialize the last packet with values that will always fail the sphere test + int lastPacket = numPackets - 1; + ReceiverCenterX[lastPacket] = new float4(float.PositiveInfinity); + ReceiverCenterY[lastPacket] = new float4(float.PositiveInfinity); + ReceiverRadius[lastPacket] = float4.zero; + + for (int i = 0; i < numSplits; ++i) + { + int packetIndex = i >> 2; + int elementIndex = i & 3; + + float2 receiverCenter = splits.TransformToLightSpaceXY(splits.Splits[i].CullingSphereCenter); + ReceiverCenterX.ElementAt(packetIndex)[elementIndex] = receiverCenter.x; + ReceiverCenterY.ElementAt(packetIndex)[elementIndex] = receiverCenter.y; + ReceiverRadius.ElementAt(packetIndex)[elementIndex] = splits.Splits[i].CullingSphereRadius; + } + } + + public int SOASphereTestSplitMask(ref CullingSplits splits, AABB aabb) + { + int numPackets = ReceiverRadius.Length; + + float4 casterRadius = new float4(math.length(aabb.Extents)); + float2 casterCenter = splits.TransformToLightSpaceXY(aabb.Center); + float4 casterCenterX = casterCenter.xxxx; + float4 casterCenterY = casterCenter.yyyy; + + int splitMask = 0; + int splitMaskShift = 0; + for (int i = 0; i < numPackets; ++i) + { + float4 dx = casterCenterX - ReceiverCenterX[i]; + float4 dy = casterCenterY - ReceiverCenterY[i]; + float4 sqrDistBetweenCasterAndCascadeCenter = dx * dx + dy * dy; + float4 combinedRadius = casterRadius + ReceiverRadius[i]; + float4 sqrCombinedRadius = combinedRadius * combinedRadius; + bool4 mightCastShadow = sqrDistBetweenCasterAndCascadeCenter <= sqrCombinedRadius; + int splitMask4 = math.bitmask(mightCastShadow); + // Packet 0 is for bits 0-3, packet 1 is for bits 4-7 etc. + splitMask |= splitMask4 << splitMaskShift; + splitMaskShift += 4; + } + + return splitMask; + } + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs.meta new file mode 100644 index 0000000..96bd3ab --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsCulling.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41638b9668f707a46acd7d4ec4cde02e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs b/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs new file mode 100644 index 0000000..31aea08 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs @@ -0,0 +1,45 @@ +using UnityEditor; + +namespace Unity.Rendering +{ + internal static class EntitiesGraphicsEditorTools + { + internal struct EntitiesGraphicsDebugSettings + { + // error CS0649: Field is never assigned to, and will always have its default value 0 +#pragma warning disable CS0649 + public bool RecreateAllBatches; + public bool ForceInstanceDataUpload; +#pragma warning restore CS0649 + } + +#if UNITY_EDITOR + [MenuItem("Edit/Rendering/Entities Graphics/Reupload all instance data")] + internal static void ReuploadAllInstanceData() + { + s_EntitiesGraphicsDebugSettings.ForceInstanceDataUpload = true; + } + + [MenuItem("Edit/Rendering/Entities Graphics/Recreate all batches")] + internal static void RecreateAllBatches() + { + s_EntitiesGraphicsDebugSettings.RecreateAllBatches = true; + } + + internal static void EndFrame() + { + s_EntitiesGraphicsDebugSettings = default; + } + + private static EntitiesGraphicsDebugSettings s_EntitiesGraphicsDebugSettings; + internal static EntitiesGraphicsDebugSettings DebugSettings => s_EntitiesGraphicsDebugSettings; + +#else + internal static void EndFrame() + { + } + + internal static EntitiesGraphicsDebugSettings DebugSettings => default; +#endif + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs.meta new file mode 100644 index 0000000..9777160 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsEditorTools.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58f51e7ea2ec4c8bb0fa59cb4a9bded7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs b/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs new file mode 100644 index 0000000..76d5356 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs @@ -0,0 +1,51 @@ +using Unity.Entities; +using UnityEngine; + +namespace Unity.Rendering +{ + /// + /// An unmanaged component that stores light baking data. + /// + /// + /// Entities Graphics uses this component to store light baking data at conversion time to restore + /// at run time. This is because this doesn't happen automatically with hybrid entities. + /// + public struct LightBakingOutputData : IComponentData + { + /// + /// The output of light baking on the entity. + /// + public LightBakingOutput Value; + } + + /// + /// A tag component that HybridLightBakingDataSystem uses to assign a LightBakingOutput to the bakingOutput of the Light component. + /// + public struct LightBakingOutputDataRestoredTag : IComponentData + {} + + /// + /// Represents a light baking system that assigns a LightBakingOutput to the bakingOutput of the Light component. + /// + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + public partial class HybridLightBakingDataSystem : SystemBase + { + /// + protected override void OnUpdate() + { + Entities + .WithStructuralChanges() + .WithNone() + .ForEach((Entity e, in LightBakingOutputData bakingOutput) => + { + var light = EntityManager.GetComponentObject(e); + + if (light != null) + light.bakingOutput = bakingOutput.Value; + + EntityManager.AddComponent(e); + }).Run(); + } + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs.meta new file mode 100644 index 0000000..0631be0 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsLightBakingDataSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 697863f572504a28a383acea537c5e49 +timeCreated: 1605641118 \ No newline at end of file diff --git a/Unity.Entities.Graphics/EntitiesGraphicsStats.cs b/Unity.Entities.Graphics/EntitiesGraphicsStats.cs new file mode 100644 index 0000000..b025613 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsStats.cs @@ -0,0 +1,236 @@ +namespace Unity.Rendering +{ + + /// + /// Represents per-thread statistics that Entities Graphics collects during runtime. + /// + public struct EntitiesGraphicsPerThreadStats + { + // This struct needs to be padded to a cache line multiple to avoid false sharing + // If you add or remove any members here you must add or remove padding to the struct + + + /// + /// The total number of chunks executed. + /// + /// + /// This stat is only available in the Editor. + /// + public int ChunkTotal; + + + /// + /// The chunk count across all LOD levels. + /// + public int ChunkCountAnyLod; + + + /// + /// The number of chunks partially culled by the frustum. + /// + /// + /// Entities Graphics considers a chunk to be partially culled if the chunk contains some entities that are within the frustum and some entities that are outside the frustum. + /// This stat is only available in the Editor. + /// + public int ChunkCountInstancesProcessed; + + /// + /// The number of chunks that contain entities which are all in the frustum. + /// + /// + /// This stat is only available in the Editor. + /// + public int ChunkCountFullyIn; + + /// + /// The total number of culling tests performed on partially culled chunks. + /// + /// + /// This stat is only available in the Editor. + /// + public int InstanceTests; + + + /// + /// Total count of the LOD executed. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodTotal; + + + /// + /// Number of the culling chunks without LOD data. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodNoRequirements; + + + /// + /// Number of enabled or disabled LODs in this frame. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodChanged; + + + /// + /// Number of tested LOD chunks. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodChunksTested; + + + /// + /// The total number of entities that Entities Graphics renderer. + /// + /// + /// This stat is only available in the Editor. + /// + public int RenderedEntityCount; + + /// + /// The number of the draw commands. + /// + public int DrawCommandCount; + + /// + /// The number of the ranges. + /// + public int DrawRangeCount; + } + + /// + /// Represents statistics that Entities Graphics collects during runtime. + /// + public struct EntitiesGraphicsStats + { + /// + /// Total number of chunks + /// + /// + /// This stat is only available in the Editor. + /// + public int ChunkTotal; + + /// + /// Total number of chunks if any of the LOD are enabled in ChunkInstanceLodEnabled entity. + /// + public int ChunkCountAnyLod; + + /// + /// Total number of chunks processed which are partially intersected with the view frustum. + /// + /// + /// This stat is only available in the Editor. + /// + public int ChunkCountInstancesProcessed; + + /// + /// Total number of chunks processed which are fully inside of the view frustum. + /// + /// + /// This stat is only available in the Editor. + /// + public int ChunkCountFullyIn; + + /// + /// Total number of instance checks across all LODs. + /// + /// + /// This stat is only available in the Editor. + /// + public int InstanceTests; + + /// + /// Total number of LODs across all archetype entities chunks. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodTotal; + + /// + /// Number of the culling chunks without LOD data. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodNoRequirements; + + /// + /// Number of enabled or disabled LODs in this frame. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodChanged; + + /// + /// Number of tested LOD chunks. + /// + /// + /// This stat is only available in the Editor. + /// + public int LodChunksTested; + + /// + /// Camera move distance since the last frame. + /// + /// + /// This stat is only available in the Editor. + /// + public float CameraMoveDistance; + + /// + /// Total number of batches. + /// + public int BatchCount; + + /// + /// Accumulated number of rendered entities across all threads. + /// + /// + /// This stat is only available in the Editor. + /// + public int RenderedInstanceCount; + + /// + /// Accumulated number of the draw commands. + /// + /// + /// This stat is only available in the Editor. + /// + public int DrawCommandCount; + + /// + /// Accumulated number of the draw ranges. + /// + /// + /// This stat is only available in the Editor. + /// + public int DrawRangeCount; + + /// + /// The total number of bytes of the GPU memory used including upload and fence memory. + /// + public long BytesGPUMemoryUsed; + + + /// + /// The number of bytes of the GPU memory used by uploaded in the current frame. + /// + public long BytesGPUMemoryUploadedCurr; + + /// + /// Maximum number of bytes of the GPU memory used for uploading. + /// + public long BytesGPUMemoryUploadedMax; + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsStats.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsStats.cs.meta new file mode 100644 index 0000000..d4efaa5 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsStats.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab519fda51669bb4d85b8617efdf487b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs b/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs new file mode 100644 index 0000000..05a416d --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs @@ -0,0 +1,60 @@ +#if true// USE_BATCH_RENDERER_GROUP +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Unity.Collections; +using Unity.Entities; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Unity.Rendering +{ +#if UNITY_EDITOR + [ExecuteAlways] + [ExecuteInEditMode] + public class EntitiesGraphicsStatsDrawer : MonoBehaviour + { + private bool m_Enabled = true;//false; + + private void Update() + { + if (Input.GetKeyDown(KeyCode.F4)) + { + m_Enabled = !m_Enabled; + } + } + + private void OnGUI() + { + if (m_Enabled && World.DefaultGameObjectInjectionWorld != null) + { + var sys = World.DefaultGameObjectInjectionWorld.GetExistingSystemManaged(); + + var stats = sys.Stats; + + GUILayout.BeginArea(new Rect { x = 10, y = 10, width = 500, height = 800 }, "Entities Graphics Stats", GUI.skin.window); + + GUILayout.Label("Culling stats (all viewports/callbacks):"); + GUILayout.Label($" Chunks:\n Total={stats.ChunkTotal}\n AnyLOD={stats.ChunkCountAnyLod}\n FullIn={stats.ChunkCountFullyIn}\n w/Instance Culling={stats.ChunkCountInstancesProcessed}"); + GUILayout.Label($" Instances tests: {stats.InstanceTests}"); + GUILayout.Label($" Select LOD:\n Total={stats.LodTotal}\n No Requirements={stats.LodNoRequirements}\n Chunks Tested={stats.LodChunksTested}\n Changed={stats.LodChanged}"); + GUILayout.Label($" Camera Move Distance: {stats.CameraMoveDistance} meters"); + GUILayout.Space(20); + GUILayout.Label("Rendering stats (all viewports/callbacks):"); + GUILayout.Label($" Batch Count: {stats.BatchCount}"); + GUILayout.Label($" Rendered Instance Count: {stats.RenderedInstanceCount}"); + GUILayout.Label($" Draw Range Count: {stats.DrawRangeCount}"); + GUILayout.Label($" Draw Command Count: {stats.DrawCommandCount}"); + GUILayout.Label($" GPU Memory:\n Total={EditorUtility.FormatBytes(stats.BytesGPUMemoryUsed)}\n Uploaded={EditorUtility.FormatBytes(stats.BytesGPUMemoryUploadedCurr)}\n Max Uploaded={EditorUtility.FormatBytes(stats.BytesGPUMemoryUploadedMax)}"); + + GUILayout.EndArea(); + } + } + } +#endif +} +#endif diff --git a/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs.meta new file mode 100644 index 0000000..6f2a20d --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsStatsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa8d9da8a80b93f47b8bbedda78d33fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs b/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs new file mode 100644 index 0000000..fc09911 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs @@ -0,0 +1,2384 @@ +// This define fails tests due to the extra log spam. Don't check this in enabled +// #define DEBUG_LOG_HYBRID_RENDERER + +// #define DEBUG_LOG_CHUNK_CHANGES +// #define DEBUG_LOG_GARBAGE_COLLECTION +// #define DEBUG_LOG_BATCH_UPDATES +// #define DEBUG_LOG_CHUNKS +// #define DEBUG_LOG_INVALID_CHUNKS +// #define DEBUG_LOG_UPLOADS +// #define DEBUG_LOG_BATCH_CREATION +// #define DEBUG_LOG_BATCH_DELETION +// #define DEBUG_LOG_PROPERTY_ALLOCATIONS +// #define DEBUG_LOG_PROPERTY_UPDATES +// #define DEBUG_LOG_VISIBLE_INSTANCES +// #define DEBUG_LOG_MATERIAL_PROPERTY_TYPES +// #define DEBUG_LOG_MEMORY_USAGE +// #define DEBUG_LOG_AMBIENT_PROBE +// #define DEBUG_LOG_DRAW_COMMANDS +// #define DEBUG_LOG_DRAW_COMMANDS_VERBOSE +// #define DEBUG_VALIDATE_DRAW_COMMAND_SORT +// #define DEBUG_LOG_BRG_MATERIAL_MESH +// #define DEBUG_LOG_GLOBAL_AABB +// #define PROFILE_BURST_JOB_INTERNALS +// #define DISABLE_HYBRID_RENDERER_ERROR_LOADING_SHADER +// #define DISABLE_INCLUDE_EXCLUDE_LIST_FILTERING + +// Entities Graphics is disabled if SRP 10 is not found, unless an override define is present +// It is also disabled if -nographics is given from the command line. +#if !(SRP_10_0_0_OR_NEWER || HYBRID_RENDERER_ENABLE_WITHOUT_SRP) +#define HYBRID_RENDERER_DISABLED +#endif + +#if UNITY_EDITOR +#define USE_PROPERTY_ASSERTS +#endif + +#if UNITY_EDITOR +#define DEBUG_PROPERTY_NAMES +#endif + +#if ENABLE_UNITY_OCCLUSION +#define USE_UNITY_OCCLUSION +#endif + +#if UNITY_EDITOR && !DISABLE_HYBRID_RENDERER_PICKING +#define ENABLE_PICKING +#endif + +using System; +using System.Collections.Generic; +using System.Text; +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Jobs.LowLevel.Unsafe; +using Unity.Mathematics; +using Unity.Profiling; +using Unity.Entities.Graphics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +#if USE_UNITY_OCCLUSION +using Unity.Rendering.Occlusion; +#endif + +namespace Unity.Rendering +{ +#if UNITY_EDITOR + internal struct BatchEditorRenderData + { + public ulong SceneCullingMask; + } +#endif + + // Describes a single material property that can be mapped to an ECS type. + // Contains the name as a string, unlike the other types. + internal struct NamedPropertyMapping + { + public string Name; + public short SizeCPU; + public short SizeGPU; + } + + internal struct EntitiesGraphicsTuningConstants + { + public const int kMaxInstancesPerDrawCommand = 4096; + public const int kMaxInstancesPerDrawRange = 4096; + public const int kMaxDrawCommandsPerDrawRange = 512; + } + + // Contains the immutable properties that are set + // upon batch creation. Only chunks with identical BatchCreateInfo + // can be combined in a single batch. + internal struct BatchCreateInfo : IEquatable, IComparable + { + // Unique deduplicated GraphicsArchetype index. Chunks can be combined if their + // index is the same. + public int GraphicsArchetypeIndex; + public ArchetypeChunk Chunk; + + public bool Equals(BatchCreateInfo other) + { + return CompareTo(other) == 0; + } + + public int CompareTo(BatchCreateInfo other) => GraphicsArchetypeIndex.CompareTo(other.GraphicsArchetypeIndex); + } + + internal struct BatchCreateInfoFactory + { + public EntitiesGraphicsArchetypes GraphicsArchetypes; + public NativeParallelHashMap TypeIndexToMaterialProperty; + + public BatchCreateInfo Create(ArchetypeChunk chunk) + { + return new BatchCreateInfo + { + GraphicsArchetypeIndex = + GraphicsArchetypes.GetGraphicsArchetypeIndex(chunk.Archetype, TypeIndexToMaterialProperty), + Chunk = chunk, + }; + } + } + + internal struct BatchInfo + { + public HeapBlock GPUMemoryAllocation; + public HeapBlock ChunkMetadataAllocation; + } + + internal struct BRGRenderMeshArray + { + public int Version; + public UnsafeList Materials; + public UnsafeList Meshes; + public uint4 Hash128; + } + + [BurstCompile] + internal struct RemapMaterialMeshIndexJob : IJobChunk + { + [ReadOnly] public SharedComponentTypeHandle RenderMeshArrayHandle; + public ComponentTypeHandle MaterialMeshInfoHandle; + [ReadOnly] public NativeParallelHashMap BRGRenderMeshArrays; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var sharedComponentIndex = chunk.GetSharedComponentIndex(RenderMeshArrayHandle); + var materialMeshInfos = chunk.GetNativeArray(MaterialMeshInfoHandle); + + BRGRenderMeshArray brgRenderMeshArray; + bool found = BRGRenderMeshArrays.TryGetValue(sharedComponentIndex, out brgRenderMeshArray); + + if (found) + { + var materials = brgRenderMeshArray.Materials; + var meshes = brgRenderMeshArray.Meshes; + + for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) + { + var materialMeshInfo = materialMeshInfos[i]; + + if (!materialMeshInfo.IsRuntimeMaterial) + materialMeshInfo.Material = (int)materials[materialMeshInfo.MaterialArrayIndex].value; + + if (!materialMeshInfo.IsRuntimeMesh) + materialMeshInfo.Mesh = (int)meshes[materialMeshInfo.MeshArrayIndex].value; + + materialMeshInfos[i] = materialMeshInfo; + } + } + } + } + + [BurstCompile] + internal struct InitializeUnreferencedIndicesScatterJob : IJobParallelFor + { + [ReadOnly] public NativeArray ExistingBatchIndices; + public NativeArray UnreferencedBatchIndices; + + public unsafe void Execute(int index) + { + int batchIndex = ExistingBatchIndices[index]; + + AtomicHelpers.IndexToQwIndexAndMask(batchIndex, out int qw, out long mask); + + Debug.Assert(qw < UnreferencedBatchIndices.Length, "Batch index out of bounds"); + + AtomicHelpers.AtomicOr((long*)UnreferencedBatchIndices.GetUnsafePtr(), qw, mask); + } + } + + internal struct BatchCreationTypeHandles + { + public ComponentTypeHandle RootLODRange; + public ComponentTypeHandle LODRange; + public ComponentTypeHandle PerInstanceCulling; + + public BatchCreationTypeHandles(ComponentSystemBase componentSystemBase) + { + RootLODRange = componentSystemBase.GetComponentTypeHandle(true); + LODRange = componentSystemBase.GetComponentTypeHandle(true); + PerInstanceCulling = componentSystemBase.GetComponentTypeHandle(true); + } + } + + internal struct ChunkProperty + { + public int ComponentTypeIndex; + public int ValueSizeBytesCPU; + public int ValueSizeBytesGPU; + public int GPUDataBegin; + } + + // Describes a single ECS component type => material property mapping + internal struct MaterialPropertyType + { + public int TypeIndex; + public int NameID; + public short SizeBytesCPU; + public short SizeBytesGPU; + + public string TypeName => EntitiesGraphicsSystem.TypeIndexToName(TypeIndex); + public string PropertyName => EntitiesGraphicsSystem.NameIDToName(NameID); + } + + /// + /// A system that registers Materials and meshes with the BatchRendererGroup. + /// + /// + /// Schedule any changes to materials and meshes before this system runs if you want automatic registration and conversion to runtime IDs. + /// After this system runs, all information on MaterialMeshInfo is assumed to be using runtime IDs only. + /// + //@TODO: Updating always necessary due to empty component group. When Component group and archetype chunks are unified, [RequireMatchingQueriesForUpdate] can be added. + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateBefore(typeof(EntitiesGraphicsSystem))] + partial class RegisterMaterialsAndMeshesSystem : SystemBase + { + // Reuse Lists used for GetAllUniqueSharedComponentData to avoid GC allocs every frame + private List m_RenderMeshArrays = new List(); + private List m_SharedComponentIndices = new List(); + private List m_SharedComponentVersions = new List(); + + NativeParallelHashMap m_BRGRenderMeshArrays; + + EntitiesGraphicsSystem m_RendererSystem; + + private EntityQuery m_ChangedMaterialMeshQuery; + + /// + protected override void OnCreate() + { + m_BRGRenderMeshArrays = new NativeParallelHashMap(256, Allocator.Persistent); + m_RendererSystem = World.GetOrCreateSystemManaged(); + + m_ChangedMaterialMeshQuery = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadWrite() + }, + Options = EntityQueryOptions.IncludeDisabledEntities, + }); + m_ChangedMaterialMeshQuery.SetChangedVersionFilter(ComponentType.ReadWrite()); + } + + /// + protected override void OnUpdate() + { + Profiler.BeginSample("RegisterMaterialsAndMeshes"); + Dependency = RegisterMaterialsAndMeshes(Dependency); + Profiler.EndSample(); + } + + /// + protected override void OnDestroy() + { + var brgRenderArrays = m_BRGRenderMeshArrays.GetValueArray(Allocator.Temp); + for (int i = 0; i < brgRenderArrays.Length; ++i) + { + var brgRenderArray = brgRenderArrays[i]; + UnregisterMaterialsMeshes(brgRenderArray); + brgRenderArray.Materials.Dispose(); + brgRenderArray.Meshes.Dispose(); + } + m_BRGRenderMeshArrays.Dispose(); + } + + private void UnregisterMaterialsMeshes(in BRGRenderMeshArray brgRenderArray) + { + foreach (var id in brgRenderArray.Materials) + { + m_RendererSystem.UnregisterMaterial(id); + } + + foreach (var id in brgRenderArray.Meshes) + { + m_RendererSystem.UnregisterMesh(id); + } + } + + private void GetFilteredRenderMeshArrays(out List renderArrays, out List sharedIndices, out List sharedVersions) + { + m_RenderMeshArrays.Clear(); + m_SharedComponentIndices.Clear(); + m_SharedComponentVersions.Clear(); + + renderArrays = m_RenderMeshArrays; + sharedIndices = m_SharedComponentIndices; + sharedVersions = m_SharedComponentVersions; + + EntityManager.GetAllUniqueSharedComponentsManaged(renderArrays, sharedIndices, sharedVersions); + //Debug.Log($"BRG update: Found {renderArrays.Count} unique RenderMeshArray components:"); + + // Discard null RenderMeshArray components + var discardedIndices = new NativeList(renderArrays.Count, Allocator.Temp); + + // Reverse iteration to make the index list sorted in decreasing order + // We need this to safely remove the indices one after the other later + for (int i = renderArrays.Count - 1; i >= 0; --i) + { + var array = renderArrays[i]; + if (array.Materials == null || array.Meshes == null) + { + discardedIndices.Add(i); + } + } + + foreach (var i in discardedIndices) + { + renderArrays.RemoveAt(i); + sharedIndices.RemoveAt(i); + sharedVersions.RemoveAt(i); + } + + discardedIndices.Dispose(); + } + + private JobHandle RegisterMaterialsAndMeshes(JobHandle inputDeps) + { + GetFilteredRenderMeshArrays(out var renderArrays, out var sharedIndices, out var sharedVersions); + + var brgArraysToDispose = new NativeList(renderArrays.Count, Allocator.Temp); + + // Remove RenderMeshArrays that no longer exist + var sortedKeys = m_BRGRenderMeshArrays.GetKeyArray(Allocator.Temp); + sortedKeys.Sort(); + + // Single pass O(n) algorithm. Both arrays are guaranteed to be sorted. + for (int i = 0, j = 0; (i < sortedKeys.Length) && (j < renderArrays.Count); i++) + { + var oldKey = sortedKeys[i]; + while ((j < renderArrays.Count) && (sharedIndices[j] < oldKey)) + { + j++; + } + + bool notFound = j == renderArrays.Count || oldKey != sharedIndices[j]; + if (notFound) + { + var brgRenderArray = m_BRGRenderMeshArrays[oldKey]; + brgArraysToDispose.Add(brgRenderArray); + + m_BRGRenderMeshArrays.Remove(oldKey); + } + } + sortedKeys.Dispose(); + + // Update/add RenderMeshArrays + for (int ri = 0; ri < renderArrays.Count; ++ri) + { + var renderArray = renderArrays[ri]; + if (renderArray.Materials == null || renderArray.Meshes == null) + { + Debug.LogError("This loop should not process null RenderMeshArray components"); + continue; + } + + var sharedIndex = sharedIndices[ri]; + var sharedVersion = sharedVersions[ri]; + var materialCount = renderArray.Materials.Length; + var meshCount = renderArray.Meshes.Length; + uint4 hash128 = renderArray.GetHash128(); + + bool update = false; + BRGRenderMeshArray brgRenderArray; + if (m_BRGRenderMeshArrays.TryGetValue(sharedIndex, out brgRenderArray)) + { + // Version change means that the shared component was deleted and another one was created with the same index + // It's also possible that the contents changed and the version number did not, so we also compare the 128-bit hash + if ((brgRenderArray.Version != sharedVersion) || + math.any(brgRenderArray.Hash128 != hash128)) + { + brgArraysToDispose.Add(brgRenderArray); + update = true; + +#if DEBUG_LOG_BRG_MATERIAL_MESH + Debug.Log($"BRG Material Mesh : RenderMeshArray version change | SharedIndex ({sharedIndex}) | SharedVersion ({brgRenderArray.Version}) -> ({sharedVersion})"); +#endif + } + } + else + { + brgRenderArray = new BRGRenderMeshArray(); + update = true; + +#if DEBUG_LOG_BRG_MATERIAL_MESH + Debug.Log($"BRG Material Mesh : New RenderMeshArray found | SharedIndex ({sharedIndex})"); +#endif + } + + if (update) + { + brgRenderArray.Version = sharedVersion; + brgRenderArray.Hash128 = hash128; + brgRenderArray.Materials = new UnsafeList(materialCount, Allocator.Persistent); + brgRenderArray.Meshes = new UnsafeList(meshCount, Allocator.Persistent); + + for (int i = 0; i < materialCount; ++i) + { + var material = renderArray.Materials[i]; + var id = m_RendererSystem.RegisterMaterial(material); + + brgRenderArray.Materials.Add(id); + } + + for (int i = 0; i < meshCount; ++i) + { + var mesh = renderArray.Meshes[i]; + var id = m_RendererSystem.RegisterMesh(mesh); + + brgRenderArray.Meshes.Add(id); + } + + m_BRGRenderMeshArrays[sharedIndex] = brgRenderArray; + } + } + + for (int i = 0; i < brgArraysToDispose.Length; ++i) + { + var brgRenderArray = brgArraysToDispose[i]; + UnregisterMaterialsMeshes(brgRenderArray); + brgRenderArray.Materials.Dispose(); + brgRenderArray.Meshes.Dispose(); + } + + // Fire jobs to remap offline->runtime indices + var remapMaterialMeshIndexHandle = new RemapMaterialMeshIndexJob + { + RenderMeshArrayHandle = GetSharedComponentTypeHandle(), + MaterialMeshInfoHandle = GetComponentTypeHandle(), + BRGRenderMeshArrays = m_BRGRenderMeshArrays, + } + .ScheduleParallel(m_ChangedMaterialMeshQuery, inputDeps); + + return remapMaterialMeshIndexHandle; + } + } + + /// + /// Renders all entities that contain both RenderMesh and LocalToWorld components. + /// + //@TODO: Updating always necessary due to empty component group. When Component group and archetype chunks are unified, [RequireMatchingQueriesForUpdate] can be added. + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateAfter(typeof(UpdatePresentationSystemGroup))] + public unsafe partial class EntitiesGraphicsSystem : SystemBase + { + static private bool s_EntitiesGraphicsEnabled = true; + + /// + /// Toggles the activation of EntitiesGraphicsSystem. + /// + /// + /// To disable this system, use the HYBRID_RENDERER_DISABLED define. + /// + public static bool EntitiesGraphicsEnabled => s_EntitiesGraphicsEnabled; + +#if !DISABLE_HYBRID_RENDERER_ERROR_LOADING_SHADER + private static bool ErrorShaderEnabled => true; +#else + private static bool ErrorShaderEnabled => false; +#endif + +#if UNITY_EDITOR && !DISABLE_HYBRID_RENDERER_ERROR_LOADING_SHADER + private static bool LoadingShaderEnabled => true; +#else + private static bool LoadingShaderEnabled => false; +#endif + + private long m_PersistentInstanceDataSize; + + // Store this in a member variable, because culling callback + // already sees the new value and we want to check against + // the value that was seen by OnUpdate. + private uint m_LastSystemVersionAtLastUpdate; + + private EntityQuery m_CullingJobDependencyGroup; + private EntityQuery m_EntitiesGraphicsRenderedQuery; + private EntityQuery m_EntitiesGraphicsRenderedQueryRO; + private EntityQuery m_LodSelectGroup; + private EntityQuery m_ChangedTransformQuery; + private EntityQuery m_MetaEntitiesForHybridRenderableChunksQuery; + + const int kInitialMaxBatchCount = 1 * 1024; + const float kMaxBatchGrowFactor = 2f; + const int kNumNewChunksPerThread = 1; // TODO: Tune this + const int kNumScatteredIndicesPerThread = 8; // TODO: Tune this + + const int kMaxChunkMetadata = 1 * 1024 * 1024; + const ulong kMaxGPUAllocatorMemory = 1024 * 1024 * 1024; // 1GiB of potential memory space + const long kGPUBufferSizeInitial = 32 * 1024 * 1024; + const long kGPUBufferSizeMax = 1023 * 1024 * 1024; + const int kGPUUploaderChunkSize = 4 * 1024 * 1024; + + private JobHandle m_CullingJobDependency; + private JobHandle m_CullingJobReleaseDependency; + private JobHandle m_UpdateJobDependency; + private JobHandle m_LODDependency; + private BatchRendererGroup m_BatchRendererGroup; + private ThreadedBatchContext m_ThreadedBatchContext; + + private GraphicsBuffer m_GPUPersistentInstanceData; + private GraphicsBufferHandle m_GPUPersistentInstanceBufferHandle; + private SparseUploader m_GPUUploader; + private ThreadedSparseUploader m_ThreadedGPUUploader; + private HeapAllocator m_GPUPersistentAllocator; + private HeapBlock m_SharedZeroAllocation; + + private GraphicsBuffer m_GlobalValuesCbuffer; + private BatchRendererGroupGlobals m_GlobalValues; + private bool m_GlobalValuesDirty; + + private HeapAllocator m_ChunkMetadataAllocator; + + private NativeList m_BatchInfos; + private NativeArray m_ChunkProperties; + private NativeParallelHashSet m_ExistingBatchIndices; + private ComponentTypeCache m_ComponentTypeCache; + + private SortedSet m_SortedBatchIds; + + private NativeList m_ValueBlits; + + // These arrays are parallel and allocated up to kMaxBatchCount. They are indexed by batch indices. + NativeList m_ForceLowLOD; + +#if UNITY_EDITOR + float m_CamMoveDistance; +#endif + +#if UNITY_EDITOR + private EntitiesGraphicsPerThreadStats* m_PerThreadStats = null; + private EntitiesGraphicsStats m_Stats; + public EntitiesGraphicsStats Stats => m_Stats; + + private void ComputeStats() + { + Profiler.BeginSample("ComputeStats"); + + var result = default(EntitiesGraphicsStats); + for (int i = 0; i < JobsUtility.MaxJobThreadCount; ++i) + { + ref var s = ref m_PerThreadStats[i]; + + result.ChunkTotal += s.ChunkTotal; + result.ChunkCountAnyLod += s.ChunkCountAnyLod; + result.ChunkCountInstancesProcessed += s.ChunkCountInstancesProcessed; + result.ChunkCountFullyIn += s.ChunkCountFullyIn; + result.InstanceTests += s.InstanceTests; + result.LodTotal += s.LodTotal; + result.LodNoRequirements += s.LodNoRequirements; + result.LodChanged += s.LodChanged; + result.LodChunksTested += s.LodChunksTested; + + result.RenderedInstanceCount += s.RenderedEntityCount; + result.DrawCommandCount += s.DrawCommandCount; + result.DrawRangeCount += s.DrawRangeCount; + } + + result.CameraMoveDistance = m_CamMoveDistance; + + result.BatchCount = m_ExistingBatchIndices.Count(); + + var uploaderStats = m_GPUUploader.ComputeStats(); + result.BytesGPUMemoryUsed = m_PersistentInstanceDataSize + uploaderStats.BytesGPUMemoryUsed; + result.BytesGPUMemoryUploadedCurr = uploaderStats.BytesGPUMemoryUploadedCurr; + result.BytesGPUMemoryUploadedMax = uploaderStats.BytesGPUMemoryUploadedMax; + + m_Stats = result; + + Profiler.EndSample(); + } + +#endif + + private bool m_ResetLod; + + LODGroupExtensions.LODParams m_PrevLODParams; + float3 m_PrevCameraPos; + float m_PrevLodDistanceScale; + + NativeMultiHashMap m_NameIDToMaterialProperties; + NativeParallelHashMap m_TypeIndexToMaterialProperty; + + static Dictionary s_TypeToPropertyMappings = new Dictionary(); + +#if DEBUG_PROPERTY_NAMES + internal static Dictionary s_NameIDToName = new Dictionary(); + internal static Dictionary s_TypeIndexToName = new Dictionary(); +#endif + +#if USE_UNITY_OCCLUSION + public OcclusionCulling OcclusionCulling { get; private set; } +#endif + + private bool m_FirstFrameAfterInit; + + private EntitiesGraphicsArchetypes m_GraphicsArchetypes; + + // Burst accessible filter settings for each RenderFilterSettings shared component index + private NativeParallelHashMap m_FilterSettings; +#if UNITY_EDITOR + private List m_EditorRenderDatas = new List(); + private NativeParallelHashMap m_BatchEditorDatas; +#endif + +#if ENABLE_PICKING + Material m_PickingMaterial; +#endif + + Material m_LoadingMaterial; + Material m_ErrorMaterial; + + // Reuse Lists used for GetAllUniqueSharedComponentData to avoid GC allocs every frame + private List m_RenderFilterSettings = new List(); + private List m_SharedComponentIndices = new List(); + + private ThreadLocalAllocator m_ThreadLocalAllocators; + + /// + protected override void OnCreate() + { + // If all graphics rendering has been disabled, early out from all HR functionality +#if HYBRID_RENDERER_DISABLED + s_EntitiesGraphicsEnabled = false; +#else + // If -nographics is enabled, or if there is no compute shader support, disable HR. + s_EntitiesGraphicsEnabled = EntitiesGraphicsUtils.IsEntitiesGraphicsSupportedOnSystem(); +#endif + if (!s_EntitiesGraphicsEnabled) + { + Debug.Log("No SRP present, no compute shader support, or running with -nographics. Entities Graphics package disabled"); + return; + } + + m_FirstFrameAfterInit = true; + + m_PersistentInstanceDataSize = kGPUBufferSizeInitial; + + //@TODO: Support SetFilter with EntityQueryDesc syntax + // This component group must include all types that are being used by the culling job + m_CullingJobDependencyGroup = GetEntityQuery( + ComponentType.ChunkComponentReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ChunkComponentReadOnly() + ); + + m_EntitiesGraphicsRenderedQuery = GetEntityQuery(EntitiesGraphicsUtils.GetEntitiesGraphicsRenderedQueryDesc()); + m_EntitiesGraphicsRenderedQueryRO = GetEntityQuery(EntitiesGraphicsUtils.GetEntitiesGraphicsRenderedQueryDescReadOnly()); + + m_LodSelectGroup = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadWrite(), + ComponentType.ReadOnly() + }, + }); + + m_ChangedTransformQuery = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ChunkComponent(), + }, + }); + m_ChangedTransformQuery.AddChangedVersionFilter(ComponentType.ReadOnly()); + m_ChangedTransformQuery.AddOrderVersionFilter(); + + m_BatchRendererGroup = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero); + // Hybrid Renderer supports all view types + m_BatchRendererGroup.SetEnabledViewTypes(new BatchCullingViewType[] + { + BatchCullingViewType.Camera, + BatchCullingViewType.Light, + BatchCullingViewType.Picking, + BatchCullingViewType.SelectionOutline + }); + m_ThreadedBatchContext = m_BatchRendererGroup.GetThreadedBatchContext(); + m_ForceLowLOD = NewNativeListResized(kInitialMaxBatchCount, Allocator.Persistent, NativeArrayOptions.ClearMemory); + + m_ResetLod = true; + + m_GPUPersistentAllocator = new HeapAllocator(kMaxGPUAllocatorMemory, 16); + m_ChunkMetadataAllocator = new HeapAllocator(kMaxChunkMetadata); + + m_BatchInfos = NewNativeListResized(kInitialMaxBatchCount, Allocator.Persistent); + m_ChunkProperties = new NativeArray(kMaxChunkMetadata, Allocator.Persistent); + m_ExistingBatchIndices = new NativeParallelHashSet(128, Allocator.Persistent); + m_ComponentTypeCache = new ComponentTypeCache(128); + + m_ValueBlits = new NativeList(Allocator.Persistent); + + // Globally allocate a single zero matrix at offset zero, so loads from zero return zero + m_SharedZeroAllocation = m_GPUPersistentAllocator.Allocate((ulong)sizeof(float4x4)); + Debug.Assert(!m_SharedZeroAllocation.Empty, "Allocation of constant-zero data failed"); + // Make sure the global zero is actually zero. + m_ValueBlits.Add(new ValueBlitDescriptor + { + Value = float4x4.zero, + DestinationOffset = (uint)m_SharedZeroAllocation.begin, + ValueSizeBytes = (uint)sizeof(float4x4), + Count = 1, + }); + Debug.Assert(m_SharedZeroAllocation.begin == 0, "Global zero allocation should have zero address"); + + ResetIds(); + + m_MetaEntitiesForHybridRenderableChunksQuery = GetEntityQuery( + new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadWrite(), + ComponentType.ReadOnly(), + }, + }); + +#if UNITY_EDITOR + m_PerThreadStats = (EntitiesGraphicsPerThreadStats*)Memory.Unmanaged.Allocate(JobsUtility.MaxJobThreadCount * sizeof(EntitiesGraphicsPerThreadStats), + 64, Allocator.Persistent); +#endif + + // Collect all components with [MaterialProperty] attribute + m_NameIDToMaterialProperties = new NativeMultiHashMap(256, Allocator.Persistent); + m_TypeIndexToMaterialProperty = new NativeParallelHashMap(256, Allocator.Persistent); + + m_GraphicsArchetypes = new EntitiesGraphicsArchetypes(256); + + m_FilterSettings = new NativeParallelHashMap(256, Allocator.Persistent); +#if UNITY_EDITOR + m_BatchEditorDatas = new NativeParallelHashMap(256, Allocator.Persistent); +#endif + + // Some hardcoded mappings to avoid dependencies to Hybrid from DOTS +#if SRP_10_0_0_OR_NEWER + RegisterMaterialPropertyType("unity_ObjectToWorld", 4 * 4 * 3); + RegisterMaterialPropertyType("unity_WorldToObject", overrideTypeSizeGPU: 4 * 4 * 3); +#else + RegisterMaterialPropertyType("unity_ObjectToWorld", 4 * 4 * 4); + RegisterMaterialPropertyType("unity_WorldToObject", 4 * 4 * 4); +#endif + +#if ENABLE_PICKING + RegisterMaterialPropertyType(typeof(Entity), "unity_EntityId"); +#endif + + m_GlobalValues = BatchRendererGroupGlobals.Default; + m_GlobalValuesDirty = true; + + foreach (var typeInfo in TypeManager.AllTypes) + { + var type = typeInfo.Type; + + bool isComponent = typeof(IComponentData).IsAssignableFrom(type); + if (isComponent) + { + var attributes = type.GetCustomAttributes(typeof(MaterialPropertyAttribute), false); + if (attributes.Length > 0) + { + var propertyAttr = (MaterialPropertyAttribute)attributes[0]; + + RegisterMaterialPropertyType(type, propertyAttr.Name, propertyAttr.OverrideSizeGPU); + } + } + } + + m_GPUPersistentInstanceData = new GraphicsBuffer( + GraphicsBuffer.Target.Raw, + GraphicsBuffer.UsageFlags.None, + (int)m_PersistentInstanceDataSize / 4, + 4); + + m_GPUPersistentInstanceBufferHandle = m_GPUPersistentInstanceData.bufferHandle; + + m_GlobalValuesCbuffer = new GraphicsBuffer( + GraphicsBuffer.Target.Constant, + 1, + UnsafeUtility.SizeOf()); + + m_GPUUploader = new SparseUploader(m_GPUPersistentInstanceData, kGPUUploaderChunkSize); + +#if USE_UNITY_OCCLUSION + OcclusionCulling = new OcclusionCulling(); + OcclusionCulling.Create(EntityManager); +#endif + + m_ThreadLocalAllocators = new ThreadLocalAllocator(-1); + + if (ErrorShaderEnabled) + { + m_ErrorMaterial = EntitiesGraphicsUtils.LoadErrorMaterial(); + if (m_ErrorMaterial != null) + { + m_BatchRendererGroup.SetErrorMaterial(m_ErrorMaterial); + } + } + + if (LoadingShaderEnabled) + { + m_LoadingMaterial = EntitiesGraphicsUtils.LoadLoadingMaterial(); + if (m_LoadingMaterial != null) + { + m_BatchRendererGroup.SetLoadingMaterial(m_LoadingMaterial); + } + } + +#if ENABLE_PICKING + m_PickingMaterial = EntitiesGraphicsUtils.LoadPickingMaterial(); + if (m_PickingMaterial != null) + { + m_BatchRendererGroup.SetPickingMaterial(m_PickingMaterial); + } +#endif + } + + internal static readonly bool UseConstantBuffers = EntitiesGraphicsUtils.UseHybridConstantBufferMode(); + internal static readonly int MaxBytesPerCBuffer = EntitiesGraphicsUtils.MaxBytesPerCBuffer; + internal static readonly uint BatchAllocationAlignment = (uint)EntitiesGraphicsUtils.BatchAllocationAlignment; + + internal const int kMaxBytesPerBatchRawBuffer = 16 * 1024 * 1024; + + /// + /// The maximum GPU buffer size (in bytes) that a batch can access. + /// + public static int MaxBytesPerBatch => UseConstantBuffers + ? MaxBytesPerCBuffer + : kMaxBytesPerBatchRawBuffer; + + /// + /// Registers a material property type with the given name. + /// + /// The type of material property to register. + /// The name of the property. + /// An optional size of the type on the GPU. + public static void RegisterMaterialPropertyType(Type type, string propertyName, short overrideTypeSizeGPU = -1) + { + Debug.Assert(type != null, "type must be non-null"); + Debug.Assert(!string.IsNullOrEmpty(propertyName), "Property name must be valid"); + + short typeSizeCPU = (short)UnsafeUtility.SizeOf(type); + if (overrideTypeSizeGPU == -1) + overrideTypeSizeGPU = typeSizeCPU; + + // For now, we only support overriding one material property with one type. + // Several types can override one property, but not the other way around. + // If necessary, this restriction can be lifted in the future. + if (s_TypeToPropertyMappings.ContainsKey(type)) + { + string prevPropertyName = s_TypeToPropertyMappings[type].Name; + Debug.Assert(propertyName.Equals(prevPropertyName), + $"Attempted to register type {type.Name} with multiple different property names. Registered with \"{propertyName}\", previously registered with \"{prevPropertyName}\"."); + } + else + { + var pm = new NamedPropertyMapping(); + pm.Name = propertyName; + pm.SizeCPU = typeSizeCPU; + pm.SizeGPU = overrideTypeSizeGPU; + s_TypeToPropertyMappings[type] = pm; + } + } + + /// + /// A templated version of the material type registration method. + /// + /// The type of material property to register. + /// The name of the property. + /// An optional size of the type on the GPU. + public static void RegisterMaterialPropertyType(string propertyName, short overrideTypeSizeGPU = -1) + where T : IComponentData + { + RegisterMaterialPropertyType(typeof(T), propertyName, overrideTypeSizeGPU); + } + + private void InitializeMaterialProperties() + { + m_NameIDToMaterialProperties.Clear(); + + foreach (var kv in s_TypeToPropertyMappings) + { + Type type = kv.Key; + string propertyName = kv.Value.Name; + + short sizeBytesCPU = kv.Value.SizeCPU; + short sizeBytesGPU = kv.Value.SizeGPU; + int typeIndex = TypeManager.GetTypeIndex(type); + int nameID = Shader.PropertyToID(propertyName); + + var materialPropertyType = + new MaterialPropertyType + { + TypeIndex = typeIndex, + NameID = nameID, + SizeBytesCPU = sizeBytesCPU, + SizeBytesGPU = sizeBytesGPU, + }; + + m_TypeIndexToMaterialProperty.Add(typeIndex, materialPropertyType); + m_NameIDToMaterialProperties.Add(nameID, materialPropertyType); + +#if DEBUG_PROPERTY_NAMES + s_TypeIndexToName[typeIndex] = type.Name; + s_NameIDToName[nameID] = propertyName; +#endif + +#if DEBUG_LOG_MATERIAL_PROPERTY_TYPES + Debug.Log($"Type \"{type.Name}\" ({sizeBytesCPU} bytes) overrides material property \"{propertyName}\" (nameID: {nameID}, typeIndex: {typeIndex})"); +#endif + + // We cache all IComponentData types that we know are capable of overriding properties + m_ComponentTypeCache.UseType(typeIndex); + } + } + + /// + protected override void OnDestroy() + { + if (!s_EntitiesGraphicsEnabled) return; + CompleteJobs(true); + Dispose(); + } + + private JobHandle UpdateEntitiesGraphicsBatches(JobHandle inputDependencies) + { + JobHandle done = default; + Profiler.BeginSample("UpdateAllBatches"); + using (var entitiesGraphicsChunks = + m_EntitiesGraphicsRenderedQuery.ToArchetypeChunkArray(Allocator.TempJob)) + { + done = UpdateAllBatches(inputDependencies); + } + + Profiler.EndSample(); + + return done; + } + + private void OnFirstFrame() + { + InitializeMaterialProperties(); + +#if DEBUG_LOG_HYBRID_RENDERER + var mode = UseConstantBuffers + ? $"UBO mode (UBO max size: {MaxBytesPerCBuffer}, alignment: {BatchAllocationAlignment}, globals: {m_GlobalWindowSize})" + : "SSBO mode"; + Debug.Log( + $"Entities Graphics active, MaterialProperty component type count {m_ComponentTypeCache.UsedTypeCount} / {ComponentTypeCache.BurstCompatibleTypeArray.kMaxTypes}, {mode}"); +#endif + } + + private JobHandle UpdateFilterSettings(JobHandle inputDeps) + { + m_RenderFilterSettings.Clear(); + m_SharedComponentIndices.Clear(); + + // TODO: Maybe this could be partially jobified? + + EntityManager.GetAllUniqueSharedComponentsManaged(m_RenderFilterSettings, m_SharedComponentIndices); + + m_FilterSettings.Clear(); + for (int i = 0; i < m_SharedComponentIndices.Count; ++i) + { + int sharedIndex = m_SharedComponentIndices[i]; + m_FilterSettings[sharedIndex] = MakeFilterSettings(m_RenderFilterSettings[i]); + } + + m_RenderFilterSettings.Clear(); + m_SharedComponentIndices.Clear(); + + return new JobHandle(); + } + + private static BatchFilterSettings MakeFilterSettings(RenderFilterSettings filterSettings) + { + return new BatchFilterSettings + { + layer = (byte) filterSettings.Layer, + renderingLayerMask = filterSettings.RenderingLayerMask, + motionMode = filterSettings.MotionMode, + shadowCastingMode = filterSettings.ShadowCastingMode, + receiveShadows = filterSettings.ReceiveShadows, + staticShadowCaster = filterSettings.StaticShadowCaster, + allDepthSorted = false, // set by culling + }; + } + + private JobHandle UpdateSceneCullingMasks(JobHandle inputDeps) + { +#if UNITY_EDITOR + m_EditorRenderDatas.Clear(); + m_SharedComponentIndices.Clear(); + + // TODO: Maybe this could be partially jobified? + + EntityManager.GetAllUniqueSharedComponentsManaged(m_EditorRenderDatas, m_SharedComponentIndices); + + m_BatchEditorDatas.Clear(); + for (int i = 0; i < m_SharedComponentIndices.Count; ++i) + { + int sharedIndex = m_SharedComponentIndices[i]; + var editorData = m_EditorRenderDatas[i]; + + var batchData = new BatchEditorRenderData(); + batchData.SceneCullingMask = editorData.SceneCullingMask; + + m_BatchEditorDatas[sharedIndex] = batchData; + } + + m_EditorRenderDatas.Clear(); + m_SharedComponentIndices.Clear(); +#endif + return new JobHandle(); + } + + /// + protected override void OnUpdate() + { + if (!s_EntitiesGraphicsEnabled) return; + + JobHandle inputDeps = Dependency; + + // Make sure any release jobs that have stored pointers in temp allocated + // memory have finished before we rewind + m_CullingJobReleaseDependency.Complete(); + m_CullingJobReleaseDependency = default; + m_ThreadLocalAllocators.Rewind(); + + m_LastSystemVersionAtLastUpdate = LastSystemVersion; + + if (m_FirstFrameAfterInit) + { + OnFirstFrame(); + m_FirstFrameAfterInit = false; + } + + Profiler.BeginSample("CompleteJobs"); + inputDeps.Complete(); // #todo + CompleteJobs(); + ResetLod(); + Profiler.EndSample(); + +#if UNITY_EDITOR + ComputeStats(); +#endif + + Profiler.BeginSample("UpdateFilterSettings"); + var updateFilterSettingsHandle = UpdateFilterSettings(inputDeps); + Profiler.EndSample(); + + inputDeps = JobHandle.CombineDependencies(inputDeps, updateFilterSettingsHandle); + +#if UNITY_EDITOR + Profiler.BeginSample("UpdateSceneCullingMasks"); + var updateSceneCullingMasks = UpdateSceneCullingMasks(inputDeps); + Profiler.EndSample(); + + inputDeps = JobHandle.CombineDependencies(inputDeps, updateSceneCullingMasks); +#endif + + UpdateSpecCubeHDRDecode(ReflectionProbe.defaultTextureHDRDecodeValues); + + var done = new JobHandle(); + try + { + Profiler.BeginSample("UpdateEntitiesGraphicsBatches"); + done = UpdateEntitiesGraphicsBatches(inputDeps); + Profiler.EndSample(); + + Profiler.BeginSample("EndUpdate"); + EndUpdate(); + Profiler.EndSample(); + } + finally + { + m_GPUUploader.FrameCleanup(); + } + + EntitiesGraphicsEditorTools.EndFrame(); + + Dependency = done; + } + + private void ResetIds() + { + m_SortedBatchIds = new SortedSet(); + m_ExistingBatchIndices.Clear(); + } + + private void EnsureHaveSpaceForNewBatch() + { + int currentCapacity = m_BatchInfos.Length; + int neededCapacity = BatchIndexRange; + + if (currentCapacity >= neededCapacity) return; + + Debug.Assert(kMaxBatchGrowFactor >= 1f, + "Grow factor should always be greater or equal to 1"); + + var newCapacity = (int)(kMaxBatchGrowFactor * neededCapacity); + + m_ForceLowLOD.Resize(newCapacity, NativeArrayOptions.ClearMemory); + m_BatchInfos.Resize(newCapacity, NativeArrayOptions.ClearMemory); + } + + private void AddBatchIndex(int id) + { + Debug.Assert(!m_SortedBatchIds.Contains(id), "New batch ID already marked as used"); + m_SortedBatchIds.Add(id); + m_ExistingBatchIndices.Add(id); + EnsureHaveSpaceForNewBatch(); + } + + private void RemoveBatchIndex(int id) + { + if (!m_SortedBatchIds.Contains(id)) Debug.Assert(false, $"Attempted to release an unused id {id}"); + m_SortedBatchIds.Remove(id); + m_ExistingBatchIndices.Remove(id); + } + + private int BatchIndexRange => m_SortedBatchIds.Max + 1; + + private void Dispose() + { + m_GPUUploader.Dispose(); + m_GPUPersistentInstanceData.Dispose(); + m_GlobalValuesCbuffer.Dispose(); + +#if UNITY_EDITOR + Memory.Unmanaged.Free(m_PerThreadStats, Allocator.Persistent); + m_PerThreadStats = null; +#endif + + if (ErrorShaderEnabled) + Material.DestroyImmediate(m_ErrorMaterial); + + if (LoadingShaderEnabled) + Material.DestroyImmediate(m_LoadingMaterial); + +#if ENABLE_PICKING + Material.DestroyImmediate(m_PickingMaterial); +#endif + + m_BatchRendererGroup.Dispose(); + m_ThreadedBatchContext.batchRendererGroup = IntPtr.Zero; + + m_ForceLowLOD.Dispose(); + m_ResetLod = true; + m_NameIDToMaterialProperties.Dispose(); + m_TypeIndexToMaterialProperty.Dispose(); + m_GPUPersistentAllocator.Dispose(); + m_ChunkMetadataAllocator.Dispose(); + + m_BatchInfos.Dispose(); + m_ChunkProperties.Dispose(); + m_ExistingBatchIndices.Dispose(); + m_ValueBlits.Dispose(); + m_ComponentTypeCache.Dispose(); + + m_SortedBatchIds = null; + +#if USE_UNITY_OCCLUSION + OcclusionCulling.Dispose(); +#endif + + m_GraphicsArchetypes.Dispose(); + + m_FilterSettings.Dispose(); +#if UNITY_EDITOR + m_BatchEditorDatas.Dispose(); +#endif + m_CullingJobReleaseDependency.Complete(); + m_ThreadLocalAllocators.Dispose(); + } + + private void ResetLod() + { + m_PrevLODParams = new LODGroupExtensions.LODParams(); + m_ResetLod = true; + } + + // This function does only return a meaningful IncludeExcludeListFilter object when called from a BRG culling callback. + static IncludeExcludeListFilter GetPickingIncludeExcludeListFilterForCurrentCullingCallback(EntityManager entityManager, in BatchCullingContext cullingContext) + { +#if ENABLE_PICKING && !DISABLE_INCLUDE_EXCLUDE_LIST_FILTERING + PickingIncludeExcludeList includeExcludeList = default; + + if (cullingContext.viewType == BatchCullingViewType.Picking) + { + includeExcludeList = HandleUtility.GetPickingIncludeExcludeList(Allocator.Temp); + } + else if (cullingContext.viewType == BatchCullingViewType.SelectionOutline) + { + includeExcludeList = HandleUtility.GetSelectionOutlineIncludeExcludeList(Allocator.Temp); + } + + NativeArray emptyArray = new NativeArray(0, Allocator.Temp); + + NativeArray includeEntityIndices = includeExcludeList.IncludeEntities; + if (cullingContext.viewType == BatchCullingViewType.SelectionOutline) + { + // Make sure the include list for the selection outline is never null even if there is nothing in it. + // Null NativeArray and empty NativeArray are treated as different things when used to construct an IncludeExcludeListFilter object: + // - Null include list means that nothing is discarded because the filtering is skipped. + // - Empty include list means that everything is discarded because the filtering is enabled but never passes. + // With selection outline culling, we want the filtering to happen in any case even if the array contains nothing so that we don't highlight everything in the latter case. + if (!includeEntityIndices.IsCreated) + includeEntityIndices = emptyArray; + } + else if (includeEntityIndices.Length == 0) + { + includeEntityIndices = default; + } + + NativeArray excludeEntityIndices = includeExcludeList.ExcludeEntities; + if (excludeEntityIndices.Length == 0) + excludeEntityIndices = default; + + IncludeExcludeListFilter includeExcludeListFilter = new IncludeExcludeListFilter( + entityManager, + includeEntityIndices, + excludeEntityIndices, + Allocator.TempJob); + + includeExcludeList.Dispose(); + emptyArray.Dispose(); + + return includeExcludeListFilter; +#else + return default; +#endif + } + + private JobHandle OnPerformCulling(BatchRendererGroup rendererGroup, BatchCullingContext cullingContext, BatchCullingOutput cullingOutput, IntPtr userContext) + { + Profiler.BeginSample("OnPerformCulling"); + + IncludeExcludeListFilter includeExcludeListFilter = GetPickingIncludeExcludeListFilterForCurrentCullingCallback(EntityManager, cullingContext); + + // If inclusive filtering is enabled and we know there are no included entities, + // we can skip all the work because we know that the result will be nothing. + if (includeExcludeListFilter.IsIncludeEnabled && includeExcludeListFilter.IsIncludeEmpty) + { + includeExcludeListFilter.Dispose(); + return m_CullingJobDependency; + } + + var lodParams = LODGroupExtensions.CalculateLODParams(cullingContext.lodParameters); + + JobHandle cullingDependency; + var resetLod = m_ResetLod || (!lodParams.Equals(m_PrevLODParams)); + if (resetLod) + { + // Depend on all component ata we access + previous jobs since we are writing to a single + // m_ChunkInstanceLodEnableds array. + var lodJobDependency = JobHandle.CombineDependencies(m_CullingJobDependency, + m_CullingJobDependencyGroup.GetDependency()); + + float cameraMoveDistance = math.length(m_PrevCameraPos - lodParams.cameraPos); + var lodDistanceScaleChanged = lodParams.distanceScale != m_PrevLodDistanceScale; + +#if UNITY_EDITOR + // Record this separately in the editor for stats display + m_CamMoveDistance = cameraMoveDistance; +#endif + + var selectLodEnabledJob = new SelectLodEnabled + { + ForceLowLOD = m_ForceLowLOD, + LODParams = lodParams, + RootLODRanges = GetComponentTypeHandle(true), + RootLODReferencePoints = GetComponentTypeHandle(true), + LODRanges = GetComponentTypeHandle(true), + LODReferencePoints = GetComponentTypeHandle(true), + EntitiesGraphicsChunkInfo = GetComponentTypeHandle(), + ChunkHeader = GetComponentTypeHandle(), + CameraMoveDistanceFixed16 = + Fixed16CamDistance.FromFloatCeil(cameraMoveDistance * lodParams.distanceScale), + DistanceScale = lodParams.distanceScale, + DistanceScaleChanged = lodDistanceScaleChanged, +#if UNITY_EDITOR + Stats = m_PerThreadStats, +#endif + }; + + cullingDependency = m_LODDependency = selectLodEnabledJob.ScheduleParallel(m_LodSelectGroup, lodJobDependency); + + m_PrevLODParams = lodParams; + m_PrevLodDistanceScale = lodParams.distanceScale; + m_PrevCameraPos = lodParams.cameraPos; + m_ResetLod = false; +#if UNITY_EDITOR + UnsafeUtility.MemClear(m_PerThreadStats, sizeof(EntitiesGraphicsPerThreadStats) * JobsUtility.MaxJobThreadCount); +#endif + } + else + { + // Depend on all component data we access + previous m_LODDependency job + cullingDependency = JobHandle.CombineDependencies( + m_LODDependency, + m_CullingJobDependency, + m_CullingJobDependencyGroup.GetDependency()); + } + + var visibilityItems = new IndirectList( + m_EntitiesGraphicsRenderedQueryRO.CalculateChunkCount(), + m_ThreadLocalAllocators.GeneralAllocator); + + var frustumCullingJob = new FrustumCullingJob + { + Splits = new CullingSplits(ref cullingContext, QualitySettings.shadowProjection, m_ThreadLocalAllocators.GeneralAllocator), + CullingViewType = cullingContext.viewType, + EntitiesGraphicsChunkInfo = GetComponentTypeHandle(true), + ChunkWorldRenderBounds = GetComponentTypeHandle(true), + BoundsComponent = GetComponentTypeHandle(true), + EntityHandle = GetEntityTypeHandle(), + IncludeExcludeListFilter = includeExcludeListFilter, + VisibilityItems = visibilityItems, + ThreadLocalAllocator = m_ThreadLocalAllocators, +#if UNITY_EDITOR + Stats = m_PerThreadStats, +#endif + }; + + var frustumCullingJobHandle = frustumCullingJob.ScheduleParallel(m_EntitiesGraphicsRenderedQueryRO, cullingDependency); + frustumCullingJob.IncludeExcludeListFilter.Dispose(frustumCullingJobHandle); + DidScheduleCullingJob(frustumCullingJobHandle); + +#if USE_UNITY_OCCLUSION + var occlusionCullingDependency = OcclusionCulling.Cull(EntityManager, cullingContext, m_CullingJobDependency, visibilityItems +#if UNITY_EDITOR + , m_PerThreadStats +#endif + ); + DidScheduleCullingJob(occlusionCullingDependency); +#endif + + // TODO: Dynamically estimate this based on past frames + int binCountEstimate = 1; + var chunkDrawCommandOutput = new ChunkDrawCommandOutput( + binCountEstimate, + m_ThreadLocalAllocators, + cullingOutput); + + var emitDrawCommandsJob = new EmitDrawCommandsJob + { + VisibilityItems = visibilityItems, + EntitiesGraphicsChunkInfo = GetComponentTypeHandle(true), + MaterialMeshInfo = GetComponentTypeHandle(true), + LocalToWorld = GetComponentTypeHandle(true), + DepthSorted = GetComponentTypeHandle(true), + DeformedMeshIndex = GetComponentTypeHandle(true), + RenderFilterSettings = GetSharedComponentTypeHandle(), + FilterSettings = m_FilterSettings, + CullingLayerMask = cullingContext.cullingLayerMask, + LightMaps = GetSharedComponentTypeHandle(), +#if UNITY_EDITOR + EditorDataComponentHandle = GetSharedComponentTypeHandle(), + BatchEditorData = m_BatchEditorDatas, +#endif + DrawCommandOutput = chunkDrawCommandOutput, + SceneCullingMask = cullingContext.sceneCullingMask, + CameraPosition = lodParams.cameraPos, + LastSystemVersion = m_LastSystemVersionAtLastUpdate, + + ProfilerEmitChunk = new ProfilerMarker("EmitChunk"), + }; + + var allocateWorkItemsJob = new AllocateWorkItemsJob + { + DrawCommandOutput = chunkDrawCommandOutput, + }; + + var collectWorkItemsJob = new CollectWorkItemsJob + { + DrawCommandOutput = chunkDrawCommandOutput, + ProfileCollect = new ProfilerMarker("Collect"), + ProfileWrite = new ProfilerMarker("Write"), + }; + + var flushWorkItemsJob = new FlushWorkItemsJob + { + DrawCommandOutput = chunkDrawCommandOutput, + }; + + var allocateInstancesJob = new AllocateInstancesJob + { + DrawCommandOutput = chunkDrawCommandOutput, + }; + + var allocateDrawCommandsJob = new AllocateDrawCommandsJob + { + DrawCommandOutput = chunkDrawCommandOutput + }; + + var expandInstancesJob = new ExpandVisibleInstancesJob + { + DrawCommandOutput = chunkDrawCommandOutput, + }; + + var generateDrawCommandsJob = new GenerateDrawCommandsJob + { + DrawCommandOutput = chunkDrawCommandOutput, +#if UNITY_EDITOR + Stats = m_PerThreadStats, +#endif + }; + + var generateDrawRangesJob = new GenerateDrawRangesJob + { + DrawCommandOutput = chunkDrawCommandOutput, + FilterSettings = m_FilterSettings, +#if UNITY_EDITOR + Stats = m_PerThreadStats, +#endif + }; + + var emitDrawCommandsDependency = emitDrawCommandsJob.ScheduleWithIndirectList(visibilityItems, 1, m_CullingJobDependency); + + var collectGlobalBinsDependency = + chunkDrawCommandOutput.BinCollector.ScheduleFinalize(emitDrawCommandsDependency); + var sortBinsDependency = DrawBinSort.ScheduleBinSort( + m_ThreadLocalAllocators.GeneralAllocator, + chunkDrawCommandOutput.SortedBins, + chunkDrawCommandOutput.UnsortedBins, + collectGlobalBinsDependency); + + var allocateWorkItemsDependency = allocateWorkItemsJob.Schedule(collectGlobalBinsDependency); + var collectWorkItemsDependency = collectWorkItemsJob.ScheduleWithIndirectList( + chunkDrawCommandOutput.UnsortedBins, 1, allocateWorkItemsDependency); + + var flushWorkItemsDependency = + flushWorkItemsJob.Schedule(ChunkDrawCommandOutput.NumThreads, 1, collectWorkItemsDependency); + + var allocateInstancesDependency = allocateInstancesJob.Schedule(flushWorkItemsDependency); + + var allocateDrawCommandsDependency = allocateDrawCommandsJob.Schedule( + JobHandle.CombineDependencies(sortBinsDependency, flushWorkItemsDependency)); + + var allocationsDependency = JobHandle.CombineDependencies( + allocateInstancesDependency, + allocateDrawCommandsDependency); + + var expandInstancesDependency = expandInstancesJob.ScheduleWithIndirectList( + chunkDrawCommandOutput.WorkItems, + 1, + allocateInstancesDependency); + var generateDrawCommandsDependency = generateDrawCommandsJob.ScheduleWithIndirectList( + chunkDrawCommandOutput.SortedBins, + 1, + allocationsDependency); + var generateDrawRangesDependency = generateDrawRangesJob.Schedule(allocateDrawCommandsDependency); + + var expansionDependency = JobHandle.CombineDependencies( + expandInstancesDependency, + generateDrawCommandsDependency, + generateDrawRangesDependency); + +#if DEBUG_VALIDATE_DRAW_COMMAND_SORT + expansionDependency = new DebugValidateSortJob + { + DrawCommandOutput = chunkDrawCommandOutput, + }.Schedule(expansionDependency); +#endif + +#if DEBUG_LOG_DRAW_COMMANDS || DEBUG_LOG_DRAW_COMMANDS_VERBOSE + DebugDrawCommands(expansionDependency, cullingOutput); +#endif + + m_CullingJobReleaseDependency = JobHandle.CombineDependencies( + m_CullingJobReleaseDependency, + chunkDrawCommandOutput.Dispose(expansionDependency)); + + DidScheduleCullingJob(emitDrawCommandsDependency); + DidScheduleCullingJob(expansionDependency); + + Profiler.EndSample(); + return m_CullingJobDependency; + } + + private void DebugDrawCommands(JobHandle drawCommandsDependency, BatchCullingOutput cullingOutput) + { + drawCommandsDependency.Complete(); + + var drawCommands = cullingOutput.drawCommands[0]; + + Debug.Log($"Draw Command summary: visibleInstanceCount: {drawCommands.visibleInstanceCount} drawCommandCount: {drawCommands.drawCommandCount} drawRangeCount: {drawCommands.drawRangeCount}"); + +#if DEBUG_LOG_DRAW_COMMANDS_VERBOSE + bool verbose = true; +#else + bool verbose = false; +#endif + if (verbose) + { + for (int i = 0; i < drawCommands.drawCommandCount; ++i) + { + var cmd = drawCommands.drawCommands[i]; + DrawCommandSettings settings = new DrawCommandSettings + { + BatchID = cmd.batchID, + MaterialID = cmd.materialID, + MeshID = cmd.meshID, + SubmeshIndex = cmd.submeshIndex, + Flags = cmd.flags, + }; + Debug.Log($"Draw Command #{i}: {settings} visibleOffset: {cmd.visibleOffset} visibleCount: {cmd.visibleCount}"); + StringBuilder sb = new StringBuilder((int)cmd.visibleCount * 30); + bool hasSortingPosition = settings.HasSortingPosition; + for (int j = 0; j < cmd.visibleCount; ++j) + { + sb.Append(drawCommands.visibleInstances[cmd.visibleOffset + j]); + if (hasSortingPosition) + sb.AppendFormat(" ({0:F3} {1:F3} {2:F3})", + drawCommands.instanceSortingPositions[cmd.sortingPosition + 0], + drawCommands.instanceSortingPositions[cmd.sortingPosition + 1], + drawCommands.instanceSortingPositions[cmd.sortingPosition + 2]); + sb.Append(", "); + } + Debug.Log($"Draw Command #{i} instances: [{sb}]"); + } + } + } + + private JobHandle UpdateAllBatches(JobHandle inputDependencies) + { + Profiler.BeginSample("GetComponentTypes"); + + var threadLocalAABBs = new NativeArray( + JobsUtility.MaxJobThreadCount, + Allocator.TempJob, + NativeArrayOptions.UninitializedMemory); + var zeroAABBJob = new ZeroThreadLocalAABBJob + { + ThreadLocalAABBs = threadLocalAABBs, + }.Schedule(threadLocalAABBs.Length, 16); + ThreadLocalAABB.AssertCacheLineSize(); + + var entitiesGraphicsRenderedChunkType= GetComponentTypeHandle(false); + var entitiesGraphicsRenderedChunkTypeRO = GetComponentTypeHandle(true); + var chunkHeadersRO = GetComponentTypeHandle(true); + var chunkWorldRenderBoundsRO = GetComponentTypeHandle(true); + var localToWorldsRO = GetComponentTypeHandle(true); + var lodRangesRO = GetComponentTypeHandle(true); + var rootLodRangesRO = GetComponentTypeHandle(true); + var materialMeshInfosRO = GetComponentTypeHandle(true); + var renderMeshArrays = GetSharedComponentTypeHandle(); + + m_ComponentTypeCache.FetchTypeHandles(this); + + Profiler.EndSample(); + + var numNewChunksArray = new NativeArray(1, Allocator.TempJob); + int totalChunks = m_EntitiesGraphicsRenderedQuery.CalculateChunkCount(); + var newChunks = new NativeArray( + totalChunks, + Allocator.TempJob, + NativeArrayOptions.UninitializedMemory); + + var classifyNewChunksJob = new ClassifyNewChunksJob + { + EntitiesGraphicsChunkInfo = entitiesGraphicsRenderedChunkTypeRO, + ChunkHeader = chunkHeadersRO, + NumNewChunks = numNewChunksArray, + NewChunks = newChunks + } + .ScheduleParallel(m_MetaEntitiesForHybridRenderableChunksQuery, inputDependencies); + + JobHandle entitiesGraphicsCompleted = new JobHandle(); + + const int kNumBitsPerLong = sizeof(long) * 8; + var unreferencedBatchIndices = new NativeArray( + (BatchIndexRange + kNumBitsPerLong) / kNumBitsPerLong, + Allocator.TempJob, + NativeArrayOptions.ClearMemory); + + JobHandle initializedUnreferenced = default; + var existingKeys = m_ExistingBatchIndices.ToNativeArray(Allocator.TempJob); + initializedUnreferenced = new InitializeUnreferencedIndicesScatterJob + { + ExistingBatchIndices = existingKeys, + UnreferencedBatchIndices = unreferencedBatchIndices, + }.Schedule(existingKeys.Length, kNumScatteredIndicesPerThread); + existingKeys.Dispose(initializedUnreferenced); + + inputDependencies = JobHandle.CombineDependencies(inputDependencies, initializedUnreferenced, zeroAABBJob); + + // Conservative estimate is that every known type is in every chunk. There will be + // at most one operation per type per chunk, which will be either an actual + // chunk data upload, or a default value blit (a single type should not have both). + int conservativeMaximumGpuUploads = totalChunks * m_ComponentTypeCache.UsedTypeCount; + var gpuUploadOperations = new NativeArray( + conservativeMaximumGpuUploads, + Allocator.TempJob, + NativeArrayOptions.UninitializedMemory); + var numGpuUploadOperationsArray = new NativeArray( + 1, + Allocator.TempJob, + NativeArrayOptions.ClearMemory); + + uint lastSystemVersion = LastSystemVersion; + + if (EntitiesGraphicsEditorTools.DebugSettings.ForceInstanceDataUpload) + { + Debug.Log("Reuploading all Entities Graphics instance data to GPU"); + lastSystemVersion = 0; + } + + classifyNewChunksJob.Complete(); + int numNewChunks = numNewChunksArray[0]; + + var maxBatchCount = math.max(kInitialMaxBatchCount, BatchIndexRange + numNewChunks); + + // Integer division with round up + var maxBatchLongCount = (maxBatchCount + kNumBitsPerLong - 1) / kNumBitsPerLong; + + var entitiesGraphicsChunkUpdater = new EntitiesGraphicsChunkUpdater + { + ComponentTypes = m_ComponentTypeCache.ToBurstCompatible(Allocator.TempJob), + UnreferencedBatchIndices = unreferencedBatchIndices, + ChunkProperties = m_ChunkProperties, + LastSystemVersion = lastSystemVersion, + + GpuUploadOperations = gpuUploadOperations, + NumGpuUploadOperations = numGpuUploadOperationsArray, + + LocalToWorldType = TypeManager.GetTypeIndex(), + WorldToLocalType = TypeManager.GetTypeIndex(), + PrevLocalToWorldType = TypeManager.GetTypeIndex(), + PrevWorldToLocalType = TypeManager.GetTypeIndex(), + + ThreadLocalAABBs = threadLocalAABBs, + ThreadIndex = 0, // set by the job system + +#if PROFILE_BURST_JOB_INTERNALS + ProfileAddUpload = new ProfilerMarker("AddUpload"), +#endif + }; + + var updateOldJob = new UpdateOldEntitiesGraphicsChunksJob + { + EntitiesGraphicsChunkInfo = entitiesGraphicsRenderedChunkType, + ChunkWorldRenderBounds = chunkWorldRenderBoundsRO, + ChunkHeader = chunkHeadersRO, + LocalToWorld = localToWorldsRO, + LodRange = lodRangesRO, + RootLodRange = rootLodRangesRO, + RenderMeshArray = renderMeshArrays, + MaterialMeshInfo = materialMeshInfosRO, + EntitiesGraphicsChunkUpdater = entitiesGraphicsChunkUpdater, + }; + + JobHandle updateOldDependencies = inputDependencies; + + // We need to wait for the job to complete here so we can process the new chunks + updateOldJob.ScheduleParallel(m_MetaEntitiesForHybridRenderableChunksQuery, updateOldDependencies).Complete(); + + // Garbage collect deleted batches before adding new ones to minimize peak memory use. + Profiler.BeginSample("GarbageCollectUnreferencedBatches"); + int numRemoved = GarbageCollectUnreferencedBatches(unreferencedBatchIndices); + Profiler.EndSample(); + + if (numNewChunks > 0) + { + Profiler.BeginSample("AddNewChunks"); + int numValidNewChunks = AddNewChunks(newChunks.GetSubArray(0, numNewChunks)); + Profiler.EndSample(); + + var updateNewChunksJob = new UpdateNewEntitiesGraphicsChunksJob + { + NewChunks = newChunks, + EntitiesGraphicsChunkInfo = entitiesGraphicsRenderedChunkTypeRO, + ChunkWorldRenderBounds = chunkWorldRenderBoundsRO, + EntitiesGraphicsChunkUpdater = entitiesGraphicsChunkUpdater, + }; + +#if DEBUG_LOG_INVALID_CHUNKS + if (numValidNewChunks != numNewChunks) + Debug.Log($"Tried to add {numNewChunks} new chunks, but only {numValidNewChunks} were valid, {numNewChunks - numValidNewChunks} were invalid"); +#endif + + entitiesGraphicsCompleted = updateNewChunksJob.Schedule(numValidNewChunks, kNumNewChunksPerThread); + } + + entitiesGraphicsChunkUpdater.ComponentTypes.Dispose(entitiesGraphicsCompleted); + newChunks.Dispose(entitiesGraphicsCompleted); + numNewChunksArray.Dispose(entitiesGraphicsCompleted); + + var drawCommandFlagsUpdated = new UpdateDrawCommandFlagsJob + { + LocalToWorld = GetComponentTypeHandle(true), + RenderFilterSettings = GetSharedComponentTypeHandle(), + EntitiesGraphicsChunkInfo = GetComponentTypeHandle(), + FilterSettings = m_FilterSettings, + DefaultFilterSettings = MakeFilterSettings(RenderFilterSettings.Default), + }.ScheduleParallel(m_ChangedTransformQuery, entitiesGraphicsCompleted); + DidScheduleUpdateJob(drawCommandFlagsUpdated); + + // TODO: Need to wait for new chunk updating to complete, so there are no more jobs writing to the bitfields. + entitiesGraphicsCompleted.Complete(); + + int numGpuUploadOperations = numGpuUploadOperationsArray[0]; + Debug.Assert(numGpuUploadOperations <= gpuUploadOperations.Length, "Maximum GPU upload operation count exceeded"); + + Profiler.BeginSample("BlitDirtyGlobalValues"); + BlitDirtyGlobalValues(); + Profiler.EndSample(); + + ComputeUploadSizeRequirements( + numGpuUploadOperations, gpuUploadOperations, + out int numOperations, out int totalUploadBytes, out int biggestUploadBytes); + +#if DEBUG_LOG_UPLOADS + if (numOperations > 0) + { + Debug.Log($"GPU upload operations: {numOperations}, GPU upload bytes: {totalUploadBytes}"); + } +#endif + Profiler.BeginSample("StartUpdate"); + StartUpdate(numOperations, totalUploadBytes, biggestUploadBytes); + Profiler.EndSample(); + + var uploadsExecuted = new ExecuteGpuUploads + { + GpuUploadOperations = gpuUploadOperations, + ThreadedSparseUploader = m_ThreadedGPUUploader, + }.Schedule(numGpuUploadOperations, 1); + numGpuUploadOperationsArray.Dispose(); + gpuUploadOperations.Dispose(uploadsExecuted); + + Profiler.BeginSample("UploadAllBlits"); + UploadAllBlits(); + Profiler.EndSample(); + +#if DEBUG_LOG_CHUNK_CHANGES + if (numNewChunks > 0 || numRemoved > 0) + Debug.Log($"Chunks changed, new chunks: {numNewChunks}, removed batches: {numRemoved}, batch count: {m_ExistingBatchBatchIndices.Count()}, chunk count: {m_MetaEntitiesForHybridRenderableChunks.CalculateEntityCount()}"); +#endif + + Profiler.BeginSample("UpdateGlobalAABB"); + UpdateGlobalAABB(threadLocalAABBs); + Profiler.EndSample(); + + unreferencedBatchIndices.Dispose(); + + uploadsExecuted.Complete(); + + JobHandle outputDeps = JobHandle.CombineDependencies(uploadsExecuted, drawCommandFlagsUpdated); + + return outputDeps; + } + + private static BatchRendererGroupGlobals[] s_GlobalsArray = new BatchRendererGroupGlobals[1] { default }; + private void BlitDirtyGlobalValues() + { + if (m_GlobalValuesDirty) + { + // SetData needs an array, so put the data in a helper array + s_GlobalsArray[0] = m_GlobalValues; + m_GlobalValuesCbuffer.SetData(s_GlobalsArray); + m_GlobalValuesDirty = false; + } + } + + private void UpdateGlobalAABB(NativeArray threadLocalAABBs) + { + MinMaxAABB aabb = MinMaxAABB.Empty; + + for (int i = 0; i < threadLocalAABBs.Length; ++i) + aabb.Encapsulate(threadLocalAABBs[i].AABB); + +#if DEBUG_LOG_GLOBAL_AABB + Debug.Log($"Global AABB min: {aabb.Min} max: {aabb.Max}"); +#endif + + var centerExtentsAABB = (AABB) aabb; + m_BatchRendererGroup.SetGlobalBounds(new Bounds(centerExtentsAABB.Center, centerExtentsAABB.Extents)); + + threadLocalAABBs.Dispose(); + } + + private void ComputeUploadSizeRequirements( + int numGpuUploadOperations, NativeArray gpuUploadOperations, + out int numOperations, out int totalUploadBytes, out int biggestUploadBytes) + { + numOperations = numGpuUploadOperations + m_ValueBlits.Length; + totalUploadBytes = 0; + biggestUploadBytes = 0; + + for (int i = 0; i < numGpuUploadOperations; ++i) + { + var numBytes = gpuUploadOperations[i].BytesRequiredInUploadBuffer; + totalUploadBytes += numBytes; + biggestUploadBytes = math.max(biggestUploadBytes, numBytes); + } + + for (int i = 0; i < m_ValueBlits.Length; ++i) + { + var numBytes = m_ValueBlits[i].BytesRequiredInUploadBuffer; + totalUploadBytes += numBytes; + biggestUploadBytes = math.max(biggestUploadBytes, numBytes); + } + } + + private int GarbageCollectUnreferencedBatches(NativeArray unreferencedBatchIndices) + { + int numRemoved = 0; + + int firstInQw = 0; + for (int i = 0; i < unreferencedBatchIndices.Length; ++i) + { + long qw = unreferencedBatchIndices[i]; + while (qw != 0) + { + int setBit = math.tzcnt(qw); + long mask = ~(1L << setBit); + int batchIndex = firstInQw + setBit; + + RemoveBatch(batchIndex); + ++numRemoved; + + qw &= mask; + } + + firstInQw += (int)AtomicHelpers.kNumBitsInLong; + } + +#if DEBUG_LOG_GARBAGE_COLLECTION + Debug.Log($"GarbageCollectUnreferencedBatches(removed: {numRemoved})"); +#endif + + return numRemoved; + } + + private void RemoveBatch(int batchIndex) + { + var batchInfo = m_BatchInfos[batchIndex]; + m_BatchInfos[batchIndex] = default; + +#if DEBUG_LOG_BATCH_DELETION + Debug.Log($"RemoveBatch({batchIndex})"); +#endif + + RemoveBatchIndex(batchIndex); + + if (!batchInfo.GPUMemoryAllocation.Empty) + { + m_GPUPersistentAllocator.Release(batchInfo.GPUMemoryAllocation); +#if DEBUG_LOG_MEMORY_USAGE + Debug.Log($"RELEASE; {batchInfo.GPUMemoryAllocation.Length}"); +#endif + } + + var metadataAllocation = batchInfo.ChunkMetadataAllocation; + if (!metadataAllocation.Empty) + { + for (ulong j = metadataAllocation.begin; j < metadataAllocation.end; ++j) + m_ChunkProperties[(int)j] = default; + + m_ChunkMetadataAllocator.Release(metadataAllocation); + } + } + + private int NumInstancesInChunk(ArchetypeChunk chunk) => chunk.Capacity; + + private int AddNewChunks(NativeArray newChunks) + { + int numValidNewChunks = 0; + + Debug.Assert(newChunks.Length > 0, "Attempted to add new chunks, but list of new chunks was empty"); + + var batchCreationTypeHandles = new BatchCreationTypeHandles(this); + + // Sort new chunks by RenderMesh so we can put + // all compatible chunks inside one batch. + var batchCreateInfoFactory = new BatchCreateInfoFactory + { + GraphicsArchetypes = m_GraphicsArchetypes, + TypeIndexToMaterialProperty = m_TypeIndexToMaterialProperty, + }; + + var sortedNewChunks = new NativeArray(newChunks.Length, Allocator.Temp); + + for (int i = 0; i < newChunks.Length; ++i) + sortedNewChunks[i] = batchCreateInfoFactory.Create(newChunks[i]); + + sortedNewChunks.Sort(); + + int batchBegin = 0; + int numInstances = NumInstancesInChunk(sortedNewChunks[0].Chunk); + int maxEntitiesPerBatch = m_GraphicsArchetypes + .GetGraphicsArchetype(sortedNewChunks[0].GraphicsArchetypeIndex) + .MaxEntitiesPerBatch; + + for (int i = 1; i <= sortedNewChunks.Length; ++i) + { + int instancesInChunk = 0; + bool breakBatch = false; + + if (i < sortedNewChunks.Length) + { + var cur = sortedNewChunks[i]; + breakBatch = !sortedNewChunks[batchBegin].Equals(cur); + instancesInChunk = NumInstancesInChunk(cur.Chunk); + } + else + { + breakBatch = true; + } + + if (numInstances + instancesInChunk > maxEntitiesPerBatch) + breakBatch = true; + + if (breakBatch) + { + int numChunks = i - batchBegin; + + bool valid = AddNewBatch( + batchCreationTypeHandles, + sortedNewChunks.GetSubArray(batchBegin, numChunks), + numInstances); + + // As soon as we encounter an invalid chunk, we know that all the rest are invalid + // too. + if (valid) + numValidNewChunks += numChunks; + else + return numValidNewChunks; + + batchBegin = i; + numInstances = instancesInChunk; + + if (batchBegin < sortedNewChunks.Length) + maxEntitiesPerBatch = m_GraphicsArchetypes + .GetGraphicsArchetype(sortedNewChunks[batchBegin].GraphicsArchetypeIndex) + .MaxEntitiesPerBatch; + } + else + { + numInstances += instancesInChunk; + } + } + + sortedNewChunks.Dispose(); + + return numValidNewChunks; + } + + private static int NextAlignedBy16(int size) + { + return ((size + 15) >> 4) << 4; + } + + internal static MetadataValue CreateMetadataValue(int nameID, int gpuAddress, bool isOverridden) + { + const uint kPerInstanceDataBit = 0x80000000; + + return new MetadataValue + { + NameID = nameID, + Value = (uint) gpuAddress + | (isOverridden ? kPerInstanceDataBit : 0), + }; + } + + private bool AddNewBatch( + BatchCreationTypeHandles typeHandles, + NativeArray batchChunks, + int numInstances) + { + var graphicsArchetype = m_GraphicsArchetypes.GetGraphicsArchetype(batchChunks[0].GraphicsArchetypeIndex); + + var overrides = graphicsArchetype.PropertyComponents; + var overrideSizes = new NativeArray(overrides.Length, Allocator.Temp); + + int numProperties = overrides.Length; + + Debug.Assert(numProperties > 0, "No overridden properties, expected at least one"); + Debug.Assert(numInstances > 0, "No instances, expected at least one"); + Debug.Assert(batchChunks.Length > 0, "No chunks, expected at least one"); + + int batchSizeBytes = 0; + // Every chunk has the same graphics archetype, so each requires the same amount + // of component metadata structs. + int batchTotalChunkMetadata = numProperties * batchChunks.Length; + + for (int i = 0; i < overrides.Length; ++i) + { + // For each component, allocate a contiguous range that's aligned by 16. + int sizeBytesComponent = NextAlignedBy16(overrides[i].SizeBytesGPU * numInstances); + overrideSizes[i] = sizeBytesComponent; + batchSizeBytes += sizeBytesComponent; + } + + BatchInfo batchInfo = default; + + // TODO: If allocations fail, bail out and stop spamming the log each frame. + + batchInfo.ChunkMetadataAllocation = m_ChunkMetadataAllocator.Allocate((ulong)batchTotalChunkMetadata); + if (batchInfo.ChunkMetadataAllocation.Empty) Debug.Assert(false, $"Out of memory in the Entities Graphics chunk metadata buffer. Attempted to allocate {batchTotalChunkMetadata} elements, buffer size: {m_ChunkMetadataAllocator.Size}, free size left: {m_ChunkMetadataAllocator.FreeSpace}."); + + batchInfo.GPUMemoryAllocation = m_GPUPersistentAllocator.Allocate((ulong)batchSizeBytes, BatchAllocationAlignment); + if (batchInfo.GPUMemoryAllocation.Empty) Debug.Assert(false, $"Out of memory in the Entities Graphics GPU instance data buffer. Attempted to allocate {batchSizeBytes}, buffer size: {m_GPUPersistentAllocator.Size}, free size left: {m_GPUPersistentAllocator.FreeSpace}."); + + // Physical offset inside the buffer, always the same on all platforms. + int allocationBegin = (int)batchInfo.GPUMemoryAllocation.begin; + + // Metadata offset depends on whether a raw buffer or cbuffer is used. + // Raw buffers index from start of buffer, cbuffers index from start of allocation. + uint bindOffset = UseConstantBuffers + ? (uint)allocationBegin + : 0; + uint bindWindowSize = UseConstantBuffers + ? (uint)MaxBytesPerBatch + : 0; + + // Compute where each individual property SoA stream starts + var overrideStreamBegin = new NativeArray(overrides.Length, Allocator.Temp); + overrideStreamBegin[0] = allocationBegin; + for (int i = 1; i < numProperties; ++i) + overrideStreamBegin[i] = overrideStreamBegin[i - 1] + overrideSizes[i - 1]; + + int numMetadata = numProperties; + var overrideMetadata = new NativeArray(numMetadata, Allocator.Temp); + + int metadataIndex = 0; + for (int i = 0; i < numProperties; ++i) + { + int gpuAddress = overrideStreamBegin[i] - (int)bindOffset; + overrideMetadata[metadataIndex] = CreateMetadataValue(overrides[i].NameID, gpuAddress, true); + ++metadataIndex; + +#if DEBUG_LOG_PROPERTY_ALLOCATIONS + Debug.Log($"Property Allocation: Property: {NameIDFormatted(overrides[i].NameID)} Type: {TypeIndexFormatted(overrides[i].TypeIndex)} Metadata: {overrideMetadata[i].Value:x8} Allocation: {overrideStreamBegin[i]}"); +#endif + } + + var batchID = m_ThreadedBatchContext.AddBatch(overrideMetadata, m_GPUPersistentInstanceBufferHandle, + bindOffset, bindWindowSize); + int batchIndex = (int)batchID.value; + +#if DEBUG_LOG_BATCH_CREATION + Debug.Log($"Created new batch, ID: {batchIndex}, chunks: {batchChunks.Length}, properties: {numProperties}, instances: {numInstances}, size: {batchSizeBytes}, buffer {m_GPUPersistentInstanceBufferHandle.value} (size {m_GPUPersistentInstanceData.count * m_GPUPersistentInstanceData.stride} bytes)"); +#endif + + if (batchIndex == 0) Debug.Assert(false, "Failed to add new BatchRendererGroup batch."); + + AddBatchIndex(batchIndex); + m_BatchInfos[batchIndex] = batchInfo; + + // Configure chunk components for each chunk + int chunkMetadataBegin = (int)batchInfo.ChunkMetadataAllocation.begin; + int chunkOffsetInBatch = 0; + for (int i = 0; i < batchChunks.Length; ++i) + { + var chunk = batchChunks[i].Chunk; + var entitiesGraphicsChunkInfo = new EntitiesGraphicsChunkInfo + { + Valid = true, + BatchIndex = batchIndex, + ChunkTypesBegin = chunkMetadataBegin, + ChunkTypesEnd = chunkMetadataBegin + numProperties, + CullingData = new EntitiesGraphicsChunkCullingData + { + Flags = ComputeCullingFlags(chunk, typeHandles), + InstanceLodEnableds = default, + ChunkOffsetInBatch = chunkOffsetInBatch, + }, + }; + + EntityManager.SetChunkComponentData(chunk, entitiesGraphicsChunkInfo); + + for (int j = 0; j < numProperties; ++j) + { + var propertyOverride = overrides[j]; + var chunkProperty = new ChunkProperty + { + ComponentTypeIndex = propertyOverride.TypeIndex, + GPUDataBegin = overrideStreamBegin[j] + chunkOffsetInBatch * propertyOverride.SizeBytesGPU, + ValueSizeBytesCPU = propertyOverride.SizeBytesCPU, + ValueSizeBytesGPU = propertyOverride.SizeBytesGPU, + }; + + m_ChunkProperties[chunkMetadataBegin + j] = chunkProperty; + } + + chunkOffsetInBatch += NumInstancesInChunk(chunk); + chunkMetadataBegin += numProperties; + } + + Debug.Assert(chunkOffsetInBatch == numInstances, "Batch instance count mismatch"); + + return true; + } + + private byte ComputeCullingFlags(ArchetypeChunk chunk, BatchCreationTypeHandles typeHandles) + { + bool hasLodData = chunk.Has(typeHandles.RootLODRange) && + chunk.Has(typeHandles.LODRange); + + // TODO: Do we need non-per-instance culling anymore? It seems to always be added + // for converted objects, and doesn't seem to be removed ever, so the only way to + // not have it is to manually remove it or create entities from scratch. + bool hasPerInstanceCulling = !hasLodData || chunk.Has(typeHandles.PerInstanceCulling); + + byte flags = 0; + + if (hasLodData) flags |= EntitiesGraphicsChunkCullingData.kFlagHasLodData; + if (hasPerInstanceCulling) flags |= EntitiesGraphicsChunkCullingData.kFlagInstanceCulling; + + return flags; + } + + private void UploadAllBlits() + { + UploadBlitJob uploadJob = new UploadBlitJob() + { + BlitList = m_ValueBlits, + ThreadedSparseUploader = m_ThreadedGPUUploader + }; + + JobHandle handle = uploadJob.Schedule(m_ValueBlits.Length, 1); + handle.Complete(); + + m_ValueBlits.Clear(); + } + + private void CompleteJobs(bool completeEverything = false) + { + m_CullingJobDependency.Complete(); + m_CullingJobDependencyGroup.CompleteDependency(); + m_CullingJobReleaseDependency.Complete(); + + // TODO: This might not be necessary, remove? + if (completeEverything) + { + m_EntitiesGraphicsRenderedQuery.CompleteDependency(); + m_LodSelectGroup.CompleteDependency(); + m_ChangedTransformQuery.CompleteDependency(); + } + + m_UpdateJobDependency.Complete(); + m_UpdateJobDependency = new JobHandle(); + } + + private void DidScheduleCullingJob(JobHandle job) + { + m_CullingJobDependency = JobHandle.CombineDependencies(job, m_CullingJobDependency); + m_CullingJobDependencyGroup.AddDependency(job); + } + + private void DidScheduleUpdateJob(JobHandle job) + { + m_UpdateJobDependency = JobHandle.CombineDependencies(job, m_UpdateJobDependency); + } + + private void StartUpdate(int numOperations, int totalUploadBytes, int biggestUploadBytes) + { + var persistentBytes = m_GPUPersistentAllocator.OnePastHighestUsedAddress; + if (persistentBytes > (ulong)m_PersistentInstanceDataSize) + { + while ((ulong)m_PersistentInstanceDataSize < persistentBytes) + { + m_PersistentInstanceDataSize *= 2; + } + + if (m_PersistentInstanceDataSize > kGPUBufferSizeMax) + { + m_PersistentInstanceDataSize = kGPUBufferSizeMax; // Some backends fails at loading 1024 MiB, but 1023 is fine... This should ideally be a device cap. + } + + if(persistentBytes > kGPUBufferSizeMax) + Debug.LogError("Entities Graphics: Current loaded scenes need more than 1GiB of persistent GPU memory. This is more than some GPU backends can allocate. Try to reduce amount of loaded data."); + + var newBuffer = new GraphicsBuffer( + GraphicsBuffer.Target.Raw, + GraphicsBuffer.UsageFlags.None, + (int)m_PersistentInstanceDataSize / 4, + 4); + m_GPUUploader.ReplaceBuffer(newBuffer, true); + + m_GPUPersistentInstanceBufferHandle = newBuffer.bufferHandle; + + UpdateBatchBufferHandles(); + + if(m_GPUPersistentInstanceData != null) + m_GPUPersistentInstanceData.Dispose(); + m_GPUPersistentInstanceData = newBuffer; + } + + m_ThreadedGPUUploader = + m_GPUUploader.Begin(totalUploadBytes, biggestUploadBytes, numOperations); + } + + private void UpdateBatchBufferHandles() + { + foreach (var b in m_ExistingBatchIndices) + { + m_BatchRendererGroup.SetBatchBuffer(new BatchID { value = (uint)b }, m_GPUPersistentInstanceBufferHandle); + } + } + +#if DEBUG_LOG_MEMORY_USAGE + private static ulong PrevUsedSpace = 0; +#endif + + private void EndUpdate() + { + m_GPUUploader.EndAndCommit(m_ThreadedGPUUploader); + + // Globally bind the BRG globals cbuffer so all shaders can access it. + Shader.SetGlobalConstantBuffer( + BatchRendererGroupGlobals.kGlobalsPropertyId, + m_GlobalValuesCbuffer, + 0, + m_GlobalValuesCbuffer.stride); + +#if DEBUG_LOG_MEMORY_USAGE + if (m_GPUPersistentAllocator.UsedSpace != PrevUsedSpace) + { + Debug.Log($"GPU memory: {m_GPUPersistentAllocator.UsedSpace / 1024.0 / 1024.0:F4} / {m_GPUPersistentAllocator.Size / 1024.0 / 1024.0:F4}"); + PrevUsedSpace = m_GPUPersistentAllocator.UsedSpace; + } +#endif + } + + internal static NativeList NewNativeListResized(int length, Allocator allocator, NativeArrayOptions resizeOptions = NativeArrayOptions.ClearMemory) where T : unmanaged + { + var list = new NativeList(length, allocator); + list.Resize(length, resizeOptions); + + return list; + } + + internal void UpdateSpecCubeHDRDecode(Vector4 specCubeHDRDecode) + { + if (!s_EntitiesGraphicsEnabled) return; + + bool hdrDecodeDirty = specCubeHDRDecode != m_GlobalValues.SpecCube0_HDR; + if (hdrDecodeDirty) + { + m_GlobalValues.SpecCube0_HDR = specCubeHDRDecode; + m_GlobalValues.SpecCube1_HDR = specCubeHDRDecode; + m_GlobalValuesDirty = true; + } + } + + internal void UpdateGlobalAmbientProbe(SHCoefficients globalAmbientProbe) + { + if (!s_EntitiesGraphicsEnabled) return; + + bool globalAmbientProbeDirty = globalAmbientProbe != m_GlobalValues.SHCoefficients; + if (globalAmbientProbeDirty) + { + m_GlobalValues.SHCoefficients = globalAmbientProbe; + m_GlobalValuesDirty = true; + +#if DEBUG_LOG_AMBIENT_PROBE + Debug.Log($"Global Ambient probe: {globalAmbientProbe.SHAr} {globalAmbientProbe.SHAg} {globalAmbientProbe.SHAb} {globalAmbientProbe.SHBr} {globalAmbientProbe.SHBg} {globalAmbientProbe.SHBb} {globalAmbientProbe.SHC}"); +#endif + } + } + + /// + /// Registers a material with the Entities Graphics System. + /// + /// The material instance to register + /// Returns the batch material ID + public BatchMaterialID RegisterMaterial(Material material) => m_BatchRendererGroup.RegisterMaterial(material); + + /// + /// Registers a mesh with the Entities Graphics System. + /// + /// Mesh instance to register + /// Returns the batch mesh ID + public BatchMeshID RegisterMesh(Mesh mesh) => m_BatchRendererGroup.RegisterMesh(mesh); + + /// + /// Unregisters a material from the Entities Graphics System. + /// + /// Material ID received from + public void UnregisterMaterial(BatchMaterialID material) => m_BatchRendererGroup.UnregisterMaterial(material); + + /// + /// Unregisters a mesh from the Entities Graphics System. + /// + /// A mesh ID received from . + public void UnregisterMesh(BatchMeshID mesh) => m_BatchRendererGroup.UnregisterMesh(mesh); + + internal Mesh GetMesh(BatchMeshID mesh) => m_BatchRendererGroup.GetRegisteredMesh(mesh); + internal Material GetMaterial(BatchMaterialID material) => m_BatchRendererGroup.GetRegisteredMaterial(material); + + /// + /// Converts a type index into a type name. + /// + /// The type index to convert. + /// The name of the type for given type index. + internal static string TypeIndexToName(int typeIndex) + { +#if DEBUG_PROPERTY_NAMES + if (s_TypeIndexToName.TryGetValue(typeIndex, out var name)) + return name; + else + return ""; +#else + return null; +#endif + } + + /// + /// Converts a name ID to a name. + /// + /// + /// The name for the given name ID. + internal static string NameIDToName(int nameID) + { +#if DEBUG_PROPERTY_NAMES + if (s_NameIDToName.TryGetValue(nameID, out var name)) + return name; + else + return ""; +#else + return null; +#endif + } + + internal static string TypeIndexFormatted(int typeIndex) + { + return $"{TypeIndexToName(typeIndex)} ({typeIndex:x8})"; + } + + /// + /// Converts a name ID to a formatted name. + /// + /// + /// The formatted name for the given name ID. + internal static string NameIDFormatted(int nameID) + { + return $"{NameIDToName(nameID)} ({nameID:x8})"; + } + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs.meta new file mode 100644 index 0000000..5f14fc2 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83369380b1fdc794f8bf14bf2b60362e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs b/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs new file mode 100644 index 0000000..bea5544 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs @@ -0,0 +1,272 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Rendering; + +[assembly: InternalsVisibleTo("Unity.Entities.Graphics.Tests")] +namespace Unity.Rendering +{ + internal static class EntitiesGraphicsUtils + { + public static EntityQueryDesc GetEntitiesGraphicsRenderedQueryDesc() + { + return new EntityQueryDesc + { + All = new[] + { + ComponentType.ChunkComponentReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ChunkComponent(), + }, + }; + } + + public static EntityQueryDesc GetEntitiesGraphicsRenderedQueryDescReadOnly() + { + return new EntityQueryDesc + { + All = new[] + { + ComponentType.ChunkComponentReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ChunkComponentReadOnly(), + }, + }; + } + + private static bool CheckGLVersion() + { + // Support GLES3 if and only if it supports compute shaders. + // In practice this should mean GLES3.1 and later. + if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3) + return SystemInfo.supportsComputeShaders; + + char[] delimiterChars = { ' ', '.' }; + var arr = SystemInfo.graphicsDeviceVersion.Split(delimiterChars); + if (arr.Length >= 3) + { + var major = Int32.Parse(arr[1]); + var minor = Int32.Parse(arr[2]); + + return major >= 4 && minor >= 3; + } + + return false; + } + + public static bool IsEntitiesGraphicsSupportedOnSystem() + { + var deviceType = SystemInfo.graphicsDeviceType; + + bool isOpenGL = deviceType == GraphicsDeviceType.OpenGLCore || + deviceType == GraphicsDeviceType.OpenGLES3; + + if (deviceType == GraphicsDeviceType.Null || + !SystemInfo.supportsComputeShaders || + (isOpenGL && !CheckGLVersion())) + return false; + + return true; + } + + public static bool UseHybridConstantBufferMode() => + BatchRendererGroup.BufferTarget == BatchBufferTarget.ConstantBuffer; + + private const int kMaxCbufferSize = 64 * 1024; + public static readonly int BatchAllocationAlignment = math.max(16, SystemInfo.constantBufferOffsetAlignment); + public static readonly int MaxBytesPerCBuffer = math.min(kMaxCbufferSize, SystemInfo.maxConstantBufferSize); + + static Material LoadMaterialWithHideAndDontSave(string name) + { + Shader shader = Shader.Find(name); + + if (shader == null) + { + Debug.LogError($"Shader \'{name}\' not found."); + return null; + } + + Material material = new Material(shader); + + // Prevent Material unloading when switching scene + material.hideFlags = HideFlags.HideAndDontSave; + + return material; + } + + public static Material LoadErrorMaterial() + { +#if HDRP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/HDRP/MaterialError"); +#elif URP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/Universal Render Pipeline/FallbackError"); +#else + // TODO: What about custom SRPs? Is it enough to just throw an error, or should + // we search for a custom shader with a specific name? + return null; +#endif + } + + public static Material LoadLoadingMaterial() + { +#if HDRP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/HDRP/MaterialLoading"); +#elif URP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/Universal Render Pipeline/FallbackLoading"); +#else + // TODO: What about custom SRPs? Is it enough to just throw an error, or should + // we search for a custom shader with a specific name? + return null; +#endif + } + + public static Material LoadPickingMaterial() + { +#if HDRP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/HDRP/BRGPicking"); +#elif URP_10_0_0_OR_NEWER + return LoadMaterialWithHideAndDontSave("Hidden/Universal Render Pipeline/BRGPicking"); +#else + // TODO: What about custom SRPs? Is it enough to just throw an error, or should + // we search for a custom shader with a specific name? + return null; +#endif + } + } + + // Burst currently does not support atomic AND and OR. Use compare-and-exchange based + // workarounds until it does. + internal struct AtomicHelpers + { + public const uint kNumBitsInLong = sizeof(long) * 8; + + public static void IndexToQwIndexAndMask(int index, out int qwIndex, out long mask) + { + uint i = (uint)index; + uint qw = i / kNumBitsInLong; + uint shift = i % kNumBitsInLong; + + qwIndex = (int)qw; + mask = 1L << (int)shift; + } + + public static unsafe long AtomicAnd(long* qwords, int index, long value) + { + // TODO: Replace this with atomic AND once it is available + long currentValue = System.Threading.Interlocked.Read(ref qwords[index]); + for (;;) + { + // If the AND wouldn't change any bits, no need to issue the atomic + if ((currentValue & value) == currentValue) + return currentValue; + + long newValue = currentValue & value; + long prevValue = + System.Threading.Interlocked.CompareExchange(ref qwords[index], newValue, currentValue); + + // If the value was equal to the expected value, we know that our atomic went through + if (prevValue == currentValue) + return prevValue; + + currentValue = prevValue; + } + } + + public static unsafe long AtomicOr(long* qwords, int index, long value) + { + // TODO: Replace this with atomic OR once it is available + long currentValue = System.Threading.Interlocked.Read(ref qwords[index]); + for (;;) + { + // If the OR wouldn't change any bits, no need to issue the atomic + if ((currentValue | value) == currentValue) + return currentValue; + + long newValue = currentValue | value; + long prevValue = + System.Threading.Interlocked.CompareExchange(ref qwords[index], newValue, currentValue); + + // If the value was equal to the expected value, we know that our atomic went through + if (prevValue == currentValue) + return prevValue; + + currentValue = prevValue; + } + } + + public static unsafe float AtomicMin(float* floats, int index, float value) + { + float currentValue = floats[index]; + + // Never propagate NaNs to memory + if (float.IsNaN(value)) + return currentValue; + + int* floatsAsInts = (int*) floats; + int valueAsInt = math.asint(value); + + // Do the CAS operations as ints to avoid problems with NaNs + for (;;) + { + // If currentValue is NaN, this comparison will fail + if (currentValue <= value) + return currentValue; + + int currentValueAsInt = math.asint(currentValue); + + int newValue = valueAsInt; + int prevValue = System.Threading.Interlocked.CompareExchange(ref floatsAsInts[index], newValue, currentValueAsInt); + float prevValueAsFloat = math.asfloat(prevValue); + + // If the value was equal to the expected value, we know that our atomic went through + // NOTE: This comparison MUST be an integer comparison, as otherwise NaNs + // would result in an infinite loop + if (prevValue == currentValueAsInt) + return prevValueAsFloat; + + currentValue = prevValueAsFloat; + } + } + + public static unsafe float AtomicMax(float* floats, int index, float value) + { + float currentValue = floats[index]; + + // Never propagate NaNs to memory + if (float.IsNaN(value)) + return currentValue; + + int* floatsAsInts = (int*) floats; + int valueAsInt = math.asint(value); + + // Do the CAS operations as ints to avoid problems with NaNs + for (;;) + { + // If currentValue is NaN, this comparison will fail + if (currentValue >= value) + return currentValue; + + int currentValueAsInt = math.asint(currentValue); + + int newValue = valueAsInt; + int prevValue = System.Threading.Interlocked.CompareExchange(ref floatsAsInts[index], newValue, currentValueAsInt); + float prevValueAsFloat = math.asfloat(prevValue); + + // If the value was equal to the expected value, we know that our atomic went through + // NOTE: This comparison MUST be an integer comparison, as otherwise NaNs + // would result in an infinite loop + if (prevValue == currentValueAsInt) + return prevValueAsFloat; + + currentValue = prevValueAsFloat; + } + } + } +} diff --git a/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs.meta b/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs.meta new file mode 100644 index 0000000..63ab117 --- /dev/null +++ b/Unity.Entities.Graphics/EntitiesGraphicsUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b418afd943138db43b156168d5498843 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/FreezeStaticLODObjects.cs b/Unity.Entities.Graphics/FreezeStaticLODObjects.cs new file mode 100644 index 0000000..99d53a8 --- /dev/null +++ b/Unity.Entities.Graphics/FreezeStaticLODObjects.cs @@ -0,0 +1,23 @@ +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.Rendering +{ + [WorldSystemFilter(WorldSystemFilterFlags.EntitySceneOptimizations)] + [UpdateAfter(typeof(LODRequirementsUpdateSystem))] + partial class FreezeStaticLODObjects : SystemBase + { + /// + protected override void OnUpdate() + { + var group = GetEntityQuery( + new EntityQueryDesc + { + Any = new ComponentType[] { typeof(MeshLODGroupComponent), typeof(MeshLODComponent), typeof(LODGroupWorldReferencePoint) }, + All = new ComponentType[] { typeof(Static) } + }); + + EntityManager.RemoveComponent(group, new ComponentTypeSet(typeof(MeshLODGroupComponent), typeof(MeshLODComponent), typeof(LODGroupWorldReferencePoint))); + } + } +} diff --git a/Unity.Entities.Graphics/FreezeStaticLODObjects.cs.meta b/Unity.Entities.Graphics/FreezeStaticLODObjects.cs.meta new file mode 100644 index 0000000..7477753 --- /dev/null +++ b/Unity.Entities.Graphics/FreezeStaticLODObjects.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6fc54855c1eaa48f299c1e5305cc63b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs b/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs new file mode 100644 index 0000000..3e3d2d1 --- /dev/null +++ b/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs @@ -0,0 +1,56 @@ +using System; +using Unity.Entities; + +namespace Unity.Rendering +{ + + /// + /// Frozen scene tag. + /// + [Serializable] + public struct FrozenRenderSceneTag : ISharedComponentData, IEquatable + { + /// + /// Scene ID. + /// + public Hash128 SceneGUID; + + /// + /// Section ID. + /// + public int SectionIndex; + + /// + /// Streaming LOD flags. + /// + public int HasStreamedLOD; + + /// + /// Determines whether two object instances are equal. + /// + /// Other instance + /// True if the objects belong to the same scene and section + public bool Equals(FrozenRenderSceneTag other) + { + return SceneGUID == other.SceneGUID && SectionIndex == other.SectionIndex; + } + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + public override int GetHashCode() + { + return SceneGUID.GetHashCode() ^ SectionIndex; + } + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + public override string ToString() + { + return $"GUID: {SceneGUID} section: {SectionIndex}"; + } + } +} diff --git a/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs.meta b/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs.meta new file mode 100644 index 0000000..e753b7a --- /dev/null +++ b/Unity.Entities.Graphics/FrozenRenderSceneTagProxy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5bad6ee22b9e4d8a8a057df378aaec0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs b/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs new file mode 100644 index 0000000..055335e --- /dev/null +++ b/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Unity.Collections; +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.Rendering +{ + [WorldSystemFilter(WorldSystemFilterFlags.EntitySceneOptimizations)] + partial class FrozenStaticRendererSystem : SystemBase + { + /// + protected override void OnUpdate() + { + var group = GetEntityQuery( + new EntityQueryDesc + { + All = new ComponentType[] { typeof(SceneSection), typeof(RenderMesh), typeof(LocalToWorld), typeof(Static) }, + None = new ComponentType[] { typeof(FrozenRenderSceneTag) } + }); + + EntityManager.GetAllUniqueSharedComponents(out var sections, Allocator.Temp); + + // @TODO: Perform full validation that all Low LOD levels are in section 0 + int hasStreamedLOD = 0; + foreach (var section in sections) + { + group.SetSharedComponentFilterManaged(section); + if (section.Section != 0) + hasStreamedLOD = 1; + } + + foreach (var section in sections) + { + group.SetSharedComponentFilterManaged(section); + EntityManager.AddSharedComponentManaged(group, new FrozenRenderSceneTag { SceneGUID = section.SceneGUID, SectionIndex = section.Section, HasStreamedLOD = hasStreamedLOD}); + } + + group.ResetFilter(); + } + } +} diff --git a/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs.meta b/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs.meta new file mode 100644 index 0000000..0b1e0f2 --- /dev/null +++ b/Unity.Entities.Graphics/FrozenStaticRendererSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2a99b19d594a84164b504ec2043bc931 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/FrustumPlanes.cs b/Unity.Entities.Graphics/FrustumPlanes.cs new file mode 100644 index 0000000..189f6ff --- /dev/null +++ b/Unity.Entities.Graphics/FrustumPlanes.cs @@ -0,0 +1,284 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + /// + /// Represents frustum planes. + /// + public struct FrustumPlanes + { + /// + /// Options for an intersection result. + /// + public enum IntersectResult + { + /// + /// The object is completely outside of the planes. + /// + Out, + + /// + /// The object is completely inside of the planes. + /// + In, + + /// + /// The object is partially intersecting the planes. + /// + Partial + }; + + /// + /// Populates the frustum plane array from the given camera frustum. + /// + /// The camera to use for calculation. + /// The result of the operation. + /// Is thrown if the planes are empty. + /// Is thrown if the planes size is not equal to 6. + public static void FromCamera(Camera camera, NativeArray planes) + { + if (planes == null) + throw new ArgumentNullException("The argument planes cannot be null."); + if (planes.Length != 6) + throw new ArgumentException("The argument planes does not have the expected length 6."); + + Plane[] sourcePlanes = GeometryUtility.CalculateFrustumPlanes(camera); + + var cameraToWorld = camera.cameraToWorldMatrix; + var eyePos = cameraToWorld.MultiplyPoint(Vector3.zero); + var viewDir = new float3(cameraToWorld.m02, cameraToWorld.m12, cameraToWorld.m22); + viewDir = -math.normalizesafe(viewDir); + + // Near Plane + sourcePlanes[4].SetNormalAndPosition(viewDir, eyePos); + sourcePlanes[4].distance -= camera.nearClipPlane; + + // Far plane + sourcePlanes[5].SetNormalAndPosition(-viewDir, eyePos); + sourcePlanes[5].distance += camera.farClipPlane; + + for (int i = 0; i < 6; ++i) + { + planes[i] = new float4(sourcePlanes[i].normal.x, sourcePlanes[i].normal.y, sourcePlanes[i].normal.z, + sourcePlanes[i].distance); + } + } + + /// + /// Performs an intersection test between an AABB and 6 culling planes. + /// + /// Planes to make the intersection. + /// Instance of the AABB to intersect. + /// Intersection result + public static IntersectResult Intersect(NativeArray cullingPlanes, AABB a) + { + float3 m = a.Center; + float3 extent = a.Extents; + + var inCount = 0; + for (int i = 0; i < cullingPlanes.Length; i++) + { + float3 normal = cullingPlanes[i].xyz; + float dist = math.dot(normal, m) + cullingPlanes[i].w; + float radius = math.dot(extent, math.abs(normal)); + if (dist + radius <= 0) + return IntersectResult.Out; + + if (dist > radius) + inCount++; + } + + return (inCount == cullingPlanes.Length) ? IntersectResult.In : IntersectResult.Partial; + } + + /// + /// Represents four three-dimensional culling planes where all coordinate components and distances are combined together. + /// + public struct PlanePacket4 + { + /// + /// The X coordinate component for the four culling planes. + /// + public float4 Xs; + + /// + /// The Y coordinate component for the four culling planes. + /// + public float4 Ys; + + /// + /// The Z coordinate component for the four culling planes. + /// + public float4 Zs; + + /// + /// The distance component for the four culling planes. + /// + public float4 Distances; + } + + private static void InitializeSOAPlanePackets(NativeArray planes, NativeArray cullingPlanes) + { + int cullingPlaneCount = cullingPlanes.Length; + int packetCount = planes.Length; + + for (int i = 0; i < cullingPlaneCount; i++) + { + var p = planes[i >> 2]; + p.Xs[i & 3] = cullingPlanes[i].normal.x; + p.Ys[i & 3] = cullingPlanes[i].normal.y; + p.Zs[i & 3] = cullingPlanes[i].normal.z; + p.Distances[i & 3] = cullingPlanes[i].distance; + planes[i >> 2] = p; + } + + // Populate the remaining planes with values that are always "in" + for (int i = cullingPlaneCount; i < 4 * packetCount; ++i) + { + var p = planes[i >> 2]; + p.Xs[i & 3] = 1.0f; + p.Ys[i & 3] = 0.0f; + p.Zs[i & 3] = 0.0f; + + // This value was before hardcoded to 32786.0f. + // It was causing the culling system to discard the rendering of entities having a X coordinate approximately less than -32786. + // We could not find anything relying on this number, so the value has been increased to 1 billion + p.Distances[i & 3] = 1e9f; + + planes[i >> 2] = p; + } + } + + internal static UnsafeList BuildSOAPlanePackets(NativeArray cullingPlanes, AllocatorManager.AllocatorHandle allocator) + { + int cullingPlaneCount = cullingPlanes.Length; + int packetCount = (cullingPlaneCount + 3) >> 2; + var planes = new UnsafeList(packetCount, allocator, NativeArrayOptions.UninitializedMemory); + planes.Resize(packetCount); + + InitializeSOAPlanePackets(planes.AsNativeArray(), cullingPlanes); + + return planes; + } + + internal static NativeArray BuildSOAPlanePackets(NativeArray cullingPlanes, Allocator allocator) + { + int cullingPlaneCount = cullingPlanes.Length; + int packetCount = (cullingPlaneCount + 3) >> 2; + var planes = new NativeArray(packetCount, allocator, NativeArrayOptions.UninitializedMemory); + + InitializeSOAPlanePackets(planes, cullingPlanes); + + return planes; + } + + /// + /// Performs an intersection test between an AABB and 6 culling planes. + /// + /// The planes to test. + /// An AABB to test. + /// Returns the intersection result. + public static IntersectResult Intersect2(NativeArray cullingPlanePackets, AABB a) + { + float4 mx = a.Center.xxxx; + float4 my = a.Center.yyyy; + float4 mz = a.Center.zzzz; + + float4 ex = a.Extents.xxxx; + float4 ey = a.Extents.yyyy; + float4 ez = a.Extents.zzzz; + + int4 outCounts = 0; + int4 inCounts = 0; + + for (int i = 0; i < cullingPlanePackets.Length; i++) + { + var p = cullingPlanePackets[i]; + float4 distances = dot4(p.Xs, p.Ys, p.Zs, mx, my, mz) + p.Distances; + float4 radii = dot4(ex, ey, ez, math.abs(p.Xs), math.abs(p.Ys), math.abs(p.Zs)); + + outCounts += (int4)(distances + radii < 0); + inCounts += (int4)(distances >= radii); + } + + int inCount = math.csum(inCounts); + int outCount = math.csum(outCounts); + + if (outCount != 0) + return IntersectResult.Out; + else + return (inCount == 4 * cullingPlanePackets.Length) ? IntersectResult.In : IntersectResult.Partial; + } + + /// + /// Performs an intersection test between an AABB and 6 culling planes. + /// + /// The planes to test. + /// The AABB to test. + /// + /// This method treats a partial intersection as being inside of the planes. + /// + /// Intersection result + public static IntersectResult Intersect2NoPartial(NativeArray cullingPlanePackets, AABB a) + { + float4 mx = a.Center.xxxx; + float4 my = a.Center.yyyy; + float4 mz = a.Center.zzzz; + + float4 ex = a.Extents.xxxx; + float4 ey = a.Extents.yyyy; + float4 ez = a.Extents.zzzz; + + int4 masks = 0; + + for (int i = 0; i < cullingPlanePackets.Length; i++) + { + var p = cullingPlanePackets[i]; + float4 distances = dot4(p.Xs, p.Ys, p.Zs, mx, my, mz) + p.Distances; + float4 radii = dot4(ex, ey, ez, math.abs(p.Xs), math.abs(p.Ys), math.abs(p.Zs)); + + masks += (int4)(distances + radii <= 0); + } + + int outCount = math.csum(masks); + return outCount > 0 ? IntersectResult.Out : IntersectResult.In; + } + + private static float4 dot4(float4 xs, float4 ys, float4 zs, float4 mx, float4 my, float4 mz) + { + return xs * mx + ys * my + zs * mz; + } + + /// + /// Performs an intersection test between an AABB and 6 culling planes. + /// + /// Planes to make the intersection. + /// Center of the bounding sphere to intersect. + /// Radius of the bounding sphere to intersect. + /// Intersection result + public static IntersectResult Intersect(NativeArray planes, float3 center, float radius) + { + var inCount = 0; + + for (int i = 0; i < planes.Length; i++) + { + var d = math.dot(planes[i].xyz, center) + planes[i].w; + if (d < -radius) + { + return IntersectResult.Out; + } + + if (d > radius) + { + inCount++; + } + } + + return (inCount == planes.Length) ? IntersectResult.In : IntersectResult.Partial; + } + } +} diff --git a/Unity.Entities.Graphics/FrustumPlanes.cs.meta b/Unity.Entities.Graphics/FrustumPlanes.cs.meta new file mode 100644 index 0000000..c303386 --- /dev/null +++ b/Unity.Entities.Graphics/FrustumPlanes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbd2134de040843b9a0759a028e053e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/GpuUploadOperation.cs b/Unity.Entities.Graphics/GpuUploadOperation.cs new file mode 100644 index 0000000..5043fd0 --- /dev/null +++ b/Unity.Entities.Graphics/GpuUploadOperation.cs @@ -0,0 +1,118 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + // Describes a single set of data to be uploaded from the CPU to the GPU during this frame. + // The operations are collected up front so their total size can be known for buffer allocation + // purposes, and for effectively load balancing the upload memcpy work. + internal unsafe struct GpuUploadOperation + { + public enum UploadOperationKind + { + Memcpy, // raw upload of a byte block to the GPU + SOAMatrixUpload3x4, // upload matrices from CPU, invert on GPU, write in SoA arrays, 3x4 destination + SOAMatrixUpload4x4, // upload matrices from CPU, invert on GPU, write in SoA arrays, 4x4 destination + // TwoMatrixUpload, // upload matrices from CPU, write them and their inverses to GPU (for transform sharing branch) + } + + // Which kind of upload operation this is + public UploadOperationKind Kind; + // If a matrix upload, what matrix type is this? + public ThreadedSparseUploader.MatrixType SrcMatrixType; + // Pointer to source data, whether raw byte data or float4x4 matrices + public void* Src; + // GPU offset to start writing destination data in + public int DstOffset; + // GPU offset to start writing any inverse matrices in, if applicable + public int DstOffsetInverse; + // Size in bytes for raw operations, size in whole matrices for matrix operations + public int Size; + + // Raw uploads require their size in bytes from the upload buffer. + // Matrix operations require a single 48-byte matrix per matrix. + public int BytesRequiredInUploadBuffer => (Kind == UploadOperationKind.Memcpy) + ? Size + : (Size * UnsafeUtility.SizeOf()); + } + + // Describes a GPU blitting operation (= same bytes replicated over a larger area). + internal struct ValueBlitDescriptor + { + public float4x4 Value; + public uint DestinationOffset; + public uint ValueSizeBytes; + public uint Count; + + public int BytesRequiredInUploadBuffer => (int)(ValueSizeBytes * Count); + } + + [BurstCompile] + internal unsafe struct ExecuteGpuUploads : IJobParallelFor + { + [ReadOnly] public NativeArray GpuUploadOperations; + public ThreadedSparseUploader ThreadedSparseUploader; + + public void Execute(int index) + { + var uploadOperation = GpuUploadOperations[index]; + + switch (uploadOperation.Kind) + { + case GpuUploadOperation.UploadOperationKind.Memcpy: + ThreadedSparseUploader.AddUpload( + uploadOperation.Src, + uploadOperation.Size, + uploadOperation.DstOffset); + break; + case GpuUploadOperation.UploadOperationKind.SOAMatrixUpload3x4: + case GpuUploadOperation.UploadOperationKind.SOAMatrixUpload4x4: + var dstType = (uploadOperation.Kind == GpuUploadOperation.UploadOperationKind.SOAMatrixUpload3x4) + ? ThreadedSparseUploader.MatrixType.MatrixType3x4 + : ThreadedSparseUploader.MatrixType.MatrixType4x4; + if (uploadOperation.DstOffsetInverse < 0) + { + ThreadedSparseUploader.AddMatrixUpload( + uploadOperation.Src, + uploadOperation.Size, + uploadOperation.DstOffset, + uploadOperation.SrcMatrixType, + dstType); + } + else + { + ThreadedSparseUploader.AddMatrixUploadAndInverse( + uploadOperation.Src, + uploadOperation.Size, + uploadOperation.DstOffset, + uploadOperation.DstOffsetInverse, + uploadOperation.SrcMatrixType, + dstType); + } + break; + default: + break; + } + } + } + + [BurstCompile] + internal unsafe struct UploadBlitJob : IJobParallelFor + { + [ReadOnly] public NativeList BlitList; + public ThreadedSparseUploader ThreadedSparseUploader; + + public void Execute(int index) + { + ValueBlitDescriptor blit = BlitList[index]; + ThreadedSparseUploader.AddUpload( + &blit.Value, + (int)blit.ValueSizeBytes, + (int)blit.DestinationOffset, + (int)blit.Count); + } + } +} diff --git a/Unity.Entities.Graphics/GpuUploadOperation.cs.meta b/Unity.Entities.Graphics/GpuUploadOperation.cs.meta new file mode 100644 index 0000000..250ea71 --- /dev/null +++ b/Unity.Entities.Graphics/GpuUploadOperation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bbc63810d8424278ba99e2e0fd8e00a0 +timeCreated: 1619539081 \ No newline at end of file diff --git a/Unity.Entities.Graphics/GraphicsArchetype.cs b/Unity.Entities.Graphics/GraphicsArchetype.cs new file mode 100644 index 0000000..b506bea --- /dev/null +++ b/Unity.Entities.Graphics/GraphicsArchetype.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + // Describes a single type in a GraphicsArchetype that overrides a material property + internal struct ArchetypePropertyOverride : IEquatable, IComparable + { + // NameID of the shader material property being overridden, also used as the sorting key + public int NameID; + // IComponentData (or ISharedComponentData) TypeIndex of the overriding component + public int TypeIndex; + // Byte size of the ECS data in the chunks + public short SizeBytesCPU; + // Byte size of the GPU data, can differ from SizeBytesCPU e.g. for transform matrices + public short SizeBytesGPU; + + public bool Equals(ArchetypePropertyOverride other) + { + return CompareTo(other) == 0; + } + + public int CompareTo(ArchetypePropertyOverride other) + { + int cmp_NameID = NameID.CompareTo(other.NameID); + int cmp_TypeIndex = TypeIndex.CompareTo(other.TypeIndex); + + if (cmp_NameID != 0) return cmp_NameID; + return cmp_TypeIndex; + } + } + + // Precomputed collection of the subset of types in an EntityArchetype that are used by Entities Graphics package + internal unsafe struct GraphicsArchetype : IDisposable, IEquatable, IComparable + { + // All IComponentData overrides, which allocate memory per entity. + // Sorted in NameID order. + public UnsafeList PropertyComponents; + + public GraphicsArchetype Clone(Allocator allocator) + { + var overrides = new UnsafeList(PropertyComponents.Length, allocator); + overrides.AddRangeNoResize(PropertyComponents); + + return new GraphicsArchetype + { + PropertyComponents = overrides, + }; + } + + public int MaxEntitiesPerBatch + { + get + { + int fixedBytes = 0; + int bytesPerEntity = 0; + + for (int i = 0; i < PropertyComponents.Length; ++i) + bytesPerEntity += PropertyComponents[i].SizeBytesGPU; + + int maxBytes = EntitiesGraphicsSystem.MaxBytesPerBatch; + int maxBytesForEntities = maxBytes - fixedBytes; + + return maxBytesForEntities / math.max(1, bytesPerEntity); + } + } + + // Shouldn't need to compare globals, as they should be completely determined + // by the components + + public bool Equals(GraphicsArchetype other) + { + return CompareTo(other) == 0; + } + + public int CompareTo(GraphicsArchetype other) + { + int numA = PropertyComponents.Length; + int numB = other.PropertyComponents.Length; + + if (numA < numB) return -1; + if (numA > numB) return 1; + + return UnsafeUtility.MemCmp( + PropertyComponents.Ptr, + other.PropertyComponents.Ptr, + numA * UnsafeUtility.SizeOf()); + } + + public override int GetHashCode() + { + return (int)xxHash3.Hash64( + PropertyComponents.Ptr, + PropertyComponents.Length * UnsafeUtility.SizeOf()).x; + } + + public void Dispose() + { + if (PropertyComponents.IsCreated) PropertyComponents.Dispose(); + } + + public struct MetadataValueComparer : IComparer + { + public int Compare(MetadataValue x, MetadataValue y) + { + return x.NameID.CompareTo(y.NameID); + } + } + } + + internal struct EntitiesGraphicsArchetypes : IDisposable + { + private NativeParallelHashMap m_GraphicsArchetypes; + private NativeParallelHashMap m_GraphicsArchetypeDeduplication; + private NativeList m_GraphicsArchetypeList; + + public EntitiesGraphicsArchetypes(int capacity) + { + m_GraphicsArchetypes = new NativeParallelHashMap(capacity, Allocator.Persistent); + m_GraphicsArchetypeDeduplication = + new NativeParallelHashMap(capacity, Allocator.Persistent); + m_GraphicsArchetypeList = new NativeList(capacity, Allocator.Persistent); + } + + public void Dispose() + { + for (int i = 0; i < m_GraphicsArchetypeList.Length; ++i) + m_GraphicsArchetypeList[i].Dispose(); + + m_GraphicsArchetypes.Dispose(); + m_GraphicsArchetypeDeduplication.Dispose(); + m_GraphicsArchetypeList.Dispose(); + } + + public GraphicsArchetype GetGraphicsArchetype(int index) => m_GraphicsArchetypeList[index]; + + public int GetGraphicsArchetypeIndex( + EntityArchetype archetype, + NativeParallelHashMap typeIndexToMaterialProperty) + { + int archetypeIndex; + + if (m_GraphicsArchetypes.TryGetValue(archetype, out archetypeIndex)) + return archetypeIndex; + + var types = archetype.GetComponentTypes(Allocator.Temp); + + var overrides = new UnsafeList(types.Length, Allocator.Temp); + void AddOverrideForType(ComponentType type) + { + if (typeIndexToMaterialProperty.TryGetValue(type.TypeIndex, out var property)) + { + // if-guard assert to avoid GC Alloc when the assert is not hit. + if (type.TypeIndex != property.TypeIndex) + Debug.Assert(false, + $"TypeIndex mismatch between key and stored property, Type: {property.TypeName} ({property.TypeIndex:x8}), Property: {property.PropertyName} ({property.NameID:x8})"); + + overrides.Add(new ArchetypePropertyOverride + { + NameID = property.NameID, + TypeIndex = property.TypeIndex, + SizeBytesCPU = property.SizeBytesCPU, + SizeBytesGPU = property.SizeBytesGPU, + }); + } + + // If the type is not found, it was a CPU only type and ignored by Entities Graphics package. + } + + for (int i = 0; i < types.Length; ++i) + AddOverrideForType(types[i]); + + // Entity is not returned by GetComponentTypes, so we handle it explicitly + AddOverrideForType(ComponentType.ReadOnly()); + + overrides.Sort(); + + GraphicsArchetype graphicsArchetype = new GraphicsArchetype + { + PropertyComponents = overrides, + }; + + // If the same archetype has already been created, make sure to use the same index + if (m_GraphicsArchetypeDeduplication.TryGetValue(graphicsArchetype, out archetypeIndex)) + { + graphicsArchetype.Dispose(); + return archetypeIndex; + } + // If this is the first time this archetype has been seen, make a permanent copy of it. + else + { + archetypeIndex = m_GraphicsArchetypeList.Length; + graphicsArchetype = graphicsArchetype.Clone(Allocator.Persistent); + overrides.Dispose(); + + m_GraphicsArchetypeDeduplication[graphicsArchetype] = archetypeIndex; + m_GraphicsArchetypeList.Add(graphicsArchetype); + return archetypeIndex; + } + } + } + +} diff --git a/Unity.Entities.Graphics/GraphicsArchetype.cs.meta b/Unity.Entities.Graphics/GraphicsArchetype.cs.meta new file mode 100644 index 0000000..8c00c11 --- /dev/null +++ b/Unity.Entities.Graphics/GraphicsArchetype.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 774894e578564959bd123c776ce4eeb9 +timeCreated: 1619539608 \ No newline at end of file diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties.meta b/Unity.Entities.Graphics/HDRPMaterialProperties.meta new file mode 100644 index 0000000..0afd58a --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 797e1526755df704296176c5543414ab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMaxAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMaxAuthoring.cs new file mode 100644 index 0000000..61c080d --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMaxAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_AORemapMax" )] + public struct HDRPMaterialPropertyAORemapMax : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyAORemapMaxAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyAORemapMax), "Value")] + public float Value; + + class HDRPMaterialPropertyAORemapMaxBaker : Baker + { + public override void Bake(HDRPMaterialPropertyAORemapMaxAuthoring authoring) + { + HDRPMaterialPropertyAORemapMax component = default(HDRPMaterialPropertyAORemapMax); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMinAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMinAuthoring.cs new file mode 100644 index 0000000..dc5b405 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAORemapMinAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_AORemapMin" )] + public struct HDRPMaterialPropertyAORemapMin : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyAORemapMinAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyAORemapMin), "Value")] + public float Value; + + class HDRPMaterialPropertyAORemapMinBaker : Baker + { + public override void Bake(HDRPMaterialPropertyAORemapMinAuthoring authoring) + { + HDRPMaterialPropertyAORemapMin component = default(HDRPMaterialPropertyAORemapMin); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAlphaCutoffAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAlphaCutoffAuthoring.cs new file mode 100644 index 0000000..0040912 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyAlphaCutoffAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_AlphaCutoff" )] + public struct HDRPMaterialPropertyAlphaCutoff : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyAlphaCutoffAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyAlphaCutoff), "Value")] + public float Value; + + class HDRPMaterialPropertyAlphaCutoffBaker : Baker + { + public override void Bake(HDRPMaterialPropertyAlphaCutoffAuthoring authoring) + { + var component = default(HDRPMaterialPropertyAlphaCutoff); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyBaseColorAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyBaseColorAuthoring.cs new file mode 100644 index 0000000..f98d3b1 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyBaseColorAuthoring.cs @@ -0,0 +1,30 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("_BaseColor" )] + public struct HDRPMaterialPropertyBaseColor : IComponentData { public float4 Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyBaseColorAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyBaseColor), "Value.x", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyBaseColor), "Value.y", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyBaseColor), "Value.z", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyBaseColor), "Value.w", true)] + public float4 Value; + + class HDRPMaterialPropertyBaseColorBaker : Baker + { + public override void Bake(HDRPMaterialPropertyBaseColorAuthoring authoring) + { + var component = default(HDRPMaterialPropertyBaseColor); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyMetallicAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyMetallicAuthoring.cs new file mode 100644 index 0000000..f90d8ac --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyMetallicAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Metallic" )] + public struct HDRPMaterialPropertyMetallic : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyMetallicAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyMetallic), "Value")] + public float Value; + + class HDRPMaterialPropertyMetallicBaker : Baker + { + public override void Bake(HDRPMaterialPropertyMetallicAuthoring authoring) + { + HDRPMaterialPropertyMetallic component = default(HDRPMaterialPropertyMetallic); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertySmoothnessAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertySmoothnessAuthoring.cs new file mode 100644 index 0000000..5fb7f37 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertySmoothnessAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Smoothness" )] + public struct HDRPMaterialPropertySmoothness : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertySmoothnessAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertySmoothness), "Value")] + public float Value; + + class HDRPMaterialPropertySmoothnessBaker : Baker + { + public override void Bake(HDRPMaterialPropertySmoothnessAuthoring authoring) + { + HDRPMaterialPropertySmoothness component = default(HDRPMaterialPropertySmoothness); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyThicknessAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyThicknessAuthoring.cs new file mode 100644 index 0000000..7197511 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyThicknessAuthoring.cs @@ -0,0 +1,26 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Thickness" )] + public struct HDRPMaterialPropertyThickness : IComponentData { public float Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyThicknessAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyThickness), "Value")] + public float Value; + + class HDRPMaterialPropertyThicknessBaker : Baker + { + public override void Bake(HDRPMaterialPropertyThicknessAuthoring authoring) + { + HDRPMaterialPropertyThickness component = default(HDRPMaterialPropertyThickness); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyUnlitColorAuthoring.cs b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyUnlitColorAuthoring.cs new file mode 100644 index 0000000..c41b790 --- /dev/null +++ b/Unity.Entities.Graphics/HDRPMaterialProperties/HDRPMaterialPropertyUnlitColorAuthoring.cs @@ -0,0 +1,30 @@ +#if HDRP_10_0_0_OR_NEWER +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("_UnlitColor" )] + public struct HDRPMaterialPropertyUnlitColor : IComponentData { public float4 Value; } + + [UnityEngine.DisallowMultipleComponent] + public class HDRPMaterialPropertyUnlitColorAuthoring : UnityEngine.MonoBehaviour + { + [RegisterBinding(typeof(HDRPMaterialPropertyUnlitColor), "Value.x", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyUnlitColor), "Value.y", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyUnlitColor), "Value.z", true)] + [RegisterBinding(typeof(HDRPMaterialPropertyUnlitColor), "Value.w", true)] + public float4 Value; + + class HDRPMaterialPropertyUnlitColorBaker : Baker + { + public override void Bake(HDRPMaterialPropertyUnlitColorAuthoring authoring) + { + HDRPMaterialPropertyUnlitColor component = default(HDRPMaterialPropertyUnlitColor); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/HeapAllocator.cs b/Unity.Entities.Graphics/HeapAllocator.cs new file mode 100644 index 0000000..dd6ba1b --- /dev/null +++ b/Unity.Entities.Graphics/HeapAllocator.cs @@ -0,0 +1,686 @@ +// #define DEBUG_ASSERTS + +using UnityEngine.Assertions; +using System; +using Unity.Collections; +using Unity.Mathematics; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Rendering +{ + /// + /// Represents a block of memory that you can use in a HeapAllocator to manage memory. + /// + [System.Diagnostics.DebuggerDisplay("({begin}, {end}), Length = {Length}")] + public struct HeapBlock : IComparable, IEquatable + { + /// + /// The beginning of the allocated heap block. + /// + public ulong begin { get { return m_Begin; } } + + /// + /// The end of the allocated heap block. + /// + public ulong end { get { return m_End; } } + + private ulong m_Begin; + private ulong m_End; + + internal HeapBlock(ulong begin, ulong end) + { + m_Begin = begin; + m_End = end; + } + + /// + /// Creates new HeapBlock that starts at the given index and is of given size. + /// + /// The start index for the block. + /// The size of the block. + /// Returns a new instance of HeapBlock. + internal static HeapBlock OfSize(ulong begin, ulong size) + { + return new HeapBlock(begin, begin + size); + } + + /// + /// The length of the HeapBlock. + /// + public ulong Length { get { return m_End - m_Begin; } } + + /// + /// Indicates whether the HeapBlock is empty. This is true if the HeapBlock is empty and false otherwise. + /// + public bool Empty { get { return Length == 0; } } + + /// + public int CompareTo(HeapBlock other) { return m_Begin.CompareTo(other.m_Begin); } + + /// + public bool Equals(HeapBlock other) { return CompareTo(other) == 0; } + } + + /// + /// Represents a generic best-fit heap allocation algorithm that operates on abstract integer indices. + /// + /// + /// You can use this to suballocate memory, GPU buffer contents, and DX12 descriptors. + /// This supports alignments, resizing, and coalescing of freed blocks. + /// + public struct HeapAllocator : IDisposable + { + /// + /// Creates a new HeapAllocator with the given initial size and alignment. + /// + /// The initial size of the allocator. + /// The initial alignment of the allocator. + /// + /// You can resize the allocator later. + /// + public HeapAllocator(ulong size = 0, uint minimumAlignment = 1) + { + m_SizeBins = new NativeList(Allocator.Persistent); + m_Blocks = new NativeList(Allocator.Persistent); + m_BlocksFreelist = new NativeList(Allocator.Persistent); + m_FreeEndpoints = new NativeParallelHashMap(0, Allocator.Persistent); + m_Size = 0; + m_Free = 0; + m_MinimumAlignmentLog2 = math.tzcnt(minimumAlignment); + m_IsCreated = true; + + Resize(size); + } + + /// + /// Minimal HeapBlock alignment of this allocator. + /// + internal uint MinimumAlignment { get { return 1u << m_MinimumAlignmentLog2; } } + + + /// + /// The amount of available free space in the allocator. + /// + public ulong FreeSpace { get { return m_Free; } } + + /// + /// The amount of used space in the allocator. + /// + public ulong UsedSpace { get { return m_Size - m_Free; } } + + internal ulong OnePastHighestUsedAddress { get { + return m_FreeEndpoints.TryGetValue(m_Size, out var tailBegin) ? tailBegin : m_Size; + } } + + /// + /// The size of the heap that the allocator manages. + /// + public ulong Size { get { return m_Size; } } + + /// + /// Indicates whether the allocator is empty. This is true if the allocator is empty and false otherwise. + /// + public bool Empty { get { return m_Free == m_Size; } } + + /// + /// Indicates whether the allocator is full. This is true if the allocator is full and false otherwise. + /// + public bool Full { get { return m_Free == 0; } } + + /// + /// Indicates whether the allocator has been created and not yet allocated. + /// + public bool IsCreated { get { return m_IsCreated; } } + + /// + /// Clears the allocator. + /// + public void Clear() + { + var size = m_Size; + + m_SizeBins.Clear(); + m_Blocks.Clear(); + m_BlocksFreelist.Clear(); + m_FreeEndpoints.Clear(); + m_Size = 0; + m_Free = 0; + + Resize(size); + } + + /// + public void Dispose() + { + if (!IsCreated) + return; + + for (int i = 0; i < m_Blocks.Length; ++i) + m_Blocks[i].Dispose(); + + m_FreeEndpoints.Dispose(); + m_Blocks.Dispose(); + m_BlocksFreelist.Dispose(); + m_SizeBins.Dispose(); + m_IsCreated = false; + } + + /// + /// Attempts to grow or shrink the allocator. Growing always succeeds, + /// but shrinking might fail if the end of the heap is allocated. + /// + /// The new size of the allocator. + /// Returns true if the operation is a success. Returns false otherwise. + public bool Resize(ulong newSize) + { + // Same size? No need to do anything. + if (newSize == m_Size) + { + return true; + } + // Growing? Release a block past the end. + else if (newSize > m_Size) + { + ulong increase = newSize - m_Size; + HeapBlock newSpace = HeapBlock.OfSize(m_Size, increase); + Release(newSpace); + m_Size = newSize; + return true; + } + // Shrinking? TODO + else + { + return false; + } + } + + /// + /// Attempt to allocate a block from the heap with at least the given + /// size and alignment. + /// + /// The size of the block to allocate. + /// Alignment of the allocated block. + /// + /// The allocated block might be bigger than the + /// requested size, but will never be smaller. + /// If the allocation fails, this method returns an empty block. + /// + /// Returns a new allocated HeapBlock on success. Returns an empty block on failure. + public HeapBlock Allocate(ulong size, uint alignment = 1) + { + // Always use at least the minimum alignment, and round all sizes + // to multiples of the minimum alignment. + size = NextAligned(size, m_MinimumAlignmentLog2); + alignment = math.max(alignment, MinimumAlignment); + + SizeBin allocBin = new SizeBin(size, alignment); + + int index = FindSmallestSufficientBin(allocBin); + while (index < m_SizeBins.Length) + { + SizeBin bin = m_SizeBins[index]; + if (CanFitAllocation(allocBin, bin)) + { + HeapBlock block = PopBlockFromBin(bin, index); + return CutAllocationFromBlock(allocBin, block); + } + else + { + ++index; + } + } + + return new HeapBlock(); + } + + /// + /// Releases a given block of memory and marks it as free. + /// + /// The HeapBlock to release to the allocator. + /// + /// You must have wholly allocated the given block before you pass it into this method. + /// However, it's legal to release big allocations in smaller non-overlapping sub-blocks. + /// + public void Release(HeapBlock block) + { + // Merge the newly released block with any free blocks on either + // side of it. Remove those blocks from the list of free blocks, + // as they no longer exist as separate blocks. + block = Coalesce(block); + + SizeBin bin = new SizeBin(block); + int index = FindSmallestSufficientBin(bin); + + // If the exact bin doesn't exist, add it. + if (index >= m_SizeBins.Length || bin.CompareTo(m_SizeBins[index]) != 0) + { + index = AddNewBin(ref bin, index); + } + + m_Blocks[m_SizeBins[index].blocksId].Push(block); + m_Free += block.Length; + +#if DEBUG_ASSERTS + Assert.IsFalse(m_FreeEndpoints.ContainsKey(block.begin)); + Assert.IsFalse(m_FreeEndpoints.ContainsKey(block.end)); +#endif + + // Store both endpoints of the free block to the hashmap for + // easy coalescing. + m_FreeEndpoints[block.begin] = block.end; + m_FreeEndpoints[block.end] = block.begin; + } + + // Do a slow exhaustive test of the allocator's internal state to verify + // that its invariants have not been broken. Should be only used for debugging. + internal void DebugValidateInternalState() + { + // Amount of size bins should be the same as the amount of block lists + // for those size bins. + int numBins = m_SizeBins.Length; + int numFreeBlockLists = m_BlocksFreelist.Length; + int numEmptyBlocks = 0; + int numNonEmptyBlocks = 0; + + for (int i = 0; i < m_Blocks.Length; ++i) + { + if (m_Blocks[i].Empty) + ++numEmptyBlocks; + else + ++numNonEmptyBlocks; + } + + Assert.AreEqual(numBins, numNonEmptyBlocks, "There should be exactly one non-empty block list per size bin"); + Assert.AreEqual(numEmptyBlocks, numFreeBlockLists, "All empty block lists should be in the free list"); + + for (int i = 0; i < m_BlocksFreelist.Length; ++i) + { + int freeBlock = m_BlocksFreelist[i]; + Assert.IsTrue(m_Blocks[freeBlock].Empty, "There should be only empty block lists in the free list"); + } + + ulong totalFreeSize = 0; + int totalFreeBlocks = 0; + + for (int i = 0; i < m_SizeBins.Length; ++i) + { + var sizeBin = m_SizeBins[i]; + var size = sizeBin.Size; + var align = sizeBin.Alignment; + var blocks = m_Blocks[sizeBin.blocksId]; + + Assert.IsFalse(blocks.Empty, "All block lists should be non-empty, empty lists should be removed"); + + int count = blocks.Length; + + for (int j = 0; j < count; ++j) + { + var b = blocks.Block(j); + var bin = new SizeBin(b); + Assert.AreEqual(size, bin.Size, "Block size should match its bin"); + Assert.AreEqual(align, bin.Alignment, "Block alignment should match its bin"); + totalFreeSize += b.Length; + + if (m_FreeEndpoints.TryGetValue(b.begin, out var foundEnd)) + Assert.AreEqual(b.end, foundEnd, "Free block end does not match stored endpoint"); + else + Assert.IsTrue(false, "No end endpoint found for free block"); + + if (m_FreeEndpoints.TryGetValue(b.end, out var foundBegin)) + Assert.AreEqual(b.begin, foundBegin, "Free block begin does not match stored endpoint"); + else + Assert.IsTrue(false, "No begin endpoint found for free block"); + + ++totalFreeBlocks; + } + } + + // Reported free space should be equal to the total size of the free blocks + Assert.AreEqual(totalFreeSize, FreeSpace, "Free size reported incorrectly"); + Assert.IsTrue(totalFreeSize <= Size, "Amount of free size larger than maximum"); + Assert.AreEqual(2 * totalFreeBlocks, m_FreeEndpoints.Count(), + "Each free block should have exactly 2 stored endpoints"); + } + + internal const int MaxAlignmentLog2 = 0x3f; + internal const int AlignmentBits = 6; + + [System.Diagnostics.DebuggerDisplay("Size = {Size}, Alignment = {Alignment}")] + private struct SizeBin : IComparable, IEquatable + { + public ulong sizeClass; + public int blocksId; + + public SizeBin(ulong size, uint alignment = 1) + { + int alignLog2 = math.tzcnt(alignment); + alignLog2 = math.min(MaxAlignmentLog2, alignLog2); + sizeClass = (size << AlignmentBits) | (uint)alignLog2; + blocksId = -1; + +#if DEBUG_ASSERTS + Assert.AreEqual(math.countbits(alignment), 1, "Only power-of-two alignments supported"); +#endif + } + + public SizeBin(HeapBlock block) + { + int alignLog2 = math.tzcnt(block.begin); + alignLog2 = math.min(MaxAlignmentLog2, alignLog2); + sizeClass = (block.Length << AlignmentBits) | (uint)alignLog2; + blocksId = -1; + } + + public int CompareTo(SizeBin other) { return sizeClass.CompareTo(other.sizeClass); } + public bool Equals(SizeBin other) { return CompareTo(other) == 0; } + + public bool HasCompatibleAlignment(SizeBin requiredAlignment) + { + int myAlign = AlignmentLog2; + int required = requiredAlignment.AlignmentLog2; + return myAlign >= required; + } + + public ulong Size { get { return sizeClass >> AlignmentBits; } } + public int AlignmentLog2 { get { return (int)sizeClass & MaxAlignmentLog2; } } + public uint Alignment { get { return 1u << AlignmentLog2; } } + } + + private unsafe struct BlocksOfSize : IDisposable + { + private UnsafeList* m_Blocks; + + public BlocksOfSize(int dummy) + { + m_Blocks = (UnsafeList*)Memory.Unmanaged.Allocate( + UnsafeUtility.SizeOf>(), + UnsafeUtility.AlignOf>(), + Allocator.Persistent); + UnsafeUtility.MemClear(m_Blocks, UnsafeUtility.SizeOf>()); + m_Blocks->Allocator = Allocator.Persistent; + } + + public bool Empty { get { return m_Blocks->Length == 0; } } + + // TODO: Priority queue semantics for address-ordered allocation + + public void Push(HeapBlock block) + { + m_Blocks->Add(block); + } + + public HeapBlock Pop() + { + int len = m_Blocks->Length; + + if (len == 0) + return new HeapBlock(); + + HeapBlock block = Block(len - 1); + m_Blocks->Resize(len - 1); + return block; + } + + public bool Remove(HeapBlock block) + { + for (int i = 0; i < m_Blocks->Length; ++i) + { + if (block.CompareTo(Block(i)) == 0) + { + m_Blocks->RemoveAtSwapBack(i); + return true; + } + } + + return false; + } + + public void Dispose() + { + m_Blocks->Dispose(); + Memory.Unmanaged.Free(m_Blocks, Allocator.Persistent); + } + + public unsafe HeapBlock Block(int i) { return UnsafeUtility.ReadArrayElement(m_Blocks->Ptr, i); } + public unsafe int Length => m_Blocks->Length; + } + + private NativeList m_SizeBins; + private NativeList m_Blocks; + private NativeList m_BlocksFreelist; + private NativeParallelHashMap m_FreeEndpoints; + private ulong m_Size; + private ulong m_Free; + private readonly int m_MinimumAlignmentLog2; + private bool m_IsCreated; + + private int FindSmallestSufficientBin(SizeBin needle) + { + if (m_SizeBins.Length == 0) + return 0; + + int lo = 0; // Low endpoint of search, inclusive + int hi = m_SizeBins.Length; // High endpoint of search, exclusive + + for (;;) + { + int d2 = (hi - lo) / 2; + + // Search has terminated. If lo is large enough, return it. + if (d2 == 0) + { + if (needle.CompareTo(m_SizeBins[lo]) <= 0) + return lo; + else + return lo + 1; + } + + int probe = lo + d2; + int cmp = needle.CompareTo(m_SizeBins[probe]); + + // Needle is smaller than probe? + if (cmp < 0) + { + hi = probe; + } + // Needle is greater than probe? + else if (cmp > 0) + { + lo = probe; + } + // Found needle exactly. + else + { + return probe; + } + } + } + + private unsafe int AddNewBin(ref SizeBin bin, int index) + { + // If there are no free block lists, make a new one + if (m_BlocksFreelist.IsEmpty) + { + bin.blocksId = m_Blocks.Length; + m_Blocks.Add(new BlocksOfSize(0)); + } + else + { + int last = m_BlocksFreelist.Length - 1; + bin.blocksId = m_BlocksFreelist[last]; + m_BlocksFreelist.ResizeUninitialized(last); + } + +#if DEBUG_ASSERTS + Assert.IsTrue(m_Blocks[bin.blocksId].Empty); +#endif + + int tail = m_SizeBins.Length - index; + m_SizeBins.ResizeUninitialized(m_SizeBins.Length + 1); + SizeBin *p = (SizeBin *)m_SizeBins.GetUnsafePtr(); + UnsafeUtility.MemMove( + p + (index + 1), + p + index, + tail * UnsafeUtility.SizeOf()); + p[index] = bin; + + return index; + } + + private unsafe void RemoveBinIfEmpty(SizeBin bin, int index) + { + if (!m_Blocks[bin.blocksId].Empty) + return; + + int tail = m_SizeBins.Length - (index + 1); + SizeBin* p = (SizeBin*)m_SizeBins.GetUnsafePtr(); + UnsafeUtility.MemMove( + p + index, + p + (index + 1), + tail * UnsafeUtility.SizeOf()); + m_SizeBins.ResizeUninitialized(m_SizeBins.Length - 1); + + m_BlocksFreelist.Add(bin.blocksId); + } + + private unsafe HeapBlock PopBlockFromBin(SizeBin bin, int index) + { + HeapBlock block = m_Blocks[bin.blocksId].Pop(); + RemoveEndpoints(block); + m_Free -= block.Length; + + RemoveBinIfEmpty(bin, index); + + return block; + } + + private void RemoveEndpoints(HeapBlock block) + { + m_FreeEndpoints.Remove(block.begin); + m_FreeEndpoints.Remove(block.end); + } + + private void RemoveFreeBlock(HeapBlock block) + { + RemoveEndpoints(block); + + SizeBin bin = new SizeBin(block); + int index = FindSmallestSufficientBin(bin); + +#if DEBUG_ASSERTS + Assert.IsTrue(index >= 0 && m_SizeBins[index].sizeClass == bin.sizeClass, + "Expected to find exact match for size bin since block was supposed to exist"); +#endif + + bool removed = m_Blocks[m_SizeBins[index].blocksId].Remove(block); + RemoveBinIfEmpty(m_SizeBins[index], index); + +#if DEBUG_ASSERTS + Assert.IsTrue(removed, "Block was supposed to exist"); +#endif + + m_Free -= block.Length; + } + + private HeapBlock Coalesce(HeapBlock block, ulong endpoint) + { + if (m_FreeEndpoints.TryGetValue(endpoint, out ulong otherEnd)) + { +#if DEBUG_ASSERTS + if (math.min(endpoint, otherEnd) == block.begin && + math.max(endpoint, otherEnd) == block.end) + { + UnityEngine.Debug.Log("Inconsistent free block endpoint data"); + } + Assert.IsFalse( + math.min(endpoint, otherEnd) == block.begin && + math.max(endpoint, otherEnd) == block.end, + "Block was already freed."); +#endif + + if (endpoint == block.begin) + { +#if DEBUG_ASSERTS + Assert.IsTrue(otherEnd < endpoint, "Unexpected endpoints"); +#endif + var coalesced = new HeapBlock(otherEnd, block.begin); + RemoveFreeBlock(coalesced); + return new HeapBlock(coalesced.begin, block.end); + } + else + { +#if DEBUG_ASSERTS + Assert.IsTrue(otherEnd > endpoint, "Unexpected endpoints"); +#endif + var coalesced = new HeapBlock(block.end, otherEnd); + RemoveFreeBlock(coalesced); + return new HeapBlock(block.begin, coalesced.end); + } + } + else + { + return block; + } + } + + private HeapBlock Coalesce(HeapBlock block) + { + block = Coalesce(block, block.begin); // Left + block = Coalesce(block, block.end); // Right + return block; + } + + private bool CanFitAllocation(SizeBin allocation, SizeBin bin) + { +#if DEBUG_ASSERTS + Assert.IsTrue(bin.sizeClass >= allocation.sizeClass, "Should have compatible size classes to begin with"); +#endif + + // Check that the bin is not empty. + if (m_Blocks[bin.blocksId].Empty) + return false; + + // If the bin meets alignment restrictions, it is usable. + if (bin.HasCompatibleAlignment(allocation)) + { + return true; + } + // Else, require one alignment worth of extra space so we can guarantee space. + else + { + return bin.Size >= (allocation.Size + allocation.Alignment); + } + } + + private static ulong NextAligned(ulong offset, int alignmentLog2) + { + int toNext = (1 << alignmentLog2) - 1; + ulong aligned = ((offset + (ulong)toNext) >> alignmentLog2) << alignmentLog2; + return aligned; + } + + private HeapBlock CutAllocationFromBlock(SizeBin allocation, HeapBlock block) + { +#if DEBUG_ASSERTS + Assert.IsTrue(block.Length >= allocation.Size, "Block is not large enough."); +#endif + + // If the match is exact, no need to cut. + if (allocation.Size == block.Length) + return block; + + // Otherwise, round the begin to next multiple of alignment, and then cut away the required size, + // potentially leaving empty space on both ends. + ulong alignedBegin = NextAligned(block.begin, allocation.AlignmentLog2); + ulong alignedEnd = alignedBegin + allocation.Size; + + if (alignedBegin > block.begin) + Release(new HeapBlock(block.begin, alignedBegin)); + + if (alignedEnd < block.end) + Release(new HeapBlock(alignedEnd, block.end)); + + return new HeapBlock(alignedBegin, alignedEnd); + } + } +} diff --git a/Unity.Entities.Graphics/HeapAllocator.cs.meta b/Unity.Entities.Graphics/HeapAllocator.cs.meta new file mode 100644 index 0000000..028d731 --- /dev/null +++ b/Unity.Entities.Graphics/HeapAllocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1e62ebd7c162e3469435552db7e339e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/LODGroupBaking.cs b/Unity.Entities.Graphics/LODGroupBaking.cs new file mode 100644 index 0000000..e89cf4f --- /dev/null +++ b/Unity.Entities.Graphics/LODGroupBaking.cs @@ -0,0 +1,38 @@ +using Unity.Entities; +using Unity.Mathematics; +using Unity.Rendering; +using UnityEngine; + +class LODGroupBaker : Baker +{ + public override void Bake(LODGroup authoring) + { + if (authoring.lodCount > 8) + { + Debug.LogWarning("LODGroup has more than 8 LOD - Not supported", authoring); + return; + } + + var lodGroupData = new MeshLODGroupComponent(); + //@TODO: LOD calculation should respect scale... + var worldSpaceSize = LODGroupExtensions.GetWorldSpaceScale(GetComponent()) * authoring.size; + lodGroupData.LocalReferencePoint = authoring.localReferencePoint; + + var lodDistances0 = new float4(float.PositiveInfinity); + var lodDistances1 = new float4(float.PositiveInfinity); + var lodGroupLODs = authoring.GetLODs(); + for (int i = 0; i < authoring.lodCount; ++i) + { + float d = worldSpaceSize / lodGroupLODs[i].screenRelativeTransitionHeight; + if (i < 4) + lodDistances0[i] = d; + else + lodDistances1[i - 4] = d; + } + + lodGroupData.LODDistances0 = lodDistances0; + lodGroupData.LODDistances1 = lodDistances1; + + AddComponent(lodGroupData); + } +} diff --git a/Unity.Entities.Graphics/LODGroupBaking.cs.meta b/Unity.Entities.Graphics/LODGroupBaking.cs.meta new file mode 100644 index 0000000..1c522bb --- /dev/null +++ b/Unity.Entities.Graphics/LODGroupBaking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8dc7182524d1a735ba2f72a63c5ad10a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/LODGroupExtensions.cs b/Unity.Entities.Graphics/LODGroupExtensions.cs new file mode 100644 index 0000000..9ec1a03 --- /dev/null +++ b/Unity.Entities.Graphics/LODGroupExtensions.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + /// + /// Provides methods that help you to work with LOD groups. + /// + public static class LODGroupExtensions + { + /// + /// Represents LOD parameters. + /// + public struct LODParams : IEqualityComparer, IEquatable + { + /// + /// The LOD distance scale. + /// + public float distanceScale; + + /// + /// The camera position. + /// + public float3 cameraPos; + + /// + /// Indicates whether the camera is in orthographic mode. + /// + public bool isOrtho; + + /// + /// The orthographic size of the camera. + /// + public float orthosize; + + /// + public bool Equals(LODParams x, LODParams y) + { + return + x.distanceScale == y.distanceScale && + x.cameraPos.Equals(y.cameraPos) && + x.isOrtho == y.isOrtho && + x.orthosize == y.orthosize; + } + + /// + public bool Equals(LODParams x) + { + return + x.distanceScale == distanceScale && + x.cameraPos.Equals(cameraPos) && + x.isOrtho == isOrtho && + x.orthosize == orthosize; + } + + /// + public int GetHashCode(LODParams obj) + { + throw new System.NotImplementedException(); + } + } + + static float CalculateLodDistanceScale(float fieldOfView, float globalLodBias, bool isOrtho, float orthoSize) + { + float distanceScale; + if (isOrtho) + { + distanceScale = 2.0f * orthoSize / globalLodBias; + } + else + { + var halfAngle = math.tan(math.radians(fieldOfView * 0.5F)); + // Half angle at 90 degrees is 1.0 (So we skip halfAngle / 1.0 calculation) + distanceScale = (2.0f * halfAngle) / globalLodBias; + } + + return distanceScale; + } + + /// + /// Calculates LOD parameters from an LODParameters object. + /// + /// The LOD parameters to use. + /// An optional LOD bias to apply. + /// Returns the calculated LOD parameters. + public static LODParams CalculateLODParams(LODParameters parameters, float overrideLODBias = 0.0f) + { + LODParams lodParams; + lodParams.cameraPos = parameters.cameraPosition; + lodParams.isOrtho = parameters.isOrthographic; + lodParams.orthosize = parameters.orthoSize; + if (overrideLODBias == 0.0F) + lodParams.distanceScale = CalculateLodDistanceScale(parameters.fieldOfView, QualitySettings.lodBias, lodParams.isOrtho, lodParams.orthosize); + else + { + // overrideLODBias is not affected by FOV etc + // This is useful if the FOV is continously changing (breaking LOD temporal cache) or you want to explicit control LOD bias. + lodParams.distanceScale = 1.0F / overrideLODBias; + } + + return lodParams; + } + + /// + /// Calculates LOD parameters from a camera. + /// + /// The camera to calculate LOD parameters from. + /// An optional LOD bias to apply. + /// Returns the calculated LOD parameters. + public static LODParams CalculateLODParams(Camera camera, float overrideLODBias = 0.0f) + { + LODParams lodParams; + lodParams.cameraPos = camera.transform.position; + lodParams.isOrtho = camera.orthographic; + lodParams.orthosize = camera.orthographicSize; + if (overrideLODBias == 0.0F) + lodParams.distanceScale = CalculateLodDistanceScale(camera.fieldOfView, QualitySettings.lodBias, lodParams.isOrtho, lodParams.orthosize); + else + { + // overrideLODBias is not affected by FOV etc. + // This is useful if the FOV is continously changing (breaking LOD temporal cache) or you want to explicit control LOD bias. + lodParams.distanceScale = 1.0F / overrideLODBias; + } + + return lodParams; + } + + /// + /// Calculates the world size of an LOD group. + /// + /// The LOD group. + /// Returns the calculated world size of the LOD group. + public static float GetWorldSpaceSize(LODGroup lodGroup) + { + return GetWorldSpaceScale(lodGroup.transform) * lodGroup.size; + } + + internal static float GetWorldSpaceScale(Transform t) + { + var scale = t.lossyScale; + float largestAxis = Mathf.Abs(scale.x); + largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.y)); + largestAxis = Mathf.Max(largestAxis, Mathf.Abs(scale.z)); + return largestAxis; + } + + /// + /// Calculates the current LOD index. + /// + /// The distances at which to switch between each LOD. + /// The current LOD scale. + /// A world-space reference point to base the LOD index calculation on. + /// The LOD parameters to use. + /// Returns the calculated LOD index. + public static int CalculateCurrentLODIndex(float4 lodDistances, float scale, float3 worldReferencePoint, ref LODParams lodParams) + { + var distanceSqr = CalculateDistanceSqr(worldReferencePoint, ref lodParams); + var lodIndex = CalculateCurrentLODIndex(lodDistances * scale, distanceSqr); + return lodIndex; + } + + /// + /// Calculates the current LOD mask. + /// + /// The distances at which to switch between each LOD. + /// Current scale. + /// A world-space reference point to base the LOD index calculation on. + /// The LOD parameters to use. + /// Returns the calculated LOD mask. + public static int CalculateCurrentLODMask(float4 lodDistances, float scale, float3 worldReferencePoint, ref LODParams lodParams) + { + var distanceSqr = CalculateDistanceSqr(worldReferencePoint, ref lodParams); + return CalculateCurrentLODMask(lodDistances * scale, distanceSqr); + } + + static int CalculateCurrentLODIndex(float4 lodDistances, float measuredDistanceSqr) + { + var lodResult = measuredDistanceSqr < (lodDistances * lodDistances); + if (lodResult.x) + return 0; + else if (lodResult.y) + return 1; + else if (lodResult.z) + return 2; + else if (lodResult.w) + return 3; + else + // Can return 0 or 16. Doesn't matter... + return -1; + } + + static int CalculateCurrentLODMask(float4 lodDistances, float measuredDistanceSqr) + { + var lodResult = measuredDistanceSqr < (lodDistances * lodDistances); + if (lodResult.x) + return 1; + else if (lodResult.y) + return 2; + else if (lodResult.z) + return 4; + else if (lodResult.w) + return 8; + else + // Can return 0 or 16. Doesn't matter... + return 16; + } + + static float CalculateDistanceSqr(float3 worldReferencePoint, ref LODParams lodParams) + { + if (lodParams.isOrtho) + { + return lodParams.distanceScale * lodParams.distanceScale; + } + else + { + return math.lengthsq(lodParams.cameraPos - worldReferencePoint) * (lodParams.distanceScale * lodParams.distanceScale); + } + } + + /// + /// Calculates the world position of an LOD group. + /// + /// The LOD group. + /// Returns the world position of the LOD group. + public static float3 GetWorldPosition(LODGroup group) + { + return group.GetComponent().TransformPoint(group.localReferencePoint); + } + + /// + /// Calculates the LOD switch distance for an LOD group. + /// + /// The field of view angle. + /// The LOD group. + /// The LOD index to use. + /// Returns the LOD switch distance. + public static float CalculateLODSwitchDistance(float fieldOfView, LODGroup group, int lodIndex) + { + float halfAngle = math.tan(math.radians(fieldOfView) * 0.5F); + return GetWorldSpaceSize(group) / (2 * group.GetLODs()[lodIndex].screenRelativeTransitionHeight * halfAngle); + } + } +} diff --git a/Unity.Entities.Graphics/LODGroupExtensions.cs.meta b/Unity.Entities.Graphics/LODGroupExtensions.cs.meta new file mode 100644 index 0000000..3749cfa --- /dev/null +++ b/Unity.Entities.Graphics/LODGroupExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04950f8d663fd4980858d0fbbeb5c32d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs b/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs new file mode 100644 index 0000000..91db1ae --- /dev/null +++ b/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs @@ -0,0 +1,367 @@ +using System.Diagnostics; +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Unity.Rendering +{ + /// + /// A tag component that allows for granular per-instance culling control. + /// + public struct PerInstanceCullingTag : IComponentData {} + + struct RootLODWorldReferencePoint : IComponentData + { + public float3 Value; + } + + struct RootLODRange : IComponentData + { + public LODRange LOD; + } + + struct LODWorldReferencePoint : IComponentData + { + public float3 Value; + } + + struct LODRange : IComponentData + { + public float MinDist; + public float MaxDist; + + public LODRange(MeshLODGroupComponent lodGroup, int lodMask) + { + float minDist = float.MaxValue; + float maxDist = 0.0F; + if ((lodMask & 0x01) == 0x01) + { + minDist = 0.0f; + maxDist = math.max(maxDist, lodGroup.LODDistances0.x); + } + if ((lodMask & 0x02) == 0x02) + { + minDist = math.min(minDist, lodGroup.LODDistances0.x); + maxDist = math.max(maxDist, lodGroup.LODDistances0.y); + } + if ((lodMask & 0x04) == 0x04) + { + minDist = math.min(minDist, lodGroup.LODDistances0.y); + maxDist = math.max(maxDist, lodGroup.LODDistances0.z); + } + if ((lodMask & 0x08) == 0x08) + { + minDist = math.min(minDist, lodGroup.LODDistances0.z); + maxDist = math.max(maxDist, lodGroup.LODDistances0.w); + } + if ((lodMask & 0x10) == 0x10) + { + minDist = math.min(minDist, lodGroup.LODDistances0.w); + maxDist = math.max(maxDist, lodGroup.LODDistances1.x); + } + if ((lodMask & 0x20) == 0x20) + { + minDist = math.min(minDist, lodGroup.LODDistances1.x); + maxDist = math.max(maxDist, lodGroup.LODDistances1.y); + } + if ((lodMask & 0x40) == 0x40) + { + minDist = math.min(minDist, lodGroup.LODDistances1.y); + maxDist = math.max(maxDist, lodGroup.LODDistances1.z); + } + if ((lodMask & 0x80) == 0x80) + { + minDist = math.min(minDist, lodGroup.LODDistances1.z); + maxDist = math.max(maxDist, lodGroup.LODDistances1.w); + } + + MinDist = minDist; + MaxDist = maxDist; + } + } + + [UpdateInGroup(typeof(StructuralChangePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + internal partial class AddLODRequirementComponents : SystemBase + { + EntityQuery m_MissingRootLODRange; + EntityQuery m_MissingRootLODWorldReferencePoint; + EntityQuery m_MissingLODRange; + EntityQuery m_MissingLODWorldReferencePoint; + EntityQuery m_MissingLODGroupWorldReferencePoint; + + /// + protected override void OnCreate() + { + m_MissingRootLODRange = GetEntityQuery(new EntityQueryDesc + { + All = new[] {ComponentType.ReadOnly()}, + None = new[] {ComponentType.ReadOnly()}, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingRootLODWorldReferencePoint = GetEntityQuery(new EntityQueryDesc + { + All = new[] { ComponentType.ReadOnly() }, + None = new[] { ComponentType.ReadOnly() }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingLODRange = GetEntityQuery(new EntityQueryDesc + { + All = new[] { ComponentType.ReadOnly() }, + None = new[] { ComponentType.ReadOnly() }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingLODWorldReferencePoint = GetEntityQuery(new EntityQueryDesc + { + All = new[] { ComponentType.ReadOnly() }, + None = new[] { ComponentType.ReadOnly() }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingLODGroupWorldReferencePoint = GetEntityQuery(new EntityQueryDesc + { + All = new[] { ComponentType.ReadOnly() }, + None = new[] { ComponentType.ReadOnly() }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + } + + /// + protected override void OnUpdate() + { + EntityManager.AddComponent(m_MissingRootLODRange, typeof(RootLODRange)); + EntityManager.AddComponent(m_MissingRootLODWorldReferencePoint, typeof(RootLODWorldReferencePoint)); + EntityManager.AddComponent(m_MissingLODRange, typeof(LODRange)); + EntityManager.AddComponent(m_MissingLODWorldReferencePoint, typeof(LODWorldReferencePoint)); + EntityManager.AddComponent(m_MissingLODGroupWorldReferencePoint, typeof(LODGroupWorldReferencePoint)); + } + } + + [RequireMatchingQueriesForUpdate] + [UpdateInGroup(typeof(UpdatePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + internal partial class LODRequirementsUpdateSystem : SystemBase + { + EntityQuery m_UpdatedLODRanges; + EntityQuery m_LODReferencePoints; + EntityQuery m_LODGroupReferencePoints; + + ComponentLookup MeshLODGroupComponent; + ComponentTypeHandle MeshLODComponent; + ComponentLookup LocalToWorldLookup; + ComponentTypeHandle RootLODRange; + ComponentTypeHandle LODRange; + + [BurstCompile] + struct UpdateLODRangesJob : IJobChunk + { + [ReadOnly] public ComponentLookup MeshLODGroupComponent; + + public ComponentTypeHandle MeshLODComponent; + [ReadOnly] public ComponentLookup LocalToWorldLookup; + public ComponentTypeHandle RootLODRange; + public ComponentTypeHandle LODRange; + + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private static void CheckDeepHLODSupport(Entity entity) + { + if (entity != Entity.Null) + throw new System.NotImplementedException("Deep HLOD is not supported yet"); + } + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var rootLODRange = chunk.GetNativeArray(RootLODRange); + var lodRange = chunk.GetNativeArray(LODRange); + var meshLods = chunk.GetNativeArray(MeshLODComponent); + var instanceCount = chunk.Count; + + for (int i = 0; i < instanceCount; i++) + { + var meshLod = meshLods[i]; + var lodGroupEntity = meshLod.Group; + var lodMask = meshLod.LODMask; + var lodGroup = MeshLODGroupComponent[lodGroupEntity]; + + lodRange[i] = new LODRange(lodGroup, lodMask); + + } + + for (int i = 0; i < instanceCount; i++) + { + var meshLod = meshLods[i]; + var lodGroupEntity = meshLod.Group; + var lodGroup = MeshLODGroupComponent[lodGroupEntity]; + var parentMask = lodGroup.ParentMask; + var parentGroupEntity = lodGroup.ParentGroup; + + // Store LOD parent group in MeshLODComponent to avoid double indirection for every entity + meshLod.ParentGroup = parentGroupEntity; + meshLods[i] = meshLod; + + RootLODRange rootLod; + + if (parentGroupEntity == Entity.Null) + { + rootLod.LOD.MinDist = 0; + rootLod.LOD.MaxDist = 1048576.0f; + } + else + { + var parentLodGroup = MeshLODGroupComponent[parentGroupEntity]; + rootLod.LOD = new LODRange(parentLodGroup, parentMask); + CheckDeepHLODSupport(parentLodGroup.ParentGroup); + } + + rootLODRange[i] = rootLod; + } + } + } + + [BurstCompile] + struct UpdateLODGroupWorldReferencePointsJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle MeshLODGroupComponent; + [ReadOnly] public ComponentTypeHandle LocalToWorld; + public ComponentTypeHandle LODGroupWorldReferencePoint; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var meshLODGroupComponent = chunk.GetNativeArray(MeshLODGroupComponent); + var localToWorld = chunk.GetNativeArray(LocalToWorld); + var lodGroupWorldReferencePoint = chunk.GetNativeArray(LODGroupWorldReferencePoint); + var instanceCount = chunk.Count; + + for (int i = 0; i < instanceCount; i++) + { + lodGroupWorldReferencePoint[i] = new LODGroupWorldReferencePoint { Value = math.transform(localToWorld[i].Value, meshLODGroupComponent[i].LocalReferencePoint) }; + } + } + } + + [BurstCompile] + struct UpdateLODWorldReferencePointsJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle MeshLODComponent; + [ReadOnly] public ComponentLookup LODGroupWorldReferencePoint; + public ComponentTypeHandle RootLODWorldReferencePoint; + public ComponentTypeHandle LODWorldReferencePoint; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var rootLODWorldReferencePoint = chunk.GetNativeArray(RootLODWorldReferencePoint); + var lodWorldReferencePoint = chunk.GetNativeArray(LODWorldReferencePoint); + var meshLods = chunk.GetNativeArray(MeshLODComponent); + var instanceCount = chunk.Count; + + for (int i = 0; i < instanceCount; i++) + { + var meshLod = meshLods[i]; + var lodGroupEntity = meshLod.Group; + var lodGroupWorldReferencePoint = LODGroupWorldReferencePoint[lodGroupEntity].Value; + + lodWorldReferencePoint[i] = new LODWorldReferencePoint { Value = lodGroupWorldReferencePoint }; + } + + for (int i = 0; i < instanceCount; i++) + { + var meshLod = meshLods[i]; + var parentGroupEntity = meshLod.ParentGroup; + + RootLODWorldReferencePoint rootPoint; + + if (parentGroupEntity == Entity.Null) + { + rootPoint.Value = new float3(0, 0, 0); + } + else + { + var parentGroupWorldReferencePoint = LODGroupWorldReferencePoint[parentGroupEntity].Value; + rootPoint.Value = parentGroupWorldReferencePoint; + } + + rootLODWorldReferencePoint[i] = rootPoint; + } + } + } + + /// + protected override void OnCreate() + { + // Change filter: LODGroupConversion add MeshLODComponent for all LOD children. When the MeshLODComponent is added/changed, we recalculate LOD ranges. + m_UpdatedLODRanges = GetEntityQuery(ComponentType.ReadOnly(), typeof(MeshLODComponent), typeof(RootLODRange), typeof(LODRange)); + m_UpdatedLODRanges.SetChangedVersionFilter(ComponentType.ReadWrite()); + + m_LODReferencePoints = GetEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly(), typeof(RootLODWorldReferencePoint), typeof(LODWorldReferencePoint)); + + // Change filter: LOD Group world reference points only change when MeshLODGroupComponent or LocalToWorld change + m_LODGroupReferencePoints = GetEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly(), typeof(LODGroupWorldReferencePoint)); + m_LODGroupReferencePoints.SetChangedVersionFilter(new[] { ComponentType.ReadWrite(), ComponentType.ReadWrite() }); + + MeshLODGroupComponent = GetComponentLookup(true); + MeshLODComponent = GetComponentTypeHandle(); + LocalToWorldLookup = GetComponentLookup(true); + RootLODRange = GetComponentTypeHandle(); + LODRange = GetComponentTypeHandle(); + } + + /// + protected override void OnUpdate() + { + MeshLODGroupComponent.Update(this); + MeshLODComponent.Update(this); + LocalToWorldLookup.Update(this); + RootLODRange.Update(this); + LODRange.Update(this); + + var updateLODRangesJob = new UpdateLODRangesJob + { + MeshLODGroupComponent = MeshLODGroupComponent, + MeshLODComponent = MeshLODComponent, + LocalToWorldLookup = LocalToWorldLookup, + RootLODRange = RootLODRange, + LODRange = LODRange + }; + + var updateGroupReferencePointJob = new UpdateLODGroupWorldReferencePointsJob + { + MeshLODGroupComponent = GetComponentTypeHandle(true), + LocalToWorld = GetComponentTypeHandle(true), + LODGroupWorldReferencePoint = GetComponentTypeHandle(), + }; + + var updateReferencePointJob = new UpdateLODWorldReferencePointsJob + { + //MeshLODGroupComponent = GetComponentLookup(true), + MeshLODComponent = GetComponentTypeHandle(true), + LODGroupWorldReferencePoint = GetComponentLookup(true), + RootLODWorldReferencePoint = GetComponentTypeHandle(), + LODWorldReferencePoint = GetComponentTypeHandle(), + }; + + var depLODRanges = updateLODRangesJob.ScheduleParallel(m_UpdatedLODRanges, Dependency); + var depGroupReferencePoints = updateGroupReferencePointJob.ScheduleParallel(m_LODGroupReferencePoints, Dependency); + var depCombined = JobHandle.CombineDependencies(depLODRanges, depGroupReferencePoints); + var depReferencePoints = updateReferencePointJob.ScheduleParallel(m_LODReferencePoints, depCombined); + + Dependency = JobHandle.CombineDependencies(depReferencePoints, depReferencePoints); + } + } +} diff --git a/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs.meta b/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs.meta new file mode 100644 index 0000000..3f9b886 --- /dev/null +++ b/Unity.Entities.Graphics/LODRequirementsUpdateSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: afb9def4b3068429cbe1bb43489ed5c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/LightMaps.cs b/Unity.Entities.Graphics/LightMaps.cs new file mode 100644 index 0000000..9e2ae74 --- /dev/null +++ b/Unity.Entities.Graphics/LightMaps.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Entities; +using UnityEngine; +using UnityEngine.Experimental.Rendering; + +namespace Unity.Rendering +{ + /// + /// Represents a container for light maps. + /// + public struct LightMaps : ISharedComponentData, IEquatable + { + /// + /// An array of color maps. + /// + public Texture2DArray colors; + + /// + /// An array of directional maps. + /// + public Texture2DArray directions; + + /// + /// An array of Shadow masks. + /// + public Texture2DArray shadowMasks; + + /// + /// Indicates whether the container stores any directional maps. + /// + public bool hasDirections => directions != null && directions.depth > 0; + + /// + /// Indicates whether the container stores any shadow masks. + /// + public bool hasShadowMask => shadowMasks != null && shadowMasks.depth > 0; + + /// + /// Indicates whether the container stores any color maps. + /// + public bool isValid => colors != null; + + /// + public bool Equals(LightMaps other) + { + return + colors == other.colors && + directions == other.directions && + shadowMasks == other.shadowMasks; + } + + /// + public override int GetHashCode() + { + int hash = 0; + if (!ReferenceEquals(colors, null)) hash ^= colors.GetHashCode(); + if (!ReferenceEquals(directions, null)) hash ^= directions.GetHashCode(); + if (!ReferenceEquals(shadowMasks, null)) hash ^= shadowMasks.GetHashCode(); + return hash; + } + + /// + /// Converts a provided list of Texture2Ds into a Texture2DArray. + /// + /// A list of Texture2Ds. + /// Returns a Texture2DArray that contains the list of Texture2Ds. + private static Texture2DArray CopyToTextureArray(List source) + { + if (source == null || !source.Any()) + return null; + + var data = source.First(); + if (data == null) + return null; + + bool isSRGB = GraphicsFormatUtility.IsSRGBFormat(data.graphicsFormat); + var result = new Texture2DArray(data.width, data.height, source.Count, source[0].format, true, !isSRGB); + result.filterMode = FilterMode.Trilinear; + result.wrapMode = TextureWrapMode.Clamp; + result.anisoLevel = 3; + + for (var sliceIndex = 0; sliceIndex < source.Count; sliceIndex++) + { + var lightMap = source[sliceIndex]; + Graphics.CopyTexture(lightMap, 0, result, sliceIndex); + } + + return result; + } + + /// + /// Constructs a LightMaps instance from a list of textures for colors, direction lights, and shadow masks. + /// + /// The list of Texture2D for colors. + /// The list of Texture2D for direction lights. + /// The list of Texture2D for shadow masks. + /// Returns a new LightMaps object. + public static LightMaps ConstructLightMaps(List inColors, List inDirections, List inShadowMasks) + { + var result = new LightMaps + { + colors = CopyToTextureArray(inColors), + directions = CopyToTextureArray(inDirections), + shadowMasks = CopyToTextureArray(inShadowMasks) + }; + return result; + } + } +} diff --git a/Unity.Entities.Graphics/LightMaps.cs.meta b/Unity.Entities.Graphics/LightMaps.cs.meta new file mode 100644 index 0000000..a12a1d8 --- /dev/null +++ b/Unity.Entities.Graphics/LightMaps.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0078a573de7a4f4a8e71e9ac1b4196e1 +timeCreated: 1589279773 \ No newline at end of file diff --git a/Unity.Entities.Graphics/MaterialColor.cs b/Unity.Entities.Graphics/MaterialColor.cs new file mode 100644 index 0000000..808df06 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialColor.cs @@ -0,0 +1,52 @@ +using System; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + /// + /// An unmanaged component that acts as an example material override for setting the RGBA color of an entity. + /// + /// + /// You should implement your own material property override components inside your project. + /// + [Serializable] + [MaterialProperty("_Color")] + public struct MaterialColor : IComponentData + { + /// + /// The RGBA color value. + /// + public float4 Value; + } + + namespace Authoring + { + /// + /// Represents the authoring component for the material color override. + /// + [DisallowMultipleComponent] + public class MaterialColor : MonoBehaviour + { + /// + /// The material color to use. + /// + public Color color; + } + + /// + /// Represents a baker that adds a MaterialColor component to entities this baker affects. + /// + public class MaterialColorBaker : Baker + { + /// + public override void Bake(MaterialColor authoring) + { + Color linearCol = authoring.color.linear; + var data = new Unity.Rendering.MaterialColor { Value = new float4(linearCol.r, linearCol.g, linearCol.b, linearCol.a) }; + AddComponent(data); + } + } + } +} diff --git a/Unity.Entities.Graphics/MaterialColor.cs.meta b/Unity.Entities.Graphics/MaterialColor.cs.meta new file mode 100644 index 0000000..e7cbc54 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialColor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb676996e2f3ffb46be05e024c7f0ff7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MaterialOverride.cs b/Unity.Entities.Graphics/MaterialOverride.cs new file mode 100644 index 0000000..4f9e389 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialOverride.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Assertions; +using UnityEngine.Rendering; + +interface IHelper +{ + void AddComponentData(EntityManager dstManager, Entity entity, IComponentData iComponentData); +} + +class Helper : IHelper where T : unmanaged, IComponentData +{ + public void AddComponentData(EntityManager dstManager, Entity entity, IComponentData iComponentData) + { + dstManager.AddComponentData(entity, (T)iComponentData); + } +} + + +/// +/// Represents a material override authoring component. +/// +[DisallowMultipleComponent] +[ExecuteInEditMode] +public class MaterialOverride : MonoBehaviour +{ + + /// + /// The material asset to override. + /// + public MaterialOverrideAsset overrideAsset; + + /// + /// The list of overridden material properties. + /// + public List overrideList = new List(); + + /// + /// Applies the material properties to the renderer. + /// + public void ApplyMaterialProperties() + { + if (overrideAsset != null) + { + if (overrideAsset.material != null) + { + //TODO(andrew.theisen): needs support for multiple renderers + var renderer = GetComponent(); + if (renderer != null) + { + renderer.SetPropertyBlock(null); + var propertyBlock = new MaterialPropertyBlock(); + foreach (var overrideData in overrideList) + { + if (overrideData.type == ShaderPropertyType.Color) + { + propertyBlock.SetColor(overrideData.name, overrideData.value); + } + else if (overrideData.type == ShaderPropertyType.Vector) + { + propertyBlock.SetVector(overrideData.name, overrideData.value); + } + else if (overrideData.type == ShaderPropertyType.Float || overrideData.type == ShaderPropertyType.Range) + { + propertyBlock.SetFloat(overrideData.name, overrideData.value.x); + } + } + + renderer.SetPropertyBlock(propertyBlock); + } + } + } + } + + /// + public void OnValidate() + { + if (overrideAsset != null) + { + var newList = new List(); + foreach (var overrideData in overrideAsset.overrideList) + { + int index = overrideList.FindIndex(d => d.name == overrideData.name); + if (index != -1) + { + if (overrideList[index].instanceOverride) + { + newList.Add(overrideList[index]); + continue; + } + } + newList.Add(overrideData); + } + overrideList = newList; + ApplyMaterialProperties(); + } + else + { + overrideList = new List(); + ClearOverrides(); + } + } + + /// + /// Resets the renderer. + /// + public void ClearOverrides() + { + var renderer = GetComponent(); + if (renderer != null) + { + renderer.SetPropertyBlock(null); + } + } + + /// + /// Calls ClearOverrides when the behaviour becomes disabled. + /// + public void OnDisable() + { + ClearOverrides(); + } +} + +class MaterialOverrideBaker : Baker +{ + public override unsafe void Bake(MaterialOverride authoring) + { + if (authoring.overrideAsset != null && authoring.overrideAsset.material != null) + { + foreach (var overrideData in authoring.overrideList) + { + Type overrideType = authoring.overrideAsset.GetTypeFromAttrs(overrideData); + if (overrideType != null) + { + var overrideTypeIndex = TypeManager.GetTypeIndex(overrideType); + var typeInfo = TypeManager.GetTypeInfo(overrideTypeIndex); + var entity = GetEntity(authoring); + int dataSize = 0; + var componentData = UnsafeUtility.Malloc(typeInfo.TypeSize, typeInfo.AlignmentInBytes, Allocator.Temp); + + if (overrideData.type == ShaderPropertyType.Vector || overrideData.type == ShaderPropertyType.Color) + { + var data = new float4(overrideData.value.x, overrideData.value.y, overrideData.value.z, overrideData.value.w); + dataSize = sizeof(float4); + + Assert.AreEqual(dataSize, typeInfo.TypeSize, "Material Override components must contain only the exact field it is overriding."); + UnsafeUtility.MemCpy(componentData, &data, dataSize); + } + else if (overrideData.type == ShaderPropertyType.Float || overrideData.type == ShaderPropertyType.Range) + { + float data = overrideData.value.x; + dataSize = sizeof(float); + + Assert.AreEqual(dataSize, typeInfo.TypeSize, "Material Override components must contain only the exact field it is overriding."); + UnsafeUtility.MemCpy(componentData, &data, dataSize); + } + + UnsafeAddComponent(entity, overrideTypeIndex, typeInfo.TypeSize, componentData); + UnsafeUtility.Free(componentData, Allocator.Temp); + } + } + } + } +} diff --git a/Unity.Entities.Graphics/MaterialOverride.cs.meta b/Unity.Entities.Graphics/MaterialOverride.cs.meta new file mode 100644 index 0000000..5e561c5 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialOverride.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7d849f6ed953e947a0ea6954432e1ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MaterialOverrideAsset.cs b/Unity.Entities.Graphics/MaterialOverrideAsset.cs new file mode 100644 index 0000000..565eafe --- /dev/null +++ b/Unity.Entities.Graphics/MaterialOverrideAsset.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using Unity.Entities; +using Unity.Rendering; +using UnityEngine; +using UnityEngine.Rendering; + + +/// +/// Represents a material property override asset. +/// +[CreateAssetMenu(fileName = "MaterialOverrideAsset", menuName = "Shader/Material Override Asset", order = 1)] //TODO(andrew.theisen): where should this live in the menu? +public class MaterialOverrideAsset : ScriptableObject +{ + /// + /// Represents a data container of material override properties. + /// + [Serializable] + public struct OverrideData + { + /// + /// The in-shader name of the material property. + /// + public string name; + + /// + /// The display name of the material property. + /// + public string displayName; + + /// + /// The name of the sahder. + /// + public string shaderName; + + /// + /// The name of the material. + /// + public string materialName; + + /// + /// The type of the shader property. + /// + public ShaderPropertyType type; + + /// + /// The override value of the material property. + /// + public Vector4 value; + + + /// + /// Instance override. + /// + public bool instanceOverride; + } + + /// + /// A list of material property overrides. + /// + public List overrideList = new List(); + + /// + /// The material to apply the overrides to. + /// + public Material material; + + /// + /// Gets the material property type from an OverrideData object. + /// + /// The OverrideDat to use. + /// Returns the type of the material property. + public Type GetTypeFromAttrs(OverrideData overrideData) + { + Type overrideType = null; + bool componentExists = false; + foreach (var t in TypeManager.GetAllTypes()) + { + if (t.Type != null) + { + //TODO(andrew.theisen): this grabs the first IComponentData that matches these attributes but multiple matches can exist such as URPMaterialPropertyBaseColor + // and HDRPMaterialPropertyBaseColor. It actually shouldn't matter which one is used can they can work either shader. + foreach (var attr in t.Type.GetCustomAttributes(typeof(MaterialPropertyAttribute), false)) + { + if (TypeManager.IsSharedComponentType(t.TypeIndex)) + { + continue; + } + + var propAttr = (MaterialPropertyAttribute)attr; + //TODO(andrew.theisen): So this won't use exisiting IComponentDatas always. for example: + // HDRPMaterialPropertyEmissiveColor is Float3, but the ShaderPropertyType + // is Color but without alpha. can fix this when we can get the DOTS + // type or byte size of the property + if (overrideData.type == ShaderPropertyType.Vector || overrideData.type == ShaderPropertyType.Color) + { + // propFormat = MaterialPropertyFormat.Float4; + } + else if (overrideData.type == ShaderPropertyType.Float || overrideData.type == ShaderPropertyType.Range) + { + // propFormat = MaterialPropertyFormat.Float; + } + else + { + break; + } + + if (propAttr.Name == overrideData.name) + { + overrideType = t.Type; + componentExists = true; + break; + } + } + } + if (componentExists) + { + break; + } + } + return overrideType; + } + + /// + public void OnValidate() + { + foreach (var overrideComponent in FindObjectsOfType()) + { + if (overrideComponent.overrideAsset == this) + { + if (material != null) + { + var newList = new List(); + foreach (var overrideData in overrideList) + { + int index = overrideComponent.overrideList.FindIndex(d => d.name == overrideData.name); + if (index != -1) + { + if (overrideComponent.overrideList[index].instanceOverride) + { + newList.Add(overrideComponent.overrideList[index]); + continue; + } + } + newList.Add(overrideData); + } + overrideComponent.overrideList = newList; + overrideComponent.ApplyMaterialProperties(); + } + else + { + overrideComponent.overrideList = new List(); + overrideComponent.ClearOverrides(); + } + } + } + } +} diff --git a/Unity.Entities.Graphics/MaterialOverrideAsset.cs.meta b/Unity.Entities.Graphics/MaterialOverrideAsset.cs.meta new file mode 100644 index 0000000..3fa182b --- /dev/null +++ b/Unity.Entities.Graphics/MaterialOverrideAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1c49a92ef1771844b9c93a4037158cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MaterialPropertyAttribute.cs b/Unity.Entities.Graphics/MaterialPropertyAttribute.cs new file mode 100644 index 0000000..d0a27b6 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialPropertyAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace Unity.Rendering +{ + /// + /// Marks an IComponentData as an input to a material property on a particular shader. + /// + [AttributeUsage(AttributeTargets.Struct, AllowMultiple = true)] + public class MaterialPropertyAttribute : Attribute + { + /// + /// Constructs a material property attribute. + /// + /// The name of the material property. + /// An optional size of the property on the GPU. + public MaterialPropertyAttribute(string materialPropertyName, short overrideSizeGPU = -1) + { + Name = materialPropertyName; + OverrideSizeGPU = overrideSizeGPU; + } + + /// + /// The name of the material property. + /// + public string Name { get; } + + /// + /// The size of the property on the GPU. + /// + public short OverrideSizeGPU { get; } + } +} diff --git a/Unity.Entities.Graphics/MaterialPropertyAttribute.cs.meta b/Unity.Entities.Graphics/MaterialPropertyAttribute.cs.meta new file mode 100644 index 0000000..b9b6361 --- /dev/null +++ b/Unity.Entities.Graphics/MaterialPropertyAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff388e0f6e5166c49a72f26fe53f4135 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MatrixPreviousSystem.cs b/Unity.Entities.Graphics/MatrixPreviousSystem.cs new file mode 100644 index 0000000..a4598d7 --- /dev/null +++ b/Unity.Entities.Graphics/MatrixPreviousSystem.cs @@ -0,0 +1,72 @@ +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.Rendering +{ + //@TODO: Updating always necessary due to empty component group. When Component group and archetype chunks are unified, [RequireMatchingQueriesForUpdate] can be added. + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateAfter(typeof(EntitiesGraphicsSystem))] + internal partial class MatrixPreviousSystem : SystemBase + { + private EntityQuery m_GroupPrev; + + [BurstCompile] + struct UpdateMatrixPrevious : IJobChunk + { + [ReadOnly] public ComponentTypeHandle LocalToWorldTypeHandle; + public ComponentTypeHandle MatrixPreviousTypeHandle; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var chunkLocalToWorld = chunk.GetNativeArray(LocalToWorldTypeHandle); + var chunkMatrixPrevious = chunk.GetNativeArray(MatrixPreviousTypeHandle); + for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) + { + var localToWorld = chunkLocalToWorld[i].Value; + chunkMatrixPrevious[i] = new BuiltinMaterialPropertyUnity_MatrixPreviousM {Value = localToWorld}; + } + } + } + + /// + protected override void OnCreate() + { + m_GroupPrev = GetEntityQuery(new EntityQueryDesc + { + All = new ComponentType[] + { + ComponentType.ReadOnly(), + ComponentType.ReadWrite(), + }, + Options = EntityQueryOptions.FilterWriteGroup + }); + m_GroupPrev.SetChangedVersionFilter(new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }); + } + + /// + protected override void OnUpdate() + { + if (!EntitiesGraphicsSystem.EntitiesGraphicsEnabled) + return; + + var updateMatrixPreviousJob = new UpdateMatrixPrevious + { + LocalToWorldTypeHandle = GetComponentTypeHandle(true), + MatrixPreviousTypeHandle = GetComponentTypeHandle(), + }; + Dependency = updateMatrixPreviousJob.ScheduleParallel(m_GroupPrev, Dependency); + } + } +} diff --git a/Unity.Entities.Graphics/MatrixPreviousSystem.cs.meta b/Unity.Entities.Graphics/MatrixPreviousSystem.cs.meta new file mode 100644 index 0000000..ea43eea --- /dev/null +++ b/Unity.Entities.Graphics/MatrixPreviousSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb9bf29db8c5bb3469fb45a2a236b9fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MeshLODComponent.cs b/Unity.Entities.Graphics/MeshLODComponent.cs new file mode 100644 index 0000000..310ac06 --- /dev/null +++ b/Unity.Entities.Graphics/MeshLODComponent.cs @@ -0,0 +1,78 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + /// + /// Represents an LOD group. + /// + /// + /// Each MeshLODGroupComponent contains multiple MeshLODComponents and can also have multiple child groups. + /// + public struct MeshLODGroupComponent : IComponentData + { + /// + /// The LOD parent group. + /// + public Entity ParentGroup; + + /// + /// The LOD mask. + /// + /// + /// Each bit matches with one of the 8 possible LOD levels. + /// + public int ParentMask; + + + /// + /// The low part of the LOD distance container. + /// + public float4 LODDistances0; + + /// + /// LOD distance container, high part. + /// + public float4 LODDistances1; + + + /// + /// Local reference point. + /// + public float3 LocalReferencePoint; + } + + + /// + /// An unmanaged component that represents a world reference point to use for LOD group. + /// + struct LODGroupWorldReferencePoint : IComponentData + { + /// + /// The world-space x, y, and z position of the reference point. + /// + public float3 Value; + } + + /// + /// An unamanged component that represents a mesh LOD entity. + /// + public struct MeshLODComponent : IComponentData + { + /// + /// The parent LOD group entity. + /// + public Entity Group; + + + /// + /// The mesh LOD parent group. + /// + public Entity ParentGroup; + + /// + /// The LOD mask. + /// + public int LODMask; + } +} diff --git a/Unity.Entities.Graphics/MeshLODComponent.cs.meta b/Unity.Entities.Graphics/MeshLODComponent.cs.meta new file mode 100644 index 0000000..b1582fb --- /dev/null +++ b/Unity.Entities.Graphics/MeshLODComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fea73841a262f40509466cfca1a0be87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MeshRendererBaking.cs b/Unity.Entities.Graphics/MeshRendererBaking.cs new file mode 100644 index 0000000..db06346 --- /dev/null +++ b/Unity.Entities.Graphics/MeshRendererBaking.cs @@ -0,0 +1,252 @@ +using System.Collections.Generic; +using System.Linq; +using Unity.Collections; +using Unity.Entities; +using Unity.Transforms; +using UnityEngine; + +namespace Unity.Rendering +{ + + [TemporaryBakingType] + struct MeshRendererBakingData : IComponentData + { + public UnityObjectRef MeshRenderer; + } + + [BakingType] + struct AdditionalMeshRendererEntity : IComponentData { } + + class MeshRendererBaker : Baker + { + public override void Bake(MeshRenderer authoring) + { + // TextMeshes don't need MeshFilters, early out + var textMesh = GetComponent(); + if (textMesh != null) + return; + + // Takes a dependency on the mesh + var meshFilter = GetComponent(); + var mesh = (meshFilter != null) ? GetComponent().sharedMesh : null; + + // Takes a dependency on the materials + var sharedMaterials = new List(); + authoring.GetSharedMaterials(sharedMaterials); + + MeshRendererBakingUtility.Convert(this, authoring, mesh, sharedMaterials, true, out var additionalEntities); + + if(additionalEntities.Count == 0) + AddComponent(new MeshRendererBakingData{ MeshRenderer = authoring }); + + foreach (var entity in additionalEntities) + { + AddComponent(entity, new MeshRendererBakingData{MeshRenderer = authoring}); + } + } + } + + [RequireMatchingQueriesForUpdate] + [WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)] + [UpdateBefore(typeof(MeshRendererBaking))] + partial class AdditionalMeshRendererFilterBakingSystem : SystemBase + { + private EntityQuery m_AdditionalEntities; + private ComponentTypeSet typesToFilterSet; + + protected override void OnCreate() + { + ComponentType[] typesToFilter = new[] + { +#if !ENABLE_TRANSFORM_V1 + ComponentType.ReadOnly(), +#else + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), +#endif + }; + typesToFilterSet = new ComponentTypeSet(typesToFilter); + + m_AdditionalEntities = GetEntityQuery(new EntityQueryDesc + { + All = new [] + { + ComponentType.ReadOnly() + }, + Any = typesToFilter, + Options = EntityQueryOptions.IncludePrefab | EntityQueryOptions.IncludeDisabledEntities, + }); + } + + protected override void OnUpdate() + { + EntityManager.RemoveComponent(m_AdditionalEntities, typesToFilterSet); + } + } + + [RequireMatchingQueriesForUpdate] + [WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)] + partial class MeshRendererBaking : SystemBase + { + // Hold a persistent light map conversion context so previously encountered light maps + // can be reused across multiple conversion batches, which is especially important + // for incremental conversion (LiveConversion). + private LightMapBakingContext m_LightMapBakingContext; + + /// + protected override void OnCreate() + { + m_LightMapBakingContext = new LightMapBakingContext(); + } + + /// + protected override void OnUpdate() + { + // TODO: When to call m_LightMapConversionContext.Reset() ? When lightmaps are baked? + var context = new RenderMeshBakingContext(m_LightMapBakingContext); + + if (m_LightMapBakingContext != null) + { + Entities.ForEach((in MeshRendererBakingData authoring) => + { + context.CollectLightMapUsage(authoring.MeshRenderer); + }).WithoutBurst().WithStructuralChanges().Run(); + } + + context.ProcessLightMapsForConversion(); + + var ecb = new EntityCommandBuffer(Allocator.TempJob); + Entities.ForEach((Entity entity, RenderMesh renderMesh, in MeshRendererBakingData authoring) => + { + Renderer renderer = authoring.MeshRenderer; + + var lightmappedMaterial = context.ConfigureHybridLightMapping( + entity, + ecb, + renderer, + renderMesh.material); + + if (lightmappedMaterial != null) + { + renderMesh.material = lightmappedMaterial; + ecb.SetSharedComponentManaged(entity, renderMesh); + } + + }).WithEntityQueryOptions(EntityQueryOptions.IncludeDisabledEntities).WithoutBurst().WithStructuralChanges().Run(); + + context.EndConversion(); + + ecb.Playback(EntityManager); + ecb.Dispose(); + } + } + + + // RenderMeshPostprocessSystem combines RenderMesh components from all found entities + // into a single RenderMeshArray component that is used for rendering. + // The RenderMesh component is no longer used at runtime, and it is removed to improve + // chunk utilization and batching. + [RequireMatchingQueriesForUpdate] + [WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)] + [UpdateAfter(typeof(MeshRendererBaking))] + partial class RenderMeshPostProcessSystem : SystemBase + { + Material m_ErrorMaterial; + + EntityQuery m_BakedEntities; + EntityQuery m_RenderMeshEntities; + + /// + protected override void OnCreate() + { + if (m_ErrorMaterial == null) + { + m_ErrorMaterial = EntitiesGraphicsUtils.LoadErrorMaterial(); + } + + m_BakedEntities = EntityManager.CreateEntityQuery(new EntityQueryDesc + { + Any = new [] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + }, + Options = EntityQueryOptions.IncludePrefab + }); + + m_RenderMeshEntities = EntityManager.CreateEntityQuery(new EntityQueryDesc + { + All = new [] + { + ComponentType.ReadWrite(), + ComponentType.ReadWrite(), + }, + Options = EntityQueryOptions.IncludePrefab + }); + + RequireForUpdate(m_BakedEntities); + } + + /// + protected override void OnUpdate() + { + int countUpperBound = EntityManager.GetSharedComponentCount(); + + var renderMeshes = new List(countUpperBound); + var meshes = new Dictionary(countUpperBound); + var materials = new Dictionary(countUpperBound); + + EntityManager.GetAllUniqueSharedComponentsManaged(renderMeshes); + renderMeshes.RemoveAt(0); // Remove null component automatically added by GetAllUniqueSharedComponentData + + foreach (var renderMesh in renderMeshes) + { + if (renderMesh.mesh != null) + meshes[renderMesh.mesh] = true; + + var material = renderMesh.material ?? m_ErrorMaterial; + materials[material] = true; + } + + if (meshes.Count == 0 && materials.Count == 0) + return; + + var renderMeshArray = new RenderMeshArray(materials.Keys.ToArray(), meshes.Keys.ToArray()); + var meshToIndex = renderMeshArray.GetMeshToIndexMapping(); + var materialToIndex = renderMeshArray.GetMaterialToIndexMapping(); + + EntityManager.SetSharedComponentManaged(m_RenderMeshEntities, renderMeshArray); + + var entities = m_RenderMeshEntities.ToEntityArray(Allocator.Temp); + + for (int i = 0; i < entities.Length; ++i) + { + var e = entities[i]; + + var renderMesh = EntityManager.GetSharedComponentManaged(e); + var material = renderMesh.material ?? m_ErrorMaterial; + + if (!materialToIndex.TryGetValue(material, out var materialIndex)) + { + Debug.LogWarning($"Material {material} not found from RenderMeshArray"); + materialIndex = 0; + } + + if (!meshToIndex.TryGetValue(renderMesh.mesh, out var meshIndex)) + { + Debug.LogWarning($"Mesh {renderMesh.mesh} not found from RenderMeshArray"); + meshIndex = 0; + } + + sbyte submesh = (sbyte) renderMesh.subMesh; + + EntityManager.SetComponentData(e, + MaterialMeshInfo.FromRenderMeshArrayIndices( + materialIndex, + meshIndex, + submesh)); + } + } + } +} diff --git a/Unity.Entities.Graphics/MeshRendererBaking.cs.meta b/Unity.Entities.Graphics/MeshRendererBaking.cs.meta new file mode 100644 index 0000000..267f5ec --- /dev/null +++ b/Unity.Entities.Graphics/MeshRendererBaking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3d4f59229b2aee81e80e731c58716c23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/MeshRendererBakingUtility.cs b/Unity.Entities.Graphics/MeshRendererBakingUtility.cs new file mode 100644 index 0000000..e8f0ad2 --- /dev/null +++ b/Unity.Entities.Graphics/MeshRendererBakingUtility.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine; + +namespace Unity.Rendering +{ + class MeshRendererBakingUtility + { + struct LODState + { + public LODGroup LodGroup; + public Entity LodGroupEntity; + public int LodGroupIndex; + } + + static void CreateLODState(Baker baker, Renderer authoringSource, out LODState lodState) where T : Component + { + // LODGroup + lodState = new LODState(); + lodState.LodGroup = baker.GetComponentInParent(); + lodState.LodGroupEntity = baker.GetEntity(lodState.LodGroup); + lodState.LodGroupIndex = FindInLODs(lodState.LodGroup, authoringSource); + } + + private static int FindInLODs(LODGroup lodGroup, Renderer authoring) + { + if (lodGroup != null) + { + var lodGroupLODs = lodGroup.GetLODs(); + + // Find the renderer inside the LODGroup + for (int i = 0; i < lodGroupLODs.Length; ++i) + { + foreach (var renderer in lodGroupLODs[i].renderers) + { + if (renderer == authoring) + { + return i; + } + } + } + } + return -1; + } + +#pragma warning disable CS0162 + private static void AddRendererComponents(Entity entity, Baker baker, in RenderMeshDescription renderMeshDescription, RenderMesh renderMesh) where T : Component + { +#if UNITY_EDITOR + // Skip the validation check in the player to minimize overhead. + if (!RenderMeshUtility.ValidateMesh(renderMesh)) + return; +#endif + + // Entities with Static are never rendered with motion vectors + bool inMotionPass = RenderMeshUtility.kUseHybridMotionPass && + renderMeshDescription.FilterSettings.IsInMotionPass && + !baker.IsStatic(); + + RenderMeshUtility.EntitiesGraphicsComponentFlags flags = RenderMeshUtility.EntitiesGraphicsComponentFlags.Baking; + if (inMotionPass) flags |= RenderMeshUtility.EntitiesGraphicsComponentFlags.InMotionPass; + flags |= RenderMeshUtility.LightProbeFlags(renderMeshDescription.LightProbeUsage); + flags |= RenderMeshUtility.DepthSortedFlags(renderMesh.material); + + // Add all components up front using as few calls as possible. + var componentTypes = RenderMeshUtility.s_EntitiesGraphicsComponentTypes.GetComponentTypes(flags); + baker.AddComponent(entity, componentTypes); + + baker.SetSharedComponentManaged(entity, renderMesh); + baker.SetSharedComponentManaged(entity, renderMeshDescription.FilterSettings); + + var localBounds = renderMesh.mesh.bounds.ToAABB(); + baker.SetComponent(entity, new RenderBounds { Value = localBounds }); + } + + internal static void Convert(Baker baker, Renderer authoring, Mesh mesh, List sharedMaterials, bool attachToPrimaryEntityForSingleMaterial, out List additionalEntities, Transform root = null) where T : Component + { + additionalEntities = new List(); + + if (mesh == null || sharedMaterials.Count == 0) + { + Debug.LogWarning( + $"Renderer is not converted because either the assigned mesh is null or no materials are assigned on GameObject {authoring.name}.", + authoring); + return; + } + + // Takes a dependency on the material + foreach (var material in sharedMaterials) + baker.DependsOn(material); + + // Takes a dependency on the mesh + baker.DependsOn(mesh); + + // RenderMeshDescription accesses the GameObject layer. + // Declaring the dependency on the GameObject with GetLayer, so the baker rebakes if the layer changes + baker.GetLayer(authoring); + var desc = new RenderMeshDescription(authoring); + var renderMesh = new RenderMesh(authoring, mesh, sharedMaterials); + + // Always disable per-object motion vectors for static objects + if (baker.IsStatic()) + { + if (desc.FilterSettings.MotionMode == MotionVectorGenerationMode.Object) + desc.FilterSettings.MotionMode = MotionVectorGenerationMode.Camera; + } + + if (attachToPrimaryEntityForSingleMaterial && sharedMaterials.Count == 1) + { + ConvertToSingleEntity( + baker, + desc, + renderMesh, + authoring); + } + else + { + ConvertToMultipleEntities( + baker, + desc, + renderMesh, + authoring, + sharedMaterials, + root, + out additionalEntities); + } + } + +#pragma warning restore CS0162 + + static void ConvertToSingleEntity( + Baker baker, + RenderMeshDescription renderMeshDescription, + RenderMesh renderMesh, + Renderer renderer) where T : Component + { + CreateLODState(baker, renderer, out var lodState); + + var entity = baker.GetEntity(renderer); + + AddRendererComponents(entity, baker, renderMeshDescription, renderMesh); + + if (lodState.LodGroupEntity != Entity.Null && lodState.LodGroupIndex != -1) + { + var lodComponent = new MeshLODComponent { Group = lodState.LodGroupEntity, LODMask = 1 << lodState.LodGroupIndex }; + baker.AddComponent(entity, lodComponent); + } + + baker.ConfigureEditorRenderData(entity, renderer.gameObject, true); + } + + internal static void ConvertToMultipleEntities( + Baker baker, + RenderMeshDescription renderMeshDescription, + RenderMesh renderMesh, + Renderer renderer, + List sharedMaterials, + Transform root, + out List additionalEntities) where T : Component + { + CreateLODState(baker, renderer, out var lodState); + + int materialCount = sharedMaterials.Count; + additionalEntities = new List(); + + for (var m = 0; m != materialCount; m++) + { + Entity meshEntity; + if (root == null) + { + meshEntity = baker.CreateAdditionalEntity(TransformUsageFlags.Default, false, $"{baker.GetName()}-MeshRendererEntity"); + + // Update Transform components: + baker.AddComponent(meshEntity); + } + else + { + meshEntity = baker.CreateAdditionalEntity(TransformUsageFlags.ManualOverride, false, $"{baker.GetName()}-MeshRendererEntity"); + + var localToWorld = root.localToWorldMatrix; + baker.AddComponent(meshEntity, new LocalToWorld {Value = localToWorld}); +#if !ENABLE_TRANSFORM_V1 + baker.AddComponent(meshEntity, + new LocalToWorldTransform { Value = UniformScaleTransform.FromMatrix(localToWorld) }); +#endif + + if (!baker.IsStatic()) + { + var rootEntity = baker.GetEntity(root); + baker.AddComponent(meshEntity, new Parent {Value = rootEntity}); +#if !ENABLE_TRANSFORM_V1 + baker.AddComponent(meshEntity, new LocalToParentTransform {Value = UniformScaleTransform.Identity}); +#else + baker.AddComponent(meshEntity, new LocalToParent {Value = float4x4.identity}); +#endif + } + } + + additionalEntities.Add(meshEntity); + + var material = sharedMaterials[m]; + + renderMesh.subMesh = m; + renderMesh.material = material; + + AddRendererComponents( + meshEntity, + baker, + renderMeshDescription, + renderMesh); + + if (lodState.LodGroupEntity != Entity.Null && lodState.LodGroupIndex != -1) + { + var lodComponent = new MeshLODComponent { Group = lodState.LodGroupEntity, LODMask = 1 << lodState.LodGroupIndex }; + baker.AddComponent(meshEntity, lodComponent); + } + + baker.ConfigureEditorRenderData(meshEntity, renderer.gameObject, true); + } + } + } +} diff --git a/Unity.Entities.Graphics/MeshRendererBakingUtility.cs.meta b/Unity.Entities.Graphics/MeshRendererBakingUtility.cs.meta new file mode 100644 index 0000000..5467b60 --- /dev/null +++ b/Unity.Entities.Graphics/MeshRendererBakingUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 90e9228b7fc54eec9bf9cfb6067ed471 +timeCreated: 1649701588 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Occlusion.meta b/Unity.Entities.Graphics/Occlusion.meta new file mode 100644 index 0000000..4265ddf --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 52fd9427620b68c48916b6dc55f336af +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked.meta b/Unity.Entities.Graphics/Occlusion/Masked.meta new file mode 100644 index 0000000..1491e32 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a46e7d254c5e82e4ead4bca63bd77d10 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs b/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs new file mode 100644 index 0000000..61af1db --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs @@ -0,0 +1,185 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Rendering.Occlusion.Masked.Visualization; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked +{ + + public class BufferGroup + { + public const int TileWidthShift = 5; + public const int TileHeightShift = 2; + public const int TileWidth = 1 << TileWidthShift; + public const int TileHeight = 1 << TileHeightShift; + // Sub-tiles (used for updating the masked HiZ buffer) are 8x4 tiles, so there are 4x2 sub-tiles in a tile + public const int SubTileWidth = 8; + public const int SubTileHeight = 4; + // The number of fixed point bits used to represent vertex coordinates / edge slopes. + public const int FpBits = 8; + public const int FpHalfPixel = 1 << (FpBits - 1); + // Tile dimensions in fixed point coordinates + public const int FpTileHeightShift = (FpBits + TileHeightShift); + public const int FpTileHeight = (1 << FpTileHeightShift); + // Size of guard band in pixels. Clipping doesn't seem to be very expensive so we use a small guard band + // to improve rasterization performance. It's not recommended to set the guard band to zero, as this may + // cause leakage along the screen border due to precision/rounding. + public const float GuardBandPixelSize = 1f; + + // Depth buffer + public int NumBuffers; + public int NumPixelsX; + public int NumPixelsY; + public int NumTilesX; + public int NumTilesY; + + public NativeArray Tiles; + + // Resolution-dependent values + public v128 PixelCenterX; + public v128 PixelCenterY; + public v128 PixelCenter; + public v128 HalfWidth; + public v128 HalfHeight; + public v128 HalfSize; + public v128 ScreenSize; + + public readonly BatchCullingViewType ViewType; + public Matrix4x4 CullingMatrix; + public BatchCullingProjectionType ProjectionType; + public float NearClip; + public NativeArray FrustumPlanes; + public ScissorRect FullScreenScissor; + + // Visualization + DebugView m_DebugView; + + public BufferGroup(BatchCullingViewType viewType) + { + ViewType = viewType; + NumBuffers = math.clamp(Jobs.LowLevel.Unsafe.JobsUtility.JobWorkerMaximumCount, 1, 10); + NearClip = float.MaxValue; + FrustumPlanes = new NativeArray(5, Allocator.Persistent); + } + + public void Dispose() + { + FrustumPlanes.Dispose(); + if (Tiles.IsCreated) + { + Tiles.Dispose(); + } + + m_DebugView?.Dispose(); + } + + public void SetResolutionAndClip(int numPixelsX, int numPixelsY, BatchCullingProjectionType projectionType, float nearClip) + { + if (numPixelsX != NumPixelsX || numPixelsY != NumPixelsY || projectionType != ProjectionType) + { + NumPixelsX = numPixelsX; + NumPixelsY = numPixelsY; + NumTilesX = (numPixelsX + TileWidth - 1) >> TileWidthShift; + NumTilesY = (numPixelsY + TileHeight - 1) >> TileHeightShift; + + float w = numPixelsX; // int -> float + float h = numPixelsY; // + float hw = w * 0.5f; + float hh = h * 0.5f; + PixelCenterX = X86.Sse.set1_ps(hw); + PixelCenterY = X86.Sse.set1_ps(hh); + PixelCenter = X86.Sse.setr_ps(hw, hw, hh, hh); + HalfWidth = X86.Sse.set1_ps(hw); + HalfHeight = X86.Sse.set1_ps(-hh); + HalfSize = X86.Sse.setr_ps(hw, hw, -hh, -hh); + ScreenSize = X86.Sse2.setr_epi32(numPixelsX - 1, numPixelsX - 1, numPixelsY - 1, numPixelsY - 1); + // TODO: Delete this after full implementation. This isn't needed because min values are zero, and + // so there is opportunity for optimization. + // Setup a full screen scissor rectangle + FullScreenScissor.mMinX = 0; + FullScreenScissor.mMinY = 0; + FullScreenScissor.mMaxX = NumTilesX << TileWidthShift; + FullScreenScissor.mMaxY = NumTilesY << TileHeightShift; + // Allocate the tiles buffers + if (Tiles.IsCreated) + { + Tiles.Dispose(); + } + + Tiles = new NativeArray(NumBuffers * NumTilesX * NumTilesY, + Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + + // Set orthographic mode and the rest of the frustum planes + { + ProjectionType = projectionType; + float guardBandWidth = 2f / NumPixelsX; + float guardBandHeight = 2f / NumPixelsY; + + if (projectionType == BatchCullingProjectionType.Orthographic) + { + FrustumPlanes[1] = X86.Sse.setr_ps(1f - guardBandWidth, 0f, 0f, 1f); + FrustumPlanes[2] = X86.Sse.setr_ps(-1f + guardBandWidth, 0f, 0f, 1f); + FrustumPlanes[3] = X86.Sse.setr_ps(0f, 1f - guardBandHeight, 0f, 1f); + FrustumPlanes[4] = X86.Sse.setr_ps(0f, -1f + guardBandHeight, 0f, 1f); + } + else + { + FrustumPlanes[1] = X86.Sse.setr_ps(1f - guardBandWidth, 0f, 1f, 0f); + FrustumPlanes[2] = X86.Sse.setr_ps(-1f + guardBandWidth, 0f, 1f, 0f); + FrustumPlanes[3] = X86.Sse.setr_ps(0f, 1f - guardBandHeight, 1f, 0f); + FrustumPlanes[4] = X86.Sse.setr_ps(0f, -1f + guardBandHeight, 1f, 0f); + } + } + } + + if (NearClip != nearClip) + { + // Set near clip + NearClip = nearClip; + FrustumPlanes[0] = X86.Sse.setr_ps(0f, 0f, 1f, -nearClip); + } + } + + public Texture2D GetVisualizationTexture() + { + return m_DebugView?.gpuDepth; + } + + public void RenderToTextures(EntityQuery testQuery, EntityQuery meshQuery, JobHandle dependency, DebugRenderMode mode) + { +#if UNITY_EDITOR + if (mode == DebugRenderMode.None && !OcclusionBrowseWindow.IsVisible) + { + return; + } + + if (m_DebugView == null) + { + m_DebugView = new DebugView(); + } + + dependency.Complete(); + + m_DebugView.ReallocateIfNeeded(NumPixelsX, NumPixelsY); + + Profiler.BeginSample("Occlusion.Debug.RenderView"); + m_DebugView.RenderToTextures(testQuery, meshQuery, this, mode, OcclusionBrowseWindow.IsVisible); + Profiler.EndSample(); +#endif + } + + public void RenderToCamera(DebugRenderMode renderMode, Camera camera, CommandBuffer cmd, Mesh fullScreenQuad) + { + m_DebugView?.RenderToCamera(renderMode, camera, cmd, fullScreenQuad, CullingMatrix); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs.meta new file mode 100644 index 0000000..784bf5c --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/BufferGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e641247e835adf4db82d2aaeebd5abd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs new file mode 100644 index 0000000..b61c625 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs @@ -0,0 +1,24 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile] + public unsafe struct ClearJob: IJobFor + { + [ReadOnly, NativeDisableUnsafePtrRestriction] public Tile* Tiles; + public void Execute(int i) + { + Tiles[i].zMin0 = X86.Sse.set1_ps(-1f); + Tiles[i].zMin1 = X86.Sse2.setzero_si128(); + Tiles[i].mask = X86.Sse2.setzero_si128(); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs.meta new file mode 100644 index 0000000..2b3afb6 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/ClearJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 100431faf4e589a4c972a61f02e4c33d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs new file mode 100644 index 0000000..f1d3cb8 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs @@ -0,0 +1,126 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using Unity.Rendering.Occlusion.Masked.Dots; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile] + unsafe struct ComputeBoundsJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle Bounds; + [ReadOnly] public ComponentTypeHandle LocalToWorld; + public ComponentTypeHandle OcclusionTest; + public ComponentTypeHandle ChunkOcclusionTest; + + [ReadOnly] public float4x4 ViewProjection; + [ReadOnly] public float NearClip; + [ReadOnly] public BatchCullingProjectionType ProjectionType; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + const float EPSILON = 1E-12f; + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var bounds = chunk.GetNativeArray(Bounds); + var localToWorld = chunk.GetNativeArray(LocalToWorld); + var tests = chunk.GetNativeArray(OcclusionTest); + + var verts = stackalloc float4[16]; + + var edges = stackalloc int2[] + { + new int2(0,1), new int2(1,3), new int2(3,2), new int2(2,0), + new int2(4,6), new int2(6,7), new int2(7,5), new int2(5,4), + new int2(4,0), new int2(2,6), new int2(1,5), new int2(7,3) + }; + + float4 screenMin = float.MaxValue; + float4 screenMax = float.MinValue; + + for (var entityIndex = 0; entityIndex < chunk.Count; entityIndex++) + { + var aabb = bounds[entityIndex].Value; + var occlusionTest = tests[entityIndex]; + var local = localToWorld[entityIndex].Value; + + occlusionTest.screenMin = float.MaxValue; + occlusionTest.screenMax = -float.MaxValue; + + // TODO: There's likely still room for optimization here. Investigate more approximate bounding box + // calculations which use less ALU ops. + var mvp = math.mul(ViewProjection, local); + + float4x2 u = new float4x2(mvp.c0 * aabb.Min.x, mvp.c0 * aabb.Max.x); + float4x2 v = new float4x2(mvp.c1 * aabb.Min.y, mvp.c1 * aabb.Max.y); + float4x2 w = new float4x2(mvp.c2 * aabb.Min.z, mvp.c2 * aabb.Max.z); + + for (int corner = 0; corner < 8; corner++) + { + float4 p = u[corner & 1] + v[(corner & 2) >> 1] + w[(corner & 4) >> 2] + mvp.c3; + p.y = -p.y; + verts[corner] = p; + } + + int vertexCount = 8; + float clipW = NearClip; + for (int i = 0; i < 12; i++) + { + var e = edges[i]; + var a = verts[e.x]; + var b = verts[e.y]; + + if ((a.w < clipW) != (b.w < clipW)) + { + var p = math.lerp(a, b, (clipW - a.w) / (b.w - a.w)); + verts[vertexCount++] = p; + } + } + + if (ProjectionType == BatchCullingProjectionType.Orthographic) + { + for (int i = 0; i < vertexCount; i++) + { + float4 p = verts[i]; + p.w = p.z; + occlusionTest.screenMin = math.min(occlusionTest.screenMin, p); + occlusionTest.screenMax = math.max(occlusionTest.screenMax, p); + } + } + else + { + for (int i = 0; i < vertexCount; i++) + { + float4 p = verts[i]; + if (p.w >= EPSILON) + { + p.xyz /= p.w; + occlusionTest.screenMin = math.min(occlusionTest.screenMin, p); + occlusionTest.screenMax = math.max(occlusionTest.screenMax, p); + } + } + } + + screenMin = math.min(screenMin, occlusionTest.screenMin); + screenMax = math.max(screenMax, occlusionTest.screenMax); + + tests[entityIndex] = occlusionTest; + } + + var combined = new ChunkOcclusionTest(); + combined.screenMin = screenMin; + combined.screenMax = screenMax; + chunk.SetChunkComponentData(ChunkOcclusionTest, combined); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs.meta new file mode 100644 index 0000000..722db14 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/ComputeBoundsJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 061fdfe4a0643d145912ec8cc920ca04 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots.meta b/Unity.Entities.Graphics/Occlusion/Masked/Dots.meta new file mode 100644 index 0000000..9a3f1c2 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c269caabbf2f57942928005151c8d275 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs new file mode 100644 index 0000000..35f3a81 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs @@ -0,0 +1,153 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Collections; +using Unity.Entities; +using Unity.Entities.Hybrid.Baking; +using UnityEngine; + +namespace Unity.Rendering.Occlusion.Masked.Dots +{ + // This is an empty tag component + [BakingType] + struct ProcessThisOccludee : IComponentData + { + } + + public class OccludeeBaker : Baker + { + public override void Bake(MeshRenderer authoring) + { + if (authoring.allowOcclusionWhenDynamic) + { + // Add the tag component, which is then picked up by our baking system + AddComponent(new ProcessThisOccludee()); + } + } + } + + [RequireMatchingQueriesForUpdate] + [WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)] + partial class AddChunkOcclusionTests : SystemBase + { + EntityQuery m_SoloQuery; + EntityQuery m_ParentQuery; + EntityQuery m_NoChunkQuery; + EntityQuery m_RemovedOccludeesQuery; + + /// + protected override void OnCreate() + { + m_SoloQuery = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + }, + None = new[] {ComponentType.ReadOnly()}, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + m_ParentQuery = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + }, + None = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + m_NoChunkQuery = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] {ComponentType.ReadOnly()}, + None = new[] {ComponentType.ReadOnly()}, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + m_RemovedOccludeesQuery = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] {ComponentType.ReadOnly()}, + None = new[] {ComponentType.ReadOnly()}, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + } + + /// + protected override void OnUpdate() + { + // By default, the `OcclusionTest` component is not cleaned up live from the entity if you remove the + // `Occludee` monobehavior from the GameObject in the subscene. + // This is why we've made `ProcessThisOccludee` a `[BakingType]` so that it isn't removed automatically in a + // Bake pass, but kept alive in the Baking-only world. + // + // Now we use this persistant tag to identify removal. This works because Bakers will automatically remove + // any components they add when the MonoBehaviour for the Baker is removed. + { + var entities = m_RemovedOccludeesQuery.ToEntityArray(Allocator.Temp); + EntityManager.RemoveComponent(entities); + for (int i = 0; i < entities.Length; i++) + { + EntityManager.RemoveChunkComponent(entities[i]); + } + entities.Dispose(); + } + + var ecb = new EntityCommandBuffer(Allocator.TempJob); + + // Solo entities have render bounds. To these entities, we attach the chunk component. No other processing + // is needed. + { + var solos = m_SoloQuery.ToEntityArray(Allocator.Temp); + for (int i = 0; i < solos.Length; i++) + { + ecb.AddComponent(solos[i], new OcclusionTest(true)); + } + solos.Dispose(); + } + + // Parent entities don't have render bounds, but during DOTS baking they might have spawned additional + // "child" entities. This can happen when the occludees have sub-meshes, In which case each sub-mesh spawns + // its own entity. + // We need to iterate through all of these child entities and add chunk components to them, if they also + // have render bounds. + var parents = m_ParentQuery.ToEntityArray(Allocator.Temp); + for (int i = 0; i < parents.Length; i++) + { + var children = EntityManager.GetBuffer(parents[i]); + for (int j = 0; j < children.Length; j++) + { + var child = children[j].Value; + if (HasComponent(child)) + { + ecb.AddComponent(child, new OcclusionTest(true)); + } + } + } + parents.Dispose(); + + ecb.Playback(EntityManager); + ecb.Dispose(); + + // Now we have added the occludee component to both, solo and parent entities. Now we add chunk components + // to all of them together. + EntityManager.AddComponent(m_NoChunkQuery, ComponentType.ChunkComponent()); + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs.meta new file mode 100644 index 0000000..18ebedb --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccludeeBaking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efc4d90ec5d67ff4c8b2ce7cfae5ca59 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs new file mode 100644 index 0000000..beca25e --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs @@ -0,0 +1,186 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) && UNITY_EDITOR + +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; +using Hash128 = UnityEngine.Hash128; +using UnityEditor; + +namespace Unity.Rendering.Occlusion.Masked.Dots +{ + public class OccluderBaker : Baker + { + public override void Bake(Occluder authoring) + { + if (IsActive() && authoring.mesh != null) + { + // This tells the baker API to create a dependency. If the referenced mesh changes, then baking will be + // re-triggered. + Mesh mesh = DependsOn(authoring.mesh); + + // Add the occluder mesh component to each submesh. This involves copying the submesh's triangle data to + // the new component. + if (mesh.subMeshCount > 1) + { + for (int i = 0; i < authoring.mesh.subMeshCount; i++) + { + Entity entity = CreateAdditionalEntity(); + AddOccluderComponent(entity, authoring, i); + } + } + else + { + Entity entity = GetEntity(authoring); + AddOccluderComponent(entity, authoring, 0); + } + + } + } + + private unsafe void AddOccluderComponent(Entity entity, Occluder occluder, int submeshIndex) + { + var component = new OcclusionMesh(); + + // Get fast zero-copy access to raw mesh data + using var meshDataArray = MeshUtility.AcquireReadOnlyMeshData(occluder.mesh); + // Since we passed in only one mesh to `Mesh.AcquireReadOnlyMeshData()`, the array is guaranteed to have + // only one mesh data. + Debug.Assert(meshDataArray.Length == 1); + var meshData = meshDataArray[0]; + + + // Each occluder component references a blob asset containing the vertex data, and another one containing + // index data. If multiple occluders in the scene have the same meshes, then we want to share their index + // and vertex data. This is why we compute two hashes. + // + // When computing the hashes, we use the raw mesh data coupled with the sub-mesh index. When creating the + // actual blob asset, we do some extra calculations like applying the sub-mesh's index offset. We skip these + // calculations when computing the hash, in the interest of speed. + Hash128 indexHash = default; + Hash128 vertexHash = default; + { + // Hash the sub-mesh index only to the index data, since the vertex buffer is the same across + // sub-meshes + HashUtilities.ComputeHash128(ref submeshIndex, ref indexHash); + + // Hash the index buffer + var indexHashPtr = (Hash128*) UnsafeUtility.AddressOf(ref indexHash); + var indices = meshData.GetIndexData(); + HashUnsafeUtilities.ComputeHash128( + indices.GetUnsafeReadOnlyPtr(), + (ulong) indices.Length, + indexHashPtr + ); + // Hash the vertex buffer + var vertexHashPtr = (Hash128*) UnsafeUtility.AddressOf(ref vertexHash); + var vertices = meshData.GetVertexData(); + HashUnsafeUtilities.ComputeHash128( + vertices.GetUnsafeReadOnlyPtr(), + (ulong) vertices.Length, + vertexHashPtr + ); + } + + SubMeshDescriptor subMesh = meshData.GetSubMesh(submeshIndex); + + // Create/get a blob asset reference with the mesh's vertices, and assign it to our new component. Submeshes + // can arbitrarily index into the mesh's vertex buffer, so instead of slowly copying out only the vertices + // that a submesh needs, we quickly copy the whole vertex buffer. + { + if (!TryGetBlobAssetReference(vertexHash, out BlobAssetReference blobAssetRef)) + { + // ^ A blob asset with the given hash doesn't already exist, so we need to add one. + var vertices = new NativeArray(meshData.vertexCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + meshData.GetVertices(vertices); + + // Vector3 arrays can be directly reinterpreted as float3 arrays. If we weren't getting a pointer, + // we could do: `NativeArray verticesFloat3s = vertices.Reinterpret();` + // The pointer makes this unnecessary. + void* verticesPtr = vertices.GetUnsafeReadOnlyPtr(); + + blobAssetRef = BlobAssetReference.Create( + verticesPtr, + sizeof(float3) * vertices.Length + ); + + vertices.Dispose(); + AddBlobAssetWithCustomHash(ref blobAssetRef, vertexHash); + } + + component.vertexCount = meshData.vertexCount; + component.vertexData = blobAssetRef; + } + + // Create a blob asset reference with the submesh's indices, and assign it to our new component + { + if (!TryGetBlobAssetReference(indexHash, out BlobAssetReference blobAssetRef)) + { + // ^ A blob asset with the given hash doesn't already exist, so we need to add one. + var indices = new NativeArray(subMesh.indexCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + meshData.GetIndices(indices, submeshIndex, false); + + blobAssetRef = BlobAssetReference.Create( + indices.GetUnsafeReadOnlyPtr(), + sizeof(int) * indices.Length + ); + + indices.Dispose(); + AddBlobAssetWithCustomHash(ref blobAssetRef, indexHash); + } + + component.indexCount = subMesh.indexCount; + component.indexData = blobAssetRef; + } + + // Create a blob asset reference to hold the transformed vertex data in our new component. This is unique + // for every occluder, since it varies with the occluder's world transform. This is why it doesn't need a + // hash check. + { + int size = UnsafeUtility.SizeOf() * component.vertexCount; + var buffer = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + component.transformedVertexData = BlobAssetReference.Create(buffer, size); + + int size_expanded_clipping = UnsafeUtility.SizeOf() * component.indexCount * 6; // multiplied by 6 as the expansion of 1 + up to 5 triangles after clipping + { + var data = Memory.Unmanaged.Allocate(size_expanded_clipping, 64, Allocator.Persistent); + component.vertex_x = BlobAssetReference.Create(data, size_expanded_clipping); + } + { + var data = Memory.Unmanaged.Allocate(size_expanded_clipping, 64, Allocator.Persistent); + component.vertex_y = BlobAssetReference.Create(data, size_expanded_clipping); + } + { + var data = Memory.Unmanaged.Allocate(size_expanded_clipping, 64, Allocator.Persistent); + component.vertex_w = BlobAssetReference.Create(data, size_expanded_clipping); + } + + int size_expanded_tri_min_max = UnsafeUtility.SizeOf() * component.indexCount / 3 * 6; + var data_min = Memory.Unmanaged.Allocate(size_expanded_tri_min_max, 64, Allocator.Persistent); + component.triangle_min = BlobAssetReference.Create(data_min, size_expanded_tri_min_max); + + var data_max = Memory.Unmanaged.Allocate(size_expanded_tri_min_max, 64, Allocator.Persistent); + component.triangle_max = BlobAssetReference.Create(data_max, size_expanded_tri_min_max); + } + + // Set the transform of the occluder + { + // Compute the full 4x4 matrix. The last row will always be (0, 0, 0, 1). We discard this row to reduce + // memory bandwidth and then reconstruct it later while transforming the occluders. + float4x4 mtx = float4x4.TRS(occluder.localPosition, occluder.localRotation, occluder.localScale); + component.localTransform = new float3x4(mtx.c0.xyz, mtx.c1.xyz, mtx.c2.xyz, mtx.c3.xyz); + } + + // Set other properties of the new component + component.screenMin = float.MaxValue; + component.screenMax = -float.MaxValue; + + // Add the component to the entity + AddComponent(entity, component); + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs.meta new file mode 100644 index 0000000..2018dfb --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderBaking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9840ed3a95ec9f141995b60497c56002 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs new file mode 100644 index 0000000..2d4cbdc --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs @@ -0,0 +1,571 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked.Dots +{ + public struct OccluderMeshComponent : IComponentData, System.IEquatable + { + public BlobAssetReference vertexData; + public BlobAssetReference indexData; + public int vertexCount; + public int indexCount; + + + public bool Equals(OccluderMeshComponent other) + { + return (vertexData.GetHashCode() == other.vertexData.GetHashCode() && indexData.GetHashCode() == other.indexData.GetHashCode()); + } + + public override int GetHashCode() + { + return vertexData.GetHashCode() ^ indexData.GetHashCode(); + } + } + + public struct OcclusionMesh : IComponentData + { + const float EPSILON = 1E-12f; + enum ClipPlanes + { + CLIP_PLANE_NONE = 0x00, + CLIP_PLANE_NEAR = 0x01, + CLIP_PLANE_LEFT = 0x02, + CLIP_PLANE_RIGHT = 0x04, + CLIP_PLANE_BOTTOM = 0x08, + CLIP_PLANE_TOP = 0x10, + CLIP_PLANE_SIDES = (CLIP_PLANE_LEFT | CLIP_PLANE_RIGHT | CLIP_PLANE_BOTTOM | CLIP_PLANE_TOP), + CLIP_PLANE_ALL = (CLIP_PLANE_LEFT | CLIP_PLANE_RIGHT | CLIP_PLANE_BOTTOM | CLIP_PLANE_TOP | CLIP_PLANE_NEAR) + }; + + unsafe public OcclusionMesh(ref OccluderMeshComponent sharedMesh, Occluder occluder) + { + // this is unused, it requires assignments for compilation but OccluderBaking.cs is where it's created + vertexCount = sharedMesh.vertexCount; + indexCount = sharedMesh.indexCount; + vertexData = sharedMesh.vertexData; + indexData = sharedMesh.indexData; + + int size = UnsafeUtility.SizeOf() * indexCount * 6; // multiplied by 6 as the expansion of 1 + up to 5 triangles after clipping + var data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + vertex_x = BlobAssetReference.Create(data, size); + + data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + vertex_y = BlobAssetReference.Create(data, size); + + data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + vertex_w = BlobAssetReference.Create(data, size); + + size = UnsafeUtility.SizeOf() * indexCount/3 * 6; + data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + triangle_min = BlobAssetReference.Create(data, size); + + data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + triangle_max = BlobAssetReference.Create(data, size); + + size = UnsafeUtility.SizeOf() * vertexCount; + data = Memory.Unmanaged.Allocate(size, 64, Allocator.Persistent); + transformedVertexData = BlobAssetReference.Create(data, size); + + expandedVertexSize = 0; + + screenMin = float.MaxValue; + screenMax = -float.MaxValue; + + // Compute the full 4x4 matrix. The last row will always be (0, 0, 0, 1). We discard this row to reduce + // memory bandwidth and then reconstruct it later while transforming the occluders. + float4x4 mtx = float4x4.TRS(occluder.localPosition, occluder.localRotation, occluder.localScale); + localTransform = new float3x4(mtx.c0.xyz, mtx.c1.xyz, mtx.c2.xyz, mtx.c3.xyz); + } + + public unsafe void Transform(float4x4 MVP, BatchCullingProjectionType projectionType, float NearClip, v128* FrustumPlanes, float HalfWidth, float HalfHeight, float PixelCenterX, float PixelCenterY) + { + + screenMin = float.MaxValue; + screenMax = -float.MaxValue; + float3* vin = (float3*)vertexData.GetUnsafePtr(); + float4* vout = (float4*)transformedVertexData.GetUnsafePtr(); + + float clipW = NearClip; + int numVertsBehindNearPlane = 0; + + expandedVertexSize = 0; + + for (int i = 0; i < vertexCount; ++i, ++vin, ++vout) + { + *vout = math.mul(MVP, new float4(*vin, 1.0f)); + vout->y = -vout->y; + + float4 p = vout->xyzw; + + if (projectionType == BatchCullingProjectionType.Orthographic) + { + p.w = p.z; + } + else + { + if (p.w < clipW) + { + numVertsBehindNearPlane++; + continue; + } + p.xyz /= p.w; + } + p.y *= -1.0f; + screenMin = math.min(screenMin, p); + screenMax = math.max(screenMax, p); + } + // if all triangles are behind the near plane we can stop it now + if (numVertsBehindNearPlane == vertexCount) + return; + + // compute the expanded data, after transforming the vertices the triangle is check if it faces the camera + vout = (float4*)transformedVertexData.GetUnsafePtr(); + int* indexPtr = (int*)indexData.GetUnsafePtr(); + + float3x3* vertices = stackalloc float3x3[6];// 1 + 5 planes triangles that can be generated + float2* max_out = (float2*)triangle_max.GetUnsafePtr(); + float2* min_out = (float2*)triangle_min.GetUnsafePtr(); + float* v_x = (float*)vertex_x.GetUnsafePtr(); + float* v_y = (float*)vertex_y.GetUnsafePtr(); + float* v_w = (float*)vertex_w.GetUnsafePtr(); + + const int singleBufferSize = 3 + 5;// 3 vertex + 5 planes = 8 maximum generated vertices per a triangle + 5 clipping planes + const int doubleBufferSize = singleBufferSize * 2; + float3* vertexClipBuffer = stackalloc float3[doubleBufferSize]; + + // Loop over three indices at a time, and clip the resulting triangle + for (int i = 0; i < indexCount; i+=3) + { + int numTriangles = 1; + // Fill out vertices[0][0..3] with the initial unclipped vertices + if (projectionType == BatchCullingProjectionType.Orthographic) + { + // Use the vertices' Z coordinate if the view is orthographic + vertices[0][0] = vout[indexPtr[i]].xyz; + vertices[0][1] = vout[indexPtr[i + 1]].xyz; + vertices[0][2] = vout[indexPtr[i + 2]].xyz; + } + else + { + // Use the vertices' W coordinate if the view is perspective + vertices[0][0] = vout[indexPtr[i]].xyw; + vertices[0][1] = vout[indexPtr[i + 1]].xyw; + vertices[0][2] = vout[indexPtr[i + 2]].xyw; + } + + // buffer group have guardBand that adds extra padding to avoid clipping triangles on the sides + // Test clipping simply checks that the projected triangles are inside the frustum exploiting + // the checks against the w or z depending if it's orthographic or perspective projection + ClippingTestResult clippingTestResult = TestClipping(vertices[0], NearClip, projectionType == BatchCullingProjectionType.Orthographic); + // If the whole triangle is outside the clipping bounds, then it is discarded entirely. We just skip to + // the next triangle. + if (clippingTestResult == ClippingTestResult.Outside) continue; + + // If the triangle is partially inside and partially outside the clipping bounds, then we turn it into a + // polygon entirely contained within the clipping bounds. + if (clippingTestResult == ClippingTestResult.Clipping) + { + numTriangles = 0; + // Load the initial vertices into the clip buffer + vertexClipBuffer[0] = vertices[0][0]; + vertexClipBuffer[1] = vertices[0][1]; + vertexClipBuffer[2] = vertices[0][2]; + + int nClippedVerts = 3; + // The vertex clip buffer is a double buffer, which means that we use half of the buffer for reading + // and the other half for writing. The `bufferSwap` variable controls which half is the read and + // which half is written. After each plane is processed, we toggle it between 0 and 1, i.e. we flip + // the swapchain. + int bufferSwap = 0; + for (int n = 0; n < 5; ++n)//clipping 5 planes + { + // Sutherland-Hodgman polygon clipping algorithm https://mikro.naprvyraz.sk/docs/Coding/2/FRUSTUM.TXT + // swapping buffers from input output, the algorithm works by clipping every plane once at a time + float3* outVtx = vertexClipBuffer + ((bufferSwap ^ 1) * singleBufferSize); + float3* inVtx = vertexClipBuffer + (bufferSwap * singleBufferSize); + float4 plane = new float4(FrustumPlanes[n].Float0, FrustumPlanes[n].Float1, FrustumPlanes[n].Float2, FrustumPlanes[n].Float3); + nClippedVerts = ClipPolygon(outVtx, inVtx, plane, nClippedVerts); + // Toggle between 0 and 1 + bufferSwap ^= 1; + } + // nClippedVerts can be lower than 3 and that's why numTriangles is 0 as default, + // from there for every extra vertex a triangle it's added as a fan + /* + * x + * |\ + * | \ + * | \ + * | \ + * x-_ \ + * `-_\ <------if the clipping plane is here it will add 2 vertex producing + * `x + * + * x + * |\ + * | \ + * | \ + * | \ <------X are the new added vertices and the final result will be + * x-_ \ + * `X_X + * `x + * + * x + * |\ + * |.\ + * | :\ + * | \ \ <------X are the new added vertices and the final result will be the extra edges cutting from the the first vertex to the new ones and between the added ones + * x-_\ \ + * `X-X + * + */ + if (nClippedVerts >= 3) + { + // Copy over the clipped vertices to the result array + vertices[0][0] = vertexClipBuffer[bufferSwap * singleBufferSize + 0]; + vertices[0][1] = vertexClipBuffer[bufferSwap * singleBufferSize + 1]; + vertices[0][2] = vertexClipBuffer[bufferSwap * singleBufferSize + 2]; + + numTriangles++; + + for (int n = 2; n < nClippedVerts - 1; n++) + { + // ^ If we have more than 3 vertices after clipping, create a triangle-fan, with the 0th + // vertex shared across all triangles of the fan. + vertices[numTriangles][0] = vertexClipBuffer[bufferSwap * singleBufferSize]; + vertices[numTriangles][1] = vertexClipBuffer[bufferSwap * singleBufferSize + n]; + vertices[numTriangles][2] = vertexClipBuffer[bufferSwap * singleBufferSize + n + 1]; + numTriangles++; + } + } + + } + + for(int n = 0; n < numTriangles; ++n) + { + int2x3 intHomogeneousVertices = int2x3.zero; + float2x3 homogeneousVertices = float2x3.zero; + float2 halfRes = new float2(HalfWidth, HalfHeight); + float2 pixelCenter = new float2(PixelCenterX, PixelCenterY); + if (projectionType == BatchCullingProjectionType.Orthographic) + { + homogeneousVertices[0] = (vertices[0][0].xy * halfRes) + pixelCenter; + homogeneousVertices[1] = (vertices[0][1].xy * halfRes) + pixelCenter; + homogeneousVertices[2] = (vertices[0][2].xy * halfRes) + pixelCenter; + + homogeneousVertices[0] *= (float)(1 << BufferGroup.FpBits); + homogeneousVertices[1] *= (float)(1 << BufferGroup.FpBits); + homogeneousVertices[2] *= (float)(1 << BufferGroup.FpBits); + + intHomogeneousVertices.c0.x = (int)math.round(homogeneousVertices.c0.x); + intHomogeneousVertices.c1.x = (int)math.round(homogeneousVertices.c1.x); + intHomogeneousVertices.c2.x = (int)math.round(homogeneousVertices.c2.x); + intHomogeneousVertices.c0.y = (int)math.round(homogeneousVertices.c0.y); + intHomogeneousVertices.c1.y = (int)math.round(homogeneousVertices.c1.y); + intHomogeneousVertices.c2.y = (int)math.round(homogeneousVertices.c2.y); + + homogeneousVertices = new float2x3(intHomogeneousVertices) / (float)(1 << BufferGroup.FpBits); + } + else + { + homogeneousVertices[0] = (vertices[0][0].xy * halfRes) / vertices[0][0].z + pixelCenter; + homogeneousVertices[1] = (vertices[0][1].xy * halfRes) / vertices[0][1].z + pixelCenter; + homogeneousVertices[2] = (vertices[0][2].xy * halfRes) / vertices[0][2].z + pixelCenter; + + homogeneousVertices[0] *= (float)(1 << BufferGroup.FpBits); + homogeneousVertices[1] *= (float)(1 << BufferGroup.FpBits); + homogeneousVertices[2] *= (float)(1 << BufferGroup.FpBits); + + intHomogeneousVertices.c0.x = (int)math.round(homogeneousVertices.c0.x); + intHomogeneousVertices.c1.x = (int)math.round(homogeneousVertices.c1.x); + intHomogeneousVertices.c2.x = (int)math.round(homogeneousVertices.c2.x); + intHomogeneousVertices.c0.y = (int)math.round(homogeneousVertices.c0.y); + intHomogeneousVertices.c1.y = (int)math.round(homogeneousVertices.c1.y); + intHomogeneousVertices.c2.y = (int)math.round(homogeneousVertices.c2.y); + + homogeneousVertices = new float2x3(intHomogeneousVertices) / (float)(1 << BufferGroup.FpBits); + } + + // Checkin determinant > 0 more details in: + // Triangle Scan Conversion using 2D Homogeneous Coordinates + // https://www.cs.cmu.edu/afs/cs/academic/class/15869-f11/www/readings/olano97_homogeneous.pdf + // Section 5.2 + // In 3D, a matrix determinant gives twice the signed volume of a tetrahedron.In + // eye space, this is the tetrahedron with the eye at the apex and the + // triangle to be rendered as the base.If all of the 2D w coordinates + // are 1, the determinant is also exactly twice the signed screenspace aren of the triangle. + // If the determinant is zero, either the triangle is degenerate or the view is edge - on. + float area = (homogeneousVertices.c1.x - homogeneousVertices.c2.x) * (homogeneousVertices.c0.y - homogeneousVertices.c2.y) + - (homogeneousVertices.c2.x - homogeneousVertices.c0.x) * (homogeneousVertices.c2.y - homogeneousVertices.c1.y); + + if (area > EPSILON) + { + if (projectionType == BatchCullingProjectionType.Orthographic) + { + homogeneousVertices[0] = vertices[n][0].xy; + homogeneousVertices[1] = vertices[n][1].xy; + homogeneousVertices[2] = vertices[n][2].xy; + } + else + { + homogeneousVertices[0] = vertices[n][0].xy / vertices[n][0].z; + homogeneousVertices[1] = vertices[n][1].xy / vertices[n][1].z; + homogeneousVertices[2] = vertices[n][2].xy / vertices[n][2].z; + } + homogeneousVertices[0].y *= -1.0f; + homogeneousVertices[1].y *= -1.0f; + homogeneousVertices[2].y *= -1.0f; + + max_out[expandedVertexSize/3] = math.max(math.max(homogeneousVertices[0], homogeneousVertices[1]), homogeneousVertices[2]); + min_out[expandedVertexSize/3] = math.min(math.min(homogeneousVertices[0], homogeneousVertices[1]), homogeneousVertices[2]); + // Copy final vertex data into output arrays + for (int m = 0; m < 3; ++m) + { + v_x[expandedVertexSize] = vertices[n][m].x; + v_y[expandedVertexSize] = vertices[n][m].y; + v_w[expandedVertexSize] = vertices[n][m].z; + expandedVertexSize++; + } + } + } + } + // If fully visible we can skip computing the screen aabb again but with the extra clipping logic + if (numVertsBehindNearPlane == 0 && !(screenMin.x > 1.0f || screenMin.y > 1.0f || screenMax.x < -1.0f || screenMax.y < -1.0f)) + return; + + if (numVertsBehindNearPlane == 0 || numVertsBehindNearPlane == vertexCount)// if triangles does not cross near plane we can skip the slow path too + return; + + var edges = stackalloc int2[] + { + new int2(0,1), new int2(1,3), new int2(3,2), new int2(2,0), + new int2(4,6), new int2(6,7), new int2(7,5), new int2(5,4), + new int2(4,0), new int2(2,6), new int2(1,5), new int2(7,3) + }; + + float3 minAABB, maxAABB; + maxAABB.x = maxAABB.y = maxAABB.z = -float.MaxValue; + minAABB.x = minAABB.y = minAABB.z = float.MaxValue; + vin = (float3*)vertexData.GetUnsafePtr(); + + for (int i = 0; i < vertexCount; ++i, ++vin) + { + float3 p = vin->xyz; + + minAABB = math.min(minAABB, p); + maxAABB = math.max(maxAABB, p); + } + + AABB aabb; + aabb.Center = (maxAABB + minAABB) * 0.5f; + aabb.Extents = (maxAABB - minAABB) * 0.5f; + + var verts = stackalloc float4[16]; + + float4x2 u = new float4x2(MVP.c0 * aabb.Min.x, MVP.c0 * aabb.Max.x); + float4x2 v = new float4x2(MVP.c1 * aabb.Min.y, MVP.c1 * aabb.Max.y); + float4x2 w = new float4x2(MVP.c2 * aabb.Min.z, MVP.c2 * aabb.Max.z); + + for (int corner = 0; corner < 8; corner++) + { + float4 p = u[corner & 1] + v[(corner & 2) >> 1] + w[(corner & 4) >> 2] + MVP.c3; + p.y = -p.y; + verts[corner] = p; + } + + int internalVertexCount = 8; + for (int i = 0; i < 12; i++) + { + var e = edges[i]; + var a = verts[e.x]; + var b = verts[e.y]; + + if ((a.w < clipW) != (b.w < clipW)) + { + var p = math.lerp(a, b, (clipW - a.w) / (b.w - a.w)); + verts[internalVertexCount++] = p; + } + } + + for (int i = 0; i < internalVertexCount; i++) + { + float4 p = verts[i]; + + if (projectionType == BatchCullingProjectionType.Orthographic) + { + p.w = p.z; + } + else + { + if (p.w < EPSILON) + continue; + + p.xyz /= p.w; + } + p.y *= -1.0f; + screenMin = math.min(screenMin, p); + screenMax = math.max(screenMax, p); + } + } + + enum ClippingTestResult + { + Inside = 0, + Clipping, + Outside + }; + unsafe ClippingTestResult TestClipping(float3x3 vertices, float NearClip, bool isOrtho) + { + + ClippingTestResult* straddleMask = stackalloc ClippingTestResult[5]; + straddleMask[0] = TestClipPlane(ClipPlanes.CLIP_PLANE_NEAR, vertices, NearClip, isOrtho); + straddleMask[1] = TestClipPlane(ClipPlanes.CLIP_PLANE_LEFT, vertices, NearClip, isOrtho); + straddleMask[2] = TestClipPlane(ClipPlanes.CLIP_PLANE_RIGHT, vertices, NearClip, isOrtho); + straddleMask[3] = TestClipPlane(ClipPlanes.CLIP_PLANE_BOTTOM, vertices, NearClip, isOrtho); + straddleMask[4] = TestClipPlane(ClipPlanes.CLIP_PLANE_TOP, vertices, NearClip, isOrtho); + + if( straddleMask[0] == ClippingTestResult.Inside && + straddleMask[1] == ClippingTestResult.Inside && + straddleMask[2] == ClippingTestResult.Inside && + straddleMask[3] == ClippingTestResult.Inside && + straddleMask[4] == ClippingTestResult.Inside) + { + return ClippingTestResult.Inside; + } + if (straddleMask[0] == ClippingTestResult.Outside || + straddleMask[1] == ClippingTestResult.Outside || + straddleMask[2] == ClippingTestResult.Outside || + straddleMask[3] == ClippingTestResult.Outside || + straddleMask[4] == ClippingTestResult.Outside) + { + return ClippingTestResult.Outside; + } + return ClippingTestResult.Clipping; + } + + // This function checks whether and how a triangle intersects a clipping plane. + // The clipping plane divides 3D space into two parts - one being inside the frustum and one being outside. This + // function returns whether the input triangle is fully on the inside, fully on the ouside, or a bit on both + // sides, i.e. clipping the plane. + unsafe ClippingTestResult TestClipPlane(ClipPlanes clipPlane, float3x3 vertices, float NearClip, bool isOrtho) + { + // Evaluate all 3 vertices against the frustum plane + float* planeDp = stackalloc float[3]; + + if (!isOrtho) + { + for (int i = 0; i < 3; ++i) + { + switch (clipPlane) + { + case ClipPlanes.CLIP_PLANE_LEFT: planeDp[i] = vertices[i].z + vertices[i].x; break; + case ClipPlanes.CLIP_PLANE_RIGHT: planeDp[i] = vertices[i].z - vertices[i].x; break; + case ClipPlanes.CLIP_PLANE_BOTTOM: planeDp[i] = vertices[i].z + vertices[i].y; break; + case ClipPlanes.CLIP_PLANE_TOP: planeDp[i] = vertices[i].z - vertices[i].y; break; + case ClipPlanes.CLIP_PLANE_NEAR: planeDp[i] = vertices[i].z - NearClip; break; + } + } + } + else + { + for (int i = 0; i < 3; ++i) + { + switch (clipPlane) + { + case ClipPlanes.CLIP_PLANE_LEFT: planeDp[i] = 1.0f + vertices[i].x; break; + case ClipPlanes.CLIP_PLANE_RIGHT: planeDp[i] = 1.0f - vertices[i].x; break; + case ClipPlanes.CLIP_PLANE_BOTTOM: planeDp[i] = 1.0f + vertices[i].y; break; + case ClipPlanes.CLIP_PLANE_TOP: planeDp[i] = 1.0f - vertices[i].y; break; + case ClipPlanes.CLIP_PLANE_NEAR: planeDp[i] = 1.0f + NearClip; break; + } + } + } + if (planeDp[0] > EPSILON && planeDp[1] > EPSILON && planeDp[2] > EPSILON) + { + return ClippingTestResult.Inside; + } + + if (planeDp[0] <= EPSILON && planeDp[1] <= EPSILON && planeDp[2] <= EPSILON) + { + return ClippingTestResult.Outside; + } + return ClippingTestResult.Clipping; + } + + unsafe int ClipPolygon(float3* outVtx, float3* inVtx, float4 plane, int n) + { + float3 p0 = inVtx[n - 1]; + float dist0 = math.dot(p0, plane.xyz) + plane.w; + + // Loop over all polygon edges and compute intersection with clip plane (if any) + int nout = 0; + + for (int k = 0; k < n; k++) + { + float3 p1 = inVtx[k]; + float dist1 = math.dot(p1, plane.xyz) + plane.w; + + if (dist0 > EPSILON) + { + outVtx[nout++] = p0; + } + + // Edge intersects the clip plane if dist0 and dist1 have opposing signs + if (math.sign(dist0) != math.sign(dist1)) + { + // Always clip from the positive side to avoid T-junctions + if (dist0 > EPSILON) + { + float t = dist0 / (dist0 - dist1); + outVtx[nout++] = t * (p1 - p0) + p0; + } + else + { + float t = dist1 / (dist1 - dist0); + outVtx[nout++] = t * (p0 - p1) + p1; + } + } + + dist0 = dist1; + p0 = p1; + } + + return nout; + } + + public int vertexCount; + public int indexCount; + + public BlobAssetReference transformedVertexData; + public BlobAssetReference vertexData; + public BlobAssetReference indexData; + + public BlobAssetReference vertex_x; + public BlobAssetReference vertex_y; + public BlobAssetReference vertex_w; + // Triangle min max contains the screen space aabb of the triangles + // to not recompute it for each bin in the rasterize job + // Used to classify triangles by bins + public BlobAssetReference triangle_min; + public BlobAssetReference triangle_max; + + // If a triangle intersects the frustum, it needs to be clipped. The + // process of clipping converts the triangle into a non-triangular + // polygon with more vertices. Up to 5 new vertices can be added in this + // process, depending on how the triangle intersects the frustum. We + // allocate a large enough vertex buffer to be able to fit all the + // vertices, and we track how many vertices are actually generated after + // clipping in this variable. + public int expandedVertexSize; + + public float4 screenMin, screenMax; + public float3x4 localTransform; + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs.meta new file mode 100644 index 0000000..f45c84d --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OccluderMesh.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f985747e07da954b9a4050ef16297e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs new file mode 100644 index 0000000..fd545c4 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs @@ -0,0 +1,31 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Transforms; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked.Dots +{ + public struct OcclusionTest : IComponentData + { + public OcclusionTest(bool enabled) + { + this.enabled = enabled; + screenMin = float.MaxValue; + screenMax = -float.MaxValue; + } + + // this flag is for toggling occlusion testing without having to add a component at runtime. + public bool enabled; + public float4 screenMin, screenMax; + } + + public struct ChunkOcclusionTest : IComponentData + { + public float4 screenMin, screenMax; + } +} +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs.meta new file mode 100644 index 0000000..b45c6e7 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Dots/OcclusionTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e45b6693f020abc498dfd0ed6757d71a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs b/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs new file mode 100644 index 0000000..931e5ee --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs @@ -0,0 +1,160 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System.Runtime.CompilerServices; +using Unity.Burst.Intrinsics; +using Unity.Mathematics; + + +namespace Unity.Rendering.Occlusion.Masked +{ + public static class IntrinsicUtils + { + + // naive approach, works with C# reference implementation + + // read access + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int getIntLane(v128 vector, uint laneIdx) + { + //Debug.Assert(laneIdx >= 0 && laneIdx < 4); + + // eat the modulo cost to not let it overflow + switch (laneIdx % 4) + { + default: // DS: incorrect, but works with modulo and silences compiler (CS0161) + case 0: { return vector.SInt0; } + case 1: { return vector.SInt1; } + case 2: { return vector.SInt2; } + case 3: { return vector.SInt3; } + } + } + + // used for "write" access (returns copy, requires assignment afterwards) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 getCopyWithIntLane(v128 vector, uint laneIdx, int laneVal) + { + //Debug.Assert(laneIdx >= 0 && laneIdx < 4); + + // eat the modulo cost to not let it overflow + switch (laneIdx % 4) + { + default: // DS: incorrect fallthrough, but works with modulo and silences compiler (CS0161) + case 0: { vector.SInt0 = laneVal; break; } + case 1: { vector.SInt1 = laneVal; break; } + case 2: { vector.SInt2 = laneVal; break; } + case 3: { vector.SInt3 = laneVal; break; } + } + + return vector; + } + + // read access + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float getFloatLane(v128 vector, uint laneIdx) + { + //Debug.Assert(laneIdx >= 0 && laneIdx < 4); + + // eat the modulo cost to not let it overflow + switch (laneIdx % 4) + { + default: // DS: incorrect fallthrough, but works with modulo and silences compiler (CS0161) + case 0: { return vector.Float0; } + case 1: { return vector.Float1; } + case 2: { return vector.Float2; } + case 3: { return vector.Float3; } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_fmadd_ps(v128 a, v128 b, v128 c) { return X86.Sse.add_ps(X86.Sse.mul_ps(a, b), c); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_fmsub_ps(v128 a, v128 b, v128 c) { return X86.Sse.sub_ps(X86.Sse.mul_ps(a, b), c); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_neg_ps(v128 a) { return X86.Sse.xor_ps((a), X86.Sse.set1_ps(-0f)); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_neg_epi32(v128 a) { return X86.Sse2.sub_epi32(X86.Sse2.set1_epi32(0), (a)); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_not_epi32(v128 a) { return X86.Sse2.xor_si128((a), X86.Sse2.set1_epi32(~0)); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_abs_ps(v128 a) { return X86.Sse.and_ps((a), X86.Sse2.set1_epi32(0x7FFFFFFF)); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_or_epi32(v128 a, v128 b) { return X86.Sse2.or_si128(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_andnot_epi32(v128 a, v128 b) { return X86.Sse2.andnot_si128(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_mullo_epi32(v128 a, v128 b) { return X86.Sse4_1.mullo_epi32(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_min_epi32(v128 a, v128 b) { return X86.Sse4_1.min_epi32(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_max_epi32(v128 a, v128 b) { return X86.Sse4_1.max_epi32(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_abs_epi32(v128 a) { return X86.Ssse3.abs_epi32(a); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_blendv_ps(v128 a, v128 b, v128 c) { return X86.Sse4_1.blendv_ps(a, b, c); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int _mmw_testz_epi32(v128 a, v128 b) { return X86.Sse4_1.testz_si128(a, b); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmx_dp4_ps(v128 a, v128 b) { return X86.Sse4_1.dp_ps(a, b, 0xFF); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_floor_ps(v128 a) { return X86.Sse4_1.round_ps(a, (int)X86.RoundingMode.FROUND_FLOOR_NOEXC); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_ceil_ps(v128 a) { return X86.Sse4_1.round_ps(a, (int)X86.RoundingMode.FROUND_CEIL_NOEXC); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_transpose_epi8(v128 a) + { + v128 shuff = X86.Sse2.setr_epi8(0x0, 0x4, 0x8, 0xC, 0x1, 0x5, 0x9, 0xD, 0x2, 0x6, 0xA, 0xE, 0x3, 0x7, 0xB, 0xF); + return X86.Ssse3.shuffle_epi8(a, shuff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static v128 _mmw_sllv_ones(v128 ishift) + { + v128 shift = X86.Sse4_1.min_epi32(ishift, X86.Sse2.set1_epi32(32)); + + // Uses lookup tables and _mm_shuffle_epi8 to perform _mm_sllv_epi32(~0, shift) + v128 byteShiftLUT; + unchecked + { + byteShiftLUT = X86.Sse2.setr_epi8((sbyte)0xFF, (sbyte)0xFE, (sbyte)0xFC, (sbyte)0xF8, (sbyte)0xF0, (sbyte)0xE0, (sbyte)0xC0, (sbyte)0x80, 0, 0, 0, 0, 0, 0, 0, 0); + } + v128 byteShiftOffset = X86.Sse2.setr_epi8(0, 8, 16, 24, 0, 8, 16, 24, 0, 8, 16, 24, 0, 8, 16, 24); + v128 byteShiftShuffle = X86.Sse2.setr_epi8(0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, 0x4, 0x8, 0x8, 0x8, 0x8, 0xC, 0xC, 0xC, 0xC); + + v128 byteShift = X86.Ssse3.shuffle_epi8(shift, byteShiftShuffle); + + // DS: TODO: change once we get Burst fix for X86.Sse2.set1_epi8() + const sbyte val = 8; + byteShift = X86.Sse4_1.min_epi8(X86.Sse2.subs_epu8(byteShift, byteShiftOffset), new v128(val) /*X86.Sse2.set1_epi8(8)*/); + + v128 retMask = X86.Ssse3.shuffle_epi8(byteShiftLUT, byteShift); + + return retMask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong find_clear_lsb(ref uint mask) + { + ulong idx = (ulong)math.tzcnt(mask); + mask &= mask - 1; + return idx; + } + } +} +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs.meta new file mode 100644 index 0000000..204f631 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/IntrinsicUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8265ed83b7038a74591a6f69d8d8f49c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs new file mode 100644 index 0000000..b9b2d8a --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs @@ -0,0 +1,180 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System.Runtime.CompilerServices; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile] + public unsafe struct MergeJob : IJobFor + { + [ReadOnly] public int NumTilesPerBuffer; + [ReadOnly] public int NumTilesPerJob; + [ReadOnly] public int NumBuffers; + + [NativeDisableUnsafePtrRestriction] public Tile* TilesBasePtr; + + public void Execute(int i) + { + // The destination buffer is the zero-th buffer + Tile* dstTiles = TilesBasePtr; + // The source buffer is the (+ 1)th buffer + for (int j = 1; j < NumBuffers; j++) + { + Tile* srcTiles = &TilesBasePtr[j * NumTilesPerBuffer]; + MergeTile(srcTiles, dstTiles, i * NumTilesPerJob, NumTilesPerJob); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + v128 _mmw_not_epi32(v128 a) { return X86.Sse2.xor_si128((a), X86.Sse2.set1_epi32(~0)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + int _mmw_testz_epi32(v128 a, v128 b) { return X86.Sse4_1.testz_si128(a, b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + v128 _mmw_or_epi32(v128 a, v128 b) { return X86.Sse2.or_si128(a, b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + v128 _mmw_blendv_ps(v128 a, v128 b, v128 c) { return X86.Sse4_1.blendv_ps(a, b, c); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + v128 _mmw_abs_ps(v128 a) { return X86.Sse.and_ps((a), X86.Sse2.set1_epi32(0x7FFFFFFF)); } + + void MergeTile(Tile* srcTiles, Tile* dstTiles, int startTileIdx, int numTiles) + { + for (int i = startTileIdx; i < startTileIdx + numTiles; i++) + { + v128* zMinB_0 = &(srcTiles[i].zMin0); + v128* zMinB_1 = &(srcTiles[i].zMin1); + + // Clear z0 to beyond infinity to ensure we never merge with clear data + v128 sign1 = X86.Sse2.srai_epi32(dstTiles[i].zMin0, 31); + // Only merge tiles that have data in zMinB[0], use the sign bit to determine if they are still in a clear state + sign1 = X86.Sse2.cmpeq_epi32(sign1, X86.Sse2.setzero_si128()); + + // Set 32bit value to -1 if any pixels are set incide the coverage mask for a subtile + v128 liveTile1 = X86.Sse2.cmpeq_epi32(dstTiles[i].mask, X86.Sse2.setzero_si128()); + // invert to have bits set for clear subtiles + v128 t1inv = _mmw_not_epi32(liveTile1); + // VPTEST sets the ZF flag if all the resulting bits are 0 (ie if all tiles are clear) + if (_mmw_testz_epi32(sign1, sign1) != 0 && _mmw_testz_epi32(t1inv, t1inv) != 0) + { + dstTiles[i].mask = srcTiles[i].mask; + dstTiles[i].zMin0 = *zMinB_0; + dstTiles[i].zMin1 = *zMinB_1; + } + else + { + // Clear z0 to beyond infinity to ensure we never merge with clear data + v128 sign0 = X86.Sse2.srai_epi32(*zMinB_0, 31); + sign0 = X86.Sse2.cmpeq_epi32(sign0, X86.Sse2.setzero_si128()); + // Only merge tiles that have data in zMinB[0], use the sign bit to determine if they are still in a clear state + if (_mmw_testz_epi32(sign0, sign0) == 0) + { + // build a mask for Zmin[0], full if the layer has been completed, or partial if tile is still partly filled. + // cant just use the completement of the mask, as tiles might not get updated by merge + v128 sign_1 = X86.Sse2.srai_epi32(*zMinB_1, 31); + v128 LayerMask0 = _mmw_not_epi32(sign_1); + v128 LayerMask1 = _mmw_not_epi32(srcTiles[i].mask); + v128 rastMask = _mmw_or_epi32(LayerMask0, LayerMask1); + + UpdateTileAccurate(dstTiles, i, rastMask, *zMinB_0); + } + + // Set 32bit value to -1 if any pixels are set incide the coverage mask for a subtile + v128 LiveTile = X86.Sse2.cmpeq_epi32(srcTiles[i].mask, X86.Sse2.setzero_si128()); + // invert to have bits set for clear subtiles + v128 t0inv = _mmw_not_epi32(LiveTile); + // VPTEST sets the ZF flag if all the resulting bits are 0 (ie if all tiles are clear) + if (_mmw_testz_epi32(t0inv, t0inv) == 0) + { + UpdateTileAccurate(dstTiles, i, srcTiles[i].mask, *zMinB_1); + } + } + } + } + + void UpdateTileAccurate(Tile* dstTiles, int tileIdx, v128 coverage, v128 zTriv) + { + v128 zMin0 = dstTiles[tileIdx].zMin0; + v128 zMin1 = dstTiles[tileIdx].zMin1; + v128 mask = dstTiles[tileIdx].mask; + + // Swizzle coverage mask to 8x4 subtiles + v128 rastMask = coverage; + + // Perform individual depth tests with layer 0 & 1 and mask out all failing pixels + v128 sdist0 = X86.Sse.sub_ps(zMin0, zTriv); + v128 sdist1 = X86.Sse.sub_ps(zMin1, zTriv); + v128 sign0 = X86.Sse2.srai_epi32(sdist0, 31); + v128 sign1 = X86.Sse2.srai_epi32(sdist1, 31); + v128 triMask = X86.Sse2.and_si128(rastMask, X86.Sse2.or_si128(X86.Sse2.andnot_si128(mask, sign0), X86.Sse2.and_si128(mask, sign1))); + + // Early out if no pixels survived the depth test (this test is more accurate than + // the early culling test in TraverseScanline()) + v128 t0 = X86.Sse2.cmpeq_epi32(triMask, X86.Sse2.setzero_si128()); + v128 t0inv = _mmw_not_epi32(t0); + + if (_mmw_testz_epi32(t0inv, t0inv) != 0) + { + return; + } + +#if MOC_ENABLE_STATS + STATS_ADD(ref mStats.mOccluders.mNumTilesUpdated, 1); +#endif + + v128 zTri = _mmw_blendv_ps(zTriv, zMin0, t0); + + // Test if incoming triangle completely overwrites layer 0 or 1 + v128 layerMask0 = X86.Sse2.andnot_si128(triMask, _mmw_not_epi32(mask)); + v128 layerMask1 = X86.Sse2.andnot_si128(triMask, mask); + v128 lm0 = X86.Sse2.cmpeq_epi32(layerMask0, X86.Sse2.setzero_si128()); + v128 lm1 = X86.Sse2.cmpeq_epi32(layerMask1, X86.Sse2.setzero_si128()); + v128 z0 = _mmw_blendv_ps(zMin0, zTri, lm0); + v128 z1 = _mmw_blendv_ps(zMin1, zTri, lm1); + + // Compute distances used for merging heuristic + v128 d0 = _mmw_abs_ps(sdist0); + v128 d1 = _mmw_abs_ps(sdist1); + v128 d2 = _mmw_abs_ps(X86.Sse.sub_ps(z0, z1)); + + // Find minimum distance + v128 c01 = X86.Sse.sub_ps(d0, d1); + v128 c02 = X86.Sse.sub_ps(d0, d2); + v128 c12 = X86.Sse.sub_ps(d1, d2); + // Two tests indicating which layer the incoming triangle will merge with or + // overwrite. d0min indicates that the triangle will overwrite layer 0, and + // d1min flags that the triangle will overwrite layer 1. + v128 d0min = X86.Sse2.or_si128(X86.Sse2.and_si128(c01, c02), X86.Sse2.or_si128(lm0, t0)); + v128 d1min = X86.Sse2.andnot_si128(d0min, X86.Sse2.or_si128(c12, lm1)); + + /////////////////////////////////////////////////////////////////////////////// + // Update depth buffer entry. NOTE: we always merge into layer 0, so if the + // triangle should be merged with layer 1, we first swap layer 0 & 1 and then + // merge into layer 0. + /////////////////////////////////////////////////////////////////////////////// + + // Update mask based on which layer the triangle overwrites or was merged into + v128 inner = _mmw_blendv_ps(triMask, layerMask1, d0min); + + // Update the zMin[0] value. There are four outcomes: overwrite with layer 1, + // merge with layer 1, merge with zTri or overwrite with layer 1 and then merge + // with zTri. + v128 e0 = _mmw_blendv_ps(z0, z1, d1min); + v128 e1 = _mmw_blendv_ps(z1, zTri, X86.Sse2.or_si128(d1min, d0min)); + + // Update the zMin[1] value. There are three outcomes: keep current value, + // overwrite with zTri, or overwrite with z1 + v128 z1t = _mmw_blendv_ps(zTri, z1, d0min); + + dstTiles[tileIdx].zMin0 = X86.Sse.min_ps(e0, e1); + dstTiles[tileIdx].zMin1 = _mmw_blendv_ps(z1t, z0, d1min); + dstTiles[tileIdx].mask = _mmw_blendv_ps(inner, layerMask0, d1min); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs.meta new file mode 100644 index 0000000..2649838 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/MergeJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 98ea43e053a4d26498c4d7167aaefc12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs new file mode 100644 index 0000000..1fc63e3 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs @@ -0,0 +1,48 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Rendering.Occlusion.Masked.Dots; +using Unity.Transforms; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile] + public unsafe struct MeshTransformJob: IJobFor + { + [ReadOnly] public float4x4 ViewProjection; + [ReadOnly] public BatchCullingProjectionType ProjectionType; + [ReadOnly] public float NearClip; + [ReadOnly, NativeDisableUnsafePtrRestriction] public v128* FrustumPlanes; + [ReadOnly] public v128 HalfWidth; + [ReadOnly] public v128 HalfHeight; + [ReadOnly] public v128 PixelCenterX; + [ReadOnly] public v128 PixelCenterY; + [ReadOnly] public NativeArray LocalToWorlds; + + public NativeArray Meshes; + public void Execute(int i) + { + var mesh = Meshes[i]; + // We discarded the last row of the occluder matrix to save memory bandwidth because it is always (0, 0, 0,1). + // However, to perform the actual math, we still need a 4x4 matrix. So we reintroduce the row here. + float4x4 occluderMtx = new float4x4( + new float4(mesh.localTransform.c0, 0f), + new float4(mesh.localTransform.c1, 0f), + new float4(mesh.localTransform.c2, 0f), + new float4(mesh.localTransform.c3, 1f) + ); + float4x4 mvp = math.mul(ViewProjection, math.mul(LocalToWorlds[i].Value, occluderMtx)); + mesh.Transform(mvp, ProjectionType, NearClip, FrustumPlanes, HalfWidth.Float0, HalfHeight.Float0, PixelCenterX.Float0, PixelCenterY.Float0); + + Meshes[i] = mesh; + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs.meta new file mode 100644 index 0000000..fe906d2 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/MeshTransformJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3359e8ac2abeeab4e89cef513b264a3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs new file mode 100644 index 0000000..4800c25 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs @@ -0,0 +1,955 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System.Runtime.CompilerServices; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using Unity.Rendering.Occlusion.Masked.Dots; +using UnityEngine.Rendering; +using System.Collections.Generic; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile(DisableSafetyChecks = true)] + public unsafe struct RasterizeJob : IJobFor + { + [ReadOnly] public NativeArray Meshes; + [ReadOnly] public BatchCullingProjectionType ProjectionType; + [ReadOnly] public int NumBuffers; + [ReadOnly] public v128 HalfWidth; + [ReadOnly] public v128 HalfHeight; + [ReadOnly] public v128 PixelCenterX; + [ReadOnly] public v128 PixelCenterY; + [ReadOnly] public v128 PixelCenter; + [ReadOnly] public v128 HalfSize; + [ReadOnly] public v128 ScreenSize; + [ReadOnly] public int NumPixelsX; + [ReadOnly] public int NumPixelsY; + [ReadOnly] public int NumTilesX; + [ReadOnly] public int NumTilesY; + [ReadOnly] public float NearClip; + [ReadOnly, NativeDisableUnsafePtrRestriction] public v128* FrustumPlanes; + [ReadOnly] public ScissorRect FullScreenScissor; + + // A bin is a screen area formed by X * Y tiles, a tile is the minimum pixels that we + // process in the system + [ReadOnly] public int TilesPerBinX; + [ReadOnly] public int TilesPerBinY; + // A buffer group contains a bunch of contiguous tile-buffers. This pointer points to the base of the one we're + // rendering to. + [NativeDisableUnsafePtrRestriction] public Tile* TilesBasePtr; + + const int MAX_CLIPPED = 32; + const int SIMD_LANES = 4; + const int SIMD_ALL_LANES_MASK = (1 << SIMD_LANES) - 1; + const int BIG_TRIANGLE = 3; + + public void Execute(int i) + { + var tiles = &TilesBasePtr[0]; + ScissorRect scissorRect = new ScissorRect(); + + int2 pixelsPerTile = new int2(NumPixelsX / NumTilesX, NumPixelsY / NumTilesY); + + const int binSize = 1024*3; + float* temp_stack_x = stackalloc float[12]; + float* temp_stack_y = stackalloc float[12]; + float* temp_stack_w = stackalloc float[12]; + int tempStackSize = 0; + NativeArray binTriangleX = new NativeArray(binSize, Allocator.Temp); + NativeArray binTriangleY = new NativeArray(binSize, Allocator.Temp); + NativeArray binTriangleW = new NativeArray(binSize, Allocator.Temp); + + int countOfTilesX = NumTilesX / TilesPerBinX; + scissorRect.mMinX = (i % countOfTilesX) * pixelsPerTile.x * TilesPerBinX; + scissorRect.mMaxX = scissorRect.mMinX + pixelsPerTile.x * TilesPerBinX; + scissorRect.mMinY = (i / countOfTilesX) * pixelsPerTile.y * TilesPerBinY; + scissorRect.mMaxY = scissorRect.mMinY + pixelsPerTile.y * TilesPerBinY; + + float4 clipRect = new float4(scissorRect.mMinX, scissorRect.mMinY, scissorRect.mMaxX, scissorRect.mMaxY); + clipRect = (2 * clipRect.xyzw / (new float2(NumPixelsX, NumPixelsY).xyxy) - 1); + + // For each mesh + // if the mesh aabb is inside the bin aabb + // check all each triangle and test against the bin aabb + // if inside the bin, add in, once the bin is full render it + // once the loop finish, render the remaining triangles in the bin + int internalBinSize = 0; + for (int m = 0; m < Meshes.Length; m += 1) + { + float2 max = Meshes[m].screenMax.xy; + float2 min = Meshes[m].screenMin.xy; + + if (math.any(min > clipRect.zw) || math.any(max < clipRect.xy)) + continue; + + OcclusionMesh mesh = Meshes[m]; + float* vertex_x = (float*) mesh.vertex_x.GetUnsafePtr(); + float* vertex_y = (float*)mesh.vertex_y.GetUnsafePtr(); + float* vertex_w = (float*)mesh.vertex_w.GetUnsafePtr(); + float2* triangle_max = (float2*)mesh.triangle_max.GetUnsafePtr(); + float2* triangle_min = (float2*)mesh.triangle_min.GetUnsafePtr(); + + int k = 0; + for (int j = 0; j < mesh.expandedVertexSize; j += 3, ++k) + { + max = triangle_max[k]; + min = triangle_min[k]; + + if (math.any(min > clipRect.zw) || math.any(max < clipRect.xy)) + continue; + + for (int n = 0; n < 3; ++n) + { + temp_stack_x[tempStackSize] = vertex_x[j + n]; + temp_stack_y[tempStackSize] = vertex_y[j + n]; + temp_stack_w[tempStackSize] = vertex_w[j + n]; + tempStackSize++; + } + + if(tempStackSize == 12) + { + for(int n = 0; n < 3; ++n) + { + for(int p = 0; p < 4; ++p) + { + binTriangleX[internalBinSize + p + n * 4] = temp_stack_x[n + p * 3]; + binTriangleY[internalBinSize + p + n * 4] = temp_stack_y[n + p * 3]; + binTriangleW[internalBinSize + p + n * 4] = temp_stack_w[n + p * 3]; + } + } + internalBinSize += 12; + tempStackSize = 0; + } + if (internalBinSize == binSize) + { + RasterizeMesh(tiles, (float*)binTriangleX.GetUnsafePtr(), (float*)binTriangleY.GetUnsafePtr(), (float*)binTriangleW.GetUnsafePtr(), internalBinSize, scissorRect); + internalBinSize = 0; + } + } + } + if (tempStackSize > 0) + { + for (int n = 0; n < 3; ++n) + { + for (int p = 0; p < 4; ++p) + { + binTriangleX[internalBinSize + p + n * 4] = temp_stack_x[n + p * 3]; + binTriangleY[internalBinSize + p + n * 4] = temp_stack_y[n + p * 3]; + binTriangleW[internalBinSize + p + n * 4] = temp_stack_w[n + p * 3]; + } + } + internalBinSize += tempStackSize; + tempStackSize = 0; + } + if (internalBinSize > 0) + { + RasterizeMesh(tiles, (float*)binTriangleX.GetUnsafePtr(), (float*)binTriangleY.GetUnsafePtr(), (float*)binTriangleW.GetUnsafePtr(), internalBinSize, scissorRect); + } + binTriangleX.Dispose(); + binTriangleY.Dispose(); + binTriangleW.Dispose(); + } + + // RasterizeMesh now gets as input all triangles that already passed backface culling, clipping and an early z test + // this should make it even easier for the system to keep using the full simd words and have better simd occupancy + // instead of removing part of the work based on the mask + void RasterizeMesh(Tile* tiles, float* binTriangleX, float* binTriangleY, float* binTriangleW, int numVert, ScissorRect screenScissor) + { + // DS: TODO: UNITY BURST FIX + //using (var roundingMode = new X86.RoundingScope(X86.MXCSRBits.RoundToNearest)) + const X86.MXCSRBits roundingMode = X86.MXCSRBits.RoundToNearest; + X86.MXCSRBits OldBits = X86.MXCSR; + X86.MXCSR = (OldBits & ~X86.MXCSRBits.RoundingControlMask) | roundingMode; + + int vertexIndex = 0; + + v128* vtxX_prealloc = stackalloc v128[3]; + v128* vtxY_prealloc = stackalloc v128[3]; + v128* vtxW_prealloc = stackalloc v128[3]; + + v128* pVtxX_prealloc = stackalloc v128[3]; + v128* pVtxY_prealloc = stackalloc v128[3]; + v128* pVtxZ_prealloc = stackalloc v128[3]; + + v128* ipVtxX_prealloc = stackalloc v128[3]; + v128* ipVtxY_prealloc = stackalloc v128[3]; + + while (vertexIndex < numVert) + { + v128* vtxX = vtxX_prealloc; + v128* vtxY = vtxY_prealloc; + v128* vtxW = vtxW_prealloc; + + + int numLanes = math.min(SIMD_LANES, numVert - vertexIndex); + uint triMask = (1u << numLanes) - 1; + + + for (int i = 0; i < 3; i++) + { + vtxX[i] = X86.Sse.load_ps(&binTriangleX[vertexIndex + i * 4]); + vtxY[i] = X86.Sse.load_ps(&binTriangleY[vertexIndex + i * 4]); + vtxW[i] = X86.Sse.load_ps(&binTriangleW[vertexIndex + i * 4]); + } + + vertexIndex += SIMD_LANES * 3; + + if (triMask == 0x0) + { + continue; + } + + /* Project, transform to screen space and perform backface culling. Note + that we use z = 1.0 / vtx.w for depth, which means that z = 0 is far and + z = 1/m_near is near. For ortho projection, we do z = (z * -1) + 1 to go from z = 0 for far and z = 2 for near + + We must also use a greater than depth test, and in effect + everything is reversed compared to regular z implementations. */ + + v128* pVtxX = pVtxX_prealloc; + v128* pVtxY = pVtxY_prealloc; + v128* pVtxZ = pVtxZ_prealloc; + + v128* ipVtxX = ipVtxX_prealloc; + v128* ipVtxY = ipVtxY_prealloc; + ProjectVertices(ipVtxX, ipVtxY, pVtxX, pVtxY, pVtxZ, vtxX, vtxY, vtxW); + + /* Setup and rasterize a SIMD batch of triangles */ + RasterizeTriangleBatch(tiles, ipVtxX, ipVtxY, pVtxX, pVtxY, pVtxZ, triMask, screenScissor); + } + + // DS: TODO: UNITY BURST FIX + X86.MXCSR = OldBits; + } + + void ProjectVertices(v128* ipVtxX, v128* ipVtxY, v128* pVtxX, v128* pVtxY, v128* pVtxZ, v128* vtxX, v128* vtxY, v128* vtxW) + { + const float FP_INV = 1f / (1 << BufferGroup.FpBits); + // Project vertices and transform to screen space. Snap to sub-pixel coordinates with BufferGroup.FpBits precision. + for (int i = 0; i < 3; i++) + { + int idx = 2 - i; + v128 rcpW; + + if (ProjectionType == BatchCullingProjectionType.Orthographic) + { + rcpW = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.set1_ps(-1.0f), vtxW[i], X86.Sse.set1_ps(1.0f)); + + v128 screenX = X86.Sse.add_ps(X86.Sse.mul_ps(vtxX[i], HalfWidth), PixelCenterX); + v128 screenY = X86.Sse.add_ps(X86.Sse.mul_ps(vtxY[i], HalfHeight), PixelCenterY); + ipVtxX[idx] = X86.Sse2.cvtps_epi32(X86.Sse.mul_ps(screenX, X86.Sse.set1_ps((float)(1 << BufferGroup.FpBits)))); + ipVtxY[idx] = X86.Sse2.cvtps_epi32(X86.Sse.mul_ps(screenY, X86.Sse.set1_ps((float)(1 << BufferGroup.FpBits)))); + } + else + { + rcpW = X86.Sse.div_ps(X86.Sse.set1_ps(1f), vtxW[i]); + + v128 screenX = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.mul_ps(vtxX[i], HalfWidth), rcpW, PixelCenterX); + v128 screenY = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.mul_ps(vtxY[i], HalfHeight), rcpW, PixelCenterY); + + ipVtxX[idx] = X86.Sse2.cvtps_epi32(X86.Sse.mul_ps(screenX, X86.Sse.set1_ps((float)(1 << BufferGroup.FpBits)))); + ipVtxY[idx] = X86.Sse2.cvtps_epi32(X86.Sse.mul_ps(screenY, X86.Sse.set1_ps((float)(1 << BufferGroup.FpBits)))); + } + + pVtxX[idx] = X86.Sse.mul_ps(X86.Sse2.cvtepi32_ps(ipVtxX[idx]), X86.Sse.set1_ps(FP_INV)); + pVtxY[idx] = X86.Sse.mul_ps(X86.Sse2.cvtepi32_ps(ipVtxY[idx]), X86.Sse.set1_ps(FP_INV)); + pVtxZ[idx] = rcpW; + } + } + + void RasterizeTriangleBatch(Tile* tiles, v128* ipVtxX, v128* ipVtxY, v128* pVtxX, v128* pVtxY, v128* pVtxZ, uint triMask, ScissorRect scissor) + { + //we are computing the bounding box again when we used it before but there are some use cases after, this check cannot be removed atm + + // Compute bounding box and clamp to tile coordinates + ComputeBoundingBox(pVtxX, pVtxY, ref scissor, out var bbPixelMinX, out var bbPixelMinY, out var bbPixelMaxX, out var bbPixelMaxY); + + // Clamp bounding box to tiles (it's already padded in computeBoundingBox) + v128 bbTileMinX = X86.Sse2.srai_epi32(bbPixelMinX, BufferGroup.TileWidthShift); + v128 bbTileMinY = X86.Sse2.srai_epi32(bbPixelMinY, BufferGroup.TileHeightShift); + v128 bbTileMaxX = X86.Sse2.srai_epi32(bbPixelMaxX, BufferGroup.TileWidthShift); + v128 bbTileMaxY = X86.Sse2.srai_epi32(bbPixelMaxY, BufferGroup.TileHeightShift); + v128 bbTileSizeX = X86.Sse2.sub_epi32(bbTileMaxX, bbTileMinX); + v128 bbTileSizeY = X86.Sse2.sub_epi32(bbTileMaxY, bbTileMinY); + + // Cull triangles with zero bounding box + v128 bboxSign = X86.Sse2.or_si128(X86.Sse2.sub_epi32(bbTileSizeX, X86.Sse2.set1_epi32(1)), X86.Sse2.sub_epi32(bbTileSizeY, X86.Sse2.set1_epi32(1))); + triMask &= (uint)((~X86.Sse.movemask_ps(bboxSign)) & SIMD_ALL_LANES_MASK); + + if (triMask == 0x0) + { + return; // View-culled + } + + // Set up screen space depth plane + ComputeDepthPlane(pVtxX, pVtxY, pVtxZ, out var zPixelDx, out var zPixelDy); + + // Compute z value at min corner of bounding box. Offset to make sure z is conservative for all 8x4 subtiles + v128 bbMinXV0 = X86.Sse.sub_ps(X86.Sse2.cvtepi32_ps(bbPixelMinX), pVtxX[0]); + v128 bbMinYV0 = X86.Sse.sub_ps(X86.Sse2.cvtepi32_ps(bbPixelMinY), pVtxY[0]); + v128 zPlaneOffset = IntrinsicUtils._mmw_fmadd_ps(zPixelDx, bbMinXV0, IntrinsicUtils._mmw_fmadd_ps(zPixelDy, bbMinYV0, pVtxZ[0])); + v128 zTileDx = X86.Sse.mul_ps(zPixelDx, X86.Sse.set1_ps(BufferGroup.TileWidth)); + v128 zTileDy = X86.Sse.mul_ps(zPixelDy, X86.Sse.set1_ps(BufferGroup.TileHeight)); + + zPlaneOffset = X86.Sse.add_ps(zPlaneOffset, X86.Sse.min_ps(X86.Sse2.setzero_si128(), X86.Sse.mul_ps(zPixelDx, X86.Sse.set1_ps(BufferGroup.SubTileWidth)))); + zPlaneOffset = X86.Sse.add_ps(zPlaneOffset, X86.Sse.min_ps(X86.Sse2.setzero_si128(), X86.Sse.mul_ps(zPixelDy, X86.Sse.set1_ps(BufferGroup.SubTileHeight)))); + + // Compute Zmin and Zmax for the triangle (used to narrow the range for difficult tiles) + v128 zMin = X86.Sse.min_ps(pVtxZ[0], X86.Sse.min_ps(pVtxZ[1], pVtxZ[2])); + v128 zMax = X86.Sse.max_ps(pVtxZ[0], X86.Sse.max_ps(pVtxZ[1], pVtxZ[2])); + + /* Sort vertices (v0 has lowest Y, and the rest is in winding order) and compute edges. Also find the middle + vertex and compute tile */ + + // Rotate the triangle in the winding order until v0 is the vertex with lowest Y value + SortVertices(ipVtxX, ipVtxY); + + // Compute edges + v128* edgeX = stackalloc v128[3]; + edgeX[0] = X86.Sse2.sub_epi32(ipVtxX[1], ipVtxX[0]); + edgeX[1] = X86.Sse2.sub_epi32(ipVtxX[2], ipVtxX[1]); + edgeX[2] = X86.Sse2.sub_epi32(ipVtxX[2], ipVtxX[0]); + + v128* edgeY = stackalloc v128[3]; + edgeY[0] = X86.Sse2.sub_epi32(ipVtxY[1], ipVtxY[0]); + edgeY[1] = X86.Sse2.sub_epi32(ipVtxY[2], ipVtxY[1]); + edgeY[2] = X86.Sse2.sub_epi32(ipVtxY[2], ipVtxY[0]); + + // Classify if the middle vertex is on the left or right and compute its position + int midVtxRight = ~X86.Sse.movemask_ps(edgeY[1]); + v128 midPixelX = IntrinsicUtils._mmw_blendv_ps(ipVtxX[1], ipVtxX[2], edgeY[1]); + v128 midPixelY = IntrinsicUtils._mmw_blendv_ps(ipVtxY[1], ipVtxY[2], edgeY[1]); + v128 midTileY = X86.Sse2.srai_epi32(IntrinsicUtils._mmw_max_epi32(midPixelY, X86.Sse2.setzero_si128()), BufferGroup.TileHeightShift + BufferGroup.FpBits); + v128 bbMidTileY = IntrinsicUtils._mmw_max_epi32(bbTileMinY, IntrinsicUtils._mmw_min_epi32(bbTileMaxY, midTileY)); + + // Compute edge events for the bottom of the bounding box, or for the middle tile in case of + // the edge originating from the middle vertex. + v128* xDiffi = stackalloc v128[2]; + xDiffi[0] = X86.Sse2.sub_epi32(ipVtxX[0], X86.Sse2.slli_epi32(bbPixelMinX, BufferGroup.FpBits)); + xDiffi[1] = X86.Sse2.sub_epi32(midPixelX, X86.Sse2.slli_epi32(bbPixelMinX, BufferGroup.FpBits)); + + v128* yDiffi = stackalloc v128[2]; + yDiffi[0] = X86.Sse2.sub_epi32(ipVtxY[0], X86.Sse2.slli_epi32(bbPixelMinY, BufferGroup.FpBits)); + yDiffi[1] = X86.Sse2.sub_epi32(midPixelY, X86.Sse2.slli_epi32(bbMidTileY, BufferGroup.FpBits + BufferGroup.TileHeightShift)); + + /* Edge slope setup - Note we do not conform to DX/GL rasterization rules */ + + // Potentially flip edge to ensure that all edges have positive Y slope. + edgeX[1] = IntrinsicUtils._mmw_blendv_ps(edgeX[1], IntrinsicUtils._mmw_neg_epi32(edgeX[1]), edgeY[1]); + edgeY[1] = IntrinsicUtils._mmw_abs_epi32(edgeY[1]); + + // Compute floating point slopes + v128* slope = stackalloc v128[3]; + slope[0] = X86.Sse.div_ps(X86.Sse2.cvtepi32_ps(edgeX[0]), X86.Sse2.cvtepi32_ps(edgeY[0])); + slope[1] = X86.Sse.div_ps(X86.Sse2.cvtepi32_ps(edgeX[1]), X86.Sse2.cvtepi32_ps(edgeY[1])); + slope[2] = X86.Sse.div_ps(X86.Sse2.cvtepi32_ps(edgeX[2]), X86.Sse2.cvtepi32_ps(edgeY[2])); + + // Modify slope of horizontal edges to make sure they mask out pixels above/below the edge. The slope is set to screen + // width to mask out all pixels above or below the horizontal edge. We must also add a small bias to acount for that + // vertices may end up off screen due to clipping. We're assuming that the round off error is no bigger than 1.0 + v128 horizontalSlopeDelta = X86.Sse.set1_ps(2f * (NumPixelsX + 2f * (BufferGroup.GuardBandPixelSize + 1.0f))); + v128 horizontalSlope0 = X86.Sse2.cmpeq_epi32(edgeY[0], X86.Sse2.setzero_si128()); + v128 horizontalSlope1 = X86.Sse2.cmpeq_epi32(edgeY[1], X86.Sse2.setzero_si128()); + slope[0] = IntrinsicUtils._mmw_blendv_ps(slope[0], horizontalSlopeDelta, horizontalSlope0); + slope[1] = IntrinsicUtils._mmw_blendv_ps(slope[1], IntrinsicUtils._mmw_neg_ps(horizontalSlopeDelta), horizontalSlope1); + + v128* vy = stackalloc v128[3]; + vy[0] = yDiffi[0]; + vy[1] = yDiffi[1]; + vy[2] = yDiffi[0]; + + v128 offset0 = X86.Sse2.and_si128(X86.Sse2.add_epi32(yDiffi[0], X86.Sse2.set1_epi32(BufferGroup.FpHalfPixel - 1)), X86.Sse2.set1_epi32((-1 << BufferGroup.FpBits))); + v128 offset1 = X86.Sse2.and_si128(X86.Sse2.add_epi32(yDiffi[1], X86.Sse2.set1_epi32(BufferGroup.FpHalfPixel - 1)), X86.Sse2.set1_epi32((-1 << BufferGroup.FpBits))); + vy[0] = IntrinsicUtils._mmw_blendv_ps(yDiffi[0], offset0, horizontalSlope0); + vy[1] = IntrinsicUtils._mmw_blendv_ps(yDiffi[1], offset1, horizontalSlope1); + + // Compute edge events for the bottom of the bounding box, or for the middle tile in case of + // the edge originating from the middle vertex. + v128* slopeSign = stackalloc v128[3]; + v128* absEdgeX = stackalloc v128[3]; + v128* slopeTileDelta = stackalloc v128[3]; + v128* eventStartRemainder = stackalloc v128[3]; + v128* slopeTileRemainder = stackalloc v128[3]; + v128* eventStart = stackalloc v128[3]; + + for (int i = 0; i < 3; i++) + { + // Common, compute slope sign (used to propagate the remainder term when overflowing) is postive or negative x-direction + slopeSign[i] = IntrinsicUtils._mmw_blendv_ps(X86.Sse2.set1_epi32(1), X86.Sse2.set1_epi32(-1), edgeX[i]); + absEdgeX[i] = IntrinsicUtils._mmw_abs_epi32(edgeX[i]); + + // Delta and error term for one vertical tile step. The exact delta is exactDelta = edgeX / edgeY, due to limited precision we + // repersent the delta as delta = qoutient + remainder / edgeY, where quotient = int(edgeX / edgeY). In this case, since we step + // one tile of scanlines at a time, the slope is computed for a tile-sized step. + slopeTileDelta[i] = X86.Sse2.cvttps_epi32(X86.Sse.mul_ps(slope[i], X86.Sse.set1_ps(BufferGroup.FpTileHeight))); + slopeTileRemainder[i] = X86.Sse2.sub_epi32(X86.Sse2.slli_epi32(absEdgeX[i], BufferGroup.FpTileHeightShift), IntrinsicUtils._mmw_mullo_epi32(IntrinsicUtils._mmw_abs_epi32(slopeTileDelta[i]), edgeY[i])); + + // Jump to bottom scanline of tile row, this is the bottom of the bounding box, or the middle vertex of the triangle. + // The jump can be in both positive and negative y-direction due to clipping / offscreen vertices. + v128 tileStartDir = IntrinsicUtils._mmw_blendv_ps(slopeSign[i], IntrinsicUtils._mmw_neg_epi32(slopeSign[i]), vy[i]); + v128 tieBreaker = IntrinsicUtils._mmw_blendv_ps(X86.Sse2.set1_epi32(0), X86.Sse2.set1_epi32(1), tileStartDir); + v128 tileStartSlope = X86.Sse2.cvttps_epi32(X86.Sse.mul_ps(slope[i], X86.Sse2.cvtepi32_ps(IntrinsicUtils._mmw_neg_epi32(vy[i])))); + v128 tileStartRemainder = X86.Sse2.sub_epi32(IntrinsicUtils._mmw_mullo_epi32(absEdgeX[i], IntrinsicUtils._mmw_abs_epi32(vy[i])), IntrinsicUtils._mmw_mullo_epi32(IntrinsicUtils._mmw_abs_epi32(tileStartSlope), edgeY[i])); + + eventStartRemainder[i] = X86.Sse2.sub_epi32(tileStartRemainder, tieBreaker); + v128 overflow = X86.Sse2.srai_epi32(eventStartRemainder[i], 31); + eventStartRemainder[i] = X86.Sse2.add_epi32(eventStartRemainder[i], X86.Sse2.and_si128(overflow, edgeY[i])); + eventStartRemainder[i] = IntrinsicUtils._mmw_blendv_ps(eventStartRemainder[i], X86.Sse2.sub_epi32(X86.Sse2.sub_epi32(edgeY[i], eventStartRemainder[i]), X86.Sse2.set1_epi32(1)), vy[i]); + + //eventStart[i] = xDiffi[i & 1] + tileStartSlope + (overflow & tileStartDir) + X86.Sse2.set1_epi32(FP_HALF_PIXEL - 1) + tieBreaker; + eventStart[i] = X86.Sse2.add_epi32(X86.Sse2.add_epi32(xDiffi[i & 1], tileStartSlope), X86.Sse2.and_si128(overflow, tileStartDir)); + eventStart[i] = X86.Sse2.add_epi32(X86.Sse2.add_epi32(eventStart[i], X86.Sse2.set1_epi32(BufferGroup.FpHalfPixel - 1)), tieBreaker); + } + + // Split bounding box into bottom - middle - top region. + v128 bbBottomIdx = X86.Sse2.add_epi32(bbTileMinX, IntrinsicUtils._mmw_mullo_epi32(bbTileMinY, X86.Sse2.set1_epi32(NumTilesX))); + v128 bbTopIdx = X86.Sse2.add_epi32(bbTileMinX, IntrinsicUtils._mmw_mullo_epi32(X86.Sse2.add_epi32(bbTileMinY, bbTileSizeY), X86.Sse2.set1_epi32(NumTilesX))); + v128 bbMidIdx = X86.Sse2.add_epi32(bbTileMinX, IntrinsicUtils._mmw_mullo_epi32(midTileY, X86.Sse2.set1_epi32(NumTilesX))); + + // Loop over non-culled triangle and change SIMD axis to per-pixel + while (triMask != 0) + { + uint triIdx = (uint)IntrinsicUtils.find_clear_lsb(ref triMask); + int triMidVtxRight = (midVtxRight >> (int)triIdx) & 1; + + // Get Triangle Zmin zMax + v128 zTriMax = X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(zMax, triIdx)); + v128 zTriMin = X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(zMin, triIdx)); + + // Setup Zmin value for first set of 8x4 subtiles + v128 SimdSubTileColOffsetF = X86.Sse.setr_ps(0, BufferGroup.SubTileWidth, BufferGroup.SubTileWidth * 2, BufferGroup.SubTileWidth * 3); + v128 z0 = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(zPixelDx, triIdx)), + SimdSubTileColOffsetF, + IntrinsicUtils._mmw_fmadd_ps(X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(zPixelDy, triIdx)), + X86.Sse2.setzero_si128(), + X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(zPlaneOffset, triIdx)))); + + float zx = IntrinsicUtils.getFloatLane(zTileDx, triIdx); + float zy = IntrinsicUtils.getFloatLane(zTileDy, triIdx); + + // Get dimension of bounding box bottom, mid & top segments + int bbWidth = IntrinsicUtils.getIntLane(bbTileSizeX, triIdx); + int bbHeight = IntrinsicUtils.getIntLane(bbTileSizeY, triIdx); + int tileRowIdx = IntrinsicUtils.getIntLane(bbBottomIdx, triIdx); + int tileMidRowIdx = IntrinsicUtils.getIntLane(bbMidIdx, triIdx); + int tileEndRowIdx = IntrinsicUtils.getIntLane(bbTopIdx, triIdx); + + if (bbWidth > BIG_TRIANGLE && bbHeight > BIG_TRIANGLE) // For big triangles we use a more expensive but tighter traversal algorithm + { + if (triMidVtxRight != 0) + { + RasterizeTriangle(tiles, true, 1, triIdx, bbWidth, tileRowIdx, tileMidRowIdx, tileEndRowIdx, eventStart, slope, slopeTileDelta, zTriMin, zTriMax, ref z0, zx, zy, edgeY, absEdgeX, slopeSign, eventStartRemainder, slopeTileRemainder); + } + else + { + RasterizeTriangle(tiles, true, 0, triIdx, bbWidth, tileRowIdx, tileMidRowIdx, tileEndRowIdx, eventStart, slope, slopeTileDelta, zTriMin, zTriMax, ref z0, zx, zy, edgeY, absEdgeX, slopeSign, eventStartRemainder, slopeTileRemainder); + } + } + else + { + if (triMidVtxRight != 0) + { + RasterizeTriangle(tiles, false, 1, triIdx, bbWidth, tileRowIdx, tileMidRowIdx, tileEndRowIdx, eventStart, slope, slopeTileDelta, zTriMin, zTriMax, ref z0, zx, zy, edgeY, absEdgeX, slopeSign, eventStartRemainder, slopeTileRemainder); + } + else + { + RasterizeTriangle(tiles, false, 0, triIdx, bbWidth, tileRowIdx, tileMidRowIdx, tileEndRowIdx, eventStart, slope, slopeTileDelta, zTriMin, zTriMax, ref z0, zx, zy, edgeY, absEdgeX, slopeSign, eventStartRemainder, slopeTileRemainder); + } + } + } + } + + void ComputeBoundingBox(v128* vX, v128* vY, ref ScissorRect scissor, out v128 bbminX, out v128 bbminY, out v128 bbmaxX, out v128 bbmaxY) + { + // Find Min/Max vertices + bbminX = X86.Sse2.cvttps_epi32(X86.Sse.min_ps(vX[0], X86.Sse.min_ps(vX[1], vX[2]))); + bbminY = X86.Sse2.cvttps_epi32(X86.Sse.min_ps(vY[0], X86.Sse.min_ps(vY[1], vY[2]))); + bbmaxX = X86.Sse2.cvttps_epi32(X86.Sse.max_ps(vX[0], X86.Sse.max_ps(vX[1], vX[2]))); + bbmaxY = X86.Sse2.cvttps_epi32(X86.Sse.max_ps(vY[0], X86.Sse.max_ps(vY[1], vY[2]))); + + // Clamp to tile boundaries + v128 SimdPadWMask = X86.Sse2.set1_epi32(~(BufferGroup.TileWidth - 1)); + v128 SimdPadHMask = X86.Sse2.set1_epi32(~(BufferGroup.TileHeight - 1)); + bbminX = X86.Sse2.and_si128(bbminX, SimdPadWMask); + bbmaxX = X86.Sse2.and_si128(X86.Sse2.add_epi32(bbmaxX, X86.Sse2.set1_epi32(BufferGroup.TileWidth)), SimdPadWMask); + bbminY = X86.Sse2.and_si128(bbminY, SimdPadHMask); + bbmaxY = X86.Sse2.and_si128(X86.Sse2.add_epi32(bbmaxY, X86.Sse2.set1_epi32(BufferGroup.TileHeight)), SimdPadHMask); + + // Clip to scissor + bbminX = IntrinsicUtils._mmw_max_epi32(bbminX, X86.Sse2.set1_epi32(scissor.mMinX)); + bbmaxX = IntrinsicUtils._mmw_min_epi32(bbmaxX, X86.Sse2.set1_epi32(scissor.mMaxX)); + bbminY = IntrinsicUtils._mmw_max_epi32(bbminY, X86.Sse2.set1_epi32(scissor.mMinY)); + bbmaxY = IntrinsicUtils._mmw_min_epi32(bbmaxY, X86.Sse2.set1_epi32(scissor.mMaxY)); + } + + void ComputeDepthPlane(v128* pVtxX, v128* pVtxY, v128* pVtxZ, out v128 zPixelDx, out v128 zPixelDy) + { + // Setup z(x,y) = z0 + dx*x + dy*y screen space depth plane equation + v128 x2 = X86.Sse.sub_ps(pVtxX[2], pVtxX[0]); + v128 x1 = X86.Sse.sub_ps(pVtxX[1], pVtxX[0]); + v128 y1 = X86.Sse.sub_ps(pVtxY[1], pVtxY[0]); + v128 y2 = X86.Sse.sub_ps(pVtxY[2], pVtxY[0]); + v128 z1 = X86.Sse.sub_ps(pVtxZ[1], pVtxZ[0]); + v128 z2 = X86.Sse.sub_ps(pVtxZ[2], pVtxZ[0]); + v128 d = X86.Sse.div_ps(X86.Sse.set1_ps(1.0f), IntrinsicUtils._mmw_fmsub_ps(x1, y2, X86.Sse.mul_ps(y1, x2))); + zPixelDx = X86.Sse.mul_ps(IntrinsicUtils._mmw_fmsub_ps(z1, y2, X86.Sse.mul_ps(y1, z2)), d); + zPixelDy = X86.Sse.mul_ps(IntrinsicUtils._mmw_fmsub_ps(x1, z2, X86.Sse.mul_ps(z1, x2)), d); + } + + void RasterizeTriangle( + Tile* tiles, + bool isTightTraversal, + int midVtxRight, + uint triIdx, + int bbWidth, + int tileRowIdx, + int tileMidRowIdx, + int tileEndRowIdx, + v128* eventStart, + v128* slope, + v128* slopeTileDelta, + v128 zTriMin, + v128 zTriMax, + ref v128 z0, + float zx, + float zy, + v128* edgeY, + v128* absEdgeX, + v128* slopeSign, + v128* eventStartRemainder, + v128* slopeTileRemainder) + { + const int LEFT_EDGE_BIAS = -1; + const int RIGHT_EDGE_BIAS = 1; + + v128* triSlopeSign = stackalloc v128[3]; + v128* triSlopeTileDelta = stackalloc v128[3]; + v128* triEdgeY = stackalloc v128[3]; + v128* triSlopeTileRemainder = stackalloc v128[3]; + v128* triEventRemainder = stackalloc v128[3]; + v128* triEvent = stackalloc v128[3]; + + for (int i = 0; i < 3; ++i) + { + triSlopeSign[i] = X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(slopeSign[i], triIdx)); + triSlopeTileDelta[i] = + X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(slopeTileDelta[i], triIdx)); + triEdgeY[i] = X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(edgeY[i], triIdx)); + triSlopeTileRemainder[i] = + X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(slopeTileRemainder[i], triIdx)); + + v128 triSlope = X86.Sse.set1_ps(IntrinsicUtils.getFloatLane(slope[i], triIdx)); + v128 triAbsEdgeX = X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(absEdgeX[i], triIdx)); + v128 triStartRemainder = + X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(eventStartRemainder[i], triIdx)); + v128 triEventStart = X86.Sse2.set1_epi32(IntrinsicUtils.getIntLane(eventStart[i], triIdx)); + + v128 SimdLaneYCoordF = X86.Sse.setr_ps(128f, 384f, 640f, 896f); + v128 scanlineDelta = X86.Sse2.cvttps_epi32(X86.Sse.mul_ps(triSlope, SimdLaneYCoordF)); + v128 SimdLaneYCoordI = X86.Sse2.setr_epi32(128, 384, 640, 896); + v128 scanlineSlopeRemainder = + X86.Sse2.sub_epi32(IntrinsicUtils._mmw_mullo_epi32(triAbsEdgeX, SimdLaneYCoordI), + IntrinsicUtils._mmw_mullo_epi32(IntrinsicUtils._mmw_abs_epi32(scanlineDelta), triEdgeY[i])); + + triEventRemainder[i] = X86.Sse2.sub_epi32(triStartRemainder, scanlineSlopeRemainder); + v128 overflow = X86.Sse2.srai_epi32(triEventRemainder[i], 31); + triEventRemainder[i] = + X86.Sse2.add_epi32(triEventRemainder[i], X86.Sse2.and_si128(overflow, triEdgeY[i])); + triEvent[i] = + X86.Sse2.add_epi32(X86.Sse2.add_epi32(triEventStart, scanlineDelta), + X86.Sse2.and_si128(overflow, triSlopeSign[i])); + } + + // For big triangles track start & end tile for each scanline and only traverse the valid region + int startDelta = 0; + int endDelta = 0; + int topDelta = 0; + int startEvent = 0; + int endEvent = 0; + int topEvent = 0; + + if (isTightTraversal) + { + startDelta = IntrinsicUtils.getIntLane(slopeTileDelta[2], triIdx) + LEFT_EDGE_BIAS; + endDelta = IntrinsicUtils.getIntLane(slopeTileDelta[0], triIdx) + RIGHT_EDGE_BIAS; + topDelta = IntrinsicUtils.getIntLane(slopeTileDelta[1], triIdx) + + (midVtxRight != 0 ? RIGHT_EDGE_BIAS : LEFT_EDGE_BIAS); + + // Compute conservative bounds for the edge events over a 32xN tile + startEvent = IntrinsicUtils.getIntLane(eventStart[2], triIdx) + Mathf.Min(0, startDelta); + endEvent = IntrinsicUtils.getIntLane(eventStart[0], triIdx) + Mathf.Max(0, endDelta) + + (BufferGroup.TileWidth << BufferGroup.FpBits); // TODO: (Apoorva) can be spun out into a const + + if (midVtxRight != 0) + { + topEvent = IntrinsicUtils.getIntLane(eventStart[1], triIdx) + Mathf.Max(0, topDelta) + + (BufferGroup.TileWidth << BufferGroup.FpBits); // TODO: (Apoorva) can be spun out into a const + } + else + { + topEvent = IntrinsicUtils.getIntLane(eventStart[1], triIdx) + Mathf.Min(0, topDelta); + } + } + + if (tileRowIdx <= tileMidRowIdx) + { + int tileStopIdx = Mathf.Min(tileEndRowIdx, tileMidRowIdx); + + // Traverse the bottom half of the triangle + while (tileRowIdx < tileStopIdx) + { + int start = 0; + int end = bbWidth; + + if (isTightTraversal) + { + // Compute tighter start and endpoints to avoid traversing empty space + start = Mathf.Max(0, Mathf.Min(bbWidth - 1, startEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); // TODO: (Apoorva) can be spun out into a const + end = Mathf.Min(bbWidth, ((int) endEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); // TODO: (Apoorva) can be spun out into a const + + startEvent += startDelta; + endEvent += endDelta; + } + + // Traverse the scanline and update the masked hierarchical z buffer + TraverseScanline(tiles, 1, 1, start, end, tileRowIdx, 0, 2, triEvent, zTriMin, zTriMax, z0, + zx); + + // move to the next scanline of tiles, update edge events and interpolate z + tileRowIdx += NumTilesX; + z0 = X86.Sse.add_ps(z0, X86.Sse.set1_ps(zy)); + + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, 0); + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, 2); + } + + // Traverse the middle scanline of tiles. We must consider all three edges only in this region + if (tileRowIdx < tileEndRowIdx) + { + int start = 0; + int end = bbWidth; + + if (isTightTraversal) + { + // Compute tighter start and endpoints to avoid traversing lots of empty space + start = Mathf.Max(0, Mathf.Min(bbWidth - 1, startEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); // TODO: (Apoorva) can be spun out into a const + end = Mathf.Min(bbWidth, ((int) endEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); // TODO: (Apoorva) can be spun out into a const + + // Switch the traversal start / end to account for the upper side edge + endEvent = midVtxRight != 0 ? topEvent : endEvent; + endDelta = midVtxRight != 0 ? topDelta : endDelta; + startEvent = midVtxRight != 0 ? startEvent : topEvent; + startDelta = midVtxRight != 0 ? startDelta : topDelta; + + startEvent += startDelta; + endEvent += endDelta; + } + + // Traverse the scanline and update the masked hierarchical z buffer. + if (midVtxRight != 0) + { + TraverseScanline(tiles, 2, 1, start, end, tileRowIdx, 0, 2, triEvent, zTriMin, zTriMax, + z0, zx); + } + else + { + TraverseScanline(tiles, 1, 2, start, end, tileRowIdx, 0, 2, triEvent, zTriMin, zTriMax, + z0, zx); + } + + tileRowIdx += NumTilesX; + } + + // Traverse the top half of the triangle + if (tileRowIdx < tileEndRowIdx) + { + // move to the next scanline of tiles, update edge events and interpolate z + z0 = X86.Sse.add_ps(z0, X86.Sse.set1_ps(zy)); + int i0 = midVtxRight + 0; + int i1 = midVtxRight + 1; + + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i0); + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i1); + + for (;;) + { + int start = 0; + int end = bbWidth; + + if (isTightTraversal) + { + // Compute tighter start and endpoints to avoid traversing lots of empty space + start = Mathf.Max(0, Mathf.Min(bbWidth - 1, startEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); + end = Mathf.Min(bbWidth, (endEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); + + startEvent += startDelta; + endEvent += endDelta; + } + + // Traverse the scanline and update the masked hierarchical z buffer + TraverseScanline(tiles, 1, 1, start, end, tileRowIdx, midVtxRight + 0, + midVtxRight + 1, triEvent, zTriMin, zTriMax, z0, zx); + + // move to the next scanline of tiles, update edge events and interpolate z + tileRowIdx += NumTilesX; + if (tileRowIdx >= tileEndRowIdx) + { + break; + } + + z0 = X86.Sse.add_ps(z0, X86.Sse.set1_ps(zy)); + + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i0); + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i1); + } + } + } + else + { + if (isTightTraversal) + { + // For large triangles, switch the traversal start / end to account for the upper side edge + endEvent = midVtxRight != 0 ? topEvent : endEvent; + endDelta = midVtxRight != 0 ? topDelta : endDelta; + startEvent = midVtxRight != 0 ? startEvent : topEvent; + startDelta = midVtxRight != 0 ? startDelta : topDelta; + } + + // Traverse the top half of the triangle + if (tileRowIdx < tileEndRowIdx) + { + int i0 = midVtxRight + 0; + int i1 = midVtxRight + 1; + + for (;;) + { + int start = 0; + int end = bbWidth; + + if (isTightTraversal) + { + // Compute tighter start and endpoints to avoid traversing lots of empty space + start = Mathf.Max(0, Mathf.Min(bbWidth - 1, startEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); + end = Mathf.Min(bbWidth, (endEvent >> (BufferGroup.TileWidthShift + BufferGroup.FpBits))); + + startEvent += startDelta; + endEvent += endDelta; + } + + // Traverse the scanline and update the masked hierarchical z buffer + TraverseScanline(tiles, 1, 1, start, end, tileRowIdx, midVtxRight + 0, + midVtxRight + 1, triEvent, zTriMin, zTriMax, z0, zx); + + // move to the next scanline of tiles, update edge events and interpolate z + tileRowIdx += NumTilesX; + if (tileRowIdx >= tileEndRowIdx) + { + break; + } + + z0 = X86.Sse.add_ps(z0, X86.Sse.set1_ps(zy)); + + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i0); + UpdateTileEventsY(triEventRemainder, triSlopeTileRemainder, triEdgeY, triEvent, + triSlopeTileDelta, triSlopeSign, i1); + } + } + } + } + + void TraverseScanline(Tile* tiles, int numRight, int numLeft, int leftOffset, int rightOffset, int tileIdx, int rightEvent, int leftEvent, v128* events, v128 zTriMin, v128 zTriMax, v128 iz0, float zx) + { + // Floor edge events to integer pixel coordinates (shift out fixed point bits) + int eventOffset = leftOffset << BufferGroup.TileWidthShift; + + v128* right = stackalloc v128[numRight]; + v128* left = stackalloc v128[numLeft]; + + for (int i = 0; i < numRight; ++i) + { + right[i] = IntrinsicUtils._mmw_max_epi32(X86.Sse2.sub_epi32(X86.Sse2.srai_epi32(events[rightEvent + i], BufferGroup.FpBits), X86.Sse2.set1_epi32(eventOffset)), X86.Sse2.setzero_si128()); + } + + for (int i = 0; i < numLeft; ++i) + { + left[i] = IntrinsicUtils._mmw_max_epi32(X86.Sse2.sub_epi32(X86.Sse2.srai_epi32(events[leftEvent - i], BufferGroup.FpBits), X86.Sse2.set1_epi32(eventOffset)), X86.Sse2.setzero_si128()); + } + + v128 z0 = X86.Sse.add_ps(iz0, X86.Sse.set1_ps(zx * leftOffset)); + int tileIdxEnd = tileIdx + rightOffset; + tileIdx += leftOffset; + + for (; ; ) + { + // Compute zMin for the overlapped layers + v128 mask = tiles[tileIdx].mask; + v128 zMin0 = IntrinsicUtils._mmw_blendv_ps(tiles[tileIdx].zMin0, tiles[tileIdx].zMin1, X86.Sse2.cmpeq_epi32(mask, X86.Sse2.set1_epi32(~0))); + v128 zMin1 = IntrinsicUtils._mmw_blendv_ps(tiles[tileIdx].zMin1, tiles[tileIdx].zMin0, X86.Sse2.cmpeq_epi32(mask, X86.Sse2.setzero_si128())); + v128 zMinBuf = X86.Sse.min_ps(zMin0, zMin1); + v128 dist0 = X86.Sse.sub_ps(zTriMax, zMinBuf); + + if (X86.Sse.movemask_ps(dist0) != SIMD_ALL_LANES_MASK) + { + // Compute coverage mask for entire 32xN using shift operations + v128 accumulatedMask = IntrinsicUtils._mmw_sllv_ones(left[0]); + + for (int i = 1; i < numLeft; ++i) + { + accumulatedMask = X86.Sse2.and_si128(accumulatedMask, IntrinsicUtils._mmw_sllv_ones(left[i])); + } + + for (int i = 0; i < numRight; ++i) + { + accumulatedMask = X86.Sse2.andnot_si128(IntrinsicUtils._mmw_sllv_ones(right[i]), accumulatedMask); + } + + // Compute interpolated min for each 8x4 subtile and update the masked hierarchical z buffer entry + v128 zSubTileMin = X86.Sse.max_ps(z0, zTriMin); + UpdateTileAccurate(tiles, tileIdx, IntrinsicUtils._mmw_transpose_epi8(accumulatedMask), zSubTileMin); + } + + // Update buffer address, interpolate z and edge events + tileIdx++; + + if (tileIdx >= tileIdxEnd) + { + break; + } + + z0 = X86.Sse.add_ps(z0, X86.Sse.set1_ps(zx)); + + v128 SimdTileWidth = X86.Sse2.set1_epi32(BufferGroup.TileWidth); + + for (int i = 0; i < numRight; ++i) + { + right[i] = X86.Sse2.subs_epu16(right[i], SimdTileWidth); // Trick, use sub saturated to avoid checking against < 0 for shift (values should fit in 16 bits) + } + + for (int i = 0; i < numLeft; ++i) + { + left[i] = X86.Sse2.subs_epu16(left[i], SimdTileWidth); + } + } + } + + void UpdateTileAccurate(Tile* tiles, int tileIdx, v128 coverage, v128 zTriv) + { + v128 zMin0 = tiles[tileIdx].zMin0; + v128 zMin1 = tiles[tileIdx].zMin1; + v128 mask = tiles[tileIdx].mask; + + // Swizzle coverage mask to 8x4 subtiles + v128 rastMask = coverage; + + // Perform individual depth tests with layer 0 & 1 and mask out all failing pixels + v128 sdist0 = X86.Sse.sub_ps(zMin0, zTriv); + v128 sdist1 = X86.Sse.sub_ps(zMin1, zTriv); + v128 sign0 = X86.Sse2.srai_epi32(sdist0, 31); + v128 sign1 = X86.Sse2.srai_epi32(sdist1, 31); + v128 triMask = X86.Sse2.and_si128(rastMask, X86.Sse2.or_si128(X86.Sse2.andnot_si128(mask, sign0), X86.Sse2.and_si128(mask, sign1))); + + // Early out if no pixels survived the depth test (this test is more accurate than + // the early culling test in TraverseScanline()) + v128 t0 = X86.Sse2.cmpeq_epi32(triMask, X86.Sse2.setzero_si128()); + v128 t0inv = IntrinsicUtils._mmw_not_epi32(t0); + + if (IntrinsicUtils._mmw_testz_epi32(t0inv, t0inv) != 0) + { + return; + } + +#if MOC_ENABLE_STATS + STATS_ADD(ref mStats.mOccluders.mNumTilesUpdated, 1); +#endif + + v128 zTri = IntrinsicUtils._mmw_blendv_ps(zTriv, zMin0, t0); + + // Test if incoming triangle completely overwrites layer 0 or 1 + v128 layerMask0 = X86.Sse2.andnot_si128(triMask, IntrinsicUtils._mmw_not_epi32(mask)); + v128 layerMask1 = X86.Sse2.andnot_si128(triMask, mask); + v128 lm0 = X86.Sse2.cmpeq_epi32(layerMask0, X86.Sse2.setzero_si128()); + v128 lm1 = X86.Sse2.cmpeq_epi32(layerMask1, X86.Sse2.setzero_si128()); + v128 z0 = IntrinsicUtils._mmw_blendv_ps(zMin0, zTri, lm0); + v128 z1 = IntrinsicUtils._mmw_blendv_ps(zMin1, zTri, lm1); + + // Compute distances used for merging heuristic + v128 d0 = IntrinsicUtils._mmw_abs_ps(sdist0); + v128 d1 = IntrinsicUtils._mmw_abs_ps(sdist1); + v128 d2 = IntrinsicUtils._mmw_abs_ps(X86.Sse.sub_ps(z0, z1)); + + // Find minimum distance + v128 c01 = X86.Sse.sub_ps(d0, d1); + v128 c02 = X86.Sse.sub_ps(d0, d2); + v128 c12 = X86.Sse.sub_ps(d1, d2); + // Two tests indicating which layer the incoming triangle will merge with or + // overwrite. d0min indicates that the triangle will overwrite layer 0, and + // d1min flags that the triangle will overwrite layer 1. + v128 d0min = X86.Sse2.or_si128(X86.Sse2.and_si128(c01, c02), X86.Sse2.or_si128(lm0, t0)); + v128 d1min = X86.Sse2.andnot_si128(d0min, X86.Sse2.or_si128(c12, lm1)); + + /* Update depth buffer entry. NOTE: we always merge into layer 0, so if the + triangle should be merged with layer 1, we first swap layer 0 & 1 and then + merge into layer 0. */ + + // Update mask based on which layer the triangle overwrites or was merged into + v128 inner = IntrinsicUtils._mmw_blendv_ps(triMask, layerMask1, d0min); + + // Update the zMin[0] value. There are four outcomes: overwrite with layer 1, + // merge with layer 1, merge with zTri or overwrite with layer 1 and then merge + // with zTri. + v128 e0 = IntrinsicUtils._mmw_blendv_ps(z0, z1, d1min); + v128 e1 = IntrinsicUtils._mmw_blendv_ps(z1, zTri, X86.Sse2.or_si128(d1min, d0min)); + + // Update the zMin[1] value. There are three outcomes: keep current value, + // overwrite with zTri, or overwrite with z1 + v128 z1t = IntrinsicUtils._mmw_blendv_ps(zTri, z1, d0min); + + tiles[tileIdx].zMin0 = X86.Sse.min_ps(e0, e1); + tiles[tileIdx].zMin1 = IntrinsicUtils._mmw_blendv_ps(z1t, z0, d1min); + tiles[tileIdx].mask = IntrinsicUtils._mmw_blendv_ps(inner, layerMask0, d1min); + } + + void UpdateTileEventsY(v128* triEventRemainder, v128* triSlopeTileRemainder, v128* triEdgeY, v128* triEvent, v128* triSlopeTileDelta, v128* triSlopeSign, int i) + { + triEventRemainder[i] = X86.Sse2.sub_epi32(triEventRemainder[i], triSlopeTileRemainder[i]); + v128 overflow = X86.Sse2.srai_epi32(triEventRemainder[i], 31); + triEventRemainder[i] = X86.Sse2.add_epi32(triEventRemainder[i], X86.Sse2.and_si128(overflow, triEdgeY[i])); + triEvent[i] = X86.Sse2.add_epi32(triEvent[i], X86.Sse2.add_epi32(triSlopeTileDelta[i], X86.Sse2.and_si128(overflow, triSlopeSign[i]))); + } + + void SortVertices(v128* vX, v128* vY) + { + // Rotate the triangle in the winding order until v0 is the vertex with lowest Y value + for (int i = 0; i < 2; i++) + { + v128 ey1 = X86.Sse2.sub_epi32(vY[1], vY[0]); + v128 ey2 = X86.Sse2.sub_epi32(vY[2], vY[0]); + v128 swapMask = X86.Sse2.or_si128(X86.Sse2.or_si128(ey1, ey2), X86.Sse2.cmpeq_epi32(ey2, X86.Sse2.setzero_si128())); + + v128 sX = IntrinsicUtils._mmw_blendv_ps(vX[2], vX[0], swapMask); + vX[0] = IntrinsicUtils._mmw_blendv_ps(vX[0], vX[1], swapMask); + vX[1] = IntrinsicUtils._mmw_blendv_ps(vX[1], vX[2], swapMask); + vX[2] = sX; + + v128 sY = IntrinsicUtils._mmw_blendv_ps(vY[2], vY[0], swapMask); + vY[0] = IntrinsicUtils._mmw_blendv_ps(vY[0], vY[1], swapMask); + vY[1] = IntrinsicUtils._mmw_blendv_ps(vY[1], vY[2], swapMask); + vY[2] = sY; + } + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs.meta new file mode 100644 index 0000000..d06ff30 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/RasterizeJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a589b429ed544146a99a8800d8f8e10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs new file mode 100644 index 0000000..72ceb76 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs @@ -0,0 +1,322 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine.Rendering; +using Unity.Rendering.Occlusion.Masked.Dots; + +namespace Unity.Rendering.Occlusion.Masked +{ + [BurstCompile] + unsafe struct TestJob : IJobParallelForDefer + { + public IndirectList VisibilityItems; + [ReadOnly] public ComponentTypeHandle ChunkHeader; + [ReadOnly] public ComponentTypeHandle OcclusionTest; + [ReadOnly] public ComponentTypeHandle ChunkOcclusionTest; + [ReadOnly] public ComponentTypeHandle EntitiesGraphicsChunkInfo; + [ReadOnly] public BatchCullingProjectionType ProjectionType; + [ReadOnly] public int NumTilesX; + [ReadOnly] public v128 HalfSize; + [ReadOnly] public v128 PixelCenter; + [ReadOnly] public v128 ScreenSize; + [ReadOnly] public BatchCullingViewType ViewType; + [ReadOnly] public int SplitIndex; + [ReadOnly, NativeDisableUnsafePtrRestriction] public Tile* Tiles; +#if UNITY_EDITOR + [ReadOnly] public bool DisplayOnlyOccluded; +#endif + + public void Execute(int index) + { + var visibilityItem = VisibilityItems.ElementAt(index); + var chunk = visibilityItem.Chunk; + var chunkVisibility = visibilityItem.Visibility; + + if (!chunk.HasChunkComponent(ChunkOcclusionTest)) + { + /* Because this is not a IJobChunk job, it will not filter by archetype. So there is no guarantee that + the current chunk has any occlusion test jobs on it. */ + return; + } + + var hybridChunkInfo = chunk.GetChunkComponentData(EntitiesGraphicsChunkInfo); + if (!hybridChunkInfo.Valid) + return; + + var anyLodEnabled = ( + hybridChunkInfo.CullingData.InstanceLodEnableds.Enabled[0] | + hybridChunkInfo.CullingData.InstanceLodEnableds.Enabled[1] + ) != 0; + + if (!anyLodEnabled) + { + /* Cull the whole chunk if no LODs are enabled */ + chunkVisibility->VisibleEntities[0] = 0; + chunkVisibility->VisibleEntities[1] = 0; + return; + } + + var chunkTest = chunk.GetChunkComponentData(ChunkOcclusionTest); + CullingResult chunkCullingResult = TestRect( + chunkTest.screenMin.xy, + chunkTest.screenMax.xy, + chunkTest.screenMin.w, + Tiles, + ProjectionType, + NumTilesX, + ScreenSize, + HalfSize, + PixelCenter + ); + + bool chunkVisible = (chunkCullingResult == CullingResult.VISIBLE); +#if UNITY_EDITOR + /* If we want to invert occlusion for debug purposes, we want to draw _only_ occluded entities. For this, we + want to run occlusion on every chunk, regardless of that chunk's test. A clearer but branch-ey way to + write this is: + + if (DisplayOnlyOccluded) { + chunkVisible = true; + } */ + chunkVisible |= DisplayOnlyOccluded; +#endif + if (!chunkVisible) + { + /* The chunk's bounding box fails the visibility test, which means that it's either frustum culled or + occlusion culled. Cull the whole chunk and early out. */ + if (ViewType != BatchCullingViewType.Light) + { + // When culling light views, we don't zero out the visible entities + // because the entity might be visible in a different split. This is + // not the case for other view types, so we must zero them out here. + chunkVisibility->VisibleEntities[0] = 0; + chunkVisibility->VisibleEntities[1] = 0; + } + return; + } + + var tests = chunk.GetNativeArray(OcclusionTest); + + /* Each chunk is guaranteed to have no more than 128 entities. So the Entities Graphics package uses `VisibleEntities`, + which is an array of two 64-bit integers to indicate whether each of these entities is visible. */ + for (int j = 0; j < 2; j++) + { + /* The pending bitfield indicates which incoming entities are to be occlusion-tested. + - If a bit is zero, the corresponding entity is already not drawn by a previously run system; e.g. it + might be frustum culled. So there's no need to process it further. + - If a bit is one, the corresponding entity needs to be occlusion-tested. */ + var pendingBitfield = chunkVisibility->VisibleEntities[j]; + ulong newBitfield = 0; + + /* Once the whole pending bitfield is zero, we don't need to do any more occlusion tests */ + while (pendingBitfield != 0) + { + /* Get the index of the first visible entity using tzcnt. For example: + + pendingBitfield = ...0000 0000 0000 1010 0000 + ▲ ▲ ▲ + │ │ │ + `leading zeros │ `trailing zeros + `tzcount = 5 + + Then add (j << 6) to it, which adds 64 if we're in the second bitfield, i.e. if we're covering + entities [65, 128]. + */ + var tzIndex = math.tzcnt(pendingBitfield); + var entityIndex = (j << 6) + tzIndex; + + /* If the view type is a light, then we check to see whether the current entity is already culled + in the current split. If the view type is not a light, we ignore the split mask and proceed to + occlusion cull. */ + bool entityAlreadyFrustumCulled = + ViewType == BatchCullingViewType.Light && + ((chunkVisibility->SplitMasks[entityIndex] & (1 <SplitMasks[entityIndex] &= (byte)~(1 << SplitIndex); + } + } + + if (ViewType != BatchCullingViewType.Light) + { + chunkVisibility->VisibleEntities[j] = newBitfield; + } + else + { + /* TODO: This will incur a bit of extra work later down the line in case of lights. It won't be too + much work because the splitMasks will indicate whether any split contains the entity. + This code will change once we handle all splits in the same job */ + chunkVisibility->VisibleEntities[j] |= newBitfield; + } + } + } + + public static CullingResult TestRect( + float2 min, + float2 max, + float wmin, + Tile* tiles, + BatchCullingProjectionType projectionType, + int NumTilesX, + v128 ScreenSize, + v128 HalfSize, + v128 PixelCenter + ) + { + if (min.x > 1.0f || min.y > 1.0f || max.x < -1.0f || max.y < -1.0f) + { + return CullingResult.VIEW_CULLED; + } + + // Compute screen space bounding box and guard for out of bounds + v128 pixelBBox = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.setr_ps(min.x, max.x, max.y, min.y), HalfSize, PixelCenter); + v128 pixelBBoxi = X86.Sse2.cvttps_epi32(pixelBBox); + pixelBBoxi = IntrinsicUtils._mmw_max_epi32(X86.Sse2.setzero_si128(), IntrinsicUtils._mmw_min_epi32(ScreenSize, pixelBBoxi)); + + // Pad bounding box to (32xN) tiles. Tile BB is used for looping / traversal + v128 SimdTilePad = X86.Sse2.setr_epi32(0, BufferGroup.TileWidth, 0, BufferGroup.TileHeight); + v128 SimdTilePadMask = X86.Sse2.setr_epi32( + ~(BufferGroup.TileWidth - 1), + ~(BufferGroup.TileWidth - 1), + ~(BufferGroup.TileHeight - 1), + ~(BufferGroup.TileHeight - 1) + ); + v128 tileBBoxi = X86.Sse2.and_si128(X86.Sse2.add_epi32(pixelBBoxi, SimdTilePad), SimdTilePadMask); + + int txMin = tileBBoxi.SInt0 >> BufferGroup.TileWidthShift; + int txMax = tileBBoxi.SInt1 >> BufferGroup.TileWidthShift; + int tileRowIdx = (tileBBoxi.SInt2 >> BufferGroup.TileHeightShift) * NumTilesX; + int tileRowIdxEnd = (tileBBoxi.SInt3 >> BufferGroup.TileHeightShift) * NumTilesX; + + // Pad bounding box to (8x4) subtiles. Skip SIMD lanes outside the subtile BB + v128 SimdSubTilePad = X86.Sse2.setr_epi32(0, BufferGroup.SubTileWidth, 0, BufferGroup.SubTileHeight); + v128 SimdSubTilePadMask = X86.Sse2.setr_epi32( + ~(BufferGroup.SubTileWidth - 1), + ~(BufferGroup.SubTileWidth - 1), + ~(BufferGroup.SubTileHeight - 1), + ~(BufferGroup.SubTileHeight - 1) + ); + v128 subTileBBoxi = X86.Sse2.and_si128(X86.Sse2.add_epi32(pixelBBoxi, SimdSubTilePad), SimdSubTilePadMask); + + v128 stxmin = X86.Sse2.set1_epi32(subTileBBoxi.SInt0 - 1); // - 1 to be able to use GT test + v128 stymin = X86.Sse2.set1_epi32(subTileBBoxi.SInt2 - 1); // - 1 to be able to use GT test + v128 stxmax = X86.Sse2.set1_epi32(subTileBBoxi.SInt1); + v128 stymax = X86.Sse2.set1_epi32(subTileBBoxi.SInt3); + + // Setup pixel coordinates used to discard lanes outside subtile BB + v128 SimdSubTileColOffset = X86.Sse2.setr_epi32( + 0, + BufferGroup.SubTileWidth, + BufferGroup.SubTileWidth * 2, + BufferGroup.SubTileWidth * 3 + ); + v128 startPixelX = X86.Sse2.add_epi32(SimdSubTileColOffset, X86.Sse2.set1_epi32(tileBBoxi.SInt0)); + // TODO: (Apoorva) LHS is zero. We can just use the RHS directly. + v128 pixelY = X86.Sse2.add_epi32(X86.Sse2.setzero_si128(), X86.Sse2.set1_epi32(tileBBoxi.SInt2)); + + // Compute z from w. Note that z is reversed order, 0 = far, 1/near = near, which + // means we use a greater than test, so zMax is used to test for visibility. (z goes from 0 = far to 2 = near for ortho) + + v128 zMax; + if (projectionType == BatchCullingProjectionType.Orthographic) + { + zMax = IntrinsicUtils._mmw_fmadd_ps(X86.Sse.set1_ps(-1.0f), X86.Sse.set1_ps(wmin), X86.Sse.set1_ps(1.0f)); + } + else + { + zMax = X86.Sse.div_ps(X86.Sse.set1_ps(1f), X86.Sse.set1_ps(wmin)); + } + + for (; ; ) + { + v128 pixelX = startPixelX; + + for (int tx = txMin; ;) + { + int tileIdx = tileRowIdx + tx; + + // Fetch zMin from masked hierarchical Z buffer + v128 mask = tiles[tileIdx].mask; + v128 zMin0 = IntrinsicUtils._mmw_blendv_ps(tiles[tileIdx].zMin0, tiles[tileIdx].zMin1, X86.Sse2.cmpeq_epi32(mask, X86.Sse2.set1_epi32(~0))); + v128 zMin1 = IntrinsicUtils._mmw_blendv_ps(tiles[tileIdx].zMin1, tiles[tileIdx].zMin0, X86.Sse2.cmpeq_epi32(mask, X86.Sse2.setzero_si128())); + v128 zBuf = X86.Sse.min_ps(zMin0, zMin1); + + // Perform conservative greater than test against hierarchical Z buffer (zMax >= zBuf means the subtile is visible) + v128 zPass = X86.Sse.cmpge_ps(zMax, zBuf); //zPass = zMax >= zBuf ? ~0 : 0 + + // Mask out lanes corresponding to subtiles outside the bounding box + v128 bboxTestMin = X86.Sse2.and_si128(X86.Sse2.cmpgt_epi32(pixelX, stxmin), X86.Sse2.cmpgt_epi32(pixelY, stymin)); + v128 bboxTestMax = X86.Sse2.and_si128(X86.Sse2.cmpgt_epi32(stxmax, pixelX), X86.Sse2.cmpgt_epi32(stymax, pixelY)); + v128 boxMask = X86.Sse2.and_si128(bboxTestMin, bboxTestMax); + zPass = X86.Sse2.and_si128(zPass, boxMask); + + // If not all tiles failed the conservative z test we can immediately terminate the test + if (IntrinsicUtils._mmw_testz_epi32(zPass, zPass) == 0) + { + return CullingResult.VISIBLE; + } + + if (++tx >= txMax) + { + break; + } + + pixelX = X86.Sse2.add_epi32(pixelX, X86.Sse2.set1_epi32(BufferGroup.TileWidth)); + } + + tileRowIdx += NumTilesX; + + if (tileRowIdx >= tileRowIdxEnd) + { + break; + } + + pixelY = X86.Sse2.add_epi32(pixelY, X86.Sse2.set1_epi32(BufferGroup.TileHeight)); + } + + return CullingResult.OCCLUDED; + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs.meta new file mode 100644 index 0000000..c292122 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/TestJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d6949e79a8777c479ab01b6fb4a913a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Types.cs b/Unity.Entities.Graphics/Occlusion/Masked/Types.cs new file mode 100644 index 0000000..73d0788 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Types.cs @@ -0,0 +1,27 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst.Intrinsics; + +namespace Unity.Rendering.Occlusion.Masked +{ + public enum CullingResult + { + VISIBLE = 0x0, + OCCLUDED = 0x1, + VIEW_CULLED = 0x3 + } + public struct Tile + { + public v128 zMin0; + public v128 zMin1; + public v128 mask; + } + public struct ScissorRect + { + public int mMinX; //!< Screen space X coordinate for left side of scissor rect, inclusive and must be a multiple of 32 + public int mMinY; //!< Screen space Y coordinate for bottom side of scissor rect, inclusive and must be a multiple of 8 + public int mMaxX; //!< Screen space X coordinate for right side of scissor rect, non inclusive and must be a multiple of 32 + public int mMaxY; //!< Screen space Y coordinate for top side of scissor rect, non inclusive and must be a multiple of 8 + } +} +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Types.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Types.cs.meta new file mode 100644 index 0000000..415b14a --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Types.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a20d2018e9b8ebd4a8596a1c06a49b39 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization.meta new file mode 100644 index 0000000..fa8f3dd --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0d5a27072eec390429c8b3a88f0f8d56 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs new file mode 100644 index 0000000..6bffd7b --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs @@ -0,0 +1,164 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) +// This class contains the debug settings exposed to the rendering debugger window +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Rendering; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + public enum DebugRenderMode + { + None = 0, + Depth = 1, + Test = 2, + Mesh = 3, + Bounds = 4, + Inverted = 5 + } + + public class DebugSettings : IDebugData + { + public bool freezeOcclusion; + public DebugRenderMode debugRenderMode; + + GUIContent[] m_ViewNames; + ulong[] m_ViewIDs; + int[] m_ViewIndices; + int m_SelectedViewIndex; + DebugUI.Widget[] m_Widgets; + + readonly string k_PanelName = "Culling"; + + void Reset() + { + freezeOcclusion = false; + debugRenderMode = DebugRenderMode.None; + m_ViewNames = new GUIContent[]{new GUIContent("None")}; + m_ViewIDs = new ulong[]{0}; + m_ViewIndices = new int[]{0}; + m_SelectedViewIndex = 0; + } + + Action IDebugData.GetReset() => Reset; + + public DebugSettings() + { + Reset(); + } + + public ulong? GetPinnedViewID() + { + return (m_SelectedViewIndex == 0) ? null : m_ViewIDs[m_SelectedViewIndex]; + } + + public void Register() + { + var widgetList = new List(); + widgetList.Add(new DebugUI.Container + { + displayName = "Occlusion Culling", + flags = DebugUI.Flags.EditorOnly, + children = + { + new DebugUI.BoolField + { + nameAndTooltip = new() + { + name = "Freeze Occlusion", + tooltip = "Enable to pause updating the occlusion while freely moving the camera." + }, + getter = () => freezeOcclusion, + setter = value => { freezeOcclusion = value; }, + }, + new DebugUI.EnumField + { + nameAndTooltip = new() + { + name = "Pinned View", + tooltip = + "Use the drop-down to pin a view. All scene views and game views will cull objects from the pinned view's perspective." + }, + getter = () => m_SelectedViewIndex, + setter = value => { m_SelectedViewIndex = value; }, + enumNames = m_ViewNames, + enumValues = m_ViewIndices, + getIndex = () => m_SelectedViewIndex, + setIndex = value => { m_SelectedViewIndex = value; } + }, + new DebugUI.EnumField + { + nameAndTooltip = new() + { + name = "Debug Mode", + tooltip = + "Use the drop-down to select a rendering mode to display as an overlay on the screen." + }, + getter = () => (int)debugRenderMode, + setter = value => { debugRenderMode = (DebugRenderMode)value; }, + getIndex = () => (int)debugRenderMode, + setIndex = value => { debugRenderMode = (DebugRenderMode)value; }, + autoEnum = typeof(DebugRenderMode), + } + } + }); + + var panel = DebugManager.instance.GetPanel(k_PanelName, true); + m_Widgets = widgetList.ToArray(); + panel.children.Add(m_Widgets); + + DebugManager.instance.RegisterData(this); + } + + public void RefreshViews(Dictionary bufferGroups) + { +#if UNITY_EDITOR + var ids = new List(bufferGroups.Count); + var names = new List(); + ids.Add(0); + names.Add(new GUIContent("None")); + + foreach (var pair in bufferGroups) + { + var instanceID = (int)(pair.Key & 0xffffffff); + var splitIndex = (int)(pair.Key >> 32); + var viewType = pair.Value.ViewType; + var obj = (Behaviour) EditorUtility.InstanceIDToObject(instanceID); + if (!obj || !obj.gameObject.activeInHierarchy) + { + continue; + } + + var label = $"{obj.name}"; + if (viewType != BatchCullingViewType.Camera) + { + label += $", Split {splitIndex}"; + } + + names.Add(new GUIContent(label)); + ids.Add(pair.Key); + } + + m_SelectedViewIndex = 0; + m_ViewNames = names.ToArray(); + m_ViewIndices = Enumerable.Range(0, m_ViewNames.Count()).ToArray(); + m_ViewIDs = ids.ToArray(); + + Unregister(); + Register(); +#endif + } + + public void Unregister() + { + var panel = DebugManager.instance.GetPanel(k_PanelName); + panel?.children.Remove(m_Widgets); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs.meta new file mode 100644 index 0000000..60403db --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 06174c0a65cb5a04698ace71a469a1c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs new file mode 100644 index 0000000..1d0aadc --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs @@ -0,0 +1,369 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.Rendering; +using Object = UnityEngine.Object; +using Unity.Rendering.Occlusion.Masked.Dots; +using Unity.Transforms; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* This class stores the resources needed to draw debug visualizations for any view that uses occlusion culling. + Views can be cameras, lights' shadowmaps, reflection probes, or anything else that renders the scene. + + A debug view is allocated only when a debug visualization is requested by the user. Otherwise no memory is + allocated for debug purposes. */ + public unsafe class DebugView + { + static readonly int s_DepthPropertyID = Shader.PropertyToID("_Depth"); + static readonly int s_OverlayPropertyID = Shader.PropertyToID("_Overlay"); + static readonly int s_YFlipPropertyID = Shader.PropertyToID("_YFlip"); + static readonly int s_TransformPropertyID = Shader.PropertyToID("_Transform"); + static readonly int s_OnlyOverlayPropertyID = Shader.PropertyToID("_OnlyOverlay"); + static readonly int s_OnlyDepthPropertyID = Shader.PropertyToID("_OnlyDepth"); + static readonly Shader s_CompositeShader = Shader.Find("Hidden/OcclusionDebugComposite"); + static readonly Shader s_OccluderShader = Shader.Find("Hidden/OcclusionDebugOccluders"); + + static readonly ProfilerMarker s_MaskedDepthToPixelDepth = new ProfilerMarker("Occlusion.Debug.RenderView.MaskedDepthToPixelDepth"); + static readonly ProfilerMarker s_OcclusionTests = new ProfilerMarker("Occlusion.Debug.RenderView.OcclusionTests"); + static readonly ProfilerMarker s_GenerateAABBMesh = new ProfilerMarker("Occlusion.Debug.RenderView.GenerateAABBMesh"); + static readonly ProfilerMarker s_GenerateOutlineMesh = new ProfilerMarker("Occlusion.Debug.RenderView.GenerateOutlineMesh"); + static readonly ProfilerMarker s_GenerateOccluderMesh = new ProfilerMarker("Occlusion.Debug.RenderView.GenerateOccluderMesh"); + + static readonly CommandBuffer s_CmdLayers = new CommandBuffer() { name = "Occlusion debug layers" }; + Material s_OccludeeAABBsMaterial; + + // Memory used to upload CPU depth to GPU + NativeArray m_CPUDepth; + public Texture2D gpuDepth; + // Debug texture drawn to the screen as an overlay + RenderTexture m_Overlay; + // Single mesh containing AABBs of all culled occludees + Mesh m_OccludeeAabBsMesh; + Material m_OccluderMaterial; + // Single mesh containing all the occluder meshes + Mesh m_OccludersMesh; + VertexAttributeDescriptor[] m_OccluderVertexLayout; + // Final composite + Material m_CompositeMaterial; + + private bool AnyMeshOrMaterialNull() + { + return m_OccludeeAabBsMesh == null || m_OccluderMaterial == null || m_OccludersMesh == null || m_CompositeMaterial == null || s_OccludeeAABBsMaterial == null; + } + private void CreateMeshAndMaterials() + { + m_OccludeeAabBsMesh = new Mesh(); + m_OccludersMesh = new Mesh(); + m_OccluderMaterial = new Material(s_OccluderShader); + m_OccluderMaterial.SetPass(0); + m_CompositeMaterial = new Material(s_CompositeShader); + m_CompositeMaterial.SetPass(0); + m_OccluderVertexLayout = new[] + { + new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 4), + new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4, stream: 1) + }; + s_OccludeeAABBsMaterial = new Material(Shader.Find("Hidden/OccludeeScreenSpaceAABB")); + } + public DebugView() + { + } + + public void Dispose() + { + m_CPUDepth.Dispose(); + Object.DestroyImmediate(gpuDepth); + Object.DestroyImmediate(m_Overlay); + } + + // Ensure that the member resources fit the dimensions of the view + public void ReallocateIfNeeded(int viewWidth, int viewHeight) + { + if (AnyMeshOrMaterialNull()) + { + CreateMeshAndMaterials(); + } + + // Reallocate depth texture if needed + if (!gpuDepth || gpuDepth.width != viewWidth || gpuDepth.height != viewHeight) + { + if (gpuDepth) + { + Object.DestroyImmediate(gpuDepth); + } + gpuDepth = new Texture2D(viewWidth, viewHeight, TextureFormat.RFloat, false); + m_CompositeMaterial.SetTexture(s_DepthPropertyID, gpuDepth); + } + // Reallocate overlay texture if needed + if (!m_Overlay || m_Overlay.width != viewWidth || m_Overlay.height != viewHeight) + { + if (m_Overlay) + { + Object.DestroyImmediate(m_Overlay); + } + m_Overlay = new RenderTexture(viewWidth, viewHeight, 1, RenderTextureFormat.RFloat, 0); + m_CompositeMaterial.SetTexture(s_OverlayPropertyID, m_Overlay); + } + // Reallocate cpu-side debug depth buffer if needed + if (!m_CPUDepth.IsCreated || m_CPUDepth.Length != viewWidth * viewHeight) + { + if (m_CPUDepth.IsCreated) + { + m_CPUDepth.Dispose(); + } + m_CPUDepth = new NativeArray(viewWidth * viewHeight, Allocator.Persistent); + } + } + + // Render to the depth buffer and the overlay texture + public void RenderToTextures( + EntityQuery testQuery, + EntityQuery meshQuery, + BufferGroup bufferGroup, + DebugRenderMode mode, + bool isOcclusionBrowseWindowVisible + ) + { + if(AnyMeshOrMaterialNull()) + { + CreateMeshAndMaterials(); + } + s_CmdLayers.Clear(); + // Write the CPU-rasterized depth buffer to a GPU texture, and then blit it to the overlay + if (mode == DebugRenderMode.Depth || + mode == DebugRenderMode.Test || + isOcclusionBrowseWindowVisible) + { + s_MaskedDepthToPixelDepth.Begin(); + int width = bufferGroup.NumPixelsX; + int height = bufferGroup.NumPixelsY; + int numTilesX = bufferGroup.NumTilesX; + var job = new DecodeMaskedDepthJob() + { + // In + NumPixelsX = width, + NumPixelsY = height, + NumTilesX = numTilesX, + Tiles = (Tile*)bufferGroup.Tiles.GetUnsafeReadOnlyPtr(), + // Out + DecodedZBuffer = m_CPUDepth, + }; + job.Schedule((width * height), 64).Complete(); + + gpuDepth.SetPixelData(m_CPUDepth, 0); + gpuDepth.Apply(); + s_MaskedDepthToPixelDepth.End(); + } + + if (mode == DebugRenderMode.Test) + { + s_OcclusionTests.Begin(); + + // Extract AABBs which are tested and culled due to occlusion culling + NativeArray allTests = testQuery.ToComponentDataArray(Allocator.TempJob); + var culledTestsQueue = new NativeQueue(Allocator.TempJob); + + var testJob = new FilterOccludedTestJob() + { + // In + ProjectionType = bufferGroup.ProjectionType, + NumTilesX = bufferGroup.NumTilesX, + HalfSize = bufferGroup.HalfSize, + PixelCenter = bufferGroup.PixelCenter, + ScreenSize = bufferGroup.ScreenSize, + Tiles = (Tile*)bufferGroup.Tiles.GetUnsafeReadOnlyPtr(), + AllTests = allTests, + // Out + culledTestsQueue = culledTestsQueue.AsParallelWriter(), + }; + testJob.Schedule(allTests.Length, 1).Complete(); + var culledTests = culledTestsQueue.ToArray(Allocator.TempJob); + culledTestsQueue.Dispose(); + allTests.Dispose(); + s_OcclusionTests.End(); + + s_GenerateAABBMesh.Begin(); + // Create a mesh with an AABB for every culled occludee + { + var dataArray = Mesh.AllocateWritableMeshData(1); + var data = dataArray[0]; + // We need 4 verts and 6 indices per AABB + data.SetVertexBufferParams(4 * culledTests.Length, new VertexAttributeDescriptor(VertexAttribute.Position)); + data.SetIndexBufferParams(6 * culledTests.Length, IndexFormat.UInt16); + NativeArray verts = data.GetVertexData(); + NativeArray indices = data.GetIndexData(); + // Fill the mesh data in a job + var job = new OccludeeAABBJob() + { + // In + CulledTests = culledTests, + // Out + Verts = verts, + Indices = indices, + }; + job.Schedule(culledTests.Length, 32).Complete(); + data.subMeshCount = 1; + data.SetSubMesh(0, new SubMeshDescriptor(0, indices.Length)); + // Create the mesh and apply data to it: + Mesh.ApplyAndDisposeWritableMeshData(dataArray, m_OccludeeAabBsMesh); + m_OccludeeAabBsMesh.RecalculateBounds(); + } + + // Draw to the overlay + s_CmdLayers.SetRenderTarget(m_Overlay); + s_CmdLayers.ClearRenderTarget(false, true, Color.clear); + s_CmdLayers.DrawMesh(m_OccludeeAabBsMesh, Matrix4x4.identity, s_OccludeeAABBsMaterial); + culledTests.Dispose(); + s_GenerateAABBMesh.End(); + } + else if (mode == DebugRenderMode.Bounds) + { + s_GenerateOutlineMesh.Begin(); + // Create a mesh with an AABB for each occludee, regardless of whether it is culled or not + { + NativeArray allTests = testQuery.ToComponentDataArray(Allocator.TempJob); + + var dataArray = Mesh.AllocateWritableMeshData(1); + var data = dataArray[0]; + // We need 4 verts and 6 indices per AABB + data.SetVertexBufferParams(16 * allTests.Length, new VertexAttributeDescriptor(VertexAttribute.Position)); + data.SetIndexBufferParams(24 * allTests.Length, IndexFormat.UInt32); + NativeArray verts = data.GetVertexData(); + NativeArray indices = data.GetIndexData(); + // Fill the mesh data in a job + var job = new OccludeeOutlineJob() + { + // In + InvResolution = new float2(1f / m_Overlay.width, 1f / m_Overlay.height), + AllTests = allTests, + // Out + Verts = verts, + Indices = indices, + }; + job.Schedule(allTests.Length, 32).Complete(); + data.subMeshCount = 1; + data.SetSubMesh(0, new SubMeshDescriptor(0, indices.Length)); + // Create the mesh and apply data to it: + Mesh.ApplyAndDisposeWritableMeshData(dataArray, m_OccludeeAabBsMesh); + m_OccludeeAabBsMesh.RecalculateBounds(); + allTests.Dispose(); + } + s_CmdLayers.SetRenderTarget(m_Overlay); + s_CmdLayers.ClearRenderTarget(false, true, Color.clear); + s_CmdLayers.DrawMesh(m_OccludeeAabBsMesh, Matrix4x4.identity, s_OccludeeAABBsMaterial); + s_GenerateOutlineMesh.End(); + } + else if (mode == DebugRenderMode.Mesh) + { + s_GenerateOccluderMesh.Begin(); + NativeArray meshes = meshQuery.ToComponentDataArray(Allocator.TempJob); + NativeArray localToWorlds = meshQuery.ToComponentDataArray(Allocator.TempJob); + // To parallelize mesh aggregation, we first need to find the total number of vertices and indices + // across all occluder meshes. We also need to precompute the offsets of each occluder mesh in the + // aggregated vertex and index buffers. + var vertOffsets = new NativeArray(meshes.Length, Allocator.TempJob); + var indexOffsets = new NativeArray(meshes.Length, Allocator.TempJob); + int numVerts = 0; + int numIndices = 0; + { + for (int i = 0; i < meshes.Length; i++) + { + vertOffsets[i] = numVerts; + numVerts += meshes[i].vertexCount; + } + + for (int i = 0; i < meshes.Length; i++) + { + indexOffsets[i] = numIndices; + numIndices += meshes[i].indexCount; + } + } + + // Prepare a single mesh containing all of the occluders + { + var dataArray = Mesh.AllocateWritableMeshData(1); + var data = dataArray[0]; + data.SetVertexBufferParams(numVerts, m_OccluderVertexLayout); + // We use a 32-bit index buffer to handle large meshes. + data.SetIndexBufferParams(numIndices, IndexFormat.UInt32); + NativeArray verts = data.GetVertexData(); + NativeArray colors = data.GetVertexData(stream: 1); + NativeArray indices = data.GetIndexData(); + // Fill the mesh data in a job + new MeshAggregationJob() + { + // In + Meshes = meshes, + LocalToWorlds = localToWorlds, + VertOffsets = vertOffsets, + IndexOffsets = indexOffsets, + // Out + Verts = verts, + Colors = colors, + Indices = indices, + }.Schedule(meshes.Length, 4) + .Complete(); + data.subMeshCount = 1; + data.SetSubMesh(0, new SubMeshDescriptor(0, indices.Length)); + // Create the mesh and apply data to it: + Mesh.ApplyAndDisposeWritableMeshData(dataArray, m_OccludersMesh); + // the vertices are already in screenspace and perspective projected + m_OccludersMesh.bounds = new Bounds(new Vector3(-1000, -1000, -1000), new Vector3(10000, 10000, 1000)); + } + + vertOffsets.Dispose(); + indexOffsets.Dispose(); + meshes.Dispose(); + localToWorlds.Dispose(); + s_GenerateOccluderMesh.End(); + } + Graphics.ExecuteCommandBuffer(s_CmdLayers); + } + + public void RenderToCamera(DebugRenderMode renderMode, Camera camera, CommandBuffer cmd, Mesh fullScreenQuad, Matrix4x4 cullingMatrix) + { + if (renderMode == DebugRenderMode.None) + return; + + float yFlip = 0f; +#if UNITY_EDITOR + if(camera.cameraType == CameraType.Preview) + { + yFlip = 1f; + } + if (Camera.current != null && camera == SceneView.currentDrawingSceneView.camera) + { + yFlip = 1f; + } +#endif // UNITY_EDITOR + + if (renderMode == DebugRenderMode.Mesh) + { + var material = m_OccluderMaterial; + material.SetFloat(s_YFlipPropertyID, yFlip); + material.SetMatrix(s_TransformPropertyID, cullingMatrix); + cmd.DrawMesh(m_OccludersMesh, Matrix4x4.identity, material); + } + else + { + m_CompositeMaterial.SetFloat(s_YFlipPropertyID, yFlip); + m_CompositeMaterial.SetFloat(s_OnlyOverlayPropertyID, (renderMode == DebugRenderMode.Bounds) ? 1f : 0f); + m_CompositeMaterial.SetFloat(s_OnlyDepthPropertyID, (renderMode == DebugRenderMode.Depth) ? 1f : 0f); + cmd.DrawMesh(fullScreenQuad, Matrix4x4.identity, m_CompositeMaterial); + } + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs.meta new file mode 100644 index 0000000..872b1e0 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DebugView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7907546ec8bf0544ca55d4bf82b6694f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs new file mode 100644 index 0000000..d07c092 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs @@ -0,0 +1,53 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* Unpack the CPU-rasterized masked depth buffer into a human-readable depth buffer. */ + [BurstCompile] + public unsafe struct DecodeMaskedDepthJob : IJobParallelFor + { + [ReadOnly] public int NumPixelsX; + [ReadOnly] public int NumPixelsY; + [ReadOnly] public int NumTilesX; + [ReadOnly, NativeDisableUnsafePtrRestriction] public Tile* Tiles; + + [WriteOnly] public NativeArray DecodedZBuffer; + + public void Execute(int i) + { + int x = i % NumPixelsX; + int y = NumPixelsY - i / NumPixelsX; + + // Compute 32xN tile index (SIMD value offset) + int tx = x / BufferGroup.TileWidth; + int ty = y / BufferGroup.TileHeight; + int tileIdx = ty * NumTilesX + tx; + + // Compute 8x4 subtile index (SIMD lane offset) + int stx = (x % BufferGroup.TileWidth) / BufferGroup.SubTileWidth; + int sty = (y % BufferGroup.TileHeight) / BufferGroup.SubTileHeight; + int subTileIdx = sty * 4 + stx; + + // Compute pixel index in subtile (bit index in 32-bit word) + int px = (x % BufferGroup.SubTileWidth); + int py = (y % BufferGroup.SubTileHeight); + int bitIdx = py * 8 + px; + + int pixelLayer = (IntrinsicUtils.getIntLane(Tiles[tileIdx].mask, (uint) subTileIdx) >> + bitIdx) & 1; + float pixelDepth = IntrinsicUtils.getFloatLane( + pixelLayer == 0 ? Tiles[tileIdx].zMin0 : Tiles[tileIdx].zMin1, + (uint) subTileIdx + ); + + DecodedZBuffer[i] = pixelDepth; + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs.meta new file mode 100644 index 0000000..3b52da4 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/DecodeMaskedDepthJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 336c766ce50905f418cde3a31b9359e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs new file mode 100644 index 0000000..74720a9 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs @@ -0,0 +1,52 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Rendering.Occlusion.Masked.Dots; +using UnityEngine.Rendering; + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* Take in all tests (i.e. occludees), and only return the ones which the occlusion system identifies as being fully + occluded by other geometry. */ + [BurstCompile] + public unsafe struct FilterOccludedTestJob : IJobParallelFor + { + [ReadOnly] public BatchCullingProjectionType ProjectionType; + [ReadOnly] public int NumTilesX; + [ReadOnly] public v128 HalfSize; + [ReadOnly] public v128 PixelCenter; + [ReadOnly] public v128 ScreenSize; + [ReadOnly, NativeDisableUnsafePtrRestriction] public Tile* Tiles; + [ReadOnly] public NativeArray AllTests; + + [WriteOnly] public NativeQueue.ParallelWriter culledTestsQueue; + + public void Execute(int i) + { + OcclusionTest test = AllTests[i]; + + CullingResult cullingResult = TestJob.TestRect( + test.screenMin.xy, + test.screenMax.xy, + test.screenMin.w, + Tiles, + ProjectionType, + NumTilesX, + ScreenSize, + HalfSize, + PixelCenter + ); + + if (cullingResult == CullingResult.OCCLUDED) + { + culledTestsQueue.Enqueue(test); + } + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs.meta new file mode 100644 index 0000000..7b7126d --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/FilterOccludedTestJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a1d8335f7892884d8e4078545785e2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs new file mode 100644 index 0000000..8e7541e --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs @@ -0,0 +1,72 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using Random = Unity.Mathematics.Random; +using Unity.Rendering.Occlusion.Masked.Dots; +using Unity.Transforms; + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* Return a single mesh containing all of the input meshes, with a stable random color assigned to each source mesh. */ + [BurstCompile] + public unsafe struct MeshAggregationJob : IJobParallelFor + { + [ReadOnly] public NativeArray Meshes; + [ReadOnly] public NativeArray LocalToWorlds; + [ReadOnly] public NativeArray VertOffsets; + [ReadOnly] public NativeArray IndexOffsets; + + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Verts; + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Colors; + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Indices; + + public void Execute(int m) + { + OcclusionMesh mesh = Meshes[m]; + var srcVerts = (float3*) mesh.vertexData.GetUnsafePtr(); + + // Create a random fully saturated color from the mesh index + Color32 col; + { + float hue = Random.CreateFromIndex((uint) m).NextFloat(); + float h6 = 6f * hue; + float3 c = math.saturate( + new float3( + 2f - math.abs(h6 - 4f), + math.abs(h6 - 3f) - 1f, + 2f - math.abs(h6 - 2f) + ) + ); + col = new Color32((byte) (c.x * 255), (byte) (c.y * 255), (byte) (c.z * 255), 255); + } + // Copy over all the vertices, transforming them into world space. Also assign a random color. + for (int i = 0; i < mesh.vertexCount; i++) + { + // We discarded the last row of the occluder matrix to save memory bandwidth because it is always (0, 0, 0,1). + // However, to perform the actual math, we still need a 4x4 matrix. So we reintroduce the row here. + float4x4 occluderMtx = new float4x4( + new float4(mesh.localTransform.c0, 0f), + new float4(mesh.localTransform.c1, 0f), + new float4(mesh.localTransform.c2, 0f), + new float4(mesh.localTransform.c3, 1f) + ); + Verts[VertOffsets[m] + i] = math.mul(math.mul(LocalToWorlds[m].Value, occluderMtx), new float4(srcVerts[i], 1.0f)); + Colors[VertOffsets[m] + i] = col; + } + + // Copy over all the indices, while adding an offset + var srcIndices = (int*) mesh.indexData.GetUnsafePtr(); + for (int i = 0; i < mesh.indexCount; i++) + { + Indices[IndexOffsets[m] + i] = VertOffsets[m] + srcIndices[i]; + } + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs.meta new file mode 100644 index 0000000..4c4eeca --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/MeshAggregationJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0467e4e78d9234b4f9ff3863971e4841 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs new file mode 100644 index 0000000..6b3a8d6 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs @@ -0,0 +1,44 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using UnityEngine; +using Unity.Rendering.Occlusion.Masked.Dots; + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* Return a mesh with one quad for each test (i.e. occludee). */ + [BurstCompile] + public struct OccludeeAABBJob : IJobParallelFor + { + [ReadOnly] public NativeArray CulledTests; + + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Verts; + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Indices; + + public void Execute(int i) + { + var test = CulledTests[i]; + + int vertBase = 4 * i; + int indexBase = 6 * i; + + Verts[vertBase] = new Vector3(test.screenMin.x, 0f - test.screenMin.y, 0.5f); + Verts[vertBase + 1] = new Vector3(test.screenMax.x, 0f - test.screenMin.y, 0.5f); + Verts[vertBase + 2] = new Vector3(test.screenMin.x, 0f - test.screenMax.y, 0.5f); + Verts[vertBase + 3] = new Vector3(test.screenMax.x, 0f - test.screenMax.y, 0.5f); + + Indices[indexBase] = (ushort) vertBase; + Indices[indexBase + 1] = (ushort) (vertBase + 1); + Indices[indexBase + 2] = (ushort) (vertBase + 2); + Indices[indexBase + 3] = (ushort) (vertBase + 2); + Indices[indexBase + 4] = (ushort) (vertBase + 1); + Indices[indexBase + 5] = (ushort) (vertBase + 3); + } + + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs.meta new file mode 100644 index 0000000..78a5a38 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeAABBJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d7a80ad4bf37204f8eb6e7385261773 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs new file mode 100644 index 0000000..0d485b0 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs @@ -0,0 +1,92 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using Unity.Rendering.Occlusion.Masked.Dots; + +namespace Unity.Rendering.Occlusion.Masked.Visualization +{ + /* Return a mesh with four quad forming an outline for each test (i.e. occludee). */ + [BurstCompile] + public struct OccludeeOutlineJob : IJobParallelFor + { + [ReadOnly] public float2 InvResolution; + [ReadOnly] public NativeArray AllTests; + + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Verts; + [WriteOnly, NativeDisableContainerSafetyRestriction] public NativeArray Indices; + + public void Execute(int i) + { + var test = AllTests[i]; + + int vertBase = 16 * i; + int indexBase = 24 * i; + + float px = 2f * InvResolution.x; // Size of 1 pixel in texture space + float py = 2f * InvResolution.y; + float xmin = test.screenMin.x; + float ymin = test.screenMin.y; + float xmax = test.screenMax.x; + float ymax = test.screenMax.y; + + // Left edge + Verts[vertBase] = new Vector3(xmin, -ymin, 0.5f); + Verts[vertBase + 1] = new Vector3(xmin + px, -ymin, 0.5f); + Verts[vertBase + 2] = new Vector3(xmin, -ymax, 0.5f); + Verts[vertBase + 3] = new Vector3(xmin + px, -ymax, 0.5f); + + // Right edge + Verts[vertBase + 4] = new Vector3(xmax - px, -ymin, 0.5f); + Verts[vertBase + 5] = new Vector3(xmax, -ymin, 0.5f); + Verts[vertBase + 6] = new Vector3(xmax - px, -ymax, 0.5f); + Verts[vertBase + 7] = new Vector3(xmax, -ymax, 0.5f); + + // Top edge + Verts[vertBase + 8] = new Vector3(xmin + px, -ymin, 0.5f); + Verts[vertBase + 9] = new Vector3(xmax - px, -ymin, 0.5f); + Verts[vertBase + 10] = new Vector3(xmin + px, -(ymin + py), 0.5f); + Verts[vertBase + 11] = new Vector3(xmax - px, -(ymin + py), 0.5f); + + // Bottom edge + Verts[vertBase + 12] = new Vector3(xmin + px, -(ymax - py), 0.5f); + Verts[vertBase + 13] = new Vector3(xmax - px, -(ymax - py), 0.5f); + Verts[vertBase + 14] = new Vector3(xmin + px, -ymax, 0.5f); + Verts[vertBase + 15] = new Vector3(xmax - px, -ymax, 0.5f); + + Indices[indexBase] = (uint) vertBase; + Indices[indexBase + 1] = (uint) (vertBase + 1); + Indices[indexBase + 2] = (uint) (vertBase + 2); + Indices[indexBase + 3] = (uint) (vertBase + 2); + Indices[indexBase + 4] = (uint) (vertBase + 1); + Indices[indexBase + 5] = (uint) (vertBase + 3); + + Indices[indexBase + 6] = (uint) (vertBase + 4); + Indices[indexBase + 7] = (uint) (vertBase + 5); + Indices[indexBase + 8] = (uint) (vertBase + 6); + Indices[indexBase + 9] = (uint) (vertBase + 6); + Indices[indexBase + 10] = (uint) (vertBase + 5); + Indices[indexBase + 11] = (uint) (vertBase + 7); + + Indices[indexBase + 12] = (uint) (vertBase + 8); + Indices[indexBase + 13] = (uint) (vertBase + 9); + Indices[indexBase + 14] = (uint) (vertBase + 10); + Indices[indexBase + 15] = (uint) (vertBase + 10); + Indices[indexBase + 16] = (uint) (vertBase + 9); + Indices[indexBase + 17] = (uint) (vertBase + 11); + + Indices[indexBase + 18] = (uint) (vertBase + 12); + Indices[indexBase + 19] = (uint) (vertBase + 13); + Indices[indexBase + 20] = (uint) (vertBase + 14); + Indices[indexBase + 21] = (uint) (vertBase + 14); + Indices[indexBase + 22] = (uint) (vertBase + 13); + Indices[indexBase + 23] = (uint) (vertBase + 15); + } + } +} + +#endif // ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) diff --git a/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs.meta b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs.meta new file mode 100644 index 0000000..3316cc1 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Masked/Visualization/OccludeeOutlineJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76adbdb6ae6e0764ba17e582d8c440a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/Occluder.cs b/Unity.Entities.Graphics/Occlusion/Occluder.cs new file mode 100644 index 0000000..6679908 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Occluder.cs @@ -0,0 +1,59 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using UnityEngine; +using UnityEngine.Serialization; + +namespace Unity.Rendering.Occlusion +{ + public class Occluder : MonoBehaviour + { + [FormerlySerializedAs("Mesh")] public Mesh mesh; + + [FormerlySerializedAs("relativePosition")] + public Vector3 localPosition = Vector3.zero; + + [FormerlySerializedAs("relativeRotation")] + public Quaternion localRotation = Quaternion.identity; + + [FormerlySerializedAs("relativeScale")] + public Vector3 localScale = Vector3.one; + + void Reset() + { + if (gameObject.TryGetComponent(out var meshFilter)) + { + mesh = meshFilter.sharedMesh; + } + + localPosition = Vector3.zero; + localRotation = Quaternion.identity; + localScale = Vector3.one; + } + + private void OnDrawGizmos() + { + DrawGizmos(false); + } + + private void OnDrawGizmosSelected() + { + DrawGizmos(true); + } + + private void DrawGizmos(bool selected) + { + if (mesh == null || mesh.vertexCount == 0) + { + return; + } + + Gizmos.color = selected ? Color.yellow : Color.white; + Matrix4x4 mtx = Gizmos.matrix; + Gizmos.matrix = transform.localToWorldMatrix * Matrix4x4.TRS(localPosition, localRotation, localScale); + Gizmos.DrawWireMesh(mesh); + Gizmos.matrix = mtx; + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/Occluder.cs.meta b/Unity.Entities.Graphics/Occlusion/Occluder.cs.meta new file mode 100644 index 0000000..d37343a --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/Occluder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c66d943f01e53542add2ad628dc91ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs b/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs new file mode 100644 index 0000000..96d2644 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs @@ -0,0 +1,66 @@ +#if UNITY_EDITOR && ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEditor; +using UnityEditor.EditorTools; +using UnityEditor.IMGUI.Controls; +using UnityEngine; + +namespace Unity.Rendering.Occlusion +{ + [CustomEditor(typeof(Occluder))] + [CanEditMultipleObjects] + internal class OccluderInspector : Editor + { + + class Contents + { + public GUIContent meshContent = EditorGUIUtility.TrTextContent("Mesh", "The occluder mesh"); + public GUIContent positionContent = EditorGUIUtility.TrTextContent("Position", "The position of this occluder relative to transform."); + public GUIContent rotationContent = EditorGUIUtility.TrTextContent("Rotation", "The rotation of this occluder relative to transform."); + public GUIContent scaleContent = EditorGUIUtility.TrTextContent("Scale", "The scale of this occluder relative to transform."); + } + static Contents s_Contents; + + SerializedProperty m_Mesh; + SerializedProperty m_Position; + SerializedProperty m_Rotation; + SerializedProperty m_Scale; + + public void OnEnable() + { + m_Mesh = serializedObject.FindProperty("mesh"); + m_Position = serializedObject.FindProperty("localPosition"); + m_Rotation = serializedObject.FindProperty("localRotation"); + m_Scale = serializedObject.FindProperty("localScale"); + } + + public override void OnInspectorGUI() + { + if (s_Contents == null) + s_Contents = new Contents(); + + if (!EditorGUIUtility.wideMode) + { + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - 212; + } + + serializedObject.Update(); + + EditorGUILayout.PropertyField(m_Mesh, s_Contents.meshContent); + EditorGUILayout.LabelField("Local Transform"); + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(m_Position, s_Contents.positionContent); + EditorGUILayout.PropertyField(m_Rotation, s_Contents.rotationContent); + EditorGUILayout.PropertyField(m_Scale, s_Contents.scaleContent); + EditorGUI.indentLevel--; + + serializedObject.ApplyModifiedProperties(); + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs.meta b/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs.meta new file mode 100644 index 0000000..16bd680 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OccluderInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 577b398f4c8b4374e8960ead06a9aa5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs new file mode 100644 index 0000000..b9c1a78 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs @@ -0,0 +1,113 @@ +#if UNITY_EDITOR && ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using Unity.Entities; + +namespace Unity.Rendering.Occlusion +{ + public class OcclusionBrowseWindow : EditorWindow + { + public static bool IsVisible; + + [MenuItem("Occlusion/Browse")] + public static void ShowMenuItem() + { + OcclusionBrowseWindow wnd = GetWindow(); + wnd.titleContent = new GUIContent("Occlusion Browser"); + } + + internal StyleSheet StyleSheet + { + get + { + return AssetDatabase.LoadAssetAtPath("Packages/com.unity.entities.graphics/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss"); + } + } + + readonly int kMargin = 10; + readonly int kPadding = 10; + readonly int kBoxSize = 200; + + public void OnInspectorUpdate() + { + rootVisualElement.Clear(); + CreateGUI(); + } + + private void OnBecameVisible() + { + IsVisible = true; + } + + private void OnBecameInvisible() + { + IsVisible = false; + } + + public void CreateGUI() + { + var root = this.rootVisualElement; + root.Clear(); + + var boxes = new VisualElement() + { + style = + { + marginLeft = kMargin, + marginTop = kMargin, + marginRight = kMargin, + marginBottom = kMargin, + backgroundColor = Color.grey, + paddingLeft = kPadding, + paddingTop = kPadding, + paddingRight = kPadding, + paddingBottom = kPadding, + alignSelf = Align.FlexStart, + flexShrink = 0f, + flexWrap = Wrap.Wrap, + flexDirection = FlexDirection.Row // makes the container horizontal + } + }; + + root.Add(boxes); + boxes.StretchToParentSize(); + + if (World.DefaultGameObjectInjectionWorld == null) + { + return; + } + var entitiesGraphicsSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystemManaged(); + + foreach (var pair in entitiesGraphicsSystem.OcclusionCulling.BufferGroups) + { + + var id = (int)(pair.Key & 0xffffffff); + var slice = (int)(pair.Key >> 32); + var obj = (Behaviour)EditorUtility.InstanceIDToObject(id); + Texture2D gpuDepth = pair.Value.GetVisualizationTexture(); + + if (!obj || !obj.gameObject.activeInHierarchy) + { + continue; + } + + var background = new StyleBackground(gpuDepth); + + // inform layout system of desired width for each box + boxes.Add(new Button() + { + text = obj.name + "\n[Slice " + slice.ToString() + "]\nid: " + $"0x{id:x8}", + style = + { + width = kBoxSize, + height = kBoxSize, + backgroundImage = background, + } + }); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs.meta b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs.meta new file mode 100644 index 0000000..305b930 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: daa77134635263d42a4f0b5d3e465885 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss new file mode 100644 index 0000000..eaa3f31 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss @@ -0,0 +1,94 @@ +.horizontalContainer { + margin: 50px; + flex-direction: row; +} + +#boxesContainer { + padding: 10px; + background-color: rgb(128, 128, 128); + align-self: flex-start; + flex-shrink: 0; +} + +#boxesContainer > VisualElement { + width: 100px; + height: 100px; +} + +#TwoPlusOneContainer { + height: 100px; + flex-shrink: 0; +} + +#large { + flex: 0.7; + background-color: rgb(255, 0, 0); +} + +#small { + flex: 0.3; + background-color: rgb(0, 0, 255); +} + +#wrapContainer { + flex-wrap: wrap; +} + +#wrapContainer > VisualElement { + width: 20px; + height: 20px; + margin: 5px; + background-color: #0000FF; +} + +.editorControlDisplayer { + flex-direction: row; +} + +.editorControlDisplayer .extraField { + flex-direction: row; + justify-content: space-between; + flex: 1; +} + +.editorControlDisplayer .controlField { + flex: 1; +} + +.focusButton { + max-height: 20px; +} + +.unity-list-view { + --unity-item-height: 20; +} + +TreeView { + --unity-item-height: 15; +} + +.split-container { + flex: 1; + padding: 10px; +} + +.inner-container { + flex: 1; + overflow: hidden; +} + +.element { + flex: 1 1 100px; + background-color: #555555; + border-bottom-width: 1px; + border-color: white; + justify-content: center; + align-items: center; +} + +.element Label { + background-color: #000000; + color: #999999; + -unity-text-align: middle-center; + flex: 0 0 auto; +} diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss.meta b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss.meta new file mode 100644 index 0000000..4e6bb46 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6bd483c2ca1589a43ad4872f7c97a9e4 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml new file mode 100644 index 0000000..248ee78 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml.meta b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml.meta new file mode 100644 index 0000000..4e62106 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionBrowseWindow.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 2804fd14938e5e04b95bf1b7b80a717e +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs b/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs new file mode 100644 index 0000000..39a07ba --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs @@ -0,0 +1,133 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; +using Unity.Rendering.Occlusion.Masked; +using Unity.Rendering.Occlusion.Masked.Visualization; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace Unity.Rendering.Occlusion +{ + + /* This system renders debug visualizations for occlusion culling. + For each debug view: + 1. It unpacks the CPU-rasterized masked depth buffer into a human-readable depth buffer + 2. It renders the visualization (such as culled occludees, or occluder meshes) into an overlay texture + 3. At the end of every frame, it composites these textures to create the final debug visualization */ + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateAfter(typeof(EntitiesGraphicsSystem))] + unsafe public partial class OcclusionDebugRenderSystem : SystemBase + { + private EntitiesGraphicsSystem entitiesGraphicsSystem; + private CommandBuffer cmdComposite = new CommandBuffer() { name = "Occlusion debug composite" }; + private Mesh fullScreenQuad; + + struct QuadVertex + { + public float4 pos; + public float2 uv; + } + + protected void SetFullScreenQuad() + { + + var layout = new[] + { + new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 4), + new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), + }; + + fullScreenQuad = new Mesh(); + fullScreenQuad.SetVertexBufferParams(4, layout); + + var verts = new NativeArray(4, Allocator.Temp); + + verts[0] = new QuadVertex() { pos = new float4(-1f, -1f, 1, 1), uv = new float2(0, 1) }; + verts[1] = new QuadVertex() { pos = new float4(1f, -1f, 1, 1), uv = new float2(1, 1) }; + verts[2] = new QuadVertex() { pos = new float4(1f, 1f, 1, 1), uv = new float2(1, 0) }; + verts[3] = new QuadVertex() { pos = new float4(-1f, 1f, 1, 1), uv = new float2(0, 0) }; + + fullScreenQuad.SetVertexBufferData(verts, 0, 0, 4); + verts.Dispose(); + + var tris = new int[6] { 0, 1, 2, 0, 2, 3 }; + fullScreenQuad.SetIndices(tris, MeshTopology.Triangles, 0); + fullScreenQuad.bounds = new Bounds(Vector3.zero, new Vector3(10000, 10000, 1000)); + } + + /// + protected override void OnCreate() + { + // Create full-screen quad + SetFullScreenQuad(); + + RenderPipelineManager.endFrameRendering += RenderComposite; + } + + /// + protected override void OnDestroy() + { + RenderPipelineManager.endFrameRendering -= RenderComposite; + } + + /// + protected override void OnUpdate() + { + + } + + // At the end of every frame, composite the depth and overlay texture for each view to create the final debug + // visualization + void RenderComposite(ScriptableRenderContext ctx, Camera[] cameras) + { + if (World.DefaultGameObjectInjectionWorld == null) + { + return; + } + + if (entitiesGraphicsSystem == null) + { + entitiesGraphicsSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystemManaged(); + } + + OcclusionCulling oc = entitiesGraphicsSystem.OcclusionCulling; + + if (!oc.IsEnabled || + oc.debugSettings.debugRenderMode == DebugRenderMode.None || + oc.debugSettings.debugRenderMode == DebugRenderMode.Inverted) + return; + + // This happens when reloading scenes + if(fullScreenQuad == null) + { + SetFullScreenQuad(); + } + + cmdComposite.Clear(); + + ulong? pinnedViewID = oc.debugSettings.GetPinnedViewID(); + + foreach (Camera camera in cameras) + { + ulong id = pinnedViewID.HasValue ? + // A view is pinned. Pick its ID instead of the current camera's ID. + pinnedViewID.Value : + // No view is pinned. Pick the current camera's ID. + (ulong) ((uint)camera.GetInstanceID()); + if (!oc.BufferGroups.TryGetValue(id, out BufferGroup bufferGroup)) + continue; + bufferGroup.RenderToCamera(oc.debugSettings.debugRenderMode, camera, cmdComposite, fullScreenQuad); + } + Graphics.ExecuteCommandBuffer(cmdComposite); + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs.meta b/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs.meta new file mode 100644 index 0000000..3130934 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionDebugRenderSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24d768ae8edcfe341b46baf92c520e75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs b/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs new file mode 100644 index 0000000..c39737c --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs @@ -0,0 +1,153 @@ +#if UNITY_EDITOR && ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using UnityEditor; +using UnityEngine; +using Unity.Rendering.Occlusion; +using System.Collections.Generic; +using System; +using System.Linq; +using Unity.Entities; +using Object = UnityEngine.Object; +using UnityEditor.SceneManagement; + +namespace Unity.Rendering.Occlusion +{ + static class OcclusionCommands + { + const string kOcclusionMenu = "Occlusion/"; + + const string kOcclusionToolsSubMenu = "Tools/"; + + const string kOcclusionDebugSubMenu = kOcclusionMenu + "Debug/"; + + const string kDebugNone = kOcclusionDebugSubMenu + "None"; + const string kDebugDepth = kOcclusionDebugSubMenu + "Depth buffer"; + const string kDebugShowMeshes = kOcclusionDebugSubMenu + "Show occluder meshes"; + const string kDebugShowBounds = kOcclusionDebugSubMenu + "Show occludee bounds"; + const string kDebugShowTest = kOcclusionDebugSubMenu + "Show depth test"; + + const string kOcclusionEnable = kOcclusionMenu + "Enable"; + const string kOcclusionDisplayOccluded = "Occlusion/DisplayOccluded"; + const string kOcclusionParallel = kOcclusionMenu + "Parallel Rasterization"; + + [MenuItem(kOcclusionEnable, false)] + static void ToggleOcclusionEnable() + { + if (World.DefaultGameObjectInjectionWorld != null) + { + var occlusionCulling = World.DefaultGameObjectInjectionWorld.GetOrCreateSystemManaged().OcclusionCulling; + occlusionCulling.IsEnabled = !occlusionCulling.IsEnabled; + } + } + + [MenuItem(kOcclusionEnable, true)] + static bool ValidateOcclusionEnable() + { + if (World.DefaultGameObjectInjectionWorld != null) + { + var occlusionCulling = World.DefaultGameObjectInjectionWorld.GetOrCreateSystemManaged().OcclusionCulling; + Menu.SetChecked(kOcclusionEnable, occlusionCulling.IsEnabled); + } + return true; + } + + static void AddComponentIfNeeded(this MeshRenderer meshRenderer) where T : MonoBehaviour + { + var gameObject = meshRenderer.gameObject; + if (!gameObject.TryGetComponent(out var occluder)) + { + occluder = gameObject.AddComponent(); + occluder.enabled = true; + } + } + + static void DestroyComponentIfNeeded(this MeshRenderer meshRenderer) where T : MonoBehaviour + { + var gameObject = meshRenderer.gameObject; + if (gameObject.TryGetComponent(out var occluder)) + Object.DestroyImmediate(occluder); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Add occlusion components to all open scenes and objects")] + static void AddAllOcclusionComponents() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.AddComponentIfNeeded(); + }, OccluderEditMode.AllObjects); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Remove occlusion components from all open scenes and objects")] + static void RemoveAllOcclusionComponents() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.DestroyComponentIfNeeded(); + }, OccluderEditMode.AllObjects); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Add occlusion components to selected")] + static void AddOcclusionComponentsToSelected() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.AddComponentIfNeeded(); + }, OccluderEditMode.SelectedObjects); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Remove occlusion components from selected")] + static void RemoveOcclusionComponentsFromSelected() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.DestroyComponentIfNeeded(); + }, OccluderEditMode.SelectedObjects); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Add occluder component to selected")] + static void AddOccluderComponentToSelected() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.AddComponentIfNeeded(); + }, OccluderEditMode.SelectedObjects); + } + + [MenuItem(kOcclusionMenu + kOcclusionToolsSubMenu + "Add occluder component to all open scenes and objects")] + static void AddOccluderComponentToAll() + { + ForEachRenderer((meshRenderer) => + { + meshRenderer.AddComponentIfNeeded(); + }, OccluderEditMode.AllObjects); + } + + [MenuItem(kOcclusionMenu + "Occlusion Window")] + static void OpenOcclusionWindow() + { + OcclusionWindow.ShowWindow(); + } + + enum OccluderEditMode + { + AllObjects, + SelectedObjects, + } + + static void ForEachRenderer(Action action, OccluderEditMode mode) + { + var renderers = mode == OccluderEditMode.AllObjects + ? Object.FindObjectsOfType() + : Selection.gameObjects.SelectMany(x => x.GetComponents()); + renderers = renderers.Distinct(); + + foreach (var renderer in renderers) + { + action(renderer); + EditorSceneManager.MarkSceneDirty(renderer.gameObject.scene); + } + } + } +} +#endif + diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs.meta b/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs.meta new file mode 100644 index 0000000..d36483c --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b775b039e68f9204fabbab91eff5c1a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs b/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs new file mode 100644 index 0000000..d3324ae --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs @@ -0,0 +1,43 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System; +using Unity.Burst; +using Unity.Mathematics; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using UnityEngine.Rendering; +using Unity.Transforms; +using System.Collections.Generic; +using Unity.Rendering.Occlusion.Masked.Dots; + + +namespace Unity.Rendering.Occlusion +{ + [BurstCompile] + unsafe struct OcclusionSortMeshesJob : IJob + { + public NativeArray Meshes; + + + struct Compare : IComparer + { + int IComparer.Compare(OcclusionMesh x, OcclusionMesh y) + { + return x.screenMin.z.CompareTo(y.screenMin.z); + } + } + + public void Execute() + { + if (Meshes.Length == 0) + return; + + // TODO: might want to do a proper parallel sort instead + Meshes.Sort(new Compare()); + } + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs.meta b/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs.meta new file mode 100644 index 0000000..089ca63 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionSortJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be9209ce1b22cbe43b49dc9c4f76ebb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs b/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs new file mode 100644 index 0000000..00e3e57 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs @@ -0,0 +1,85 @@ +#if UNITY_EDITOR && ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) + +using System.Linq; +using UnityEditor; +using UnityEngine; +using Unity.Rendering.Occlusion; +using Unity.Entities; +using Unity.Rendering; + +public class OcclusionWindow : EditorWindow +{ + enum VisibilityFilter + { + None, + Occluders, + Occludees, + } + + private VisibilityFilter _visibilityFilter = VisibilityFilter.None; + bool occlusionEnabled; + + // Add menu item named "My Window" to the Window menu + public static void ShowWindow() + { + //Show existing window instance. If one doesn't exist, make one. + EditorWindow.GetWindow(typeof(OcclusionWindow)); + } + + void OnGUI() + { + if (World.DefaultGameObjectInjectionWorld == null) + { + return; + } + + var occlusionCulling = World.DefaultGameObjectInjectionWorld.GetOrCreateSystemManaged().OcclusionCulling; + + if (occlusionCulling == null) + { + return; + } + + GUILayout.Space(5); + GUILayout.Label ("Options", EditorStyles.boldLabel); + + occlusionCulling.IsEnabled = GUILayout.Toggle(occlusionCulling.IsEnabled, "Enable Occlusion"); + + GUILayout.Space(20); + GUILayout.Label ("Tools", EditorStyles.boldLabel); + GUILayout.Space(5); + + VisibilityFilter currentVis = _visibilityFilter; + if (GUILayout.Toggle(_visibilityFilter == VisibilityFilter.Occluders, "Show Only Ocludders")) + { + var list = FindObjectsOfType().Select(x => x.gameObject).ToArray(); + ScriptableSingleton.instance.Isolate(list, true); + _visibilityFilter = VisibilityFilter.Occluders; + } + else + { + if (_visibilityFilter == VisibilityFilter.Occluders) + { + ScriptableSingleton.instance.ExitIsolation(); + _visibilityFilter = VisibilityFilter.None; + } + } + + if (GUILayout.Button("Select Occluders")) + { + Selection.objects = FindObjectsOfType().Select(x => x.gameObject).ToArray(); + } + + if (GUILayout.Button("Remove Occluders from Selected")) + { + foreach (var go in Selection.gameObjects) + { + foreach (var o in go.GetComponents()) + { + DestroyImmediate(o); + } + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs.meta b/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs.meta new file mode 100644 index 0000000..8c5f9c8 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/OcclusionWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19128c2c2d982e44db1b5df4aecf384c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs b/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs new file mode 100644 index 0000000..0fa29c8 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs @@ -0,0 +1,330 @@ +#if ENABLE_UNITY_OCCLUSION && (HDRP_10_0_0_OR_NEWER || URP_10_0_0_OR_NEWER) +// #define WAIT_FOR_EACH_JOB // This is useful for profiling individual jobs, but should be commented out for performance + +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Entities; +using Unity.Jobs; +using Unity.Transforms; +using UnityEngine.Rendering; +using System.Collections.Generic; +using Unity.Burst.Intrinsics; +using Unity.Profiling; +using Unity.Rendering.Occlusion.Masked; +using Unity.Rendering.Occlusion.Masked.Dots; +using Unity.Rendering.Occlusion.Masked.Visualization; + +namespace Unity.Rendering.Occlusion +{ + public unsafe class OcclusionCulling + { + public bool IsEnabled = true; + public DebugSettings debugSettings = new DebugSettings(); + public Dictionary BufferGroups { get; } = new Dictionary(); + EntityQuery m_OcclusionMeshQuery; + EntityQuery m_ReadonlyTestQuery; + EntityQuery m_ReadonlyMeshQuery; + + static readonly ProfilerMarker s_Cull = new ProfilerMarker("Occlusion.Cull"); + static readonly ProfilerMarker s_SetResolution = new ProfilerMarker("Occlusion.Cull.SetResolution"); + static readonly ProfilerMarker s_Clear = new ProfilerMarker("Occlusion.Cull.Clear"); + static readonly ProfilerMarker s_MeshTransform = new ProfilerMarker("Occlusion.Cull.MeshTransform"); + static readonly ProfilerMarker s_SortMeshes = new ProfilerMarker("Occlusion.Cull.SortMeshes"); + static readonly ProfilerMarker s_ComputeBounds = new ProfilerMarker("Occlusion.Cull.ComputeBounds"); + static readonly ProfilerMarker s_Rasterize = new ProfilerMarker("Occlusion.Cull.Rasterize"); + static readonly ProfilerMarker s_Test = new ProfilerMarker("Occlusion.Cull.Test"); + + public void Create(EntityManager entityManager) + { + debugSettings.Register(); + m_OcclusionMeshQuery = entityManager.CreateEntityQuery( + typeof(OcclusionMesh), + typeof(LocalToWorld) + ); + m_ReadonlyTestQuery = entityManager.CreateEntityQuery(ComponentType.ReadOnly()); + m_ReadonlyMeshQuery = entityManager.CreateEntityQuery( + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + + + m_OcclusionTestTransformGroup = entityManager.CreateEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ChunkComponentReadOnly(), + }, + }); + + m_OcclusionTestGroup = entityManager.CreateEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }, + }); + } + + public void Dispose() + { + debugSettings.Unregister(); + foreach (var bufferGroup in BufferGroups.Values) + { + bufferGroup.Dispose(); + } + } + + JobHandle CullView( + BufferGroup bufferGroup, + int splitIndex, + JobHandle incomingJob, + EntityManager entityManager, + IndirectList visibilityItems, + bool InvertOcclusion, + BatchCullingViewType viewType + ) + { + s_Cull.Begin(); + + /* Clear memory + ------------ */ + s_Clear.Begin(); + /* We want to maximize occupancy. The workload of each job is tiny: It's three assembly instructions, + which is why we want to have the large number of inner-loop batches that can spread across all + available worker threads */ + var clearJob = new ClearJob + { + Tiles = (Tile*) bufferGroup.Tiles.GetUnsafePtr() + }.ScheduleParallel(bufferGroup.Tiles.Length, 1024, new JobHandle()); +#if WAIT_FOR_EACH_JOB + clearJob.Complete(); +#endif // WAIT_FOR_EACH_JOB + s_Clear.End(); + + var localToWorlds = m_OcclusionMeshQuery.ToComponentDataArray(Allocator.TempJob); + var meshes = m_OcclusionMeshQuery.ToComponentDataArray(Allocator.TempJob); + + /* Transform occluder meshes + ------------------------- */ + s_MeshTransform.Begin(); + var transformJob = new MeshTransformJob() + { + ViewProjection = bufferGroup.CullingMatrix, + ProjectionType = bufferGroup.ProjectionType, + NearClip = bufferGroup.NearClip, + FrustumPlanes = (v128*)bufferGroup.FrustumPlanes.GetUnsafePtr(), + HalfWidth = bufferGroup.HalfWidth, + HalfHeight = bufferGroup.HalfHeight, + PixelCenterX = bufferGroup.PixelCenterX, + PixelCenterY = bufferGroup.PixelCenterY, + LocalToWorlds = localToWorlds, + Meshes = meshes, + }.ScheduleParallel(meshes.Length, 4, incomingJob); +#if WAIT_FOR_EACH_JOB + transformJob.Complete(); +#endif // WAIT_FOR_EACH_JOB + localToWorlds.Dispose(transformJob); + s_MeshTransform.End(); + + /* Sort meshes by vertex count + --------------------------- */ + // TODO: Look at perf. Evaluate whether running this job is even worth it. It only takes 0.02ms in Viking Village, + // which is why I haven't looked at it yet. + s_SortMeshes.Begin(); + var sortJob = new OcclusionSortMeshesJob + { + Meshes = meshes, + }.Schedule(transformJob); +#if WAIT_FOR_EACH_JOB + sortJob.Complete(); +#endif // WAIT_FOR_EACH_JOB + s_SortMeshes.End(); + + /* Compute occludee bounds + ----------------------- */ + s_ComputeBounds.Begin(); + var computeBoundsJob = new ComputeBoundsJob + { + ViewProjection = bufferGroup.CullingMatrix, + NearClip = bufferGroup.NearClip, + Bounds = entityManager.GetComponentTypeHandle(true), + LocalToWorld = entityManager.GetComponentTypeHandle(true), + OcclusionTest = entityManager.GetComponentTypeHandle(false), + ChunkOcclusionTest = entityManager.GetComponentTypeHandle(false), + ProjectionType = bufferGroup.ProjectionType, + }.ScheduleParallel(m_OcclusionTestTransformGroup, incomingJob); +#if WAIT_FOR_EACH_JOB + computeBoundsJob.Complete(); +#endif // WAIT_FOR_EACH_JOB + s_ComputeBounds.End(); + + /* Rasterize + --------- */ + JobHandle rasterizeJob; + if (meshes.Length > 0) + { + s_Rasterize.Begin(); + const int TilesPerBinX = 2;//16 tiles per X axis, values can be 1 2 4 8 16 + const int TilesPerBinY = 4;//128 tiles per X axis, values can be 1 2 4 8 16 32 64 128 + const int TilesPerBin = TilesPerBinX * TilesPerBinY; + int numBins = bufferGroup.NumTilesX * bufferGroup.NumTilesY / TilesPerBin; + rasterizeJob = new RasterizeJob + { + Meshes = meshes, + ProjectionType = bufferGroup.ProjectionType, + NumBuffers = bufferGroup.NumBuffers, + HalfWidth = bufferGroup.HalfWidth, + HalfHeight = bufferGroup.HalfHeight, + PixelCenterX = bufferGroup.PixelCenterX, + PixelCenterY = bufferGroup.PixelCenterY, + PixelCenter = bufferGroup.PixelCenter, + HalfSize = bufferGroup.HalfSize, + ScreenSize = bufferGroup.ScreenSize, + NumPixelsX = bufferGroup.NumPixelsX, + NumPixelsY = bufferGroup.NumPixelsY, + NumTilesX = bufferGroup.NumTilesX, + NumTilesY = bufferGroup.NumTilesY, + NearClip = bufferGroup.NearClip, + FrustumPlanes = (v128*)bufferGroup.FrustumPlanes.GetUnsafePtr(), + FullScreenScissor = bufferGroup.FullScreenScissor, + TilesBasePtr = (Tile*) bufferGroup.Tiles.GetUnsafePtr(), + TilesPerBinX = TilesPerBinX, + TilesPerBinY = TilesPerBinY, + }.ScheduleParallel(numBins, 1, JobHandle.CombineDependencies(clearJob, sortJob)); + #if WAIT_FOR_EACH_JOB + rasterizeJob.Complete(); + #endif // WAIT_FOR_EACH_JOB + s_Rasterize.End(); + }else + { + rasterizeJob = JobHandle.CombineDependencies(clearJob, sortJob); + } + + /* Test + ---- */ + s_Test.Begin(); + + var testJob = new TestJob + { + VisibilityItems = visibilityItems, + ChunkHeader = entityManager.GetComponentTypeHandle(true), + OcclusionTest = entityManager.GetComponentTypeHandle(true), + ChunkOcclusionTest = entityManager.GetComponentTypeHandle(true), + EntitiesGraphicsChunkInfo = entityManager.GetComponentTypeHandle(false), + ProjectionType = bufferGroup.ProjectionType, + NumTilesX = bufferGroup.NumTilesX, + HalfSize = bufferGroup.HalfSize, + PixelCenter = bufferGroup.PixelCenter, + ScreenSize = bufferGroup.ScreenSize, + ViewType = viewType, + SplitIndex = splitIndex, + Tiles = (Tile*)bufferGroup.Tiles.GetUnsafePtr(), +#if UNITY_EDITOR + DisplayOnlyOccluded = InvertOcclusion, +#endif + }.ScheduleWithIndirectList(visibilityItems, 1, JobHandle.CombineDependencies(rasterizeJob, computeBoundsJob)); +#if WAIT_FOR_EACH_JOB + testJob.Complete(); +#endif // WAIT_FOR_EACH_JOB + s_Test.End(); + s_Cull.End(); + + bufferGroup.RenderToTextures(m_ReadonlyTestQuery, m_ReadonlyMeshQuery, testJob, debugSettings.debugRenderMode); + meshes.Dispose(rasterizeJob); + return testJob; + } + + internal JobHandle Cull( + EntityManager entityManager, + BatchCullingContext cullingContext, + JobHandle cullingJobDependency, + IndirectList visibilityItems +#if UNITY_EDITOR + , EntitiesGraphicsPerThreadStats* cullingStats +#endif + ) + { + if (World.DefaultGameObjectInjectionWorld == null) + { + return new JobHandle(); + } + + if (!IsEnabled) + return new JobHandle(); + +#if WAIT_FOR_EACH_JOB + cullingJobDependency.Complete(); +#endif // WAIT_FOR_EACH_JOB + + JobHandle combinedHandle = new JobHandle(); + int instanceID = cullingContext.viewID.GetInstanceID(); + ulong? pinnedViewID = debugSettings.GetPinnedViewID(); + + for (int i = 0; i < cullingContext.cullingSplits.Length; i++) + { + // Pack the instance ID and the split index into a 64-bit value + ulong viewID = (uint)instanceID | ((ulong)i << 32); + + // Add a buffer-group for the current view if one doesn't already exist + if (!BufferGroups.TryGetValue(viewID, out var bufferGroup)) + { + bufferGroup = new BufferGroup(cullingContext.viewType); + BufferGroups[viewID] = bufferGroup; + debugSettings.RefreshViews(BufferGroups); + } + + if (pinnedViewID.HasValue && BufferGroups.TryGetValue(pinnedViewID.Value, out var pinnedBufferGroup)) + { + // ^ A view is pinned. Use that view's buffer group instead of the view that's currently being + // drawn. This allows us to experience culling from the pinned view's perspective. + bufferGroup = pinnedBufferGroup; + } + + if (!debugSettings.freezeOcclusion && (!pinnedViewID.HasValue || pinnedViewID.Value == viewID)) + { + // Update the buffer-group's view-related parameters + s_SetResolution.Begin(); + bufferGroup.SetResolutionAndClip( + m_MOCDepthSize, + m_MOCDepthSize, + cullingContext.projectionType, + cullingContext.cullingSplits[i].nearPlane + ); + bufferGroup.CullingMatrix = cullingContext.cullingSplits[i].cullingMatrix; + s_SetResolution.End(); + } + + bool invertOcclusion = debugSettings.debugRenderMode == DebugRenderMode.Inverted; + + JobHandle viewJob = CullView( + bufferGroup, + i, + /* TODO: Remove this dependency to run views asynchronously. To enable this, we will need to move + the data out of occlusion test components. Since currently there is only up to one occlusion test + component on an entity, it can only be culled from one view at a time. */ + JobHandle.CombineDependencies(cullingJobDependency, combinedHandle), + entityManager, + visibilityItems, + invertOcclusion, + cullingContext.viewType + ); + combinedHandle = JobHandle.CombineDependencies(combinedHandle, viewJob); + } + + return combinedHandle; + } + + private EntityQuery m_OcclusionTestTransformGroup; + private EntityQuery m_OcclusionTestGroup; + + static readonly int m_MOCDepthSize = 512; + + } +} + +#endif diff --git a/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs.meta b/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs.meta new file mode 100644 index 0000000..cc7e360 --- /dev/null +++ b/Unity.Entities.Graphics/Occlusion/UnityOcclusion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac2c3a59629403045aec2d6a16cf212f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Probes.meta b/Unity.Entities.Graphics/Probes.meta new file mode 100644 index 0000000..43b7a2c --- /dev/null +++ b/Unity.Entities.Graphics/Probes.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 488c88a56c584cc5b6d489d050bb2602 +timeCreated: 1591271249 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Probes/BlendProbeTag.cs b/Unity.Entities.Graphics/Probes/BlendProbeTag.cs new file mode 100644 index 0000000..40ae7e3 --- /dev/null +++ b/Unity.Entities.Graphics/Probes/BlendProbeTag.cs @@ -0,0 +1,14 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// A tag component that marks an entity as a blend probe. + /// + /// + /// The LightProbeUpdateSystem uses this to manage light probes. + /// + public struct BlendProbeTag : IComponentData + { + } +} diff --git a/Unity.Entities.Graphics/Probes/BlendProbeTag.cs.meta b/Unity.Entities.Graphics/Probes/BlendProbeTag.cs.meta new file mode 100644 index 0000000..a83a36d --- /dev/null +++ b/Unity.Entities.Graphics/Probes/BlendProbeTag.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9e17c857bd7744e7b712502719c807c7 +timeCreated: 1590664784 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Probes/CustomProbeTag.cs b/Unity.Entities.Graphics/Probes/CustomProbeTag.cs new file mode 100644 index 0000000..76e05a1 --- /dev/null +++ b/Unity.Entities.Graphics/Probes/CustomProbeTag.cs @@ -0,0 +1,14 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// A tag component that marks an entity as a custom light probe. + /// + /// + /// The ManageSHPropertiesSystem uses this to manage shadow harmonics. + /// + public struct CustomProbeTag : IComponentData + { + } +} diff --git a/Unity.Entities.Graphics/Probes/CustomProbeTag.cs.meta b/Unity.Entities.Graphics/Probes/CustomProbeTag.cs.meta new file mode 100644 index 0000000..4c467ad --- /dev/null +++ b/Unity.Entities.Graphics/Probes/CustomProbeTag.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 151ba261808947d988c70caad2941144 +timeCreated: 1591270135 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs b/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs new file mode 100644 index 0000000..59bead5 --- /dev/null +++ b/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + [UpdateInGroup(typeof(UpdatePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + partial class LightProbeUpdateSystem : SystemBase + { + EntityQuery m_ProbeGridQuery; + + private ComponentType[] gridQueryFilter = {ComponentType.ReadOnly(), ComponentType.ReadWrite()}; + private ComponentType[] gridQueryFilterForAmbient = { ComponentType.ReadWrite() }; + + /// + protected override void OnCreate() + { + m_ProbeGridQuery = GetEntityQuery( + ComponentType.ReadWrite(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + m_ProbeGridQuery.SetChangedVersionFilter(gridQueryFilter); + } + + internal static bool IsValidLightProbeGrid() + { + var probes = LightmapSettings.lightProbes; + bool validGrid = probes != null && probes.count > 0; + return validGrid; + } + + /// + protected override void OnUpdate() + { + var ambientProbe = RenderSettings.ambientProbe; + + if (IsValidLightProbeGrid()) + { + UpdateEntitiesFromGrid(); + } + + // Update the global ambient probe. If there is no valid grid, BlendProbeTag + // entities will have their SH components deleted and will also fall back + // to this. + UpdateGlobalAmbientProbe(ambientProbe); + } + + private void UpdateGlobalAmbientProbe(SphericalHarmonicsL2 ambientProbe) + { + var entitiesGraphicsSystem = World.GetExistingSystemManaged(); + entitiesGraphicsSystem?.UpdateGlobalAmbientProbe(new SHCoefficients(ambientProbe)); + } + + private static void UpdateEntitiesFromAmbientProbe( + LightProbeUpdateSystem system, + EntityQuery query, + ComponentType[] queryFilter, + SphericalHarmonicsL2 ambientProbe, + SphericalHarmonicsL2 lastProbe) + { + Profiler.BeginSample("UpdateEntitiesFromAmbientProbe"); + var updateAll = ambientProbe != lastProbe; + if (updateAll) + { + query.ResetFilter(); + } + + var job = new UpdateSHValuesJob + { + Coefficients = new SHCoefficients(ambientProbe), + SHType = system.GetComponentTypeHandle(), + }; + + system.Dependency = job.ScheduleParallel(query, system.Dependency); + + if (updateAll) + { + query.SetChangedVersionFilter(queryFilter); + } + Profiler.EndSample(); + } + + private List m_Positions = new List(512); + private List m_LightProbes = new List(512); + private List m_OcclusionProbes = new List(512); + private void UpdateEntitiesFromGrid() + { + Profiler.BeginSample("UpdateEntitiesFromGrid"); + + var SHType = GetComponentTypeHandle(); + var localToWorldType = GetComponentTypeHandle(); + + var chunks = m_ProbeGridQuery.ToArchetypeChunkArray(Allocator.Temp); + if (chunks.Length == 0) + { + Profiler.EndSample(); + return; + } + + //TODO: Bring this off the main thread when we have new c++ API + Dependency.Complete(); + + foreach (var chunk in chunks) + { + var chunkSH = chunk.GetNativeArray(SHType); + var chunkLocalToWorld = chunk.GetNativeArray(localToWorldType); + + m_Positions.Clear(); + m_LightProbes.Clear(); + m_OcclusionProbes.Clear(); + + for (int i = 0; i != chunkLocalToWorld.Length; i++) + m_Positions.Add(chunkLocalToWorld[i].Position); + + LightProbes.CalculateInterpolatedLightAndOcclusionProbes(m_Positions, m_LightProbes, m_OcclusionProbes); + + for (int i = 0; i < m_Positions.Count; ++i) + { + var shCoefficients = new SHCoefficients(m_LightProbes[i]); + chunkSH[i] = new BuiltinMaterialPropertyUnity_SHCoefficients() {Value = shCoefficients}; + } + } + Profiler.EndSample(); + } + + [BurstCompile] + struct UpdateSHValuesJob : IJobChunk + { + public SHCoefficients Coefficients; + public ComponentTypeHandle SHType; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var chunkSH = chunk.GetNativeArray(SHType); + + for (var i = 0; i < chunkSH.Length; i++) + { + chunkSH[i] = new BuiltinMaterialPropertyUnity_SHCoefficients {Value = Coefficients}; + } + } + } + } +} diff --git a/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs.meta b/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs.meta new file mode 100644 index 0000000..8ab780d --- /dev/null +++ b/Unity.Entities.Graphics/Probes/LightProbeUpdateSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 92fc4be930aa43d48e07b9e541478cf7 +timeCreated: 1590656473 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs b/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs new file mode 100644 index 0000000..b8c77eb --- /dev/null +++ b/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs @@ -0,0 +1,96 @@ +// #define DISABLE_HYBRID_LIGHT_PROBES + +using System; +using System.Linq; +using Unity.Entities; +using UnityEngine; + +#if !DISABLE_HYBRID_LIGHT_PROBES +namespace Unity.Rendering +{ + [RequireMatchingQueriesForUpdate] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(StructuralChangePresentationSystemGroup))] + partial class ManageSHPropertiesSystem : SystemBase + { + // Match entities with CustomProbeTag, but without the SH component + EntityQuery m_MissingSHQueryCustom; + // Match entities with BlendProbeTag, but without the SH component + EntityQuery m_MissingSHQueryBlend; + + // Matches entities with the SH component, but neither CustomProbeTag or BlendProbeTag + EntityQuery m_MissingProbeTagQuery; + // Matches entities with SH components and BlendProbeTag + EntityQuery m_RemoveSHFromBlendProbeTagQuery; + + ComponentType[] m_SHComponentType; + + /// + protected override void OnCreate() + { + m_SHComponentType = new[] + { + ComponentType.ReadOnly(), + }; + + m_MissingSHQueryCustom = GetEntityQuery(new EntityQueryDesc + { + Any = new[] + { + ComponentType.ReadOnly() + }, + None = m_SHComponentType, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingSHQueryBlend = GetEntityQuery(new EntityQueryDesc + { + Any = new[] + { + ComponentType.ReadOnly(), + }, + None = m_SHComponentType, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_MissingProbeTagQuery = GetEntityQuery(new EntityQueryDesc + { + Any = m_SHComponentType, + None = new[] + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + }); + + m_RemoveSHFromBlendProbeTagQuery = GetEntityQuery(new EntityQueryDesc + { + Any = m_SHComponentType, + All = new []{ ComponentType.ReadOnly(), }, + }); + } + + /// + protected override void OnUpdate() + { + // If there is a valid light probe grid, BlendProbeTag entities should have SH components + // If there is no valid light probe grid, BlendProbeTag entities will not have SH components + // and behave as if they had AmbientProbeTag instead (read from global probe). + bool validGrid = LightProbeUpdateSystem.IsValidLightProbeGrid(); + + // CustomProbeTag entities should always have SH components + EntityManager.AddComponent(m_MissingSHQueryCustom, m_SHComponentType[0]); + + // BlendProbeTag entities have SH components if and only if there's a valid light probe grid + if (validGrid) + EntityManager.AddComponent(m_MissingSHQueryBlend, m_SHComponentType[0]); + else + EntityManager.RemoveComponent(m_RemoveSHFromBlendProbeTagQuery, m_SHComponentType[0]); + + // AmbientProbeTag entities never have SH components + EntityManager.RemoveComponent(m_MissingProbeTagQuery, m_SHComponentType[0]); + } + } +} +#endif diff --git a/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs.meta b/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs.meta new file mode 100644 index 0000000..594a2fb --- /dev/null +++ b/Unity.Entities.Graphics/Probes/ManageSHPropertiesSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9fe24ea5d0cf44758e3c001be63906de +timeCreated: 1590664823 \ No newline at end of file diff --git a/Unity.Entities.Graphics/RemoveLocalBounds.cs b/Unity.Entities.Graphics/RemoveLocalBounds.cs new file mode 100644 index 0000000..fba81f1 --- /dev/null +++ b/Unity.Entities.Graphics/RemoveLocalBounds.cs @@ -0,0 +1,23 @@ +using Unity.Entities; +using Unity.Transforms; + +namespace Unity.Rendering +{ + /* Disabled for now. Makes chunk bounds go out of sync. + [WorldSystemFilter(WorldSystemFilterFlags.EntitySceneOptimizations)] + [UpdateAfter(typeof(RenderBoundsUpdateSystem))] + class RemoveLocalBounds : ComponentSystem + { + protected override void OnUpdate() + { + var group = GetEntityQuery( + new EntityQueryDesc + { + All = new ComponentType[] { typeof(RenderBounds), typeof(Static) } + }); + + EntityManager.RemoveComponent(group, new ComponentTypes (typeof(RenderBounds))); + } + } + */ +} diff --git a/Unity.Entities.Graphics/RemoveLocalBounds.cs.meta b/Unity.Entities.Graphics/RemoveLocalBounds.cs.meta new file mode 100644 index 0000000..ea19c61 --- /dev/null +++ b/Unity.Entities.Graphics/RemoveLocalBounds.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0dea4e1e91b046069bbf7e179bd5786 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderBoundsComponent.cs b/Unity.Entities.Graphics/RenderBoundsComponent.cs new file mode 100644 index 0000000..d48eb1b --- /dev/null +++ b/Unity.Entities.Graphics/RenderBoundsComponent.cs @@ -0,0 +1,40 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + + /// + /// An unmanaged component that represent the render bounds. + /// + public struct RenderBounds : IComponentData + { + /// + /// The axis-aligned render bounds. + /// + public AABB Value; + } + + + /// + /// An unmanaged component that represents the world render bounds. + /// + public struct WorldRenderBounds : IComponentData + { + /// + /// The axis-aligned render bounds. + /// + public AABB Value; + } + + /// + /// An unmanaged component that represents the render bounds of a chunk. + /// + public struct ChunkWorldRenderBounds : IComponentData + { + /// + /// The axis-aligned render bounds. + /// + public AABB Value; + } +} diff --git a/Unity.Entities.Graphics/RenderBoundsComponent.cs.meta b/Unity.Entities.Graphics/RenderBoundsComponent.cs.meta new file mode 100644 index 0000000..b4839ee --- /dev/null +++ b/Unity.Entities.Graphics/RenderBoundsComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd2d1996180b04bb78e591be147e4342 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs b/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs new file mode 100644 index 0000000..c353fb4 --- /dev/null +++ b/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs @@ -0,0 +1,247 @@ +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Jobs; +using Unity.Mathematics; +using Unity.Transforms; + +namespace Unity.Rendering +{ + + /// + /// A system that generates a scene bounding volume for each section at conversion time. + /// + [WorldSystemFilter(WorldSystemFilterFlags.EntitySceneOptimizations)] + [UpdateAfter(typeof(RenderBoundsUpdateSystem))] + partial class UpdateSceneBoundingVolumeFromRendererBounds : SystemBase + { + [BurstCompile] + struct CollectSceneBoundsJob : IJob + { + [ReadOnly] + [DeallocateOnJobCompletion] + public NativeArray RenderBounds; + + public Entity SceneBoundsEntity; + public ComponentLookup SceneBounds; + + public void Execute() + { + var minMaxAabb = MinMaxAABB.Empty; + for (int i = 0; i != RenderBounds.Length; i++) + { + var aabb = RenderBounds[i].Value; + + // MUST BE FIXED BY DOTS-2518 + // + // Avoid empty RenderBounds AABB because is means it hasn't been computed yet + // There are some unfortunate cases where RenderBoundsUpdateSystem is executed after this system + // and a bad Scene AABB is computed if we consider these empty RenderBounds AABB. + if (math.lengthsq(aabb.Center) != 0.0f && math.lengthsq(aabb.Extents) != 0.0f) + { + minMaxAabb.Encapsulate(aabb); + } + } + SceneBounds[SceneBoundsEntity] = new Unity.Scenes.SceneBoundingVolume { Value = minMaxAabb }; + } + } + + /// + protected override void OnUpdate() + { + //@TODO: API does not allow me to use ChunkComponentData. + //Review with simon how we can improve it. + + var query = GetEntityQuery(typeof(WorldRenderBounds), typeof(SceneSection)); + + EntityManager.GetAllUniqueSharedComponents(out var sections, Allocator.Temp); + foreach (var section in sections) + { + if (section.Equals(default(SceneSection))) + continue; + + query.SetSharedComponentFilter(section); + + var entity = EntityManager.CreateEntity(typeof(Unity.Scenes.SceneBoundingVolume)); + EntityManager.AddSharedComponent(entity, section); + + var job = new CollectSceneBoundsJob(); + job.RenderBounds = query.ToComponentDataArray(Allocator.TempJob); + job.SceneBoundsEntity = entity; + job.SceneBounds = GetComponentLookup(); + job.Run(); + } + + query.ResetFilter(); + } + } + + + [UpdateInGroup(typeof(StructuralChangePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + partial class AddWorldAndChunkRenderBounds : SystemBase + { + EntityQuery m_MissingWorldRenderBounds; + EntityQuery m_MissingWorldChunkRenderBounds; + + /// + protected override void OnCreate() + { + m_MissingWorldRenderBounds = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] {ComponentType.ReadOnly(), ComponentType.ReadOnly()}, + None = new[] {ComponentType.ReadOnly()}, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + + m_MissingWorldChunkRenderBounds = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] {ComponentType.ReadOnly(), ComponentType.ReadOnly()}, + None = new[] { ComponentType.ChunkComponentReadOnly() }, + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab + } + ); + } + + /// + protected override void OnUpdate() + { + EntityManager.AddComponent(m_MissingWorldRenderBounds, ComponentType.ReadWrite()); + EntityManager.AddComponent(m_MissingWorldChunkRenderBounds, ComponentType.ChunkComponent()); + } + } + + /// + /// A system that updates the WorldRenderBounds for entities that have both a LocalToWorld and RenderBounds component. + /// + /// + /// This system also ensures that a WorldRenderBounds exists on entities that have a LocalToWorld and RenderBounds component. + /// + [RequireMatchingQueriesForUpdate] + [UpdateInGroup(typeof(UpdatePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + partial class RenderBoundsUpdateSystem : SystemBase + { + EntityQuery m_WorldRenderBounds; + + [BurstCompile] + struct BoundsJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle RendererBounds; + [ReadOnly] public ComponentTypeHandle LocalToWorld; + public ComponentTypeHandle WorldRenderBounds; + public ComponentTypeHandle ChunkWorldRenderBounds; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var worldBounds = chunk.GetNativeArray(WorldRenderBounds); + var localBounds = chunk.GetNativeArray(RendererBounds); + var localToWorld = chunk.GetNativeArray(LocalToWorld); + MinMaxAABB combined = MinMaxAABB.Empty; + for (int i = 0; i != localBounds.Length; i++) + { + var transformed = AABB.Transform(localToWorld[i].Value, localBounds[i].Value); + + worldBounds[i] = new WorldRenderBounds { Value = transformed }; + combined.Encapsulate(transformed); + } + + chunk.SetChunkComponentData(ChunkWorldRenderBounds, new ChunkWorldRenderBounds { Value = combined }); + } + } + + /// + protected override void OnCreate() + { + m_WorldRenderBounds = GetEntityQuery + ( + new EntityQueryDesc + { + All = new[] { ComponentType.ChunkComponent(), ComponentType.ReadWrite(), ComponentType.ReadOnly(), ComponentType.ReadOnly() }, + } + ); + m_WorldRenderBounds.SetChangedVersionFilter(new[] { ComponentType.ReadOnly(), ComponentType.ReadOnly()}); + m_WorldRenderBounds.AddOrderVersionFilter(); + } + + /// + protected override void OnUpdate() + { + if (!EntitiesGraphicsSystem.EntitiesGraphicsEnabled) + return; + + var boundsJob = new BoundsJob + { + RendererBounds = GetComponentTypeHandle(true), + LocalToWorld = GetComponentTypeHandle(true), + WorldRenderBounds = GetComponentTypeHandle(), + ChunkWorldRenderBounds = GetComponentTypeHandle(), + }; + Dependency = boundsJob.ScheduleParallel(m_WorldRenderBounds, Dependency); + } + +#if false + public void DrawGizmos() + { + var boundsQuery = GetEntityQuery(typeof(LocalToWorld), typeof(WorldRenderBounds), typeof(RenderBounds)); + var localToWorlds = boundsQuery.ToComponentDataArray(Allocator.TempJob); + var worldBounds = boundsQuery.ToComponentDataArray(Allocator.TempJob); + var localBounds = boundsQuery.ToComponentDataArray(Allocator.TempJob); + + var chunkBoundsQuery = GetEntityQuery(ComponentType.ReadOnly(), typeof(ChunkHeader)); + var chunksBounds = chunkBoundsQuery.ToComponentDataArray(Allocator.TempJob); + + Gizmos.matrix = Matrix4x4.identity; + + // world bounds + Gizmos.color = Color.green; + for (int i = 0; i != worldBounds.Length; i++) + Gizmos.DrawWireCube(worldBounds[i].Value.Center, worldBounds[i].Value.Size); + + // chunk world bounds + Gizmos.color = Color.yellow; + for (int i = 0; i != chunksBounds.Length; i++) + Gizmos.DrawWireCube(chunksBounds[i].Value.Center, chunksBounds[i].Value.Size); + + // local render bounds + Gizmos.color = Color.blue; + for (int i = 0; i != localToWorlds.Length; i++) + { + Gizmos.matrix = new Matrix4x4(localToWorlds[i].Value.c0, localToWorlds[i].Value.c1, localToWorlds[i].Value.c2, localToWorlds[i].Value.c3); + Gizmos.DrawWireCube(localBounds[i].Value.Center, localBounds[i].Value.Size); + } + + localToWorlds.Dispose(); + worldBounds.Dispose(); + localBounds.Dispose(); + chunksBounds.Dispose(); + } + + //@TODO: We really need a system level gizmo callback. + [UnityEditor.DrawGizmo(UnityEditor.GizmoType.NonSelected)] + public static void DrawGizmos(Light light, UnityEditor.GizmoType type) + { + if (light.type == LightType.Directional && light.isActiveAndEnabled) + { + if (World.DefaultGameObjectInjectionWorld == null) + return; + + var renderer = World.DefaultGameObjectInjectionWorld.GetExistingSystem(); + if (renderer != null) + renderer.DrawGizmos(); + } + } + +#endif + } +} diff --git a/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs.meta b/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs.meta new file mode 100644 index 0000000..2a02fa9 --- /dev/null +++ b/Unity.Entities.Graphics/RenderBoundsUpdateSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52d81b600fb8b4996a8af917e06723bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderFilterSettings.cs b/Unity.Entities.Graphics/RenderFilterSettings.cs new file mode 100644 index 0000000..926f1ca --- /dev/null +++ b/Unity.Entities.Graphics/RenderFilterSettings.cs @@ -0,0 +1,130 @@ +using System; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Entities.Graphics +{ + /// + /// Represents settings that control when to render a given entity. + /// + /// + /// For example, you can set the layermask of the entity and also set whether to render an entity in shadow maps or motion passes. + /// + public struct RenderFilterSettings : ISharedComponentData, IEquatable + { + /// + /// The [LayerMask](https://docs.unity3d.com/ScriptReference/LayerMask.html) index. + /// + /// + /// For entities that Unity converts from GameObjects, this value is the same as the Layer setting of the source + /// GameObject. + /// + [LayerField] public int Layer; + + /// + /// The rendering layer the entity is part of. + /// + /// + /// This value corresponds to . + /// + public uint RenderingLayerMask; + + /// + /// Specifies what kinds of motion vectors to generate for the entity, if any. + /// + /// + /// This value corresponds to . + /// + /// This value only affects render pipelines that use motion vectors. + /// + public MotionVectorGenerationMode MotionMode; + + /// + /// Specifies how the entity should cast shadows. + /// + /// + /// For entities that Unity converts from GameObjects, this value is the same as the Cast Shadows property of the source + /// Mesh Renderer component. + /// For more information, refer to [ShadowCastingMode](https://docs.unity3d.com/ScriptReference/Rendering.ShadowCastingMode.html). + /// + public ShadowCastingMode ShadowCastingMode; + + /// + /// Indicates whether to cast shadows onto the entity. + /// + /// + /// For entities that Unity converts from GameObjects, this value is the same as the Receive Shadows property of the source + /// Mesh Renderer component. + /// This value only affects [Progressive Lightmappers](https://docs.unity3d.com/Manual/ProgressiveLightmapper.html). + /// + public bool ReceiveShadows; + + /// + /// Indicates whether the entity is a static shadow caster. + /// + /// + /// This value is important to the BatchRenderGroup. + /// + public bool StaticShadowCaster; + + /// + /// Returns a new default instance of RenderFilterSettings. + /// + public static RenderFilterSettings Default => new RenderFilterSettings + { + Layer = 0, + RenderingLayerMask = 0xffffffff, + MotionMode = MotionVectorGenerationMode.Object, + ShadowCastingMode = ShadowCastingMode.On, + ReceiveShadows = true, + StaticShadowCaster = false, + }; + + /// + /// Indicates whether the motion mode for the current pass is not camera. + /// + public bool IsInMotionPass => + MotionMode != MotionVectorGenerationMode.Camera; + + /// + public override bool Equals(object obj) + { + if (obj is RenderFilterSettings) + return Equals((RenderFilterSettings) obj); + + return false; + } + + /// + public bool Equals(RenderFilterSettings other) + { + return Layer == other.Layer && RenderingLayerMask == other.RenderingLayerMask && MotionMode == other.MotionMode && ShadowCastingMode == other.ShadowCastingMode && ReceiveShadows == other.ReceiveShadows && StaticShadowCaster == other.StaticShadowCaster; + } + + /// + public override int GetHashCode() + { + var hash = new xxHash3.StreamingState(true); + hash.Update(Layer); + hash.Update(RenderingLayerMask); + hash.Update(MotionMode); + hash.Update(ShadowCastingMode); + hash.Update(ReceiveShadows); + hash.Update(StaticShadowCaster); + return (int)hash.DigestHash64().x; + } + + /// + public static bool operator ==(RenderFilterSettings left, RenderFilterSettings right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(RenderFilterSettings left, RenderFilterSettings right) + { + return !left.Equals(right); + } + } +} diff --git a/Unity.Entities.Graphics/RenderFilterSettings.cs.meta b/Unity.Entities.Graphics/RenderFilterSettings.cs.meta new file mode 100644 index 0000000..e680b7d --- /dev/null +++ b/Unity.Entities.Graphics/RenderFilterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03581ea10f654975877c111d31906d9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderMeshArray.cs b/Unity.Entities.Graphics/RenderMeshArray.cs new file mode 100644 index 0000000..da012aa --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshArray.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + /// + /// Represents which materials and meshes to use to render an entity. + /// + /// + /// This struct supports both a serializable static encoding in which case Material and Mesh are + /// array indices to some array (typically a RenderMeshArray), and direct use of + /// runtime BatchRendererGroup BatchMaterialID / BatchMeshID values. + /// + public struct MaterialMeshInfo : IComponentData + { + + /// + /// The material ID. + /// + public int Material; + + /// + /// The mesh ID. + /// + public int Mesh; + + /// + /// The sub-mesh ID. + /// + public sbyte Submesh; + + internal bool IsRuntimeMaterial => Material >= 0; + internal bool IsRuntimeMesh => Mesh >= 0; + + + /// + /// Converts the given array index (typically the index inside RenderMeshArray) into + /// a negative number that denotes that array position. + /// + /// The index to convert. + /// Returns the converted index. + public static int ArrayIndexToStaticIndex(int index) => (index < 0) + ? index + : (-index - 1); + + /// + /// Converts the given static index (a negative value) to a valid array index. + /// + /// The index to convert. + /// Returns the converted index. + public static int StaticIndexToArrayIndex(int staticIndex) => math.abs(staticIndex) - 1; + + /// + /// Creates an instance of MaterialMeshInfo from material and mesh/sub-mesh ids. + /// + /// The material ID. + /// The mesh ID. + /// An optional submesh ID. + /// + public static MaterialMeshInfo FromRenderMeshArrayIndices( + int materialIndexInRenderMeshArray, + int meshIndexInRenderMeshArray, + sbyte submeshIndex = 0) + { + return new MaterialMeshInfo( + ArrayIndexToStaticIndex(materialIndexInRenderMeshArray), + ArrayIndexToStaticIndex(meshIndexInRenderMeshArray), + submeshIndex); + } + + private MaterialMeshInfo(int materialIndex, int meshIndex, sbyte submeshIndex = 0) + { + Material = materialIndex; + Mesh = meshIndex; + Submesh = submeshIndex; + } + + private MaterialMeshInfo(BatchMaterialID materialID, BatchMeshID meshID, sbyte submeshIndex = 0) + : this((int)materialID.value, (int)meshID.value, submeshIndex) + {} + + + /// + /// The mesh ID property. + /// + public BatchMeshID MeshID + { + get + { + Debug.Assert(IsRuntimeMesh); + return new BatchMeshID { value = (uint)Mesh }; + } + + set => Mesh = (int) value.value; + } + + /// + /// The material ID property. + /// + public BatchMaterialID MaterialID + { + get + { + Debug.Assert(IsRuntimeMaterial); + return new BatchMaterialID() { value = (uint)Material }; + } + + set => Material = (int) value.value; + } + + internal int MeshArrayIndex + { + get => IsRuntimeMesh ? -1 : StaticIndexToArrayIndex(Mesh); + set => Mesh = ArrayIndexToStaticIndex(value); + } + + internal int MaterialArrayIndex + { + get => IsRuntimeMaterial ? -1 : StaticIndexToArrayIndex(Material); + set => Material = ArrayIndexToStaticIndex(value); + } + } + + internal struct AssetHash + { + public static void UpdateAsset(ref xxHash3.StreamingState hash, UnityEngine.Object asset) + { + // In the editor we can compute a stable serializable hash using an asset GUID +#if UNITY_EDITOR + bool success = AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out string guid, out long localId); + hash.Update(success); + if (!success) + { + hash.Update(asset.GetInstanceID()); + return; + } + var guidBytes = Encoding.UTF8.GetBytes(guid); + + hash.Update(guidBytes.Length); + for (int j = 0; j < guidBytes.Length; ++j) + hash.Update(guidBytes[j]); + hash.Update(localId); +#else + // In standalone, we have to resort to using the instance ID which is not serializable, + // but should be usable in the context of this execution. + hash.Update(asset.GetInstanceID()); +#endif + } + } + + /// + /// A shared component that contains meshes and materials. + /// + public struct RenderMeshArray : ISharedComponentData, IEquatable + { + [SerializeField] private Material[] m_Materials; + [SerializeField] private Mesh[] m_Meshes; + // Memoize the expensive 128-bit hash + [SerializeField] private uint4 m_Hash128; + + /// + /// Constructs an instance of RenderMeshArray from an array of materials and an array of meshes. + /// + /// The array of materials to use in the RenderMeshArray. + /// The array of meshes to use in the RenderMeshArray. + public RenderMeshArray(Material[] materials, Mesh[] meshes) + { + m_Meshes = meshes; + m_Materials = materials; + m_Hash128 = uint4.zero; + ResetHash128(); + } + + /// + /// Accessor property for the meshes array. + /// + public Mesh[] Meshes + { + get => m_Meshes; + set + { + m_Hash128 = uint4.zero; + m_Meshes = value; + } + } + + /// + /// Accessor property for the materials array. + /// + public Material[] Materials + { + get => m_Materials; + set + { + m_Hash128 = uint4.zero; + m_Materials = value; + } + } + + internal Mesh GetMeshWithStaticIndex(int staticMeshIndex) + { + Debug.Assert(staticMeshIndex <= 0, "Mesh index must be a static index (non-positive)"); + + if (staticMeshIndex >= 0) + return null; + + return m_Meshes[MaterialMeshInfo.StaticIndexToArrayIndex(staticMeshIndex)]; + } + + internal Material GetMaterialWithStaticIndex(int staticMaterialIndex) + { + Debug.Assert(staticMaterialIndex <= 0, "Material index must be a static index (non-positive)"); + + if (staticMaterialIndex >= 0) + return null; + + return m_Materials[MaterialMeshInfo.StaticIndexToArrayIndex(staticMaterialIndex)]; + } + + internal Dictionary GetMeshToIndexMapping() + { + var mapping = new Dictionary(); + + if (m_Meshes == null) + return mapping; + + int numMeshes = m_Meshes.Length; + + for (int i = 0; i < numMeshes; ++i) + mapping[m_Meshes[i]] = MaterialMeshInfo.ArrayIndexToStaticIndex(i); + + return mapping; + } + + internal Dictionary GetMaterialToIndexMapping() + { + var mapping = new Dictionary(); + + if (m_Materials == null) + return mapping; + + int numMaterials = m_Materials.Length; + + for (int i = 0; i < numMaterials; ++i) + mapping[m_Materials[i]] = MaterialMeshInfo.ArrayIndexToStaticIndex(i); + + return mapping; + } + + + /// + /// Returns a 128-bit hash that (almost) uniquely identifies the contents of the component. + /// + /// + /// This is useful to help make comparisons between RenderMeshArray instances less resource intensive. + /// + /// Returns the 128-bit hash value. + public uint4 GetHash128() + { + return m_Hash128; + } + + /// + /// Recalculates the 128-bit hash value of the component. + /// + public void ResetHash128() + { + m_Hash128 = ComputeHash128(); + } + + /// + /// Calculates and returns the 128-bit hash value of the component contents. + /// + /// + /// This is equivalent to calling and then . + /// + /// Returns the calculated 128-bit hash value. + public uint4 ComputeHash128() + { + var hash = new xxHash3.StreamingState(false); + + int numMeshes = m_Meshes?.Length ?? 0; + int numMaterials = m_Materials?.Length ?? 0; + + hash.Update(numMeshes); + hash.Update(numMaterials); + + for (int i = 0; i < numMeshes; ++i) + AssetHash.UpdateAsset(ref hash, m_Meshes[i]); + + for (int i = 0; i < numMaterials; ++i) + AssetHash.UpdateAsset(ref hash, m_Materials[i]); + + uint4 H = hash.DigestHash128(); + + // Make sure the hash is never exactly zero, to keep zero as a null value + if (math.all(H == uint4.zero)) + return new uint4(1, 0, 0, 0); + + return H; + } + + /// + /// Combines a list of RenderMeshes into one RenderMeshArray. + /// + /// The list of RenderMesh instances to combine. + /// Returns a RenderMeshArray instance that contains containing all of the meshes and materials. + public static RenderMeshArray CombineRenderMeshes(List renderMeshes) + { + var meshes = new Dictionary(renderMeshes.Count); + var materials = new Dictionary(renderMeshes.Count); + + foreach (var rm in renderMeshes) + { + meshes[rm.mesh] = true; + materials[rm.material] = true; + } + + return new RenderMeshArray(materials.Keys.ToArray(), meshes.Keys.ToArray()); + } + + /// + /// Combines a list of RenderMeshArrays into one RenderMeshArray. + /// + /// The list of RenderMeshArray instances to combine. + /// Returns a RenderMeshArray instance that contains all of the meshes and materials. + public static RenderMeshArray CombineRenderMeshArrays(List renderMeshArrays) + { + int totalMeshes = 0; + int totalMaterials = 0; + + foreach (var rma in renderMeshArrays) + { + totalMeshes += rma.Meshes?.Length ?? 0; + totalMaterials += rma.Meshes?.Length ?? 0; + } + + var meshes = new Dictionary(totalMeshes); + var materials = new Dictionary(totalMaterials); + + foreach (var rma in renderMeshArrays) + { + foreach (var mesh in rma.Meshes) + meshes[mesh] = true; + + foreach (var material in rma.Materials) + materials[material] = true; + } + + return new RenderMeshArray(materials.Keys.ToArray(), meshes.Keys.ToArray()); + } + + /// + /// Creates the new instance of the RenderMeshArray from given mesh and material lists, removing duplicate entries. + /// + /// The list of the materials. + /// The list of the meshes. + /// Returns a RenderMeshArray instance that contains all off the meshes and materials, and with no duplicates. + public static RenderMeshArray CreateWithDeduplication( + List materialsWithDuplicates, List meshesWithDuplicates) + { + var meshes = new Dictionary(meshesWithDuplicates.Count); + var materials = new Dictionary(materialsWithDuplicates.Count); + + foreach (var mat in materialsWithDuplicates) + materials[mat] = true; + + foreach (var mesh in meshesWithDuplicates) + meshes[mesh] = true; + + return new RenderMeshArray(materials.Keys.ToArray(), meshes.Keys.ToArray()); + } + + + /// + /// Gets the material for given MaterialMeshInfo. + /// + /// The MaterialMeshInfo to use. + /// Returns the associated material instance, or null if the material is runtime. + public Material GetMaterial(MaterialMeshInfo materialMeshInfo) + { + if (materialMeshInfo.IsRuntimeMaterial) + return null; + else + return Materials[materialMeshInfo.MaterialArrayIndex]; + } + + /// + /// Gets the mesh for given MaterialMeshInfo. + /// + /// The MaterialMeshInfo to use. + /// Returns the associated Mesh instance or null if the mesh is runtime. + public Mesh GetMesh(MaterialMeshInfo materialMeshInfo) + { + if (materialMeshInfo.IsRuntimeMesh) + return null; + else + return Meshes[materialMeshInfo.MeshArrayIndex]; + } + + /// + /// Determines whether two object instances are equal based on their hashes. + /// + /// The object to compare with the current object. + /// Returns true if the specified object is equal to the current object. Otherwise, returns false. + public bool Equals(RenderMeshArray other) + { + return math.all(GetHash128() == other.GetHash128()); + } + + /// + public override bool Equals(object obj) + { + return obj is RenderMeshArray other && Equals(other); + } + + /// + public override int GetHashCode() + { + return (int) GetHash128().x; + } + + /// + /// The equality operator == returns true if its operands are equal, false otherwise. + /// + /// The left instance to compare. + /// The right instance to compare. + /// True if left and right instances are equal and false otherwise. + public static bool operator ==(RenderMeshArray left, RenderMeshArray right) + { + return left.Equals(right); + } + + /// + /// The not equality operator != returns false if its operands are equal, true otherwise. + /// + /// The left instance to compare. + /// The right instance to compare. + /// False if left and right instances are equal and true otherwise. + public static bool operator !=(RenderMeshArray left, RenderMeshArray right) + { + return !left.Equals(right); + } + } +} diff --git a/Unity.Entities.Graphics/RenderMeshArray.cs.meta b/Unity.Entities.Graphics/RenderMeshArray.cs.meta new file mode 100644 index 0000000..54fc92e --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshArray.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a47c521b641e417c88a65f06d8811c90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderMeshBakingContext.cs b/Unity.Entities.Graphics/RenderMeshBakingContext.cs new file mode 100644 index 0000000..de1da3e --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshBakingContext.cs @@ -0,0 +1,317 @@ +// #define DEBUG_LOG_LIGHT_MAP_CONVERSION + +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Collections; +using Unity.Entities; +using UnityEngine; +using Hash128 = UnityEngine.Hash128; + +namespace Unity.Rendering +{ + class LightMapBakingContext + { + [Flags] + enum LightMappingFlags + { + None = 0, + Lightmapped = 1, + Directional = 2, + ShadowMask = 4 + } + + struct MaterialLookupKey + { + public Material BaseMaterial; + public LightMaps LightMaps; + public LightMappingFlags Flags; + } + + struct LightMapKey : IEquatable + { + public Hash128 ColorHash; + public Hash128 DirectionHash; + public Hash128 ShadowMaskHash; + + public LightMapKey(LightmapData lightmapData) + : this(lightmapData.lightmapColor, + lightmapData.lightmapDir, + lightmapData.shadowMask) + { + } + + public LightMapKey(Texture2D color, Texture2D direction, Texture2D shadowMask) + { + ColorHash = default; + DirectionHash = default; + ShadowMaskHash = default; + +#if UNITY_EDITOR + // imageContentsHash only available in the editor, but this type is only used + // during conversion, so it's only used in the editor. + if (color != null) ColorHash = color.imageContentsHash; + if (direction != null) DirectionHash = direction.imageContentsHash; + if (shadowMask != null) ShadowMaskHash = shadowMask.imageContentsHash; +#endif + } + + public bool Equals(LightMapKey other) + { + return ColorHash.Equals(other.ColorHash) && DirectionHash.Equals(other.DirectionHash) && ShadowMaskHash.Equals(other.ShadowMaskHash); + } + + public override int GetHashCode() + { + var hash = new xxHash3.StreamingState(true); + hash.Update(ColorHash); + hash.Update(DirectionHash); + hash.Update(ShadowMaskHash); + return (int) hash.DigestHash64().x; + } + } + + public class LightMapReference + { + public LightMaps LightMaps; + public int LightMapIndex; + } + + private int m_NumLightMapCacheHits; + private int m_NumLightMapCacheMisses; + private int m_NumLightMappedMaterialCacheHits; + private int m_NumLightMappedMaterialCacheMisses; + + private Dictionary m_LightMapArrayCache; + private Dictionary m_LightMappedMaterialCache = new Dictionary(); + + private List m_UsedLightmapIndices = new List(); + private Dictionary m_LightMapReferences; + + public LightMapBakingContext() + { + Reset(); + } + + public void Reset() + { + m_LightMapArrayCache = new Dictionary(); + m_LightMappedMaterialCache = new Dictionary(); + + BeginConversion(); + } + + public void BeginConversion() + { + m_UsedLightmapIndices = new List(); + m_LightMapReferences = new Dictionary(); + + m_NumLightMapCacheHits = 0; + m_NumLightMapCacheMisses = 0; + m_NumLightMappedMaterialCacheHits = 0; + m_NumLightMappedMaterialCacheMisses = 0; + } + + public void EndConversion() + { +#if DEBUG_LOG_LIGHT_MAP_CONVERSION + Debug.Log($"Light map cache: {m_NumLightMapCacheHits} hits, {m_NumLightMapCacheMisses} misses. Light mapped material cache: {m_NumLightMappedMaterialCacheHits} hits, {m_NumLightMappedMaterialCacheMisses} misses."); +#endif + } + + public void CollectLightMapUsage(Renderer renderer) + { + m_UsedLightmapIndices.Add(renderer.lightmapIndex); + } + + // Check all light maps referenced within the current batch of converted Renderers. + // Any references to light maps that have already been inserted into a LightMaps array + // will be implemented by reusing the existing LightMaps object. Any leftover previously + // unseen (or changed = content hash changed) light maps are combined into a new LightMaps array. + public void ProcessLightMapsForConversion() + { + var lightmaps = LightmapSettings.lightmaps; + var uniqueIndices = m_UsedLightmapIndices + .Distinct() + .OrderBy(x => x) + .Where(x=> x >= 0 && x != 65534 && x < lightmaps.Length) + .ToArray(); + + var colors = new List(); + var directions = new List(); + var shadowMasks = new List(); + var lightMapIndices = new List(); + + // Each light map reference is converted into a LightMapKey which identifies the light map + // using the content hashes regardless of the index number. Previously encountered light maps + // should be found from the cache even if their index number has changed. New or changed + // light maps are placed into a new array. + for (var i = 0; i < uniqueIndices.Length; i++) + { + var index = uniqueIndices[i]; + var lightmapData = lightmaps[index]; + var key = new LightMapKey(lightmapData); + + if (m_LightMapArrayCache.TryGetValue(key, out var lightMapRef)) + { + m_LightMapReferences[index] = lightMapRef; + ++m_NumLightMapCacheHits; + } + else + { + colors.Add(lightmapData.lightmapColor); + directions.Add(lightmapData.lightmapDir); + shadowMasks.Add(lightmapData.shadowMask); + lightMapIndices.Add(index); + ++m_NumLightMapCacheMisses; + } + } + + if (lightMapIndices.Count > 0) + { +#if DEBUG_LOG_LIGHT_MAP_CONVERSION + Debug.Log($"Creating new DOTS light map array from {lightMapIndices.Count} light maps."); +#endif + + var lightMapArray = LightMaps.ConstructLightMaps(colors, directions, shadowMasks); + + for (int i = 0; i < lightMapIndices.Count; ++i) + { + var lightMapRef = new LightMapReference + { + LightMaps = lightMapArray, + LightMapIndex = i, + }; + + m_LightMapReferences[lightMapIndices[i]] = lightMapRef; + m_LightMapArrayCache[new LightMapKey(colors[i], directions[i], shadowMasks[i])] = lightMapRef; + } + } + } + + public LightMapReference GetLightMapReference(Renderer renderer) + { + if (m_LightMapReferences.TryGetValue(renderer.lightmapIndex, out var lightMapRef)) + return lightMapRef; + else + return null; + } + + public Material GetLightMappedMaterial(Material baseMaterial, LightMapReference lightMapRef) + { + var flags = LightMappingFlags.Lightmapped; + if (lightMapRef.LightMaps.hasDirections) + flags |= LightMappingFlags.Directional; + if (lightMapRef.LightMaps.hasShadowMask) + flags |= LightMappingFlags.ShadowMask; + + var key = new MaterialLookupKey + { + BaseMaterial = baseMaterial, + LightMaps = lightMapRef.LightMaps, + Flags = flags + }; + + if (m_LightMappedMaterialCache.TryGetValue(key, out var lightMappedMaterial)) + { + ++m_NumLightMappedMaterialCacheHits; + return lightMappedMaterial; + } + else + { + ++m_NumLightMappedMaterialCacheMisses; + lightMappedMaterial = CreateLightMappedMaterial(baseMaterial, lightMapRef.LightMaps); + m_LightMappedMaterialCache[key] = lightMappedMaterial; + return lightMappedMaterial; + } + } + + private static Material CreateLightMappedMaterial(Material material, LightMaps lightMaps) + { + var lightMappedMaterial = new Material(material); + lightMappedMaterial.name = $"{lightMappedMaterial.name}_Lightmapped_"; + lightMappedMaterial.EnableKeyword("LIGHTMAP_ON"); + + lightMappedMaterial.SetTexture("unity_Lightmaps", lightMaps.colors); + lightMappedMaterial.SetTexture("unity_LightmapsInd", lightMaps.directions); + lightMappedMaterial.SetTexture("unity_ShadowMasks", lightMaps.shadowMasks); + + if (lightMaps.hasDirections) + { + lightMappedMaterial.name = lightMappedMaterial.name + "_DIRLIGHTMAP"; + lightMappedMaterial.EnableKeyword("DIRLIGHTMAP_COMBINED"); + } + + if (lightMaps.hasShadowMask) + { + lightMappedMaterial.name = lightMappedMaterial.name + "_SHADOW_MASK"; + } + + return lightMappedMaterial; + } + } + + class RenderMeshBakingContext + { + private LightMapBakingContext m_LightMapBakingContext; + + /// + /// Constructs a baking context that operates within a baking system. + /// + public RenderMeshBakingContext( + LightMapBakingContext lightMapBakingContext = null) + { + m_LightMapBakingContext = lightMapBakingContext; + m_LightMapBakingContext?.BeginConversion(); + } + + public void EndConversion() + { + m_LightMapBakingContext?.EndConversion(); + } + + public void CollectLightMapUsage(Renderer renderer) + { + Debug.Assert(m_LightMapBakingContext != null, + "LightMapConversionContext must be set to call light mapping conversion methods."); + m_LightMapBakingContext.CollectLightMapUsage(renderer); + } + + public void ProcessLightMapsForConversion() + { + Debug.Assert(m_LightMapBakingContext != null, + "LightMapConversionContext must be set to call light mapping conversion methods."); + m_LightMapBakingContext.ProcessLightMapsForConversion(); + } + + internal Material ConfigureHybridLightMapping( + Entity entity, + EntityCommandBuffer ecb, + Renderer renderer, + Material material) + { + var staticLightingMode = RenderMeshUtility.StaticLightingModeFromRenderer(renderer); + if (staticLightingMode == RenderMeshUtility.StaticLightingMode.LightMapped) + { + var lightMapRef = m_LightMapBakingContext.GetLightMapReference(renderer); + + if (lightMapRef != null) + { + Material lightMappedMaterial = + m_LightMapBakingContext.GetLightMappedMaterial(material, lightMapRef); + + ecb.AddComponent(entity, + new BuiltinMaterialPropertyUnity_LightmapST() + {Value = renderer.lightmapScaleOffset}); + ecb.AddComponent(entity, + new BuiltinMaterialPropertyUnity_LightmapIndex() {Value = lightMapRef.LightMapIndex}); + ecb.AddSharedComponentManaged(entity, lightMapRef.LightMaps); + + return lightMappedMaterial; + } + } + + return null; + } + } +} diff --git a/Unity.Entities.Graphics/RenderMeshBakingContext.cs.meta b/Unity.Entities.Graphics/RenderMeshBakingContext.cs.meta new file mode 100644 index 0000000..b5eaed3 --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshBakingContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26e487fbe58cee2dd9577aca3b52fdea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderMeshProxy.cs b/Unity.Entities.Graphics/RenderMeshProxy.cs new file mode 100644 index 0000000..bffd85e --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshProxy.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using Unity.Core; +using Unity.Entities; +using UnityEngine; + +namespace Unity.Rendering +{ + + /// + /// Defines the mesh and rendering properties of an entity. + /// + /// + /// Add a RenderMesh component to an entity to define its graphical attributes. For Entities Graphics to render the entity, + /// the entity must also have a LocalToWorld component from the Unity.Transforms namespace. + /// + /// The standard ECS conversion systems add RenderMesh components to entities created from GameObjects that contain + /// [UnityEngine.MeshRenderer](https://docs.unity3d.com/ScriptReference/MeshRenderer.html) and + /// [UnityEngine.MeshFilter](https://docs.unity3d.com/ScriptReference/MeshFilter.html) components. + /// + /// RenderMesh is a shared component, which means all entities of the same Archetype and same RenderMesh settings + /// are stored together in the same chunks of memory. The rendering system batches the entities together to reduce + /// the number of draw calls. + /// + [Serializable] + // Culling system requires a maximum of 128 entities per chunk (See ChunkInstanceLodEnabled) + [MaximumChunkCapacity(128)] + [TemporaryBakingType] + public struct RenderMesh : ISharedComponentData, IEquatable + { + /// + /// A reference to a [UnityEngine.Mesh](https://docs.unity3d.com/ScriptReference/Mesh.html) object. + /// + public Mesh mesh; + /// + /// A reference to a [UnityEngine.Material](https://docs.unity3d.com/ScriptReference/Material.html) object. + /// + /// For efficient rendering, the material should enable GPU instancing. + /// For entities converted from GameObjects, this value is derived from the Materials array of the source + /// Mesh Renderer Component. + /// + public Material material; + /// + /// The submesh index. + /// + public int subMesh; + + /// + /// Constructs a RenderMesh using the given Renderer, Mesh, optional list of shared Materials, and option sub-mesh index. + /// + /// The Renderer to use. + /// The Mesh to use. + /// An optional list of Materials to use. + /// An options sub-mesh index that represents a sub-mesh in the mesh parameter. + public RenderMesh( + Renderer renderer, + Mesh mesh, + List sharedMaterials = null, + int subMeshIndex = 0) + { + Debug.Assert(renderer != null, "Must have a non-null Renderer to create RenderMesh."); + Debug.Assert(mesh != null, "Must have a non-null Mesh to create RenderMesh."); + + if (sharedMaterials is null) + sharedMaterials = new List(capacity: 10); + + if (sharedMaterials.Count == 0) + renderer.GetSharedMaterials(sharedMaterials); + + Debug.Assert(subMeshIndex >= 0 && subMeshIndex < sharedMaterials.Count, + "Sub-mesh index out of bounds, no matching material."); + + this.mesh = mesh; + material = sharedMaterials[subMeshIndex]; + subMesh = subMeshIndex; + } + + /// + /// Two RenderMesh objects are equal if their respective property values are equal. + /// + /// Another RenderMesh. + /// True, if the properties of both RenderMeshes are equal. + public bool Equals(RenderMesh other) + { + return + mesh == other.mesh && + material == other.material && + subMesh == other.subMesh; + } + + /// + /// A representative hash code. + /// + /// A number that is guaranteed to be the same when generated from two objects that are the same. + public override int GetHashCode() + { + int hash = 0; + + unsafe + { + var buffer = stackalloc[] + { + ReferenceEquals(mesh, null) ? 0 : mesh.GetHashCode(), + ReferenceEquals(material, null) ? 0 : material.GetHashCode(), + subMesh.GetHashCode(), + }; + + hash = (int)XXHash.Hash32((byte*)buffer, 3 * 4); + } + + return hash; + } + } +} diff --git a/Unity.Entities.Graphics/RenderMeshProxy.cs.meta b/Unity.Entities.Graphics/RenderMeshProxy.cs.meta new file mode 100644 index 0000000..5d37256 --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshProxy.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9b0fd4427893a4a16ba0c267dfd00217 +timeCreated: 1497278756 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 100 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/RenderMeshUtility.cs b/Unity.Entities.Graphics/RenderMeshUtility.cs new file mode 100644 index 0000000..a5d9fe5 --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshUtility.cs @@ -0,0 +1,417 @@ +#if HDRP_10_0_0_OR_NEWER +#define USE_HYBRID_MOTION_PASS +#endif + +using System; +using System.Collections.Generic; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Entities.Graphics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + /// + /// Represents how to setup and configure Entities Graphics entities. + /// + /// + /// This is useful to convert GameObjects into entities, or to set component values on entities directly. + /// + public struct RenderMeshDescription + { + /// + /// Filtering settings that determine when to draw the entity. + /// + public RenderFilterSettings FilterSettings; + + /// + /// Determines what kinds of light probes the entity uses, if any. + /// + /// + /// This value corresponds to . + /// + public LightProbeUsage LightProbeUsage; + + /// + /// Construct a using defaults from the given object. + /// + /// The renderer object (e.g. a ) to get default settings from. + public RenderMeshDescription(Renderer renderer) + { + Debug.Assert(renderer != null, "Must have a non-null Renderer to create RenderMeshDescription."); + + FilterSettings = new RenderFilterSettings + { + Layer = renderer.gameObject.layer, + RenderingLayerMask = renderer.renderingLayerMask, + ShadowCastingMode = renderer.shadowCastingMode, + ReceiveShadows = renderer.receiveShadows, + MotionMode = renderer.motionVectorGenerationMode, + StaticShadowCaster = renderer.staticShadowCaster, + }; + + var staticLightingMode = RenderMeshUtility.StaticLightingModeFromRenderer(renderer); + var lightProbeUsage = renderer.lightProbeUsage; + + LightProbeUsage = (staticLightingMode == RenderMeshUtility.StaticLightingMode.LightProbes) + ? lightProbeUsage + : LightProbeUsage.Off; + } + + /// + /// Construct a using the given values. + /// + /// Mode for shadow casting + /// Mode for shadow receival + /// Mode for motion vectors generation + /// Rendering layer + /// Rendering layer mask + /// Light probe usage mode + /// Static shadow caster flag + public RenderMeshDescription( + ShadowCastingMode shadowCastingMode, + bool receiveShadows = false, + MotionVectorGenerationMode motionVectorGenerationMode = MotionVectorGenerationMode.Camera, + int layer = 0, + uint renderingLayerMask = 0xffffffff, + LightProbeUsage lightProbeUsage = LightProbeUsage.Off, + bool staticShadowCaster = false) + { + FilterSettings = new RenderFilterSettings + { + Layer = layer, + RenderingLayerMask = renderingLayerMask, + ShadowCastingMode = shadowCastingMode, + ReceiveShadows = receiveShadows, + MotionMode = motionVectorGenerationMode, + StaticShadowCaster = staticShadowCaster, + }; + + LightProbeUsage = lightProbeUsage; + } + } + + /// + /// Helper class that contains static methods for populating entities + /// so that they are compatible with the Entities Graphics package. + /// + public static class RenderMeshUtility + { + // Settings which affect what components an entity will get + [Flags] + internal enum EntitiesGraphicsComponentFlags + { + None = 0, + GameObjectConversion = 1 << 0, + InMotionPass = 1 << 1, + LightProbesBlend = 1 << 2, + LightProbesCustom = 1 << 3, + DepthSorted = 1 << 4, + Baking = 1 << 5, + } + + // Pre-generate ComponentTypes objects for each flag combination, so all the components + // can be added at once, minimizing structural changes. + internal class EntitiesGraphicsComponentTypes + { + private ComponentTypeSet[] m_ComponentTypePermutations; + + public EntitiesGraphicsComponentTypes() + { + // Subtract one because of "None" + int numFlags = Enum.GetValues(typeof(EntitiesGraphicsComponentFlags)).Length - 1; + + var permutations = new List(); + for (int flags = 0; flags < (1 << numFlags); ++flags) + permutations.Add(GenerateComponentTypes((EntitiesGraphicsComponentFlags)flags)); + + m_ComponentTypePermutations = permutations.ToArray(); + } + + public ComponentTypeSet GetComponentTypes(EntitiesGraphicsComponentFlags flags) => + m_ComponentTypePermutations[(int) flags]; + + public static ComponentTypeSet GenerateComponentTypes(EntitiesGraphicsComponentFlags flags) + { + List components = new List() + { + // Absolute minimum set of components required by Entities Graphics + // to be considered for rendering. Entities without these components will + // not match queries and will never be rendered. + ComponentType.ReadWrite(), + ComponentType.ReadWrite(), + ComponentType.ReadWrite(), + ComponentType.ChunkComponent(), + ComponentType.ChunkComponent(), + // Extra transform related components required to render correctly + // using many default SRP shaders. Custom shaders could potentially + // work without it. + ComponentType.ReadWrite(), + // Components required by Entities Graphics package visibility culling. + ComponentType.ReadWrite(), + ComponentType.ReadWrite(), + }; + + // RenderMesh is no longer used at runtime, it is only used during conversion. + // At runtime all entities use RenderMeshArray. + if (flags.HasFlag(EntitiesGraphicsComponentFlags.GameObjectConversion) | flags.HasFlag(EntitiesGraphicsComponentFlags.Baking) ) + components.Add(ComponentType.ReadWrite()); + + if (!flags.HasFlag(EntitiesGraphicsComponentFlags.GameObjectConversion) | flags.HasFlag(EntitiesGraphicsComponentFlags.Baking) ) + components.Add(ComponentType.ReadWrite()); + + // Baking uses TransformUsageFlags, and as such should not be explicitly adding LocalToWorld to anything + if(!flags.HasFlag(EntitiesGraphicsComponentFlags.Baking)) + components.Add(ComponentType.ReadWrite()); + + // Components required by objects that need to be rendered in per-object motion passes. + #if USE_HYBRID_MOTION_PASS + if (flags.HasFlag(EntitiesGraphicsComponentFlags.InMotionPass)) + components.Add(ComponentType.ReadWrite()); + #endif + + if (flags.HasFlag(EntitiesGraphicsComponentFlags.LightProbesBlend)) + components.Add(ComponentType.ReadWrite()); + else if (flags.HasFlag(EntitiesGraphicsComponentFlags.LightProbesCustom)) + components.Add(ComponentType.ReadWrite()); + + if (flags.HasFlag(EntitiesGraphicsComponentFlags.DepthSorted)) + components.Add(ComponentType.ReadWrite()); + + return new ComponentTypeSet(components.ToArray()); + } + } + + internal static EntitiesGraphicsComponentTypes s_EntitiesGraphicsComponentTypes = new EntitiesGraphicsComponentTypes(); + + // Use a boolean constant for guarding most of the code so both ifdef branches are + // always compiled. + // This leads to the following warning due to the other branch being unreachable, so disable it + // warning CS0162: Unreachable code detected +#pragma warning disable CS0162 + +#if USE_HYBRID_MOTION_PASS + internal const bool kUseHybridMotionPass = true; +#else + internal const bool kUseHybridMotionPass = false; +#endif + /// + /// Set the Entities Graphics component values to render the given entity using the given description. + /// Any missing components will be added, which results in structural changes. + /// + /// The entity to set the component values for. + /// The used to set the component values. + /// The description that determines how the entity is to be rendered. + /// The description that determines how the entity is to be rendered. + /// + /// void CodeExample() + /// { + /// var world = World.DefaultGameObjectInjectionWorld; + /// var entityManager = world.EntityManager; + /// + /// var desc = new RenderMeshDescription( + /// Mesh, + /// Material + /// ); + /// + /// // RenderMeshUtility can be used to easily create Entities Graphics + /// // compatible entities, but it can only be called from the main thread. + /// var entity = entityManager.CreateEntity(); + /// RenderMeshUtility.AddComponents( + /// entity, + /// entityManager, + /// desc, + /// renderMesh); + /// entityManager.AddComponentData(entity, new ExampleComponent()); + /// + /// // If multiple similar entities are to be created, 'entity' can now + /// // be instantiated using Instantiate(), and its component values changed + /// // afterwards. + /// // This can also be done in Burst jobs using EntityCommandBuffer.ParallelWriter. + /// var secondEntity = entityManager.Instantiate(entity); + /// entityManager.SetComponentData(secondEntity, new Translation {Value = new float3(1, 2, 3)}); + /// } + /// + public static void AddComponents( + Entity entity, + EntityManager entityManager, + in RenderMeshDescription renderMeshDescription, + RenderMesh renderMesh) + { +#if UNITY_EDITOR + // Skip the validation check in the player to minimize overhead. + if (!ValidateMesh(renderMesh)) + return; +#endif + // Entities with Static are never rendered with motion vectors + bool inMotionPass = kUseHybridMotionPass && + renderMeshDescription.FilterSettings.IsInMotionPass && + !entityManager.HasComponent(entity); + + EntitiesGraphicsComponentFlags flags = EntitiesGraphicsComponentFlags.GameObjectConversion; + if (inMotionPass) flags |= EntitiesGraphicsComponentFlags.InMotionPass; + flags |= LightProbeFlags(renderMeshDescription.LightProbeUsage); + flags |= DepthSortedFlags(renderMesh.material); + + // Add all components up front using as few calls as possible. + entityManager.AddComponent(entity, s_EntitiesGraphicsComponentTypes.GetComponentTypes(flags)); + + entityManager.SetSharedComponentManaged(entity, renderMesh); + entityManager.SetSharedComponentManaged(entity, renderMeshDescription.FilterSettings); + + var localBounds = renderMesh.mesh.bounds.ToAABB(); + entityManager.SetComponentData(entity, new RenderBounds { Value = localBounds }); + } + + /// + /// Set the Entities Graphics component values to render the given entity using the given description. + /// Any missing components will be added, which results in structural changes. + /// + /// The entity to set the component values for. + /// The used to set the component values. + /// The description that determines how the entity is to be rendered. + /// The instance of the RenderMeshArray which contains mesh and material. + /// The MaterialMeshInfo used to index into renderMeshArray. + public static void AddComponents( + Entity entity, + EntityManager entityManager, + in RenderMeshDescription renderMeshDescription, + RenderMeshArray renderMeshArray, + MaterialMeshInfo materialMeshInfo = default) + { + var material = renderMeshArray.GetMaterial(materialMeshInfo); + var mesh = renderMeshArray.GetMesh(materialMeshInfo); + + // Entities with Static are never rendered with motion vectors + bool inMotionPass = kUseHybridMotionPass && + renderMeshDescription.FilterSettings.IsInMotionPass && + !entityManager.HasComponent(entity); + + EntitiesGraphicsComponentFlags flags = EntitiesGraphicsComponentFlags.None; + if (inMotionPass) flags |= EntitiesGraphicsComponentFlags.InMotionPass; + flags |= LightProbeFlags(renderMeshDescription.LightProbeUsage); + flags |= DepthSortedFlags(material); + + // Add all components up front using as few calls as possible. + entityManager.AddComponent(entity, s_EntitiesGraphicsComponentTypes.GetComponentTypes(flags)); + + entityManager.SetSharedComponentManaged(entity, renderMeshDescription.FilterSettings); + entityManager.SetSharedComponentManaged(entity, renderMeshArray); + entityManager.SetComponentData(entity, materialMeshInfo); + + if (mesh != null) + { + var localBounds = mesh.bounds.ToAABB(); + entityManager.SetComponentData(entity, new RenderBounds { Value = localBounds }); + } + } + +#pragma warning restore CS0162 + internal static EntitiesGraphicsComponentFlags DepthSortedFlags(Material material) + { + if (IsMaterialTransparent(material)) + return EntitiesGraphicsComponentFlags.DepthSorted; + else + return EntitiesGraphicsComponentFlags.None; + } + + + /// + /// Return true if the given is known to be transparent. Works + /// for materials that use HDRP or URP conventions for transparent materials. + /// + private const string kSurfaceTypeHDRP = "_SurfaceType"; + private const string kSurfaceTypeURP = "_Surface"; + private static int kSurfaceTypeHDRPNameID = Shader.PropertyToID(kSurfaceTypeHDRP); + private static int kSurfaceTypeURPNameID = Shader.PropertyToID(kSurfaceTypeURP); + private static bool IsMaterialTransparent(Material material) + { + if (material == null) + return false; + +#if HDRP_10_0_0_OR_NEWER + // Material.GetSurfaceType() is not public, so we try to do what it does internally. + const int kSurfaceTypeTransparent = 1; // Corresponds to non-public SurfaceType.Transparent + if (material.HasProperty(kSurfaceTypeHDRPNameID)) + return (int) material.GetFloat(kSurfaceTypeHDRPNameID) == kSurfaceTypeTransparent; + else + return false; +#elif URP_10_0_0_OR_NEWER + const int kSurfaceTypeTransparent = 1; // Corresponds to SurfaceType.Transparent + if (material.HasProperty(kSurfaceTypeURPNameID)) + return (int) material.GetFloat(kSurfaceTypeURPNameID) == kSurfaceTypeTransparent; + else + return false; +#else + return false; +#endif + } + + internal enum StaticLightingMode + { + None = 0, + LightMapped = 1, + LightProbes = 2, + } + + internal static StaticLightingMode StaticLightingModeFromRenderer(Renderer renderer) + { + var staticLightingMode = StaticLightingMode.None; + if (renderer.lightmapIndex >= 65534 || renderer.lightmapIndex < 0) + staticLightingMode = StaticLightingMode.LightProbes; + else if (renderer.lightmapIndex >= 0) + staticLightingMode = StaticLightingMode.LightMapped; + + return staticLightingMode; + } + + internal static EntitiesGraphicsComponentFlags LightProbeFlags(LightProbeUsage lightProbeUsage) + { + switch (lightProbeUsage) + { + case LightProbeUsage.BlendProbes: + return EntitiesGraphicsComponentFlags.LightProbesBlend; + case LightProbeUsage.CustomProvided: + return EntitiesGraphicsComponentFlags.LightProbesCustom; + default: + return EntitiesGraphicsComponentFlags.None; + } + } + + internal static string FormatRenderMesh(RenderMesh renderMesh) => + $"RenderMesh(material: {renderMesh.material}, mesh: {renderMesh.mesh}, subMesh: {renderMesh.subMesh})"; + + internal static bool ValidateMesh(RenderMesh renderMesh) + { + if (renderMesh.mesh == null) + { + Debug.LogWarning($"RenderMesh must have a valid non-null Mesh. {FormatRenderMesh(renderMesh)}"); + return false; + } + else if (renderMesh.subMesh < 0 || renderMesh.subMesh >= renderMesh.mesh.subMeshCount) + { + Debug.LogWarning($"RenderMesh subMesh index out of bounds. {FormatRenderMesh(renderMesh)}"); + return false; + } + + return true; + } + + internal static bool ValidateMaterial(RenderMesh renderMesh) + { + if (renderMesh.material == null) + { + Debug.LogWarning($"RenderMesh must have a valid non-null Material. {FormatRenderMesh(renderMesh)}"); + return false; + } + + return true; + } + + internal static bool ValidateRenderMesh(RenderMesh renderMesh) => + ValidateMaterial(renderMesh) && ValidateMesh(renderMesh); + + } +} diff --git a/Unity.Entities.Graphics/RenderMeshUtility.cs.meta b/Unity.Entities.Graphics/RenderMeshUtility.cs.meta new file mode 100644 index 0000000..9b838be --- /dev/null +++ b/Unity.Entities.Graphics/RenderMeshUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c06ec28325f4496dbfdc0931f8edfdc5 +timeCreated: 1604333954 \ No newline at end of file diff --git a/Unity.Entities.Graphics/Resources.meta b/Unity.Entities.Graphics/Resources.meta new file mode 100644 index 0000000..e94b564 --- /dev/null +++ b/Unity.Entities.Graphics/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9331fe9f21fd1af4e8afc6696bd0b62a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Resources/Occlusion.meta b/Unity.Entities.Graphics/Resources/Occlusion.meta new file mode 100644 index 0000000..4e03068 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4a355b15023399c478dffdc77a0d460d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader b/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader new file mode 100644 index 0000000..96c8386 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader @@ -0,0 +1,46 @@ +Shader "Hidden/OccludeeScreenSpaceAABB" +{ + SubShader + { + Lighting Off + Blend SrcAlpha OneMinusSrcAlpha + ZWrite Off + Cull Off + Fog { Mode Off } + ZTest Always + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + v2f vert(appdata v) + { + v2f o; + o.vertex = v.vertex; + return o; + } + + fixed4 frag(v2f i) : SV_Target + { + return fixed4(0.4, 0.0, 0.0, 1.0); + } + ENDCG + } + } +} diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader.meta b/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader.meta new file mode 100644 index 0000000..3f78b58 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OccludeeScreenSpaceAABB.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 2f2082001cc113a40b017a19e7618df1 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader new file mode 100644 index 0000000..f5f1471 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader @@ -0,0 +1,80 @@ +Shader "Hidden/OcclusionDebugComposite" +{ + Properties + { + _Depth("Depth", 2D) = "white" {} + _Overlay("Overlay", 2D) = "white" {} + } + + SubShader + { + Lighting Off + Blend SrcAlpha OneMinusSrcAlpha + ZWrite Off + Cull Off + Fog { Mode Off } + ZTest Always + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + sampler2D _Depth; + sampler2D _Overlay; + float _YFlip; + float _OnlyOverlay; + float _OnlyDepth; + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + float2 uv : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + v2f vert(appdata v) + { + v2f o; + o.vertex = v.vertex; + o.uv = v.uv; + if (_YFlip > 0.5) + { + o.uv.y = 1 - o.uv.y; + } + return o; + } + + fixed4 frag(v2f i) : SV_Target + { + if (_OnlyOverlay > 0.5) + { + return fixed4(1, 1, 1, tex2D(_Overlay, i.uv).r); + } + if (_OnlyDepth > 0.5) + { + return fixed4(tex2D(_Depth, i.uv).rrr, 1); + } + + { + fixed back = min(1.0, tex2D(_Depth, i.uv).r); + fixed3 fore = fixed3(1, 0, 0); + float alpha = tex2D(_Overlay, i.uv).r; + // Perform a simple alpha composite in compute, with the background alpha = 1 + return fixed4(fore * alpha + back.rrr * (1 - alpha), 1); + } + } + ENDCG + } + } +} diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader.meta b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader.meta new file mode 100644 index 0000000..dd2c0e2 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugComposite.shader.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 9a4ee44acff63334fa3b1848ad17796e +ShaderImporter: + externalObjects: {} + defaultTextures: + - _Depth: {instanceID: 0} + - _Test: {instanceID: 0} + - _Bounds: {instanceID: 0} + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader new file mode 100644 index 0000000..d764145 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader @@ -0,0 +1,76 @@ +Shader "Hidden/OcclusionDebugOccluders" +{ + SubShader + { + Lighting Off + Blend SrcAlpha OneMinusSrcAlpha + ZWrite On + Cull Off + Fog { Mode Off } + ZTest LEqual + + Pass + { + Name "ForwardOnly" + Tags { "LightMode" = "ForwardOnly" } + + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + float4x4 _Transform; + float _YFlip; + + struct appdata + { + float4 vertex : POSITION; + float4 color: COLOR; + }; + + struct v2f + { + float4 vertex : SV_POSITION; + float4 color: COLOR; + float z : TexCoord0; + }; + + v2f vert(appdata v) + { + v2f o; + + o.vertex = mul(_Transform, float4(v.vertex.xyz, 1)); + if (_YFlip > 0.5) + { + o.vertex.y = -o.vertex.y; + } + o.z = v.vertex.w; + o.color = v.color; + + return o; + } + + struct FrameOut + { + fixed4 color : SV_Target; + float depth : SV_Depth; + }; + + FrameOut frag(v2f i) + { + UNITY_SETUP_INSTANCE_ID(i); + fixed4 col = i.color; + + float2 dp = normalize(float2(ddx(i.vertex.z), ddy(i.vertex.z))); + col.rgb *= 0.5 / (abs(dp.x) + abs(dp.y)); + + FrameOut result; + result.color = col; + result.depth = 1 / i.vertex.w; + return result; + } + ENDCG + } + } +} diff --git a/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader.meta b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader.meta new file mode 100644 index 0000000..7a3d5cd --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/OcclusionDebugOccluders.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5d04157bb62aeec4e89192daf42b5f21 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph b/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph new file mode 100644 index 0000000..0cb469c --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph @@ -0,0 +1,24 @@ +{ + "m_SerializedProperties": [], + "m_SerializedKeywords": [], + "m_SerializableNodes": [ + { + "typeInfo": { + "fullName": "UnityEditor.ShaderGraph.UnlitMasterNode" + }, + "JSONnodeData": "{\n \"m_GuidSerialized\": \"d0d9a133-1f91-4072-b45d-ba9fc60b5228\",\n \"m_GroupGuidSerialized\": \"00000000-0000-0000-0000-000000000000\",\n \"m_Name\": \"Unlit Master\",\n \"m_NodeVersion\": 0,\n \"m_DrawState\": {\n \"m_Expanded\": true,\n \"m_Position\": {\n \"serializedVersion\": \"2\",\n \"x\": 0.0,\n \"y\": 0.0,\n \"width\": 0.0,\n \"height\": 0.0\n }\n },\n \"m_SerializableSlots\": [\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.PositionMaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 9,\\n \\\"m_DisplayName\\\": \\\"Vertex Position\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"Vertex Position\\\",\\n \\\"m_StageCapability\\\": 1,\\n \\\"m_Value\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_DefaultValue\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_Labels\\\": [\\n \\\"X\\\",\\n \\\"Y\\\",\\n \\\"Z\\\"\\n ],\\n \\\"m_Space\\\": 0\\n}\"\n },\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.NormalMaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 10,\\n \\\"m_DisplayName\\\": \\\"Vertex Normal\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"Vertex Normal\\\",\\n \\\"m_StageCapability\\\": 1,\\n \\\"m_Value\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_DefaultValue\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_Labels\\\": [\\n \\\"X\\\",\\n \\\"Y\\\",\\n \\\"Z\\\"\\n ],\\n \\\"m_Space\\\": 0\\n}\"\n },\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.TangentMaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 11,\\n \\\"m_DisplayName\\\": \\\"Vertex Tangent\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"Vertex Tangent\\\",\\n \\\"m_StageCapability\\\": 1,\\n \\\"m_Value\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_DefaultValue\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_Labels\\\": [\\n \\\"X\\\",\\n \\\"Y\\\",\\n \\\"Z\\\"\\n ],\\n \\\"m_Space\\\": 0\\n}\"\n },\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.ColorRGBMaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 0,\\n \\\"m_DisplayName\\\": \\\"Color\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"Color\\\",\\n \\\"m_StageCapability\\\": 2,\\n \\\"m_Value\\\": {\\n \\\"x\\\": 0.11213953047990799,\\n \\\"y\\\": 0.7924528121948242,\\n \\\"z\\\": 0.6817682981491089\\n },\\n \\\"m_DefaultValue\\\": {\\n \\\"x\\\": 0.0,\\n \\\"y\\\": 0.0,\\n \\\"z\\\": 0.0\\n },\\n \\\"m_Labels\\\": [\\n \\\"X\\\",\\n \\\"Y\\\",\\n \\\"Z\\\"\\n ],\\n \\\"m_ColorMode\\\": 0\\n}\"\n },\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.Vector1MaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 7,\\n \\\"m_DisplayName\\\": \\\"Alpha\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"Alpha\\\",\\n \\\"m_StageCapability\\\": 2,\\n \\\"m_Value\\\": 0.699999988079071,\\n \\\"m_DefaultValue\\\": 1.0,\\n \\\"m_Labels\\\": [\\n \\\"X\\\"\\n ]\\n}\"\n },\n {\n \"typeInfo\": {\n \"fullName\": \"UnityEditor.ShaderGraph.Vector1MaterialSlot\"\n },\n \"JSONnodeData\": \"{\\n \\\"m_Id\\\": 8,\\n \\\"m_DisplayName\\\": \\\"AlphaClipThreshold\\\",\\n \\\"m_SlotType\\\": 0,\\n \\\"m_Priority\\\": 2147483647,\\n \\\"m_Hidden\\\": false,\\n \\\"m_ShaderOutputName\\\": \\\"AlphaClipThreshold\\\",\\n \\\"m_StageCapability\\\": 2,\\n \\\"m_Value\\\": 0.0,\\n \\\"m_DefaultValue\\\": 0.0,\\n \\\"m_Labels\\\": [\\n \\\"X\\\"\\n ]\\n}\"\n }\n ],\n \"m_Precision\": 0,\n \"m_PreviewExpanded\": true,\n \"m_CustomColors\": {\n \"m_SerializableColors\": []\n },\n \"m_SurfaceType\": 0,\n \"m_AlphaMode\": 0,\n \"m_TwoSided\": false,\n \"m_AddPrecomputedVelocity\": false,\n \"m_DOTSInstancing\": false,\n \"m_ShaderGUIOverride\": \"\",\n \"m_OverrideEnabled\": false\n}" + } + ], + "m_Groups": [], + "m_StickyNotes": [], + "m_SerializableEdges": [], + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + } + }, + "m_Path": "Shader Graphs", + "m_ConcretePrecision": 0, + "m_ActiveOutputNodeGuidSerialized": "d0d9a133-1f91-4072-b45d-ba9fc60b5228" +} \ No newline at end of file diff --git a/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph.meta b/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph.meta new file mode 100644 index 0000000..31ee762 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/Occlusion/ShowOccluderMesh.shadergraph.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 7bca4ad68221f3a4097ff5a051b2cf00 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} diff --git a/Unity.Entities.Graphics/Resources/SparseUploader.compute b/Unity.Entities.Graphics/Resources/SparseUploader.compute new file mode 100644 index 0000000..869abfb --- /dev/null +++ b/Unity.Entities.Graphics/Resources/SparseUploader.compute @@ -0,0 +1,369 @@ +#pragma kernel CopyKernel +#pragma kernel ReplaceKernel + +static const float kEpsilon = 0.000001f; + +static const uint kOperationType_Upload = 0; +static const uint kOperationType_Matrix_4x4 = 1; +static const uint kOperationType_Matrix_Inverse_4x4 = 2; +static const uint kOperationType_Matrix_3x4 = 3; +static const uint kOperationType_Matrix_Inverse_3x4 = 4; +static const uint kOperationType_StridedUpload = 5; + +struct Operation +{ + uint type; + uint srcOffset; + int srcStride; + uint dstOffset; + uint dstOffsetExtra; + int dstStride; + uint size; + uint count; +}; +static const uint SizeofOperation = 8 * 4; + +ByteAddressBuffer srcBuffer; +RWByteAddressBuffer dstBuffer; + +// Place constants into globals so CBUFFER macros (and SRP headers) are not needed +int operationsBase; +int replaceOperationSize; + + +Operation LoadOperation(int operationIndex) +{ + int offset = operationIndex * SizeofOperation; + + Operation operation; + operation.type = srcBuffer.Load(offset + 0 * 4); + operation.srcOffset = srcBuffer.Load(offset + 1 * 4); + operation.srcStride = srcBuffer.Load(offset + 2 * 4); + operation.dstOffset = srcBuffer.Load(offset + 3 * 4); + operation.dstOffsetExtra = srcBuffer.Load(offset + 4 * 4); + operation.dstStride = srcBuffer.Load(offset + 5 * 4); + operation.size = srcBuffer.Load(offset + 6 * 4); + operation.count = srcBuffer.Load(offset + 7 * 4); + + return operation; +} + +#define GROUP_SIZE 64 +#define NUM_BYTES_PER_THREAD 4 +#define NUM_BYTES_PER_GROUP (GROUP_SIZE * NUM_BYTES_PER_THREAD) + +void CopyToGPU(Operation operation, uint threadID) +{ + uint numFullWaves = (operation.size * operation.count) / NUM_BYTES_PER_GROUP; + + uint srcIndex = (threadID * NUM_BYTES_PER_THREAD) % operation.size; + uint dstIndex = threadID * NUM_BYTES_PER_THREAD; + for (uint i = 0; i < numFullWaves; i += 1) + { + uint val = srcBuffer.Load(srcIndex + operation.srcOffset); + dstBuffer.Store(dstIndex + operation.dstOffset, val); + + srcIndex = (srcIndex + NUM_BYTES_PER_GROUP) % operation.size; + dstIndex += NUM_BYTES_PER_GROUP; + } + + if (dstIndex < (operation.size * operation.count)) + { + uint val = srcBuffer.Load(srcIndex + operation.srcOffset); + dstBuffer.Store(dstIndex + operation.dstOffset, val); + } +} + +void DivModUsingFloats(int x, int y, out int quotient, out int remainder) +{ + float fx = x; + float fy = y; + float integral = 0; + float fractional = 0; + fractional = modf(fx / fy, integral); + int rem = round(fractional * fy); + // This can happen on GPU because the fractional can be like 0.999 + if (rem == y) + { + quotient = round(integral) + 1; + remainder = 0; + } + else + { + quotient = round(integral); + remainder = rem; + } +} + +#define NUM_DWORDS_PER_GROUP (NUM_BYTES_PER_GROUP / 4) + +void CopyToGPUStrided(Operation operation, uint threadID) +{ + int elemSizeInDwords = operation.size >> 2; + + int elemsPerGroup = 0; + int dwordLoopIncrement = 0; + + DivModUsingFloats( + NUM_DWORDS_PER_GROUP, elemSizeInDwords, + elemsPerGroup, dwordLoopIncrement); + + int elemIndex = 0; + int dwordIndex = 0; + + DivModUsingFloats(threadID, elemSizeInDwords, + elemIndex, dwordIndex); + + int srcOffset = operation.srcOffset + elemIndex * operation.srcStride + (dwordIndex << 2); + int dstOffset = operation.dstOffset + elemIndex * operation.dstStride + (dwordIndex << 2); + + int srcEnd = operation.srcOffset + operation.count * operation.srcStride; + int srcBytesPerLoop = elemsPerGroup * operation.srcStride + (dwordLoopIncrement << 2); + int dstBytesPerLoop = elemsPerGroup * operation.dstStride + (dwordLoopIncrement << 2); + + while (srcOffset < srcEnd) + { + uint data = srcBuffer.Load(srcOffset); + dstBuffer.Store(dstOffset, data); + + srcOffset += srcBytesPerLoop; + dstOffset += dstBytesPerLoop; + dwordIndex += dwordLoopIncrement; + + if (dwordIndex >= elemSizeInDwords) + { + int newDwordIndex = dwordIndex - elemSizeInDwords; + int byteAdjustment = (newDwordIndex - dwordIndex) << 2; + + srcOffset += operation.srcStride + byteAdjustment; + dstOffset += operation.dstStride + byteAdjustment; + + dwordIndex = newDwordIndex; + } + } +} + +float3x4 loadMatrixSrc(ByteAddressBuffer buf, uint offset) +{ + // Read in 4 columns of float3 data each. + // Done in 3 load4 and then repacking into final 3x4 matrix + float4 p1 = asfloat(buf.Load4(offset + 0 * 16)); + float4 p2 = asfloat(buf.Load4(offset + 1 * 16)); + float4 p3 = asfloat(buf.Load4(offset + 2 * 16)); + + return float3x4( + p1.x, p1.w, p2.z, p3.y, + p1.y, p2.x, p2.w, p3.z, + p1.z, p2.y, p3.x, p3.w + ); +} + +void storeMatrixDst_4x4(RWByteAddressBuffer buf, uint offset, float3x4 mat) +{ + // Write our the columns + buf.Store4(offset + 0, asuint(float4(mat._m00, mat._m10, mat._m20, 0.0))); + buf.Store4(offset + 16, asuint(float4(mat._m01, mat._m11, mat._m21, 0.0))); + buf.Store4(offset + 32, asuint(float4(mat._m02, mat._m12, mat._m22, 0.0))); + buf.Store4(offset + 48, asuint(float4(mat._m03, mat._m13, mat._m23, 1.0))); +} + +void storeMatrixDst_3x4(RWByteAddressBuffer buf, uint offset, float3x4 mat) +{ + // Pack the 4 float3 columns as 3 float4 so we can write it out using store4 + float4 p1 = float4(mat._m00, mat._m10, mat._m20, mat._m01); + float4 p2 = float4(mat._m11, mat._m21, mat._m02, mat._m12); + float4 p3 = float4(mat._m22, mat._m03, mat._m13, mat._m23); + buf.Store4(offset + 0, asuint(p1)); + buf.Store4(offset + 16, asuint(p2)); + buf.Store4(offset + 32, asuint(p3)); +} + +float csum(float3 v) +{ + return v.x + v.y + v.z; +} + +float3x3 inverse3x3(float3x3 m) +{ + float3 c0 = m._m00_m10_m20; + float3 c1 = m._m01_m11_m21; + float3 c2 = m._m02_m12_m22; + + float3 t0 = float3(c1.x, c2.x, c0.x); + float3 t1 = float3(c1.y, c2.y, c0.y); + float3 t2 = float3(c1.z, c2.z, c0.z); + + float3 m0 = t1 * t2.yzx - t1.yzx * t2; + float3 m1 = t0.yzx * t2 - t0 * t2.yzx; + float3 m2 = t0 * t1.yzx - t0.yzx * t1; + + float det = csum(t0.zxy * m0); + + if (abs(det) < kEpsilon) + { + return float3x3( + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f); + } + else + { + return float3x3(m0, m1, m2) * rcp(det); + } +} + +float3x4 inverseAffine(float3x4 m) +{ + float3x3 inv = inverse3x3((float3x3)m); + float3 trans = float3(m._m03, m._m13, m._m23); + float3 invTrans = -mul(trans, inv); + return float3x4( + inv._m00, inv._m10, inv._m20, invTrans.x, + inv._m01, inv._m11, inv._m21, invTrans.y, + inv._m02, inv._m12, inv._m22, invTrans.z); +} + +void CopyMatrix_4x4(Operation operation, uint din, uint dout) +{ + float3x4 orig = loadMatrixSrc(srcBuffer, operation.srcOffset + din); + storeMatrixDst_4x4(dstBuffer, operation.dstOffset + dout, orig); +} + +void CopyMatrix_3x4(Operation operation, uint din, uint dout) +{ + float3x4 orig = loadMatrixSrc(srcBuffer, operation.srcOffset + din); + storeMatrixDst_3x4(dstBuffer, operation.dstOffset + dout, orig); +} + +void CopyAndInverseMatrix_4x4(Operation operation, uint din, uint dout) +{ + float3x4 orig = loadMatrixSrc(srcBuffer, operation.srcOffset + din); + float3x4 inv = inverseAffine(orig); + storeMatrixDst_4x4(dstBuffer, operation.dstOffset + dout, orig); + storeMatrixDst_4x4(dstBuffer, operation.dstOffsetExtra + dout, inv); +} + +void CopyAndInverseMatrix_3x4(Operation operation, uint din, uint dout) +{ + float3x4 orig = loadMatrixSrc(srcBuffer, operation.srcOffset + din); + float3x4 inv = inverseAffine(orig); + storeMatrixDst_3x4(dstBuffer, operation.dstOffset + dout, orig); + storeMatrixDst_3x4(dstBuffer, operation.dstOffsetExtra + dout, inv); +} + +#define NUM_MATRIX_INPUT_BYTES_PER_THREAD 48 +#define NUM_MATRIX_INPUT_BYTES_PER_GROUP (GROUP_SIZE * NUM_MATRIX_INPUT_BYTES_PER_THREAD) +#define NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_4x4 64 +#define NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_4x4 (GROUP_SIZE * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_4x4) +#define NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_3x4 48 +#define NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_3x4 (GROUP_SIZE * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_3x4) + +void MatricesToGPU_4x4(Operation operation, uint threadID) +{ + uint numFullWaves = operation.size / NUM_MATRIX_INPUT_BYTES_PER_GROUP; + uint inputIndex = threadID * NUM_MATRIX_INPUT_BYTES_PER_THREAD; + uint outputIndex = threadID * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_4x4; + + for (uint i = 0; i < numFullWaves; ++i) + { + CopyMatrix_4x4(operation, inputIndex, outputIndex); + inputIndex += NUM_MATRIX_INPUT_BYTES_PER_GROUP; + outputIndex += NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_4x4; + } + + if (inputIndex < operation.size) + { + CopyMatrix_4x4(operation, inputIndex, outputIndex); + } +} + +void MatricesToGPU_3x4(Operation operation, uint threadID) +{ + uint numFullWaves = operation.size / NUM_MATRIX_INPUT_BYTES_PER_GROUP; + uint inputIndex = threadID * NUM_MATRIX_INPUT_BYTES_PER_THREAD; + uint outputIndex = threadID * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_3x4; + + for (uint i = 0; i < numFullWaves; ++i) + { + CopyMatrix_3x4(operation, inputIndex, outputIndex); + inputIndex += NUM_MATRIX_INPUT_BYTES_PER_GROUP; + outputIndex += NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_3x4; + } + + if (inputIndex < operation.size) + { + CopyMatrix_3x4(operation, inputIndex, outputIndex); + } +} + +void InverseMatricesToGPU_4x4(Operation operation, uint threadID) +{ + uint numFullWaves = operation.size / NUM_MATRIX_INPUT_BYTES_PER_GROUP; + uint inputIndex = threadID * NUM_MATRIX_INPUT_BYTES_PER_THREAD; + uint outputIndex = threadID * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_4x4; + + for (uint i = 0; i < numFullWaves; ++i) + { + CopyAndInverseMatrix_4x4(operation, inputIndex, outputIndex); + inputIndex += NUM_MATRIX_INPUT_BYTES_PER_GROUP; + outputIndex += NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_4x4; + } + + if (inputIndex < operation.size) + { + CopyAndInverseMatrix_4x4(operation, inputIndex, outputIndex); + } +} + +void InverseMatricesToGPU_3x4(Operation operation, uint threadID) +{ + uint numFullWaves = operation.size / NUM_MATRIX_INPUT_BYTES_PER_GROUP; + uint inputIndex = threadID * NUM_MATRIX_INPUT_BYTES_PER_THREAD; + uint outputIndex = threadID * NUM_MATRIX_OUTPUT_BYTES_PER_THREAD_3x4; + + for (uint i = 0; i < numFullWaves; ++i) + { + CopyAndInverseMatrix_3x4(operation, inputIndex, outputIndex); + inputIndex += NUM_MATRIX_INPUT_BYTES_PER_GROUP; + outputIndex += NUM_MATRIX_OUTPUT_BYTES_PER_GROUP_3x4; + } + + if (inputIndex < operation.size) + { + CopyAndInverseMatrix_3x4(operation, inputIndex, outputIndex); + } +} + +[numthreads(GROUP_SIZE, 1, 1)] +void CopyKernel(uint threadID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + Operation operation = LoadOperation(operationsBase + groupID); + + if (operation.type == kOperationType_Upload) + CopyToGPU(operation, threadID); + else if (operation.type == kOperationType_Matrix_4x4) + MatricesToGPU_4x4(operation, threadID); + else if (operation.type == kOperationType_Matrix_Inverse_4x4) + InverseMatricesToGPU_4x4(operation, threadID); + else if (operation.type == kOperationType_Matrix_3x4) + MatricesToGPU_3x4(operation, threadID); + else if (operation.type == kOperationType_Matrix_Inverse_3x4) + InverseMatricesToGPU_3x4(operation, threadID); + else if (operation.type == kOperationType_StridedUpload) + CopyToGPUStrided(operation, threadID); +} + +[numthreads(GROUP_SIZE, 1, 1)] +void ReplaceKernel(uint threadID : SV_GroupThreadID, uint groupID : SV_GroupID) +{ + Operation operation; + operation.type = kOperationType_Upload; + operation.srcOffset = 0; + operation.dstOffset = 0; + operation.dstOffsetExtra = 0; + operation.size = replaceOperationSize; + operation.count = 1; + + CopyToGPU(operation, threadID); +} + diff --git a/Unity.Entities.Graphics/Resources/SparseUploader.compute.meta b/Unity.Entities.Graphics/Resources/SparseUploader.compute.meta new file mode 100644 index 0000000..e9495f8 --- /dev/null +++ b/Unity.Entities.Graphics/Resources/SparseUploader.compute.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4880c6ca92d523f4d8c9d48fe3815420 +ComputeShaderImporter: + externalObjects: {} + currentAPIMask: 4 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs b/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs new file mode 100644 index 0000000..fa6540b --- /dev/null +++ b/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using Unity.Assertions; +using Unity.Deformations; +using Unity.Entities; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + [TemporaryBakingType] + struct SkinnedMeshRendererBakingData : IComponentData + { + public UnityObjectRef SkinnedMeshRenderer; + } + + class SkinnedMeshRendererBaker : Baker + { + static int s_SkinMatrixIndexProperty = Shader.PropertyToID("_SkinMatrixIndex"); + static int s_ComputeMeshIndexProperty = Shader.PropertyToID("_ComputeMeshIndex"); + +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + static int s_DOTSDeformedProperty = Shader.PropertyToID("_DotsDeformationParams"); +#endif + + public override void Bake(SkinnedMeshRenderer authoring) + { + var materials = new List(); + authoring.GetSharedMaterials(materials); + + foreach (var material in materials) + { + if (material == null) + continue; + + var supportsSkinning = material.HasProperty(s_SkinMatrixIndexProperty) +#if ENABLE_DOTS_DEFORMATION_MOTION_VECTORS + || material.HasProperty(s_DOTSDeformedProperty) +#endif + || material.HasProperty(s_ComputeMeshIndexProperty); + if (!supportsSkinning) + { + string errorMsg = ""; + errorMsg += + $"Shader [{material.shader.name}] on [{authoring.name}] does not support skinning. This can result in incorrect rendering.{System.Environment.NewLine}"; + errorMsg += + $"Please see documentation for Linear Blend Skinning Node and Compute Deformation Node in Shader Graph.{System.Environment.NewLine}"; + Debug.LogWarning(errorMsg, authoring); + } + } + + // Takes a dependency on the transform + var root = authoring.rootBone ? GetComponent(authoring.rootBone) : GetComponent(authoring); + + var mesh = authoring.sharedMesh; + MeshRendererBakingUtility.Convert(this, authoring, mesh, materials, false, out var additionalEntities, root); + + var hasSkinning = mesh == null ? false : mesh.boneWeights.Length > 0 && mesh.bindposeCount > 0; + var hasBlendShapes = mesh == null ? false : mesh.blendShapeCount > 0; + var deformedEntity = GetEntity(); + foreach (var entity in additionalEntities) + { + // Add relevant deformation tags to converted render entities and link them to the DeformedEntity. + AddComponent(entity, new DeformedMeshIndex()); + AddComponent(entity, new DeformedEntity {Value = deformedEntity}); + + // Add SkinnedMeshRendererBakingData on the additional entities to allow RenderMeshPostProcessSystem to process on SkinnedMeshRenderer as well + AddComponent(entity, new SkinnedMeshRendererBakingData {SkinnedMeshRenderer = authoring}); + SetComponent(entity, new RenderBounds { Value = authoring.localBounds.ToAABB() }); + } + + // Fill the blend shape weights. + if (hasBlendShapes) + { + var weights = AddBuffer(deformedEntity); + weights.ResizeUninitialized(mesh.blendShapeCount); + + for (int i = 0; i < weights.Length; ++i) + { + weights[i] = new BlendShapeWeight {Value = authoring.GetBlendShapeWeight(i)}; + } + } + + // Fill the skin matrices with bindpose skin matrices. + if (hasSkinning) + { + var bones = authoring.bones; + var rootMatrixInv = root.localToWorldMatrix.inverse; + + var skinMatrices = AddBuffer(deformedEntity); + skinMatrices.ResizeUninitialized(bones.Length); + + for (int i = 0; i < bones.Length; ++i) + { + if (bones[i] == null) + continue; + + // If the transform changes the skin matrices need to be updated. + DependsOn(bones[i]); + + Assert.IsTrue(i < authoring.sharedMesh.bindposeCount, $"No corresponding bindpose found for the bone ({bones[i].name}) at index {i}."); + + var bindPose = mesh.bindposes[i]; + var boneMatRootSpace = math.mul(rootMatrixInv, bones[i].localToWorldMatrix); + var skinMatRootSpace = math.mul(boneMatRootSpace, bindPose); + skinMatrices[i] = new SkinMatrix + { + Value = new float3x4(skinMatRootSpace.c0.xyz, skinMatRootSpace.c1.xyz, skinMatRootSpace.c2.xyz, + skinMatRootSpace.c3.xyz) + }; + } + } + } + } +} diff --git a/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs.meta b/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs.meta new file mode 100644 index 0000000..3e8d5cb --- /dev/null +++ b/Unity.Entities.Graphics/SkinnedMeshRendererBaking.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 40837c2fcff4460ba1ace14e285cbf15 +timeCreated: 1648662936 \ No newline at end of file diff --git a/Unity.Entities.Graphics/SparseUploader.cs b/Unity.Entities.Graphics/SparseUploader.cs new file mode 100644 index 0000000..8db62fc --- /dev/null +++ b/Unity.Entities.Graphics/SparseUploader.cs @@ -0,0 +1,781 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Unity.Collections; +using Unity.Mathematics; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + internal enum OperationType : int + { + Upload = 0, + Matrix_4x4 = 1, + Matrix_Inverse_4x4 = 2, + Matrix_3x4 = 3, + Matrix_Inverse_3x4 = 4, + StridedUpload = 5, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Operation + { + public uint type; + public uint srcOffset; + public uint srcStride; + public uint dstOffset; + public uint dstOffsetExtra; + public int dstStride; + public uint size; + public uint count; + } + + internal unsafe struct MappedBuffer + { + public byte* m_Data; + public long m_Marker; + public int m_BufferID; + + public static long PackMarker(long operationOffset, long dataOffset) + { + return (dataOffset << 32) | (operationOffset & 0xFFFFFFFF); + } + + public static void UnpackMarker(long marker, out long operationOffset, out long dataOffset) + { + operationOffset = marker & 0xFFFFFFFF; + dataOffset = (marker >> 32) & 0xFFFFFFFF; + } + + public bool TryAlloc(int operationSize, int dataSize, out byte* ptr, out int operationOffset, out int dataOffset) + { + long originalMarker; + long newMarker; + long currOperationOffset; + long currDataOffset; + do + { + // Read the marker as is right now + originalMarker = Interlocked.Read(ref m_Marker); + UnpackMarker(originalMarker, out currOperationOffset, out currDataOffset); + + // Calculate the new offsets for operation and data + // Operations are stored in the beginning of the buffer + // Data is stored at the end of the buffer + var newOperationOffset = currOperationOffset + operationSize; + var newDataOffset = currDataOffset - dataSize; + + // Check if there was enough space in the buffer for this allocation + if (newDataOffset < newOperationOffset) + { + // Not enough space, return false + ptr = null; + operationOffset = 0; + dataOffset = 0; + return false; + } + + newMarker = PackMarker(newOperationOffset, newDataOffset); + + // Finally we try to CAS the new marker in. + // If anyone has allocated from the buffer in the meantime this will fail and the loop will rerun + } while (Interlocked.CompareExchange(ref m_Marker, newMarker, originalMarker) != originalMarker); + + // Now we have succeeded in getting a data slot out and can return true. + ptr = m_Data; + operationOffset = (int)currOperationOffset; + dataOffset = (int)(currDataOffset - dataSize); + return true; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct ThreadedSparseUploaderData + { + [NativeDisableUnsafePtrRestriction] public MappedBuffer* m_Buffers; + public int m_NumBuffers; + public int m_CurrBuffer; + } + + /// + /// An unmanaged and Burst-compatible interface for SparseUploader. + /// + /// + /// This should be created each frame by a call to SparseUploader.Begin and is later returned by a call to SparseUploader.EndAndCommit. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ThreadedSparseUploader + { + // TODO: safety handle? + [NativeDisableUnsafePtrRestriction] internal ThreadedSparseUploaderData* m_Data; + + private bool TryAlloc(int operationSize, int dataSize, out byte* ptr, out int operationOffset, out int dataOffset) + { + // Fetch current buffer and ensure we are not already out of GPU buffers to allocate from; + var numBuffers = m_Data->m_NumBuffers; + var buffer = m_Data->m_CurrBuffer; + if (buffer < numBuffers) + { + do + { + // Try to allocate from the current buffer + if (m_Data->m_Buffers[buffer].TryAlloc(operationSize, dataSize, out var p, out var op, out var d)) + { + // Success, we can return true at onnce + ptr = p; + operationOffset = op; + dataOffset = d; + return true; + } + + // Try to increment the buffer. + // If someone else has done this while we where trying to alloc we will use their + // value and du another iteration. Otherwise we will use our new value + buffer = Interlocked.CompareExchange(ref m_Data->m_CurrBuffer, buffer + 1, buffer); + } while (buffer < m_Data->m_NumBuffers); + } + + // We have run out of buffers, return false + ptr = null; + operationOffset = 0; + dataOffset = 0; + return false; + } + + /// + /// Adds a new pending upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source pointer. + /// + /// The source pointer of data to upload. + /// The amount of data, in bytes, to read from the source pointer. + /// The destination offset of the data in the GPU buffer. + /// The number of times to repeat the source data in the destination buffer when uploading. + public void AddUpload(void* src, int size, int offsetInBytes, int repeatCount = 1) + { + var opsize = UnsafeUtility.SizeOf(); + var allocSucceeded = TryAlloc(opsize, size, out var dst, out var operationOffset, out var dataOffset); + + if (!allocSucceeded) + { + Debug.Log("SparseUploader failed to allocate upload memory for AddUpload operation"); + return; + } + + if (repeatCount <= 0) + repeatCount = 1; + + // TODO: Vectorized memcpy + UnsafeUtility.MemCpy(dst + dataOffset, src, size); + var op = new Operation + { + type = (uint)OperationType.Upload, + srcOffset = (uint)dataOffset, + dstOffset = (uint)offsetInBytes, + dstOffsetExtra = 0, + size = (uint)size, + count = (uint)repeatCount + }; + UnsafeUtility.MemCpy(dst + operationOffset, &op, opsize); + } + + /// + /// Adds a new pending upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source value. + /// + /// The source data to upload. + /// The destination offset of the data in the GPU buffer. + /// The number of times to repeat the source data in the destination buffer when uploading. + /// Any unmanaged simple type. + public void AddUpload(T val, int offsetInBytes, int repeatCount = 1) where T : unmanaged + { + var size = UnsafeUtility.SizeOf(); + AddUpload(&val, size, offsetInBytes, repeatCount); + } + + /// + /// Adds a new pending upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source array. + /// + /// The source array of data to upload. + /// The destination offset of the data in the GPU buffer. + /// The number of times to repeat the source data in the destination buffer when uploading. + /// Any unmanaged simple type. + public void AddUpload(NativeArray array, int offsetInBytes, int repeatCount = 1) where T : unmanaged + { + var size = UnsafeUtility.SizeOf() * array.Length; + AddUpload(array.GetUnsafeReadOnlyPtr(), size, offsetInBytes, repeatCount); + } + + /// + /// Options for the type of matrix to use in matrix uploads. + /// + public enum MatrixType + { + /// + /// A float4x4 matrix. + /// + MatrixType4x4, + + /// + /// A float3x4 matrix. + /// + MatrixType3x4, + } + + private void MatrixUploadHelper(void* src, int numMatrices, int offset, int offsetInverse, MatrixType srcType, MatrixType dstType) + { + var size = numMatrices * sizeof(float3x4); + var opsize = UnsafeUtility.SizeOf(); + + var allocSucceeded = TryAlloc(opsize, size, out var dst, out var operationOffset, out var dataOffset); + + if (!allocSucceeded) + { + Debug.Log("SparseUploader failed to allocate upload memory for AddMatrixUpload operation"); + return; + } + + if (srcType == MatrixType.MatrixType4x4) + { + var srcLocal = (byte*)src; + var dstLocal = dst + dataOffset; + for (int i = 0; i < numMatrices; ++i) + { + for (int j = 0; j < 4; ++j) + { + UnsafeUtility.MemCpy(dstLocal, srcLocal, 12); + dstLocal += 12; + srcLocal += 16; + } + } + } + else + { + UnsafeUtility.MemCpy(dst + dataOffset, src, size); + } + + var uploadType = (offsetInverse == -1) ? (uint)OperationType.Matrix_4x4 : (uint)OperationType.Matrix_Inverse_4x4; + uploadType += (dstType == MatrixType.MatrixType3x4) ? 2u : 0u; + + var op = new Operation + { + type = uploadType, + srcOffset = (uint)dataOffset, + dstOffset = (uint)offset, + dstOffsetExtra = (uint)offsetInverse, + size = (uint)size, + count = 1, + }; + UnsafeUtility.MemCpy(dst + operationOffset, &op, opsize); + } + + /// + /// Adds a new pending matrix upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source pointer. + /// + /// A pointer to a memory area that contains matrices of the type specified by srcType. + /// The number of matrices to upload. + /// The destination offset of the copy part of the upload operation. + /// The source matrix format. + /// The destination matrix format. + public void AddMatrixUpload(void* src, int numMatrices, int offset, MatrixType srcType, MatrixType dstType) + { + MatrixUploadHelper(src, numMatrices, offset, -1, srcType, dstType); + } + + /// + /// Adds a new pending matrix upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source pointer. + /// + /// The upload operation automatically inverts matrices during the upload operation and it then stores the inverted matrices in a + /// separate offset in the GPU buffer. + /// + /// A pointer to a memory area that contains matrices of the type specified by srcType. + /// The number of matrices to upload. + /// The destination offset of the copy part of the upload operation. + /// The destination offset of the inverse part of the upload operation. + /// The source matrix format. + /// The destination matrix format. + public void AddMatrixUploadAndInverse(void* src, int numMatrices, int offset, int offsetInverse, MatrixType srcType, MatrixType dstType) + { + MatrixUploadHelper(src, numMatrices, offset, offsetInverse, srcType, dstType); + } + + /// + /// Adds a new pending upload operation to execute when you call SparseUploader.EndAndCommit. + /// + /// + /// When this operation executes, the SparseUploader copies data from the source pointer. + /// + /// The upload operations reads data with the specified source stride from the source pointer and then stores the data with the specified destination stride. + /// + /// The source data pointer. + /// The size of each data element to upload. + /// The stride of each data element as stored in the source pointer. + /// The number of data elements to upload. + /// The destination offset + /// The destination stride + public void AddStridedUpload(void* src, uint elemSize, uint srcStride, uint count, uint dstOffset, int dstStride) + { + int opSize = UnsafeUtility.SizeOf(); + uint dataSize = count * srcStride; + + var allocSucceeded = TryAlloc(opSize, (int)dataSize, out var dst, out var operationOffset, out var dataOffset); + + if (!allocSucceeded) + { + Debug.Log("SparseUploader failed to allocate upload memory for AddStridedUpload operation"); + return; + } + + UnsafeUtility.MemCpy(dst + dataOffset, src, dataSize); + var op = new Operation + { + type = (uint)OperationType.StridedUpload, + srcOffset = (uint)dataOffset, + srcStride = srcStride, + dstOffset = (uint)dstOffset, + dstOffsetExtra = 0, + dstStride = dstStride, + size = elemSize, + count = count, + }; + UnsafeUtility.MemCpy(dst + operationOffset, &op, opSize); + } + } + + internal class BufferPool : IDisposable + { + private List m_Buffers; + private Stack m_FreeBufferIds; + + private int m_Count; + private int m_Stride; + private GraphicsBuffer.Target m_Target; + private GraphicsBuffer.UsageFlags m_UsageFlags; + + + public BufferPool(int count, int stride, GraphicsBuffer.Target target, GraphicsBuffer.UsageFlags usageFlags) + { + m_Buffers = new List(); + m_FreeBufferIds = new Stack(); + + m_Count = count; + m_Stride = stride; + m_Target = target; + m_UsageFlags = usageFlags; + } + + public void Dispose() + { + for (int i = 0; i < m_Buffers.Count; ++i) + { + m_Buffers[i].Dispose(); + } + } + + private int AllocateBuffer() + { + var id = m_Buffers.Count; + var cb = new GraphicsBuffer(m_Target, m_UsageFlags, m_Count, m_Stride); + m_Buffers.Add(cb); + return id; + } + + public int GetBufferId() + { + if (m_FreeBufferIds.Count == 0) + return AllocateBuffer(); + + return m_FreeBufferIds.Pop(); + } + + public GraphicsBuffer GetBufferFromId(int id) + { + return m_Buffers[id]; + } + + public void PutBufferId(int id) + { + m_FreeBufferIds.Push(id); + } + + public int TotalBufferCount => m_Buffers.Count; + public int TotalBufferSize => TotalBufferCount * m_Count * m_Stride; + } + + /// + /// Represents SparseUploader statistics. + /// + public struct SparseUploaderStats + { + /// + /// The amount of GPU memory the SparseUploader uses internally. + /// + /// + /// This value doesn't include memory in the managed GPU buffer that you pass into the SparseUploader on construction, + /// or when you use SparseUploader.ReplaceBuffer. + /// + public long BytesGPUMemoryUsed; + + /// + /// The amount of memory the SparseUploader used to upload during the current frame. + /// + public long BytesGPUMemoryUploadedCurr; + + /// + /// The highest amount of memory the SparseUploader used for upload during a previous frame. + /// + public long BytesGPUMemoryUploadedMax; + } + + /// + /// Provides utility methods that you can use to upload data into GPU memory. + /// + /// + /// To add uploads from jobs, use a ThreadedSparseUploader which you can create using SparseUploader.Begin. + /// If you add uploads from jobs, the ThreadedSparseUploader submits them to the GPU in a series of compute shader dispatches when you call SparseUploader.EndAndCommit. + /// + public unsafe struct SparseUploader : IDisposable + { + const int k_MaxThreadGroupsPerDispatch = 65535; + + int m_BufferChunkSize; + + GraphicsBuffer m_DestinationBuffer; + + BufferPool m_FenceBufferPool; + BufferPool m_UploadBufferPool; + + NativeArray m_MappedBuffers; + + private long m_CurrentFrameUploadSize; + private long m_MaxUploadSize; + + class FrameData + { + public Stack m_Buffers; + public int m_FenceBuffer; + public AsyncGPUReadbackRequest m_Fence; + + public FrameData() + { + m_Buffers = new Stack(); + m_FenceBuffer = -1; + } + } + + Stack m_FreeFrameData; + List m_FrameData; + + ThreadedSparseUploaderData* m_ThreadData; + + ComputeShader m_SparseUploaderShader; + int m_CopyKernelIndex; + int m_ReplaceKernelIndex; + + int m_SrcBufferID; + int m_DstBufferID; + int m_OperationsBaseID; + int m_ReplaceOperationSize; + + /// + /// Constructs a new sparse uploader with the specified buffer as the target. + /// + /// The target buffer to write uploads into. + /// The upload buffer chunk size. + public SparseUploader(GraphicsBuffer destinationBuffer, int bufferChunkSize = 16 * 1024 * 1024) + { + m_BufferChunkSize = bufferChunkSize; + + m_DestinationBuffer = destinationBuffer; + + m_FenceBufferPool = new BufferPool(1, 4, GraphicsBuffer.Target.Raw, GraphicsBuffer.UsageFlags.None); + m_UploadBufferPool = new BufferPool(m_BufferChunkSize / 4, 4, GraphicsBuffer.Target.Raw, GraphicsBuffer.UsageFlags.LockBufferForWrite); + m_MappedBuffers = new NativeArray(); + m_FreeFrameData = new Stack(); + m_FrameData = new List(); + + m_ThreadData = (ThreadedSparseUploaderData*)Memory.Unmanaged.Allocate(sizeof(ThreadedSparseUploaderData), + UnsafeUtility.AlignOf(), Allocator.Persistent); + m_ThreadData->m_Buffers = null; + m_ThreadData->m_NumBuffers = 0; + m_ThreadData->m_CurrBuffer = 0; + + m_SparseUploaderShader = Resources.Load("SparseUploader"); + m_CopyKernelIndex = m_SparseUploaderShader.FindKernel("CopyKernel"); + m_ReplaceKernelIndex = m_SparseUploaderShader.FindKernel("ReplaceKernel"); + + m_SrcBufferID = Shader.PropertyToID("srcBuffer"); + m_DstBufferID = Shader.PropertyToID("dstBuffer"); + m_OperationsBaseID = Shader.PropertyToID("operationsBase"); + m_ReplaceOperationSize = Shader.PropertyToID("replaceOperationSize"); + + m_CurrentFrameUploadSize = 0; + m_MaxUploadSize = 0; + } + + /// + /// Disposes of the SparseUploader. + /// + public void Dispose() + { + m_FenceBufferPool.Dispose(); + m_UploadBufferPool.Dispose(); + Memory.Unmanaged.Free(m_ThreadData, Allocator.Persistent); + } + + /// + /// Replaces the destination GPU buffer with a new one. + /// + /// + /// If the new buffer is non-null and copyFromPrevious is true, this method + /// dispatches a copy operation that copies data from the previous buffer to the new one. + /// + /// This is useful when the persistent storage buffer needs to grow. + /// + /// The new buffer to replace the old one with. + /// Indicates whether to copy the contents of the old buffer to the new buffer. + public void ReplaceBuffer(GraphicsBuffer buffer, bool copyFromPrevious = false) + { + if (copyFromPrevious && m_DestinationBuffer != null) + { + // Since we have no code such as Graphics.CopyBuffer(dst, src) currently + // we have to do this ourselves in a compute shader + var srcSize = m_DestinationBuffer.count * m_DestinationBuffer.stride; + m_SparseUploaderShader.SetBuffer(m_ReplaceKernelIndex, m_SrcBufferID, m_DestinationBuffer); + m_SparseUploaderShader.SetBuffer(m_ReplaceKernelIndex, m_DstBufferID, buffer); + m_SparseUploaderShader.SetInt(m_ReplaceOperationSize, srcSize); + + m_SparseUploaderShader.Dispatch(m_ReplaceKernelIndex, 1, 1, 1); + } + + m_DestinationBuffer = buffer; + } + + private void RecoverBuffers() + { + var numFree = 0; + if (SystemInfo.supportsAsyncGPUReadback) + { + for (int i = 0; i < m_FrameData.Count; ++i) + { + if (m_FrameData[i].m_Fence.done) + { + numFree = i + 1; + } + } + } + else + { + // Platform does not support async readbacks so we assume 3 frames in flight and once building on CPU + // always pop one from the frame data queue + if (m_FrameData.Count > 3) + numFree = 1; + } + + for (int i = 0; i < numFree; ++i) + { + m_FenceBufferPool.PutBufferId(m_FrameData[i].m_FenceBuffer); + while (m_FrameData[i].m_Buffers.Count > 0) + { + var buffer = m_FrameData[i].m_Buffers.Pop(); + m_UploadBufferPool.PutBufferId(buffer); + } + m_FreeFrameData.Push(m_FrameData[i]); + } + + if (numFree > 0) + { + m_FrameData.RemoveRange(0, numFree); + } + } + + /// + /// Begins a new upload frame and returns a new ThreadedSparseUploader that is valid until the next call to + /// SparseUploader.EndAndCommit. + /// + /// + /// You must follow this method with a call to SparseUploader.EndAndCommit later in the frame. You must also pass + /// the returned value from a Begin method to the next SparseUploader.EndAndCommit. + /// + /// An upper bound of total data size that you want to upload this frame. + /// The size of the largest upload operation that will occur. + /// An upper bound of the total number of upload operations that will occur this frame. + /// Returns a new ThreadedSparseUploader that must be passed to SparseUploader.EndAndCommit later. + public ThreadedSparseUploader Begin(int maxDataSizeInBytes, int biggestDataUpload, int maxOperationCount) + { + // First: recover all buffers from the previous frames (if any) + RecoverBuffers(); + + // Second: calculate total size needed this frame, allocate buffers and map what is needed + var operationSize = UnsafeUtility.SizeOf(); + var maxOperationSizeInBytes = maxOperationCount * operationSize; + var sizeNeeded = maxOperationSizeInBytes + maxDataSizeInBytes; + var bufferSizeWithMaxPaddingRemoved = m_BufferChunkSize - operationSize - biggestDataUpload; + var numBuffersNeeded = (sizeNeeded + bufferSizeWithMaxPaddingRemoved - 1) / bufferSizeWithMaxPaddingRemoved; + + if (numBuffersNeeded < 0) + numBuffersNeeded = 0; + + m_CurrentFrameUploadSize = sizeNeeded; + if (m_CurrentFrameUploadSize > m_MaxUploadSize) + m_MaxUploadSize = m_CurrentFrameUploadSize; + + m_MappedBuffers = new NativeArray(numBuffersNeeded, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + + for(int i = 0; i < numBuffersNeeded; ++i) + { + var id = m_UploadBufferPool.GetBufferId(); + var cb = m_UploadBufferPool.GetBufferFromId(id); + var data = cb.LockBufferForWrite(0, m_BufferChunkSize); + var marker = MappedBuffer.PackMarker(0, m_BufferChunkSize); + m_MappedBuffers[i] = new MappedBuffer + { + m_Data = (byte*)data.GetUnsafePtr(), + m_Marker = marker, + m_BufferID = id, + }; + } + + m_ThreadData->m_Buffers = (MappedBuffer*)m_MappedBuffers.GetUnsafePtr(); + m_ThreadData->m_NumBuffers = numBuffersNeeded; + + // TODO: set safety handle on thread data + return new ThreadedSparseUploader + { + m_Data = m_ThreadData + }; + } + + private void DispatchUploads(int numOps, GraphicsBuffer graphicsBuffer) + { + for (int iOp = 0; iOp < numOps; iOp += k_MaxThreadGroupsPerDispatch) + { + int opsBegin = iOp; + int opsEnd = math.min(opsBegin + k_MaxThreadGroupsPerDispatch, numOps); + int numThreadGroups = opsEnd - opsBegin; + + m_SparseUploaderShader.SetBuffer(m_CopyKernelIndex, m_SrcBufferID, graphicsBuffer); + m_SparseUploaderShader.SetBuffer(m_CopyKernelIndex, m_DstBufferID, m_DestinationBuffer); + m_SparseUploaderShader.SetInt(m_OperationsBaseID, opsBegin); + + m_SparseUploaderShader.Dispatch(m_CopyKernelIndex, numThreadGroups, 1, 1); + } + } + + private void StepFrame() + { + // TODO: release safety handle of thread data + m_ThreadData->m_Buffers = null; + m_ThreadData->m_NumBuffers = 0; + m_ThreadData->m_CurrBuffer = 0; + } + + /// + /// Ends an upload frame and dispatches any upload operations added to the passed in ThreadedSparseUploader. + /// + /// The ThreadedSparseUploader to consume and process upload dispatches for. You must have created this with a call to SparseUploader.Begin. + public void EndAndCommit(ThreadedSparseUploader tsu) + { + var numBuffers = m_ThreadData->m_NumBuffers; + var frameData = m_FreeFrameData.Count > 0 ? m_FreeFrameData.Pop() : new FrameData(); + for (int iBuf = 0; iBuf < numBuffers; ++iBuf) + { + var mappedBuffer = m_MappedBuffers[iBuf]; + MappedBuffer.UnpackMarker(mappedBuffer.m_Marker, out var operationOffset, out var dataOffset); + var numOps = (int) (operationOffset / UnsafeUtility.SizeOf()); + var graphicsBufferID = mappedBuffer.m_BufferID; + var graphicsBuffer = m_UploadBufferPool.GetBufferFromId(graphicsBufferID); + + if (numOps > 0) + { + graphicsBuffer.UnlockBufferAfterWrite(m_BufferChunkSize); + + DispatchUploads(numOps, graphicsBuffer); + + frameData.m_Buffers.Push(graphicsBufferID); + } + else + { + graphicsBuffer.UnlockBufferAfterWrite(0); + m_UploadBufferPool.PutBufferId(graphicsBufferID); + } + } + + if (SystemInfo.supportsAsyncGPUReadback) + { + var fenceBufferId = m_FenceBufferPool.GetBufferId(); + frameData.m_FenceBuffer = fenceBufferId; + frameData.m_Fence = AsyncGPUReadback.Request(m_FenceBufferPool.GetBufferFromId(fenceBufferId)); + } + + m_FrameData.Add(frameData); + + m_MappedBuffers.Dispose(); + + StepFrame(); + } + + + /// + /// Cleans up internal data and recovers buffers into the free buffer pool. + /// + /// + /// You should call this once per frame. + /// + public void FrameCleanup() + { + var numBuffers = m_ThreadData->m_NumBuffers; + + if (numBuffers == 0) + return; + + // These buffers where never used, so they gets returned to the pool at once + for (int iBuf = 0; iBuf < numBuffers; ++iBuf) + { + var mappedBuffer = m_MappedBuffers[iBuf]; + MappedBuffer.UnpackMarker(mappedBuffer.m_Marker, out var operationOffset, out var dataOffset); + var graphicsBufferID = mappedBuffer.m_BufferID; + var graphicsBuffer = m_UploadBufferPool.GetBufferFromId(graphicsBufferID); + + graphicsBuffer.UnlockBufferAfterWrite(0); + m_UploadBufferPool.PutBufferId(graphicsBufferID); + } + + m_MappedBuffers.Dispose(); + + StepFrame(); + } + + /// + /// Calculates statistics about the current and previous frame uploads. + /// + /// Returns a new statistics struct that contains information about the frame uploads. + public SparseUploaderStats ComputeStats() + { + var stats = default(SparseUploaderStats); + + var totalUploadMemory = m_UploadBufferPool.TotalBufferSize; + var totalFenceMemory = m_FenceBufferPool.TotalBufferSize; + stats.BytesGPUMemoryUsed = totalUploadMemory + totalFenceMemory; + stats.BytesGPUMemoryUploadedCurr = m_CurrentFrameUploadSize; + stats.BytesGPUMemoryUploadedMax = m_MaxUploadSize; + + return stats; + } + } +} diff --git a/Unity.Entities.Graphics/SparseUploader.cs.meta b/Unity.Entities.Graphics/SparseUploader.cs.meta new file mode 100644 index 0000000..215998c --- /dev/null +++ b/Unity.Entities.Graphics/SparseUploader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42e65ec7377f83e468e7c2546ca1f94e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs b/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs new file mode 100644 index 0000000..e30cf1c --- /dev/null +++ b/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs @@ -0,0 +1,17 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// Represents a system group that contains systems that perform structural changes. + /// + /// + /// Any system that makes structural changes must be in this system group. Structural changes performed after can result in undefined behavior + /// or even crashing the application. + /// + [UpdateInGroup(typeof(PresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + public class StructuralChangePresentationSystemGroup : ComponentSystemGroup + { + } +} diff --git a/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs.meta b/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs.meta new file mode 100644 index 0000000..fa864b8 --- /dev/null +++ b/Unity.Entities.Graphics/StructuralChangePresentationSystemGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8494c024d5044d4680514cfa569038e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/ThreadLocalAABB.cs b/Unity.Entities.Graphics/ThreadLocalAABB.cs new file mode 100644 index 0000000..b4cb0ed --- /dev/null +++ b/Unity.Entities.Graphics/ThreadLocalAABB.cs @@ -0,0 +1,41 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Jobs.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Rendering +{ + internal unsafe struct ThreadLocalAABB + { + private const int kAABBNumFloats = 6; + private const int kCacheLineNumFloats = JobsUtility.CacheLineSize / 4; + private const int kCacheLinePadding = kCacheLineNumFloats - kAABBNumFloats; + + public MinMaxAABB AABB; + // Pad the size of this struct to a single cache line, to ensure that thread local updates + // don't cause false sharing + public fixed float CacheLinePadding[kCacheLinePadding]; + + public static void AssertCacheLineSize() + { + Debug.Assert(UnsafeUtility.SizeOf() == JobsUtility.CacheLineSize, + "ThreadLocalAABB should have a size equal to the CPU cache line size"); + } + } + + [BurstCompile] + internal unsafe struct ZeroThreadLocalAABBJob : IJobParallelFor + { + public NativeArray ThreadLocalAABBs; + + public void Execute(int index) + { + var threadLocalAABB = ((ThreadLocalAABB*) ThreadLocalAABBs.GetUnsafePtr()) + index; + threadLocalAABB->AABB = MinMaxAABB.Empty; + } + } + +} diff --git a/Unity.Entities.Graphics/ThreadLocalAABB.cs.meta b/Unity.Entities.Graphics/ThreadLocalAABB.cs.meta new file mode 100644 index 0000000..207b8c0 --- /dev/null +++ b/Unity.Entities.Graphics/ThreadLocalAABB.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cea216b95eae4163b4589d2fee629a98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties.meta b/Unity.Entities.Graphics/URPMaterialProperties.meta new file mode 100644 index 0000000..4bdb243 --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6d00248cf1bd1443874aeda73682921 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs new file mode 100644 index 0000000..6bb519f --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs @@ -0,0 +1,33 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("_BaseColor")] + public struct URPMaterialPropertyBaseColor : IComponentData + { + public float4 Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertyBaseColorAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyBaseColor), "Value.x", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyBaseColor), "Value.y", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyBaseColor), "Value.z", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyBaseColor), "Value.w", true)] + public Unity.Mathematics.float4 Value; + + class URPMaterialPropertyBaseColorBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertyBaseColorAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertyBaseColor component = default(Unity.Rendering.URPMaterialPropertyBaseColor); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs.meta b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs.meta new file mode 100644 index 0000000..f483089 --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBaseColorAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54b6467bcd530cd4d809fae70ea0ca9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs new file mode 100644 index 0000000..41709b8 --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs @@ -0,0 +1,29 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_BumpScale")] + public struct URPMaterialPropertyBumpScale : IComponentData + { + public float Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertyBumpScaleAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyBumpScale), "Value")] + public float Value; + + class URPMaterialPropertyBumpScaleBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertyBumpScaleAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertyBumpScale component = default(Unity.Rendering.URPMaterialPropertyBumpScale); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs.meta b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs.meta new file mode 100644 index 0000000..90deedd --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyBumpScaleAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f1823f2f3a999942a19693ea11758e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs new file mode 100644 index 0000000..38fadbd --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs @@ -0,0 +1,29 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Cutoff")] + public struct URPMaterialPropertyCutoff : IComponentData + { + public float Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertyCutoffAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyCutoff), "Value")] + public float Value; + + class URPMaterialPropertyCutoffBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertyCutoffAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertyCutoff component = default(Unity.Rendering.URPMaterialPropertyCutoff); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs.meta b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs.meta new file mode 100644 index 0000000..58ac042 --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyCutoffAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1dd3991dc8ab6c4a9d487b330fc4454 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyEmissionColorAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyEmissionColorAuthoring.cs new file mode 100644 index 0000000..a7357ce --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyEmissionColorAuthoring.cs @@ -0,0 +1,33 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("_EmissionColor")] + public struct URPMaterialPropertyEmissionColor : IComponentData + { + public float4 Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertyEmissionColorAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyEmissionColor), "Value.x", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyEmissionColor), "Value.y", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyEmissionColor), "Value.z", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyEmissionColor), "Value.w", true)] + public Unity.Mathematics.float4 Value; + + class URPMaterialPropertyEmissionColorBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertyEmissionColorAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertyEmissionColor component = default(Unity.Rendering.URPMaterialPropertyEmissionColor); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs new file mode 100644 index 0000000..6c0069a --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs @@ -0,0 +1,29 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Metallic")] + public struct URPMaterialPropertyMetallic : IComponentData + { + public float Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertyMetallicAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertyMetallic), "Value")] + public float Value; + + class URPMaterialPropertyMetallicBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertyMetallicAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertyMetallic component = default(Unity.Rendering.URPMaterialPropertyMetallic); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs.meta b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs.meta new file mode 100644 index 0000000..03d229c --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertyMetallicAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff794cc00a1437d4a80d1f01fa1bf6c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySmoothnessAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySmoothnessAuthoring.cs new file mode 100644 index 0000000..fbb6dea --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySmoothnessAuthoring.cs @@ -0,0 +1,29 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; + +namespace Unity.Rendering +{ + [MaterialProperty("_Smoothness")] + public struct URPMaterialPropertySmoothness : IComponentData + { + public float Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertySmoothnessAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertySmoothness), "Value")] + public float Value; + + class URPMaterialPropertySmoothnessBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertySmoothnessAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertySmoothness component = default(Unity.Rendering.URPMaterialPropertySmoothness); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs new file mode 100644 index 0000000..9412cba --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs @@ -0,0 +1,33 @@ +#if URP_10_0_0_OR_NEWER +using Unity.Entities; +using Unity.Mathematics; + +namespace Unity.Rendering +{ + [MaterialProperty("_SpecColor")] + public struct URPMaterialPropertySpecColor : IComponentData + { + public float4 Value; + } + + [UnityEngine.DisallowMultipleComponent] + public class URPMaterialPropertySpecColorAuthoring : UnityEngine.MonoBehaviour + { + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertySpecColor), "Value.x", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertySpecColor), "Value.y", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertySpecColor), "Value.z", true)] + [Unity.Entities.RegisterBinding(typeof(URPMaterialPropertySpecColor), "Value.w", true)] + public Unity.Mathematics.float4 Value; + + class URPMaterialPropertySpecColorBaker : Unity.Entities.Baker + { + public override void Bake(URPMaterialPropertySpecColorAuthoring authoring) + { + Unity.Rendering.URPMaterialPropertySpecColor component = default(Unity.Rendering.URPMaterialPropertySpecColor); + component.Value = authoring.Value; + AddComponent(component); + } + } + } +} +#endif diff --git a/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs.meta b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs.meta new file mode 100644 index 0000000..c9bf0b6 --- /dev/null +++ b/Unity.Entities.Graphics/URPMaterialProperties/URPMaterialPropertySpecColorAuthoring.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4260aa09e1098184599f5f0177a7f797 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef b/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef new file mode 100644 index 0000000..bd9b3aa --- /dev/null +++ b/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef @@ -0,0 +1,79 @@ +{ + "name": "Unity.Entities.Graphics", + "rootNamespace": "", + "references": [ + "Unity.Entities", + "Unity.Entities.Hybrid", + "Unity.Transforms", + "Unity.Transforms.Hybrid", + "Unity.Collections", + "Unity.Burst", + "Unity.Jobs", + "Unity.Mathematics", + "Unity.Mathematics.Extensions", + "Unity.Mathematics.Extensions.Hybrid", + "Unity.MOC.Runtime", + "Unity.RenderPipelines.HighDefinition.Runtime", + "Unity.RenderPipelines.Universal.Runtime", + "Unity.RenderPipelines.Core.Runtime", + "Unity.Deformations", + "Unity.Scenes" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [ + "!UNITY_DOTSRUNTIME" + ], + "versionDefines": [ + { + "name": "com.unity.render-pipelines.high-definition", + "expression": "9.9.9", + "define": "HDRP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.render-pipelines.universal", + "expression": "9.9.9", + "define": "URP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.render-pipelines.core", + "expression": "9.9.9", + "define": "SRP_10_0_0_OR_NEWER" + }, + { + "name": "com.unity.tiny", + "expression": "0.21.9", + "define": "TINY_0_22_0_OR_NEWER" + }, + { + "name": "com.unity.cinemachine.dots", + "expression": "0.60.0-preview.81", + "define": "ENABLE_TRANSFORM_V1" + }, + { + "name": "com.unity.stableid", + "expression": "0.60.0-preview.91", + "define": "ENABLE_TRANSFORM_V1" + }, + { + "name": "com.unity.physics", + "expression": "0.60.0-preview.88", + "define": "ENABLE_TRANSFORM_V1" + }, + { + "name": "com.unity.2d.entities.physics", + "expression": "0.5.0-preview.1", + "define": "ENABLE_TRANSFORM_V1" + }, + { + "name": "com.unity.netcode", + "expression": "0.60.0-preview.90", + "define": "ENABLE_TRANSFORM_V1" + } + ], + "noEngineReferences": false +} diff --git a/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef.meta b/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef.meta new file mode 100644 index 0000000..59cfb4c --- /dev/null +++ b/Unity.Entities.Graphics/Unity.Entities.Graphics.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7a450cf7ca9694b5a8bfa3fd83ec635a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs b/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs new file mode 100644 index 0000000..9b1efc2 --- /dev/null +++ b/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs @@ -0,0 +1,69 @@ +using Unity.Assertions; +using Unity.Burst; +using Unity.Burst.Intrinsics; +using Unity.Collections; +using Unity.Entities; +using Unity.Mathematics; +using Unity.Entities.Graphics; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Unity.Rendering +{ + [BurstCompile] + internal unsafe struct UpdateDrawCommandFlagsJob : IJobChunk + { + [ReadOnly] public ComponentTypeHandle LocalToWorld; + [ReadOnly] public SharedComponentTypeHandle RenderFilterSettings; + public ComponentTypeHandle EntitiesGraphicsChunkInfo; + + [ReadOnly] public NativeParallelHashMap FilterSettings; + public BatchFilterSettings DefaultFilterSettings; + + public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) + { + // This job is not written to support queries with enableable component types. + Assert.IsFalse(useEnabledMask); + + var chunkInfo = chunk.GetChunkComponentData(EntitiesGraphicsChunkInfo); + Debug.Assert(chunkInfo.Valid, "Attempted to process a chunk with uninitialized Hybrid chunk info"); + + var localToWorld = chunk.GetNativeArray(LocalToWorld); + + // This job runs for all chunks that have structural changes, so if different + // RenderFilterSettings get set on entities, they should be picked up by + // the order change filter. + int filterIndex = chunk.GetSharedComponentIndex(RenderFilterSettings); + if (!FilterSettings.TryGetValue(filterIndex, out var filterSettings)) + filterSettings = DefaultFilterSettings; + + bool hasPerObjectMotion = filterSettings.motionMode != MotionVectorGenerationMode.Camera; + if (hasPerObjectMotion) + chunkInfo.CullingData.Flags |= EntitiesGraphicsChunkCullingData.kFlagPerObjectMotion; + else + chunkInfo.CullingData.Flags &= unchecked((byte)~EntitiesGraphicsChunkCullingData.kFlagPerObjectMotion); + + for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) + { + bool flippedWinding = RequiresFlippedWinding(localToWorld[i]); + + int qwordIndex = i / 64; + int bitIndex = i % 64; + ulong mask = 1ul << bitIndex; + + if (flippedWinding) + chunkInfo.CullingData.FlippedWinding[qwordIndex] |= mask; + else + chunkInfo.CullingData.FlippedWinding[qwordIndex] &= ~mask; + } + + chunk.SetChunkComponentData(EntitiesGraphicsChunkInfo, chunkInfo); + } + + private bool RequiresFlippedWinding(LocalToWorld localToWorld) + { + return math.determinant(localToWorld.Value) < 0.0; + } + } +} diff --git a/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs.meta b/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs.meta new file mode 100644 index 0000000..ddaa810 --- /dev/null +++ b/Unity.Entities.Graphics/UpdateDrawCommandFlags.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64a26ba0c16d419185d5ad72074f6b67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs b/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs new file mode 100644 index 0000000..4bcd1a3 --- /dev/null +++ b/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs @@ -0,0 +1,84 @@ +using Unity.Entities; +using Unity.Transforms; +using UnityEngine; + +namespace Unity.Rendering +{ + /// + /// A system that renders all entities that contain both RenderMesh and LocalToWorld components. + /// + //@TODO: Updating always necessary due to empty component group. When Component group and archetype chunks are unified, [RequireMatchingQueriesForUpdate] can be added. + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] + [UpdateInGroup(typeof(StructuralChangePresentationSystemGroup))] + public partial class UpdateHybridChunksStructure : SystemBase + { + private EntityQuery m_MissingHybridChunkInfo; + private EntityQuery m_DisabledRenderingQuery; +#if UNITY_EDITOR + private EntityQuery m_HasHybridChunkInfo; +#endif + + /// + protected override void OnCreate() + { + m_MissingHybridChunkInfo = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ChunkComponentReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + ComponentType.ReadOnly(), + }, + None = new[] + { + ComponentType.ChunkComponentReadOnly(), + ComponentType.ReadOnly(), + }, + + // TODO: Add chunk component to disabled entities and prefab entities to work around + // the fragmentation issue where entities are not added to existing chunks with chunk + // components. Remove this once chunk components don't affect archetype matching + // on entity creation. + Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab, + }); + + m_DisabledRenderingQuery = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ReadOnly(), + }, + }); + +#if UNITY_EDITOR + m_HasHybridChunkInfo = GetEntityQuery(new EntityQueryDesc + { + All = new[] + { + ComponentType.ChunkComponentReadOnly(), + }, + }); +#endif + } + + /// + protected override void OnUpdate() + { + UnityEngine.Profiling.Profiler.BeginSample("UpdateHybridChunksStructure"); + { +#if UNITY_EDITOR + if (EntitiesGraphicsEditorTools.DebugSettings.RecreateAllBatches) + { + Debug.Log("Recreating all batches"); + EntityManager.RemoveChunkComponentData(m_HasHybridChunkInfo); + } +#endif + + EntityManager.AddComponent(m_MissingHybridChunkInfo, ComponentType.ChunkComponent()); + EntityManager.RemoveChunkComponentData(m_DisabledRenderingQuery); + } + UnityEngine.Profiling.Profiler.EndSample(); + } + } +} diff --git a/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs.meta b/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs.meta new file mode 100644 index 0000000..37184b6 --- /dev/null +++ b/Unity.Entities.Graphics/UpdateEntitiesGraphicsChunksStructure.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26bdba8d91439064cb03b8389e872d38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs b/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs new file mode 100644 index 0000000..313e840 --- /dev/null +++ b/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs @@ -0,0 +1,14 @@ +using Unity.Entities; + +namespace Unity.Rendering +{ + /// + /// Represents a system group that is used to establish the order of execution of the other systems. + /// + [UpdateInGroup(typeof(PresentationSystemGroup))] + [UpdateAfter(typeof(StructuralChangePresentationSystemGroup))] + [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.EntitySceneOptimizations | WorldSystemFilterFlags.Editor)] + public class UpdatePresentationSystemGroup : ComponentSystemGroup + { + } +} diff --git a/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs.meta b/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs.meta new file mode 100644 index 0000000..d1f2897 --- /dev/null +++ b/Unity.Entities.Graphics/UpdatePresentationSystemGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e7f6b6b97e53b4c2a9b0e177b5a358fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ValidationExceptions.json b/ValidationExceptions.json new file mode 100644 index 0000000..4bcd459 --- /dev/null +++ b/ValidationExceptions.json @@ -0,0 +1,20 @@ +{ + "ErrorExceptions": [], + "WarningExceptions": [ + { + "ValidationTest": "Manifest Validation", + "ExceptionMessage": "Package dependency com.unity.entities@1.0.0-exp.3 must be promoted to production before this package is promoted to production. (Except for core packages)", + "PackageVersion": "1.0.0-exp.3" + }, + { + "ValidationTest": "Folder Structure Validation", + "ExceptionMessage": "The Resources Directory should not be used in packages. For more guidance, please visit https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity6.html", + "PackageVersion": "1.0.0-exp.3" + }, + { + "ValidationTest": "Package Lifecycle Validation", + "ExceptionMessage": "com.unity.entities.graphics has never been promoted to production before. Please contact Release Management through slack in #devs-pkg-promotion to promote the first version of your package before trying to use this automated pipeline. Read more about this error and potential solutions at https://docs.unity3d.com/Packages/com.unity.package-validation-suite@latest/index.html?preview=1&subfolder=/manual/lifecycle_validation_error.html#the-very-first-version-of-a-package-must-be-promoted-by-release-management", + "PackageVersion": "1.0.0-exp.3" + } + ] +} diff --git a/ValidationExceptions.json.meta b/ValidationExceptions.json.meta new file mode 100644 index 0000000..500be16 --- /dev/null +++ b/ValidationExceptions.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 32a6ae1add8873447a3705248c87b166 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..86461c4 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "com.unity.entities.graphics", + "displayName": "Entities Graphics", + "version": "1.0.0-exp.8", + "unity": "2022.2", + "unityRelease": "0b5", + "description": "The Entities Graphics package provides systems and components for drawing meshes using DOTS, including support for instanced mesh rendering and LOD.", + "dependencies": { + "com.unity.entities": "1.0.0-exp.8", + "com.unity.render-pipelines.core": "14.0.3" + }, + "keywords": [ + "dots", + "hybrid", + "rendering", + "unity" + ], + "upmCi": { + "footprint": "92d1614a8290659f835896095c73f0a71705d6b9" + }, + "repository": { + "url": "https://github.cds.internal.unity3d.com/unity/dots.git", + "type": "git", + "revision": "7f4180a58b7e1c75f6375bd73fb80bd99d195d4e" + } +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..4341a13 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f0fa43c6b5abdb0438d6cfa559bf526f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: