diff --git a/SHADE_Engine/SHADE_Engine.vcxproj b/SHADE_Engine/SHADE_Engine.vcxproj index 37ddf8f5..2aa3b608 100644 --- a/SHADE_Engine/SHADE_Engine.vcxproj +++ b/SHADE_Engine/SHADE_Engine.vcxproj @@ -138,6 +138,7 @@ xcopy /s /r /y /q "$(SolutionDir)/Dependencies/dotnet/bin" "$(OutDir)" + @@ -167,13 +168,31 @@ xcopy /s /r /y /q "$(SolutionDir)/Dependencies/dotnet/bin" "$(OutDir)" + + + + + + + + + + + + + + + + + + @@ -250,6 +269,7 @@ xcopy /s /r /y /q "$(SolutionDir)/Dependencies/dotnet/bin" "$(OutDir)" + @@ -273,12 +293,26 @@ xcopy /s /r /y /q "$(SolutionDir)/Dependencies/dotnet/bin" "$(OutDir)" + + + + + + + + + + + + + + diff --git a/SHADE_Engine/SHADE_Engine.vcxproj.filters b/SHADE_Engine/SHADE_Engine.vcxproj.filters index f52aebb0..74be0df1 100644 --- a/SHADE_Engine/SHADE_Engine.vcxproj.filters +++ b/SHADE_Engine/SHADE_Engine.vcxproj.filters @@ -28,6 +28,9 @@ {078AA1A3-F318-2B6D-9C37-3F6888A53B13} + + {8C1A20B0-78BC-4A86-6177-5EDA4DB8D1D6} + {DBC7D3B0-C769-FE86-B024-12DB9C6585D7} @@ -70,15 +73,30 @@ {4B204703-3704-0859-A064-02AC8C67F2DA} + + {7A02D7B0-E60F-0597-6FF6-0082DB02D14D} + + + {85EFB65D-F107-9E87-BAB4-2D21268C3221} + {EBA1D3FF-D75C-C3AB-8014-3CF66CAE0D3C} + + {1F603FFC-8B22-7386-D4D2-011340D44B64} + {8CDBA7C9-F8E8-D5AF-81CF-D19AEDDBA166} + + {D04C14FB-3C5A-42E1-C540-3ECC314D0E98} + {2460C057-1070-6C28-7929-D14665585BC1} + + {7E76F08B-EA83-1E72-736A-1A5DDF76EA28} + {FBD334F8-67EA-328E-B061-BEAF1CB70316} @@ -121,6 +139,9 @@ {EAD6C33D-5697-3F74-1FD2-88F18B518450} + + {3AB383AD-2681-77B3-0F15-E8D9FB815318} + {F1B75745-5D6D-D03A-E661-CA115216C73E} @@ -207,6 +228,9 @@ ECS_Base\UnitTesting + + Editor + Engine @@ -294,18 +318,69 @@ Graphics\Instance + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\GlobalData + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + Graphics\MiddleEnd\Interface + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + Graphics\MiddleEnd\Interface + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Meshes + + + Graphics\MiddleEnd\Meshes + Graphics\MiddleEnd\PerFrame Graphics\MiddleEnd\PerFrame + + Graphics\MiddleEnd\Pipeline + Graphics\MiddleEnd\Shaders @@ -315,6 +390,9 @@ Graphics\MiddleEnd\Shaders + + Graphics\MiddleEnd\Textures + Graphics\Pipeline @@ -421,7 +499,13 @@ Math - Math + Math\Transform + + + Math\Transform + + + Math\Transform Math\Vector @@ -498,8 +582,6 @@ Tools - - @@ -529,6 +611,9 @@ ECS_Base\UnitTesting + + Editor + Engine @@ -598,24 +683,66 @@ Graphics\Instance + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\Batching + + + Graphics\MiddleEnd\GlobalData + + + Graphics\MiddleEnd\Interface + Graphics\MiddleEnd\Interface + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + Graphics\MiddleEnd\Interface + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Interface + + + Graphics\MiddleEnd\Meshes + Graphics\MiddleEnd\PerFrame Graphics\MiddleEnd\PerFrame + + Graphics\MiddleEnd\Pipeline + Graphics\MiddleEnd\Shaders Graphics\MiddleEnd\Shaders + + Graphics\MiddleEnd\Textures + Graphics\Pipeline @@ -698,7 +825,13 @@ Math - Math + Math\Transform + + + Math\Transform + + + Math\Transform Math\Vector @@ -740,7 +873,5 @@ Tools - - \ No newline at end of file diff --git a/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.cpp b/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.cpp index 6898b7e0..cddb6cdb 100644 --- a/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.cpp +++ b/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.cpp @@ -8,6 +8,7 @@ #include "Graphics/Framebuffer/SHVkFramebuffer.h" #include "Graphics/Pipeline/SHVkPipeline.h" #include "Graphics/Buffers/SHVkBuffer.h" +#include "Graphics/Images/SHVkImage.h" #include "Graphics/Descriptors/SHVkDescriptorSetGroup.h" @@ -421,9 +422,42 @@ namespace SHADE vkCommandBuffer.drawIndexed(indexCount, 1, firstIndex, vertexOffset, 0); } + + /***************************************************************************/ + /*! + \brief + Issues a multi indirect draw call. - void SHVkCommandBuffer::PipelineBarrier ( + \param indirectDrawData + SHVkBuffer containing the data for the multi indirect draw call. + \param drawCount + Number of multi indirect draw sub-calls stored in indirectDrawData. + + */ + /***************************************************************************/ + + void SHVkCommandBuffer::DrawMultiIndirect(Handle indirectDrawData, uint32_t drawCount) + { + if (cmdBufferState != SH_CMD_BUFFER_STATE::RECORDING) + { + SHLOG_ERROR("Command buffer must have started recording before a pipeline can be bound."); + return; + } + + vkCommandBuffer.drawIndexedIndirect(indirectDrawData->GetVkBuffer(), 0, drawCount, sizeof(vk::DrawIndexedIndirectCommand)); + } + + void SHVkCommandBuffer::CopyBufferToImage(const vk::Buffer& src, const vk::Image& dst, const std::vector& copyInfo) + { + vkCommandBuffer.copyBufferToImage + ( + src, dst, vk::ImageLayout::eTransferDstOptimal, + static_cast(copyInfo.size()), copyInfo.data() + ); + } + + void SHVkCommandBuffer::PipelineBarrier( vk::PipelineStageFlags srcStage, vk::PipelineStageFlags dstStage, vk::DependencyFlags deps, @@ -457,33 +491,6 @@ namespace SHADE // //vkCommandBuffer.pipelineBarrier() //} - /***************************************************************************/ - /*! - - \brief - Issues a multi indirect draw call. - - \param indirectDrawData - SHVkBuffer containing the data for the multi indirect draw call. - \param drawCount - Number of multi indirect draw sub-calls stored in indirectDrawData. - - */ - /***************************************************************************/ - - void SHVkCommandBuffer::DrawMultiIndirect(Handle indirectDrawData, uint32_t drawCount) - { - if (cmdBufferState != SH_CMD_BUFFER_STATE::RECORDING) - { - SHLOG_ERROR("Command buffer must have started recording before a pipeline can be bound."); - return; - } - - vkCommandBuffer.drawIndexedIndirect(indirectDrawData->GetVkBuffer(), 0, drawCount, sizeof(vk::DrawIndexedIndirectCommand)); - } - - - /***************************************************************************/ /*! diff --git a/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.h b/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.h index d945fce8..f780638a 100644 --- a/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.h +++ b/SHADE_Engine/src/Graphics/Commands/SHVkCommandBuffer.h @@ -15,6 +15,7 @@ namespace SHADE class SHVkFramebuffer; class SHVkPipeline; class SHVkBuffer; + class SHVkImage; class SHVkDescriptorSetGroup; enum class SH_CMD_BUFFER_TYPE @@ -116,6 +117,10 @@ namespace SHADE // Draw Commands void DrawArrays (uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) const noexcept; void DrawIndexed (uint32_t indexCount, uint32_t firstIndex, uint32_t vertexOffset) const noexcept; + void DrawMultiIndirect (Handle indirectDrawData, uint32_t drawCount); + + // Buffer Copy + void CopyBufferToImage (const vk::Buffer& src, const vk::Image& dst, const std::vector& copyInfo); // memory barriers void PipelineBarrier ( @@ -129,7 +134,6 @@ namespace SHADE bool IsReadyToSubmit (void) const noexcept; void HandlePostSubmit (void) noexcept; - void DrawMultiIndirect (Handle indirectDrawData, uint32_t drawCount); // Push Constant variable setting template diff --git a/SHADE_Engine/src/Graphics/Images/SHVkImage.cpp b/SHADE_Engine/src/Graphics/Images/SHVkImage.cpp index 6ec1c9f2..b8bf273e 100644 --- a/SHADE_Engine/src/Graphics/Images/SHVkImage.cpp +++ b/SHADE_Engine/src/Graphics/Images/SHVkImage.cpp @@ -234,7 +234,7 @@ namespace SHADE return SHVkInstance::GetResourceManager().Create(inLogicalDeviceHdl, parent, createParams); } - void SHVkImage::TransferToDeviceResource(void) noexcept + void SHVkImage::TransferToDeviceResource(Handle cmdBufferHdl) noexcept { // prepare copy regions std::vector copyRegions{mipOffsets.size()}; @@ -252,7 +252,7 @@ namespace SHADE copyRegions[i].imageExtent = vk::Extent3D{ width >> i, height >> i, 1 }; } - //PrepareImageTransition(); + cmdBufferHdl->CopyBufferToImage(stagingBuffer, vkImage, copyRegions); } /***************************************************************************/ @@ -274,7 +274,7 @@ namespace SHADE */ /***************************************************************************/ - void SHVkImage::PrepareImageTransition(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageMemoryBarrier& barrier) noexcept + void SHVkImage::PrepareImageTransitionInfo(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageMemoryBarrier& barrier) noexcept { barrier.oldLayout = oldLayout; barrier.newLayout = newLayout; @@ -286,33 +286,6 @@ namespace SHADE barrier.subresourceRange.levelCount = mipLevelCount; barrier.subresourceRange.baseArrayLayer = 0; barrier.subresourceRange.layerCount = layerCount; - - vk::PipelineStageFlagBits srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - vk::PipelineStageFlagBits dstStage = vk::PipelineStageFlagBits::eTopOfPipe; - - if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) - { - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eTransfer; - - barrier.srcAccessMask = vk::AccessFlagBits::eNone; - barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - } - else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) - { - srcStage = vk::PipelineStageFlagBits::eTransfer; - - // TODO, what if we want to access in compute shader - dstStage = vk::PipelineStageFlagBits::eFragmentShader; - - barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - } - else - { - SHLOG_ERROR("Image layouts are invalid. "); - } - } void SHVkImage::LinkWithExteriorImage(vk::Image inVkImage, vk::ImageType type, uint32_t inWidth, uint32_t inHeight, uint32_t inDepth, uint32_t layers, uint8_t levels, vk::Format format, vk::ImageUsageFlags flags) noexcept diff --git a/SHADE_Engine/src/Graphics/Images/SHVkImage.h b/SHADE_Engine/src/Graphics/Images/SHVkImage.h index eec8dc7e..91d4f2d2 100644 --- a/SHADE_Engine/src/Graphics/Images/SHVkImage.h +++ b/SHADE_Engine/src/Graphics/Images/SHVkImage.h @@ -135,8 +135,8 @@ namespace SHADE /* PUBLIC MEMBER FUNCTIONS */ /*-----------------------------------------------------------------------*/ Handle CreateImageView (Handle const& inLogicalDeviceHdl, Handle const& parent, SHImageViewDetails const& createParams) const noexcept; - void TransferToDeviceResource (void) noexcept; - void PrepareImageTransition (vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageMemoryBarrier& barrier) noexcept; + void TransferToDeviceResource (Handle cmdBufferHdl) noexcept; + void PrepareImageTransitionInfo (vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageMemoryBarrier& barrier) noexcept; /*-----------------------------------------------------------------------*/ /* GETTERS AND SETTERS */ diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.cpp index 97b72977..3be4dd11 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.cpp @@ -13,29 +13,130 @@ of DigiPen Institute of Technology is prohibited. #include "SHpch.h" #include "SHTextureLibrary.h" +#include "Graphics/SHVulkanIncludes.h" + #include "Graphics/Devices/SHVkLogicalDevice.h" #include "Graphics/Buffers/SHVkBuffer.h" #include "Graphics/Commands/SHVkCommandBuffer.h" #include "Graphics/SHVkUtil.h" +#include "Tools/SHLogger.h" namespace SHADE { - /*-----------------------------------------------------------------------------*/ - /* Usage Functions */ - /*-----------------------------------------------------------------------------*/ - Handle SHTextureLibrary::Add(uint32_t pixelCount, const SHTexture::PixelChannel* const pixelData) + /*---------------------------------------------------------------------------------*/ + /* Usage Functions */ + /*---------------------------------------------------------------------------------*/ + Handle SHTextureLibrary::Add(uint32_t pixelCount, const SHTexture::PixelChannel* const pixelData, SHTexture::TextureFormat format, int mipLevels) { - return {}; + isDirty = true; + + auto handle = resourceManager.Create(); + addJobs.emplace_back(AddJob { pixelCount, pixelData, format, mipLevels }); + return handle; } - void SHTextureLibrary::Remove(Handle mesh) + void SHTextureLibrary::Remove(Handle texture) { + if (!texture) + throw std::invalid_argument("Attempted to remove a Texture that did not belong to the Texture Library!"); + removeJobs.emplace_back(texture); + isDirty = true; } - void SHTextureLibrary::BuildBuffers(Handle device, Handle cmdBuffer) + void SHTextureLibrary::BuildImages(Handle device, Handle cmdBuffer, Handle graphicsQueue, Handle descPool, Handle descLayout) { + /* Remove Textures */ + std::vector pipelineBarriers(addJobs.size()); + /* Add Textures */ + // Transition + for (int i = 0; auto& job : addJobs) + { + job.Image = resourceManager.Create(); + job.Image->PrepareImageTransitionInfo(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, pipelineBarriers[i]); + ++i; + } + vk::PipelineStageFlagBits srcStage = vk::PipelineStageFlagBits::eTopOfPipe; + vk::PipelineStageFlagBits dstStage = vk::PipelineStageFlagBits::eTopOfPipe; + preparePipelineBarriers(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, srcStage, dstStage, pipelineBarriers); + cmdBuffer->PipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, pipelineBarriers); + + // Copy + for (auto& job : addJobs) + { + job.Image->TransferToDeviceResource(cmdBuffer); + } + + // Transition + for (int i = 0; auto & job : addJobs) + { + // Transition + job.Image->PrepareImageTransitionInfo(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, pipelineBarriers[i]); + } + preparePipelineBarriers(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, srcStage, dstStage, pipelineBarriers); + cmdBuffer->PipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, pipelineBarriers); + + // Execute Commands + graphicsQueue->SubmitCommandBuffer({ cmdBuffer }); + device->WaitIdle(); + + // Create Image View + for (auto& job : addJobs) + { + const SHImageViewDetails DETAILS + { + .viewType = vk::ImageViewType::e2D, + .format = job.TextureFormat, + .imageAspectFlags = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .mipLevelCount = job.MipLevels, + .baseArrayLayer = 0, + .layerCount = 0 + }; + job.Handle->ImageView = job.Image->CreateImageView(device, job.Image, DETAILS); + } + + // Build Descriptor + Handle descSetGroup = descPool->Allocate({ descLayout }, { 1 }).front(); + + + isDirty = false; + } + + /*---------------------------------------------------------------------------------*/ + /* Usage Functions */ + /*---------------------------------------------------------------------------------*/ + void SHTextureLibrary::preparePipelineBarriers(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::PipelineStageFlagBits& srcStage, vk::PipelineStageFlagBits& dstStage, std::vector& barriers) + { + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) + { + srcStage = vk::PipelineStageFlagBits::eTopOfPipe; + dstStage = vk::PipelineStageFlagBits::eTransfer; + + for (auto& barrier : barriers) + { + barrier.srcAccessMask = vk::AccessFlagBits::eNone; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + } + } + else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) + { + srcStage = vk::PipelineStageFlagBits::eTransfer; + + // TODO, what if we want to access in compute shader + dstStage = vk::PipelineStageFlagBits::eFragmentShader; + + for (auto& barrier : barriers) + { + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + } + } + else + { + SHLOG_ERROR("Image layouts are invalid. "); + } } } diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.h b/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.h index a0167cba..d30b02ed 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.h +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Textures/SHTextureLibrary.h @@ -37,11 +37,15 @@ namespace SHADE /*-----------------------------------------------------------------------------*/ /* Type Definitions */ /*-----------------------------------------------------------------------------*/ - using PixelChannel = uint8_t; + using PixelChannel = void; + using TextureFormat = vk::Format; // TODO: Change + using Index = uint32_t; /*-----------------------------------------------------------------------------*/ /* Data Members */ /*-----------------------------------------------------------------------------*/ + Handle ImageView; + Index TextureArrayIndex = std::numeric_limits::max(); }; /***********************************************************************************/ /*! @@ -61,7 +65,7 @@ namespace SHADE \brief Adds a texture to the Texture Library. But this does not mean that the - textures have been added yet. A call to "BuildBuffers()" is required to + textures have been added yet. A call to "BuildImages()" is required to transfer all textures into the GPU. \param pixelCount @@ -69,20 +73,22 @@ namespace SHADE \param positions Pointer to the first in a contiguous array of SHMathVec3s that define vertex positions. + \param format + Format of the texture loaded in. \return Handle to the created Texture. This is not valid to be used until a call to - BuildBuffers(). + BuildImages(). */ /*******************************************************************************/ - Handle Add(uint32_t pixelCount, const SHTexture::PixelChannel* const pixelData); + Handle Add(uint32_t pixelCount, const SHTexture::PixelChannel* const pixelData, SHTexture::TextureFormat format, int mipLevels); /*******************************************************************************/ /*! \brief Removes a mesh from the Texture Library. But this does not mean that the - textures have been removed yet. A call to "BuildBuffers()" is required to + textures have been removed yet. A call to "BuildImages()" is required to finalise all changes. \param mesh @@ -106,7 +112,7 @@ namespace SHADE queue. */ /***************************************************************************/ - void BuildBuffers(Handle device, Handle cmdBuffer); + void BuildImages(Handle device, Handle cmdBuffer, Handle graphicsQueue); /*-----------------------------------------------------------------------------*/ /* Getter Functions */ @@ -119,10 +125,14 @@ namespace SHADE /*-----------------------------------------------------------------------------*/ struct AddJob { - uint32_t PixelCount = 0; - const SHTexture::PixelChannel* PixelData = nullptr; - Handle Handle; + uint32_t PixelCount = 0; + const SHTexture::PixelChannel* PixelData = nullptr; + SHTexture::TextureFormat TextureFormat = {}; + int MipLevels = 0; + Handle Image; + Handle Handle; }; + /*-----------------------------------------------------------------------------*/ /* Data Members */ /*-----------------------------------------------------------------------------*/ @@ -130,7 +140,7 @@ namespace SHADE std::vector addJobs; std::vector> removeJobs; // Tracking - ResourceLibrary textures{}; + ResourceManager resourceManager; std::vector> texOrder; // CPU Storage std::vector texStorage; @@ -138,5 +148,10 @@ namespace SHADE Handle texStorageBuffer{}; // Flags bool isDirty = true; + + /*-----------------------------------------------------------------------------*/ + /* Helper Functions */ + /*-----------------------------------------------------------------------------*/ + void preparePipelineBarriers(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::PipelineStageFlagBits& srcStage, vk::PipelineStageFlagBits& dstStage, std::vector& barriers); }; } diff --git a/SHADE_Engine/src/Math/SHMath.h b/SHADE_Engine/src/Math/SHMath.h index 55bf73a9..5fcea9fc 100644 --- a/SHADE_Engine/src/Math/SHMath.h +++ b/SHADE_Engine/src/Math/SHMath.h @@ -9,4 +9,4 @@ #include "SHQuaternion.h" #include "SHMatrix.h" -#include "SHTransform.h" \ No newline at end of file +#include "Transform/SHTransform.h" \ No newline at end of file