From 00bb6b3a96096b1b81cd9056686ec948c753164b Mon Sep 17 00:00:00 2001 From: Dynamitos Date: Tue, 25 Jun 2024 11:21:56 +0200 Subject: [PATCH] Adding view culling to non-depth culling variant --- res/shaders/DepthCullingTask.slang | 25 +-- res/shaders/DepthMipGen.slang | 1 - res/shaders/DrawListTask.slang | 3 - res/shaders/lib/Bounding.slang | 32 +++- src/Editor/Asset/MeshLoader.cpp | 11 +- src/Engine/Graphics/RenderPass/BasePass.cpp | 8 - .../Graphics/RenderPass/CachedDepthPass.cpp | 8 - .../Graphics/RenderPass/DepthCullingPass.cpp | 163 ++++++++---------- src/Engine/Graphics/StaticMeshVertexData.h | 12 -- src/Engine/Graphics/Vulkan/Buffer.cpp | 56 +++--- 10 files changed, 144 insertions(+), 175 deletions(-) diff --git a/res/shaders/DepthCullingTask.slang b/res/shaders/DepthCullingTask.slang index 7870bb2..4a6d36d 100644 --- a/res/shaders/DepthCullingTask.slang +++ b/res/shaders/DepthCullingTask.slang @@ -24,27 +24,8 @@ bool isBoxVisible(AABB bounding) // now we calculate what mip level we need to only sample up to 4 texels covering the entire meshlet uint2 screenCornerMin = mipDimensions; uint2 screenCornerMax = uint2(0, 0); - // we use reverse depth, so higher values are closer - float maxDepth = 0; - { - float4 corners[8]; - corners[0] = float4(bounding.min.x, bounding.min.y, bounding.min.z, 1.0f); - corners[1] = float4(bounding.min.x, bounding.min.y, bounding.max.z, 1.0f); - corners[2] = float4(bounding.min.x, bounding.max.y, bounding.min.z, 1.0f); - corners[3] = float4(bounding.min.x, bounding.max.y, bounding.max.z, 1.0f); - corners[4] = float4(bounding.max.x, bounding.min.y, bounding.min.z, 1.0f); - corners[5] = float4(bounding.max.x, bounding.min.y, bounding.max.z, 1.0f); - corners[6] = float4(bounding.max.x, bounding.max.y, bounding.min.z, 1.0f); - corners[7] = float4(bounding.max.x, bounding.max.y, bounding.max.z, 1.0f); - for(uint i = 0; i < 8; ++i) - { - float4 clipCorner = mul(modelViewProjection, corners[i]); - float4 screenCorner = clipToScreen(clipCorner); - screenCornerMin = uint2(min(screenCornerMin.x, uint(screenCorner.x)), min(screenCornerMin.y, uint(screenCorner.y))); - screenCornerMax = uint2(max(screenCornerMax.x, uint(screenCorner.x)), max(screenCornerMax.y, uint(screenCorner.y))); - maxDepth = max(maxDepth, screenCorner.z); - } - } + // lower values are closer + float maxDepth = bounding.projectScreenDepth(modelViewProjection, screenCornerMin, screenCornerMax); uint mipOffset = 0; // in theory this wouldnt work if no corner was in screen, as min would be greater that max, however we verified that with view culling while(screenCornerMax.x - screenCornerMin.x > 1 || screenCornerMax.y - screenCornerMin.y > 1) @@ -118,8 +99,10 @@ void taskMain( // if the meshlet is outside of the frustum, we skip it since we cant do depth culling anyways if(meshlet.bounding.insideFrustum(viewFrustum)) { +#ifdef DEPTH_CULLING // if the meshlet bounding box is behind the cached depth buffer, we skip if(isBoxVisible(meshlet.bounding)) +#endif { uint index; InterlockedAdd(head, 1, index); diff --git a/res/shaders/DepthMipGen.slang b/res/shaders/DepthMipGen.slang index 661be58..9fa73f0 100644 --- a/res/shaders/DepthMipGen.slang +++ b/res/shaders/DepthMipGen.slang @@ -62,5 +62,4 @@ void initialReduce( int2 texCoord = groupOffset + threadID.xy; float fDepth = pDepthAttachment.texture.Load(int3(texCoord, 0)).r; pDepthAttachment.buffer[texCoord.x + (texCoord.y * width)] = fDepth; - } diff --git a/res/shaders/DrawListTask.slang b/res/shaders/DrawListTask.slang index 795a9f8..f3b10d6 100644 --- a/res/shaders/DrawListTask.slang +++ b/res/shaders/DrawListTask.slang @@ -26,10 +26,7 @@ void taskMain( uint cull = p.cullingOffset + i; MeshletDescription meshlet = pScene.meshletInfos[m]; MeshletCullingInfo culling = pScene.cullingInfos[cull]; -#ifdef DEPTH_CULLING - // if depth culling is disabled, we draw the whole scene as if cached if(culling.wasVisible()) -#endif { uint index; InterlockedAdd(head, 1, index); diff --git a/res/shaders/lib/Bounding.slang b/res/shaders/lib/Bounding.slang index 08992c2..baa3269 100644 --- a/res/shaders/lib/Bounding.slang +++ b/res/shaders/lib/Bounding.slang @@ -16,7 +16,7 @@ struct BoundingSphere bool result = true; for(int i = 0; i < 4 && result; ++i) { - if(dot(frustum.sides[i].n, centerRadius.xyz) - frustum.sides[i].d < -getRadius()) + if(dot(frustum.sides[i].n, getCenter()) - frustum.sides[i].d < -getRadius()) { result = false; } @@ -27,15 +27,15 @@ struct BoundingSphere struct AABB { - float3 min; + float3 minCorner; float pad0; - float3 max; + float3 maxCorner; float pad1; // modified version from https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling bool insideFrustum(Frustum frustum) { - float3 center = (min + max) * 0.5; - float3 extents = float3(max.x - center.x, max.y - center.y, max.z - center.z); + float3 center = (minCorner + maxCorner) * 0.5; + float3 extents = float3(maxCorner.x - center.x, maxCorner.y - center.y, maxCorner.z - center.z); bool result = true; for(int i = 0; i < 4 && result; ++i) { @@ -51,4 +51,26 @@ struct AABB } return result; } + float projectScreenDepth(float4x4 mvp, inout uint2 screenCornerMin, inout uint2 screenCornerMax) + { + float maxDepth = 0; + float4 corners[8]; + corners[0] = float4(minCorner.x, minCorner.y, minCorner.z, 1.0f); + corners[1] = float4(minCorner.x, minCorner.y, maxCorner.z, 1.0f); + corners[2] = float4(minCorner.x, maxCorner.y, minCorner.z, 1.0f); + corners[3] = float4(minCorner.x, maxCorner.y, maxCorner.z, 1.0f); + corners[4] = float4(maxCorner.x, minCorner.y, minCorner.z, 1.0f); + corners[5] = float4(maxCorner.x, minCorner.y, maxCorner.z, 1.0f); + corners[6] = float4(maxCorner.x, maxCorner.y, minCorner.z, 1.0f); + corners[7] = float4(maxCorner.x, maxCorner.y, maxCorner.z, 1.0f); + for(uint i = 0; i < 8; ++i) + { + float4 clipCorner = mul(mvp, corners[i]); + float4 screenCorner = clipToScreen(clipCorner); + screenCornerMin = uint2(min(screenCornerMin.x, uint(screenCorner.x)), min(screenCornerMin.y, uint(screenCorner.y))); + screenCornerMax = uint2(max(screenCornerMax.x, uint(screenCorner.x)), max(screenCornerMax.y, uint(screenCorner.y))); + maxDepth = max(maxDepth, screenCorner.z); + } + return maxDepth; + } }; diff --git a/src/Editor/Asset/MeshLoader.cpp b/src/Editor/Asset/MeshLoader.cpp index 7e3db98..cd4b1ad 100644 --- a/src/Editor/Asset/MeshLoader.cpp +++ b/src/Editor/Asset/MeshLoader.cpp @@ -404,19 +404,19 @@ void findMeshRoots(aiNode* node, List& meshNodes) { void MeshLoader::loadGlobalMeshes(const aiScene* scene, const Array& materials, Array& globalMeshes, Component::Collider& collider) { - //List> work; + List> work; for (int32 meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) { aiMesh* mesh = scene->mMeshes[meshIndex]; if (!(mesh->mPrimitiveTypes & aiPrimitiveType_TRIANGLE)) continue; - globalMeshes.add(nullptr); + globalMeshes[meshIndex] = new Mesh(); StaticMeshVertexData* vertexData = StaticMeshVertexData::getInstance(); MeshId id = vertexData->allocateVertexData(mesh->mNumVertices); uint64 offset = vertexData->getMeshOffset(id); collider.boundingbox.adjust(Vector(mesh->mAABB.mMin.x, mesh->mAABB.mMin.y, mesh->mAABB.mMin.z)); collider.boundingbox.adjust(Vector(mesh->mAABB.mMax.x, mesh->mAABB.mMax.y, mesh->mAABB.mMax.z)); - //work.add([&]() { + work.add([=, &globalMeshes]() { // assume static mesh for now Array positions(mesh->mNumVertices); StaticArray, MAX_TEXCOORDS> texCoords; @@ -475,16 +475,15 @@ void MeshLoader::loadGlobalMeshes(const aiScene* scene, const ArrayvertexData = vertexData; globalMeshes[meshIndex]->id = id; globalMeshes[meshIndex]->referencedMaterial = materials[mesh->mMaterialIndex]; globalMeshes[meshIndex]->meshlets = std::move(meshlets); globalMeshes[meshIndex]->indices = std::move(indices); globalMeshes[meshIndex]->vertexCount = mesh->mNumVertices; - //}); + }); } - //getThreadPool().runAndWait(std::move(work)); + getThreadPool().runAndWait(std::move(work)); } Matrix4 convertMatrix(aiMatrix4x4 matrix) { diff --git a/src/Engine/Graphics/RenderPass/BasePass.cpp b/src/Engine/Graphics/RenderPass/BasePass.cpp index 9d2329f..c5c532b 100644 --- a/src/Engine/Graphics/RenderPass/BasePass.cpp +++ b/src/Engine/Graphics/RenderPass/BasePass.cpp @@ -143,10 +143,6 @@ void BasePass::render() { { .cullMode = Gfx::SeCullModeFlags(twoSided ? Gfx::SE_CULL_MODE_NONE : Gfx::SE_CULL_MODE_BACK_BIT), }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER_OR_EQUAL, - }, .colorBlend = { .attachmentCount = 1, @@ -168,10 +164,6 @@ void BasePass::render() { { .cullMode = Gfx::SeCullModeFlags(twoSided ? Gfx::SE_CULL_MODE_NONE : Gfx::SE_CULL_MODE_BACK_BIT), }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER_OR_EQUAL, - }, .colorBlend = { .attachmentCount = 1, diff --git a/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp b/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp index 7fea6cb..7435c89 100644 --- a/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp +++ b/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp @@ -81,10 +81,6 @@ void CachedDepthPass::render() { { .samples = viewport->getSamples(), }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER, - }, .colorBlend = { .attachmentCount = 1, @@ -102,10 +98,6 @@ void CachedDepthPass::render() { { .samples = viewport->getSamples(), }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER, - }, .colorBlend = { .attachmentCount = 1, diff --git a/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp b/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp index 1ce4b0c..1a117a9 100644 --- a/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp +++ b/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp @@ -118,101 +118,90 @@ void DepthCullingPass::render() { query->beginQuery(); graphics->beginRenderPass(renderPass); - if (useDepthCulling) { + Array commands; - Array commands; + Gfx::ShaderPermutation permutation = graphics->getShaderCompiler()->getTemplate("DepthPass"); + permutation.setPositionOnly(usePositionOnly); + permutation.setDepthCulling(useDepthCulling); + for (VertexData* vertexData : VertexData::getList()) { + permutation.setVertexData(vertexData->getTypeName()); + vertexData->getInstanceDataSet()->updateBuffer(6, cullingBuffer); + vertexData->getInstanceDataSet()->writeChanges(); + // Create Pipeline(VertexData) + // Descriptors: + // ViewData => global, static + // VertexData => per meshtype + // SceneData => per meshtype + Gfx::PermutationId id(permutation); - Gfx::ShaderPermutation permutation = graphics->getShaderCompiler()->getTemplate("DepthPass"); - permutation.setPositionOnly(usePositionOnly); - permutation.setDepthCulling(useDepthCulling); - for (VertexData* vertexData : VertexData::getList()) { - permutation.setVertexData(vertexData->getTypeName()); - vertexData->getInstanceDataSet()->updateBuffer(6, cullingBuffer); - vertexData->getInstanceDataSet()->writeChanges(); - // Create Pipeline(VertexData) - // Descriptors: - // ViewData => global, static - // VertexData => per meshtype - // SceneData => per meshtype - Gfx::PermutationId id(permutation); + Gfx::ORenderCommand command = graphics->createRenderCommand("DepthRender"); + command->setViewport(viewport); - Gfx::ORenderCommand command = graphics->createRenderCommand("DepthRender"); - command->setViewport(viewport); - - const Gfx::ShaderCollection* collection = graphics->getShaderCompiler()->findShaders(id); - assert(collection != nullptr); - if (graphics->supportMeshShading()) { - Gfx::MeshPipelineCreateInfo pipelineInfo = { - .taskShader = collection->taskShader, - .meshShader = collection->meshShader, - .fragmentShader = collection->fragmentShader, - .renderPass = renderPass, - .pipelineLayout = collection->pipelineLayout, - .multisampleState = - { - .samples = viewport->getSamples(), - }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER, - }, - .colorBlend = - { - .attachmentCount = 1, - }, - }; - Gfx::PGraphicsPipeline pipeline = graphics->createGraphicsPipeline(std::move(pipelineInfo)); - command->bindPipeline(pipeline); - } else { - Gfx::LegacyPipelineCreateInfo pipelineInfo = { - .vertexShader = collection->vertexShader, - .fragmentShader = collection->fragmentShader, - .renderPass = renderPass, - .pipelineLayout = collection->pipelineLayout, - .multisampleState = - { - .samples = viewport->getSamples(), - }, - .depthStencilState = - { - .depthCompareOp = Gfx::SE_COMPARE_OP_GREATER, - }, - .colorBlend = - { - .attachmentCount = 1, - }, - }; - Gfx::PGraphicsPipeline pipeline = graphics->createGraphicsPipeline(std::move(pipelineInfo)); - command->bindPipeline(pipeline); - } - command->bindDescriptor({viewParamsSet, vertexData->getVertexDataSet(), vertexData->getInstanceDataSet(), set}); - uint32 offset = 0; - command->pushConstants(Gfx::SE_SHADER_STAGE_TASK_BIT_EXT | Gfx::SE_SHADER_STAGE_VERTEX_BIT, 0, - sizeof(VertexData::DrawCallOffsets), &offset); - if (graphics->supportMeshShading()) { - command->drawMesh(vertexData->getNumInstances(), 1, 1); - } else { - const auto& materials = vertexData->getMaterialData(); - for (const auto& materialData : materials) { - for (const auto& drawCall : materialData.instances) { - // material not used for any active meshes, skip - if (materialData.instances.size() == 0) - continue; - command->bindIndexBuffer(vertexData->getIndexBuffer()); - uint32 inst = drawCall.offsets.instanceOffset; - for (const auto& meshData : drawCall.instanceMeshData) { - // all meshlets of a mesh share the same indices offset - command->drawIndexed(meshData.numIndices, 1, meshData.firstIndex, - vertexData->getIndicesOffset(meshData.meshletOffset), inst++); - } + const Gfx::ShaderCollection* collection = graphics->getShaderCompiler()->findShaders(id); + assert(collection != nullptr); + if (graphics->supportMeshShading()) { + Gfx::MeshPipelineCreateInfo pipelineInfo = { + .taskShader = collection->taskShader, + .meshShader = collection->meshShader, + .fragmentShader = collection->fragmentShader, + .renderPass = renderPass, + .pipelineLayout = collection->pipelineLayout, + .multisampleState = + { + .samples = viewport->getSamples(), + }, + .colorBlend = + { + .attachmentCount = 1, + }, + }; + Gfx::PGraphicsPipeline pipeline = graphics->createGraphicsPipeline(std::move(pipelineInfo)); + command->bindPipeline(pipeline); + } else { + Gfx::LegacyPipelineCreateInfo pipelineInfo = { + .vertexShader = collection->vertexShader, + .fragmentShader = collection->fragmentShader, + .renderPass = renderPass, + .pipelineLayout = collection->pipelineLayout, + .multisampleState = + { + .samples = viewport->getSamples(), + }, + .colorBlend = + { + .attachmentCount = 1, + }, + }; + Gfx::PGraphicsPipeline pipeline = graphics->createGraphicsPipeline(std::move(pipelineInfo)); + command->bindPipeline(pipeline); + } + command->bindDescriptor({viewParamsSet, vertexData->getVertexDataSet(), vertexData->getInstanceDataSet(), set}); + uint32 offset = 0; + command->pushConstants(Gfx::SE_SHADER_STAGE_TASK_BIT_EXT | Gfx::SE_SHADER_STAGE_VERTEX_BIT, 0, sizeof(VertexData::DrawCallOffsets), + &offset); + if (graphics->supportMeshShading()) { + command->drawMesh(vertexData->getNumInstances(), 1, 1); + } else { + const auto& materials = vertexData->getMaterialData(); + for (const auto& materialData : materials) { + for (const auto& drawCall : materialData.instances) { + // material not used for any active meshes, skip + if (materialData.instances.size() == 0) + continue; + command->bindIndexBuffer(vertexData->getIndexBuffer()); + uint32 inst = drawCall.offsets.instanceOffset; + for (const auto& meshData : drawCall.instanceMeshData) { + // all meshlets of a mesh share the same indices offset + command->drawIndexed(meshData.numIndices, 1, meshData.firstIndex, + vertexData->getIndicesOffset(meshData.meshletOffset), inst++); } } } - commands.add(std::move(command)); } - - graphics->executeCommands(std::move(commands)); + commands.add(std::move(command)); } + + graphics->executeCommands(std::move(commands)); graphics->endRenderPass(); query->endQuery(); // Sync depth read/write with compute read diff --git a/src/Engine/Graphics/StaticMeshVertexData.h b/src/Engine/Graphics/StaticMeshVertexData.h index e617098..f43a69f 100644 --- a/src/Engine/Graphics/StaticMeshVertexData.h +++ b/src/Engine/Graphics/StaticMeshVertexData.h @@ -9,16 +9,6 @@ namespace Seele { class StaticMeshVertexData : public VertexData { public: - struct StaticMatInstance { - PMaterialInstance instance; - Array meshletIds; - Gfx::OShaderBuffer culledMeshletBuffer; - // Gfx::OShaderBuffer indirectDrawBuffer; - }; - struct StaticMatData { - PMaterial material; - Array staticInstance; - }; StaticMeshVertexData(); virtual ~StaticMeshVertexData(); static StaticMeshVertexData* getInstance(); @@ -37,12 +27,10 @@ class StaticMeshVertexData : public VertexData { virtual Gfx::PDescriptorSet getVertexDataSet() override; virtual std::string getTypeName() const override { return "StaticMeshVertexData"; } virtual Gfx::PShaderBuffer getPositionBuffer() const override { return positions; } - constexpr const Array& getStaticMeshes() const { return staticData; } private: virtual void resizeBuffers() override; virtual void updateBuffers() override; - Array staticData; Gfx::OShaderBuffer positions; Array positionData; diff --git a/src/Engine/Graphics/Vulkan/Buffer.cpp b/src/Engine/Graphics/Vulkan/Buffer.cpp index 9cabc14..b5db025 100644 --- a/src/Engine/Graphics/Vulkan/Buffer.cpp +++ b/src/Engine/Graphics/Vulkan/Buffer.cpp @@ -184,10 +184,14 @@ Buffer::~Buffer() { } void Buffer::updateContents(uint64 regionOffset, uint64 regionSize, void* buffer) { + if (buffers.size() == 0) + return; getAlloc()->updateContents(regionOffset, regionSize, buffer); } void Buffer::readContents(uint64 regionOffset, uint64 regionSize, void* buffer) { + if (buffers.size() == 0) + return; getAlloc()->readContents(regionOffset, regionSize, buffer); } @@ -218,29 +222,27 @@ void Buffer::rotateBuffer(uint64 size, bool preserveContents) { } void Buffer::createBuffer(uint64 size, uint32 destIndex) { - if (size > 0) { - uint32 family = graphics->getFamilyMapping().getQueueTypeFamilyIndex(initialOwner); - VkBufferCreateInfo info = { - .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .size = size, - .usage = usage, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 1, - .pQueueFamilyIndices = &family, - }; - VmaAllocationCreateInfo allocInfo = { - .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, - }; - buffers[destIndex] = new BufferAllocation(graphics, name, info, allocInfo, initialOwner); - if (createCleared) - { - PCommand command = graphics->getQueueCommands(initialOwner)->getCommands(); - vkCmdFillBuffer(command->getHandle(), buffers[destIndex]->buffer, 0, VK_WHOLE_SIZE, clearValue); - pipelineBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT); - } + uint32 family = graphics->getFamilyMapping().getQueueTypeFamilyIndex(initialOwner); + VkBufferCreateInfo info = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = size, + .usage = usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 1, + .pQueueFamilyIndices = &family, + }; + VmaAllocationCreateInfo allocInfo = { + .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, + }; + buffers[destIndex] = new BufferAllocation(graphics, name, info, allocInfo, initialOwner); + if (createCleared) + { + PCommand command = graphics->getQueueCommands(initialOwner)->getCommands(); + vkCmdFillBuffer(command->getHandle(), buffers[destIndex]->buffer, 0, VK_WHOLE_SIZE, clearValue); + pipelineBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT); } } @@ -283,10 +285,16 @@ void Buffer::copyBuffer(uint64 src, uint64 dst) { &dstBarrier, 0, nullptr); } -void Buffer::transferOwnership(Gfx::QueueType newOwner) { getAlloc()->transferOwnership(newOwner); } +void Buffer::transferOwnership(Gfx::QueueType newOwner) { + if (buffers.size() == 0) + return; + getAlloc()->transferOwnership(newOwner); +} void Buffer::pipelineBarrier(VkAccessFlags srcAccess, VkPipelineStageFlags srcStage, VkAccessFlags dstAccess, VkPipelineStageFlags dstStage) { + if (buffers.size() == 0) + return; getAlloc()->pipelineBarrier(srcAccess, srcStage, dstAccess, dstStage); }