Commits (27)
......@@ -51,7 +51,7 @@ artifacts/
*.ilk
*.meta
*.obj
!hw3d/Models/*.obj
!hw3d/Models/**/*.obj
!hw3d/assimp/bin/*
*.pch
*.pdb
......
......@@ -5,6 +5,7 @@
#include "Surface.h"
#include "GDIPlusManager.h"
#include "imgui/imgui.h"
#include "VertexBuffer.h"
namespace dx = DirectX;
......@@ -26,6 +27,7 @@ void App::DoFrame()
light.Bind( wnd.Gfx(),cam.GetMatrix() );
nano.Draw( wnd.Gfx() );
nano2.Draw( wnd.Gfx() );
light.Draw( wnd.Gfx() );
while( const auto e = wnd.kbd.ReadKey() )
......@@ -95,7 +97,8 @@ void App::DoFrame()
cam.SpawnControlWindow();
light.SpawnControlWindow();
ShowImguiDemoWindow();
nano.ShowWindow();
nano.ShowWindow( "Model 1" );
nano2.ShowWindow( "Model 2" );
// present
wnd.Gfx().EndFrame();
......
......@@ -25,5 +25,6 @@ private:
float speed_factor = 1.0f;
Camera cam;
PointLight light;
Model nano{ wnd.Gfx(),"Models\\nano.gltf" };
Model nano{ wnd.Gfx(),"Models\\nano_textured\\nanosuit.obj" };
Model nano2{ wnd.Gfx(),"Models\\nano_textured\\nanosuit.obj" };
};
\ No newline at end of file
......@@ -8,6 +8,11 @@ namespace Bind
{
public:
virtual void Bind( Graphics& gfx ) noexcept = 0;
virtual std::string GetUID() const noexcept
{
assert( false );
return "";
}
virtual ~Bindable() = default;
protected:
static ID3D11DeviceContext* GetContext( Graphics& gfx ) noexcept;
......
#pragma once
#include "Bindable.h"
#include "BindableCodex.h"
#include <type_traits>
#include <memory>
#include <unordered_map>
namespace Bind
{
class Codex
{
public:
template<class T,typename...Params>
static std::shared_ptr<T> Resolve( Graphics& gfx,Params&&...p ) noxnd
{
static_assert( std::is_base_of<Bindable,T>::value,"Can only resolve classes derived from Bindable" );
return Get().Resolve_<T>( gfx,std::forward<Params>( p )... );
}
private:
template<class T,typename...Params>
std::shared_ptr<T> Resolve_( Graphics& gfx,Params&&...p ) noxnd
{
const auto key = T::GenerateUID( std::forward<Params>( p )... );
const auto i = binds.find( key );
if( i == binds.end() )
{
auto bind = std::make_shared<T>( gfx,std::forward<Params>( p )... );
binds[key] = bind;
return bind;
}
else
{
return std::static_pointer_cast<T>( i->second );
}
}
static Codex& Get()
{
static Codex codex;
return codex;
}
private:
std::unordered_map<std::string,std::shared_ptr<Bindable>> binds;
};
}
\ No newline at end of file
......@@ -8,3 +8,5 @@
#include "TransformCbuf.h"
#include "VertexBuffer.h"
#include "VertexShader.h"
#include "Texture.h"
#include "Sampler.h"
\ No newline at end of file
#pragma once
#include "Bindable.h"
#include "GraphicsThrowMacros.h"
#include "BindableCodex.h"
namespace Bind
{
......@@ -71,6 +72,27 @@ namespace Bind
{
GetContext( gfx )->VSSetConstantBuffers( slot,1u,pConstantBuffer.GetAddressOf() );
}
static std::shared_ptr<VertexConstantBuffer> Resolve( Graphics& gfx,const C& consts,UINT slot = 0 )
{
return Codex::Resolve<VertexConstantBuffer>( gfx,consts,slot );
}
static std::shared_ptr<VertexConstantBuffer> Resolve( Graphics& gfx,UINT slot = 0 )
{
return Codex::Resolve<VertexConstantBuffer>( gfx,slot );
}
static std::string GenerateUID( const C&,UINT slot )
{
return GenerateUID( slot );
}
static std::string GenerateUID( UINT slot = 0 )
{
using namespace std::string_literals;
return typeid(VertexConstantBuffer).name() + "#"s + std::to_string( slot );
}
std::string GetUID() const noexcept override
{
return GenerateUID( slot );
}
};
template<typename C>
......@@ -85,5 +107,26 @@ namespace Bind
{
GetContext( gfx )->PSSetConstantBuffers( slot,1u,pConstantBuffer.GetAddressOf() );
}
static std::shared_ptr<PixelConstantBuffer> Resolve( Graphics& gfx,const C& consts,UINT slot = 0 )
{
return Codex::Resolve<PixelConstantBuffer>( gfx,consts,slot );
}
static std::shared_ptr<PixelConstantBuffer> Resolve( Graphics& gfx,UINT slot = 0 )
{
return Codex::Resolve<PixelConstantBuffer>( gfx,slot );
}
static std::string GenerateUID( const C&,UINT slot )
{
return GenerateUID( slot );
}
static std::string GenerateUID( UINT slot = 0 )
{
using namespace std::string_literals;
return typeid(PixelConstantBuffer).name() + "#"s + std::to_string( slot );
}
std::string GetUID() const noexcept override
{
return GenerateUID( slot );
}
};
}
\ No newline at end of file
......@@ -11,22 +11,16 @@ void Drawable::Draw( Graphics& gfx ) const noxnd
{
b->Bind( gfx );
}
for( auto& b : GetStaticBinds() )
{
b->Bind( gfx );
}
gfx.DrawIndexed( pIndexBuffer->GetCount() );
}
void Drawable::AddBind( std::unique_ptr<Bindable> bind ) noxnd
void Drawable::AddBind( std::shared_ptr<Bindable> bind ) noxnd
{
assert( "*Must* use AddIndexBuffer to bind index buffer" && typeid(*bind) != typeid(IndexBuffer) );
// special case for index buffer
if( typeid(*bind) == typeid(IndexBuffer) )
{
assert( "Binding multiple index buffers not allowed" && pIndexBuffer == nullptr );
pIndexBuffer = &static_cast<IndexBuffer&>(*bind);
}
binds.push_back( std::move( bind ) );
}
\ No newline at end of file
void Drawable::AddIndexBuffer( std::unique_ptr<IndexBuffer> ibuf ) noxnd
{
assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
pIndexBuffer = ibuf.get();
binds.push_back( std::move( ibuf ) );
}
\ No newline at end of file
......@@ -2,6 +2,7 @@
#include "Graphics.h"
#include <DirectXMath.h>
#include "ConditionalNoexcept.h"
#include <memory>
namespace Bind
{
......@@ -11,15 +12,11 @@ namespace Bind
class Drawable
{
template<class T>
friend class DrawableBase;
public:
Drawable() = default;
Drawable( const Drawable& ) = delete;
virtual DirectX::XMMATRIX GetTransformXM() const noexcept = 0;
void Draw( Graphics& gfx ) const noxnd;
virtual void Update( float dt ) noexcept
{}
virtual ~Drawable() = default;
protected:
template<class T>
......@@ -34,11 +31,8 @@ protected:
}
return nullptr;
}
void AddBind( std::unique_ptr<Bind::Bindable> bind ) noxnd;
void AddIndexBuffer( std::unique_ptr<Bind::IndexBuffer> ibuf ) noxnd;
private:
virtual const std::vector<std::unique_ptr<Bind::Bindable>>& GetStaticBinds() const noexcept = 0;
void AddBind( std::shared_ptr<Bind::Bindable> bind ) noxnd;
private:
const Bind::IndexBuffer* pIndexBuffer = nullptr;
std::vector<std::unique_ptr<Bind::Bindable>> binds;
std::vector<std::shared_ptr<Bind::Bindable>> binds;
};
\ No newline at end of file
#pragma once
#include "Drawable.h"
#include "IndexBuffer.h"
#include "ConditionalNoexcept.h"
template<class T>
class DrawableBase : public Drawable
{
protected:
static bool IsStaticInitialized() noexcept
{
return !staticBinds.empty();
}
static void AddStaticBind( std::unique_ptr<Bind::Bindable> bind ) noxnd
{
assert( "*Must* use AddStaticIndexBuffer to bind index buffer" && typeid(*bind) != typeid(Bind::IndexBuffer) );
staticBinds.push_back( std::move( bind ) );
}
void AddStaticIndexBuffer( std::unique_ptr<Bind::IndexBuffer> ibuf ) noxnd
{
assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
pIndexBuffer = ibuf.get();
staticBinds.push_back( std::move( ibuf ) );
}
void SetIndexFromStatic() noxnd
{
assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
for( const auto& b : staticBinds )
{
if( const auto p = dynamic_cast<Bind::IndexBuffer*>(b.get()) )
{
pIndexBuffer = p;
return;
}
}
assert( "Failed to find index buffer in static binds" && pIndexBuffer != nullptr );
}
private:
const std::vector<std::unique_ptr<Bind::Bindable>>& GetStaticBinds() const noexcept override
{
return staticBinds;
}
private:
static std::vector<std::unique_ptr<Bind::Bindable>> staticBinds;
};
template<class T>
std::vector<std::unique_ptr<Bind::Bindable>> DrawableBase<T>::staticBinds;
\ No newline at end of file
#include "IndexBuffer.h"
#include "GraphicsThrowMacros.h"
#include "BindableCodex.h"
namespace Bind
{
IndexBuffer::IndexBuffer( Graphics& gfx,const std::vector<unsigned short>& indices )
:
IndexBuffer( gfx,"?",indices )
{}
IndexBuffer::IndexBuffer( Graphics& gfx,std::string tag,const std::vector<unsigned short>& indices )
:
tag( tag ),
count( (UINT)indices.size() )
{
INFOMAN( gfx );
......@@ -30,4 +36,19 @@ namespace Bind
{
return count;
}
std::shared_ptr<IndexBuffer> IndexBuffer::Resolve( Graphics& gfx,const std::string& tag,
const std::vector<unsigned short>& indices )
{
assert( tag != "?" );
return Codex::Resolve<IndexBuffer>( gfx,tag,indices );
}
std::string IndexBuffer::GenerateUID_( const std::string& tag )
{
using namespace std::string_literals;
return typeid(IndexBuffer).name() + "#"s + tag;
}
std::string IndexBuffer::GetUID() const noexcept
{
return GenerateUID_( tag );
}
}
......@@ -7,9 +7,21 @@ namespace Bind
{
public:
IndexBuffer( Graphics& gfx,const std::vector<unsigned short>& indices );
IndexBuffer( Graphics& gfx,std::string tag,const std::vector<unsigned short>& indices );
void Bind( Graphics& gfx ) noexcept override;
UINT GetCount() const noexcept;
static std::shared_ptr<IndexBuffer> Resolve( Graphics& gfx,const std::string& tag,
const std::vector<unsigned short>& indices );
template<typename...Ignore>
static std::string GenerateUID( const std::string& tag,Ignore&&...ignore )
{
return GenerateUID_( tag );
}
std::string GetUID() const noexcept override;
private:
static std::string GenerateUID_( const std::string& tag );
protected:
std::string tag;
UINT count;
Microsoft::WRL::ComPtr<ID3D11Buffer> pIndexBuffer;
};
......
#pragma once
#include "Vertex.h"
#include <vector>
#include <DirectXMath.h>
template<class T>
class IndexedTriangleList
{
public:
IndexedTriangleList() = default;
IndexedTriangleList( std::vector<T> verts_in,std::vector<unsigned short> indices_in )
IndexedTriangleList( Dvtx::VertexBuffer verts_in,std::vector<unsigned short> indices_in )
:
vertices( std::move( verts_in ) ),
indices( std::move( indices_in ) )
{
assert( vertices.size() > 2 );
assert( vertices.Size() > 2 );
assert( indices.size() % 3 == 0 );
}
void Transform( DirectX::FXMMATRIX matrix )
{
for( auto& v : vertices )
using Elements = Dvtx::VertexLayout::ElementType;
for( int i = 0; i < vertices.Size(); i++ )
{
const DirectX::XMVECTOR pos = DirectX::XMLoadFloat3( &v.pos );
auto& pos = vertices[i].Attr<Elements::Position3D>();
DirectX::XMStoreFloat3(
&v.pos,
DirectX::XMVector3Transform( pos,matrix )
&pos,
DirectX::XMVector3Transform( DirectX::XMLoadFloat3( &pos ),matrix )
);
}
}
// asserts face-independent vertices w/ normals cleared to zero
void SetNormalsIndependentFlat() noxnd
{
using namespace DirectX;
assert( indices.size() % 3 == 0 && indices.size() > 0 );
for( size_t i = 0; i < indices.size(); i += 3 )
{
auto& v0 = vertices[indices[i]];
auto& v1 = vertices[indices[i + 1]];
auto& v2 = vertices[indices[i + 2]];
const auto p0 = XMLoadFloat3( &v0.pos );
const auto p1 = XMLoadFloat3( &v1.pos );
const auto p2 = XMLoadFloat3( &v2.pos );
const auto n = XMVector3Normalize( XMVector3Cross( (p1 - p0),(p2 - p0) ) );
//// asserts face-independent vertices w/ normals cleared to zero
//void SetNormalsIndependentFlat() noxnd
//{
// using namespace DirectX;
// for( size_t i = 0; i < indices.size(); i += 3 )
// {
// auto& v0 = vertices[indices[i]];
// auto& v1 = vertices[indices[i + 1]];
// auto& v2 = vertices[indices[i + 2]];
// const auto p0 = XMLoadFloat3( &v0.pos );
// const auto p1 = XMLoadFloat3( &v1.pos );
// const auto p2 = XMLoadFloat3( &v2.pos );
XMStoreFloat3( &v0.n,n );
XMStoreFloat3( &v1.n,n );
XMStoreFloat3( &v2.n,n );
}
}
// const auto n = XMVector3Normalize( XMVector3Cross( (p1 - p0),(p2 - p0) ) );
//
// XMStoreFloat3( &v0.n,n );
// XMStoreFloat3( &v1.n,n );
// XMStoreFloat3( &v2.n,n );
// }
//}
public:
std::vector<T> vertices;
Dvtx::VertexBuffer vertices;
std::vector<unsigned short> indices;
};
\ No newline at end of file
#include "InputLayout.h"
#include "GraphicsThrowMacros.h"
#include "BindableCodex.h"
#include "Vertex.h"
namespace Bind
{
InputLayout::InputLayout( Graphics& gfx,
const std::vector<D3D11_INPUT_ELEMENT_DESC>& layout,
Dvtx::VertexLayout layout_in,
ID3DBlob* pVertexShaderBytecode )
:
layout( std::move( layout_in ) )
{
INFOMAN( gfx );
const auto d3dLayout = layout.GetD3DLayout();
GFX_THROW_INFO( GetDevice( gfx )->CreateInputLayout(
layout.data(),(UINT)layout.size(),
d3dLayout.data(),(UINT)d3dLayout.size(),
pVertexShaderBytecode->GetBufferPointer(),
pVertexShaderBytecode->GetBufferSize(),
&pInputLayout
......@@ -21,4 +27,18 @@ namespace Bind
{
GetContext( gfx )->IASetInputLayout( pInputLayout.Get() );
}
std::shared_ptr<InputLayout> InputLayout::Resolve( Graphics& gfx,
const Dvtx::VertexLayout& layout,ID3DBlob* pVertexShaderBytecode )
{
return Codex::Resolve<InputLayout>( gfx,layout,pVertexShaderBytecode );
}
std::string InputLayout::GenerateUID( const Dvtx::VertexLayout& layout,ID3DBlob* pVertexShaderBytecode )
{
using namespace std::string_literals;
return typeid(InputLayout).name() + "#"s + layout.GetCode();
}
std::string InputLayout::GetUID() const noexcept
{
return GenerateUID( layout );
}
}
#pragma once
#include "Bindable.h"
#include "Vertex.h"
namespace Bind
{
......@@ -7,10 +8,15 @@ namespace Bind
{
public:
InputLayout( Graphics& gfx,
const std::vector<D3D11_INPUT_ELEMENT_DESC>& layout,
Dvtx::VertexLayout layout,
ID3DBlob* pVertexShaderBytecode );
void Bind( Graphics& gfx ) noexcept override;
static std::shared_ptr<InputLayout> Resolve( Graphics& gfx,
const Dvtx::VertexLayout& layout,ID3DBlob* pVertexShaderBytecode );
static std::string GenerateUID( const Dvtx::VertexLayout& layout,ID3DBlob* pVertexShaderBytecode = nullptr );
std::string GetUID() const noexcept override;
protected:
Dvtx::VertexLayout layout;
Microsoft::WRL::ComPtr<ID3D11InputLayout> pInputLayout;
};
}
\ No newline at end of file
#include "Mesh.h"
#include "imgui/imgui.h"
#include "Surface.h"
#include <unordered_map>
#include <sstream>
......@@ -31,27 +32,16 @@ const std::string& ModelException::GetNote() const noexcept
}
// Mesh
Mesh::Mesh( Graphics& gfx,std::vector<std::unique_ptr<Bind::Bindable>> bindPtrs )
Mesh::Mesh( Graphics& gfx,std::vector<std::shared_ptr<Bind::Bindable>> bindPtrs )
{
if( !IsStaticInitialized() )
{
AddStaticBind( std::make_unique<Bind::Topology>( gfx,D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST ) );
}
AddBind( Bind::Topology::Resolve( gfx,D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST ) );
for( auto& pb : bindPtrs )
{
if( auto pi = dynamic_cast<Bind::IndexBuffer*>(pb.get()) )
{
AddIndexBuffer( std::unique_ptr<Bind::IndexBuffer>{ pi } );
pb.release();
}
else
{
AddBind( std::move( pb ) );
}
}
AddBind( std::make_unique<Bind::TransformCbuf>( gfx,*this ) );
AddBind( std::make_shared<Bind::TransformCbuf>( gfx,*this ) );
}
void Mesh::Draw( Graphics& gfx,DirectX::FXMMATRIX accumulatedTransform ) const noxnd
{
......@@ -212,7 +202,7 @@ Model::Model( Graphics& gfx,const std::string fileName )
for( size_t i = 0; i < pScene->mNumMeshes; i++ )
{
meshPtrs.push_back( ParseMesh( gfx,*pScene->mMeshes[i] ) );
meshPtrs.push_back( ParseMesh( gfx,*pScene->mMeshes[i],pScene->mMaterials ) );
}
int nextId = 0;
......@@ -236,21 +226,24 @@ void Model::ShowWindow( const char* windowName ) noexcept
Model::~Model() noexcept
{}
std::unique_ptr<Mesh> Model::ParseMesh( Graphics& gfx,const aiMesh& mesh )
std::unique_ptr<Mesh> Model::ParseMesh( Graphics& gfx,const aiMesh& mesh,const aiMaterial* const* pMaterials )
{
using Dvtx::VertexLayout;
using namespace Bind;
Dvtx::VertexBuffer vbuf( std::move(
VertexLayout{}
.Append( VertexLayout::Position3D )
.Append( VertexLayout::Normal )
.Append( VertexLayout::Texture2D )
) );
for( unsigned int i = 0; i < mesh.mNumVertices; i++ )
{
vbuf.EmplaceBack(
*reinterpret_cast<dx::XMFLOAT3*>(&mesh.mVertices[i]),
*reinterpret_cast<dx::XMFLOAT3*>(&mesh.mNormals[i])
*reinterpret_cast<dx::XMFLOAT3*>(&mesh.mNormals[i]),
*reinterpret_cast<dx::XMFLOAT2*>(&mesh.mTextureCoords[0][i])
);
}
......@@ -265,28 +258,66 @@ std::unique_ptr<Mesh> Model::ParseMesh( Graphics& gfx,const aiMesh& mesh )
indices.push_back( face.mIndices[2] );
}
std::vector<std::unique_ptr<Bind::Bindable>> bindablePtrs;
std::vector<std::shared_ptr<Bindable>> bindablePtrs;
using namespace std::string_literals;
const auto base = "Models\\nano_textured\\"s;
bool hasSpecularMap = false;
float shininess = 35.0f;
if( mesh.mMaterialIndex >= 0 )
{
auto& material = *pMaterials[mesh.mMaterialIndex];
aiString texFileName;
material.GetTexture( aiTextureType_DIFFUSE,0,&texFileName );
bindablePtrs.push_back( Texture::Resolve( gfx,base + texFileName.C_Str() ) );
if( material.GetTexture( aiTextureType_SPECULAR,0,&texFileName ) == aiReturn_SUCCESS )
{
bindablePtrs.push_back( Texture::Resolve( gfx,base + texFileName.C_Str(),1 ) );
hasSpecularMap = true;
}
else
{
material.Get( AI_MATKEY_SHININESS,shininess );
}
bindablePtrs.push_back( Bind::Sampler::Resolve( gfx ) );
}
bindablePtrs.push_back( std::make_unique<Bind::VertexBuffer>( gfx,vbuf ) );
auto meshTag = base + "%" + mesh.mName.C_Str();
bindablePtrs.push_back( std::make_unique<Bind::IndexBuffer>( gfx,indices ) );
bindablePtrs.push_back( VertexBuffer::Resolve( gfx,meshTag,vbuf ) );
auto pvs = std::make_unique<Bind::VertexShader>( gfx,L"PhongVS.cso" );
bindablePtrs.push_back( IndexBuffer::Resolve( gfx,meshTag,indices ) );
auto pvs = VertexShader::Resolve( gfx,"PhongVS.cso" );
auto pvsbc = pvs->GetBytecode();
bindablePtrs.push_back( std::move( pvs ) );
bindablePtrs.push_back( std::make_unique<Bind::PixelShader>( gfx,L"PhongPS.cso" ) );
bindablePtrs.push_back( InputLayout::Resolve( gfx,vbuf.GetLayout(),pvsbc ) );
bindablePtrs.push_back( std::make_unique<Bind::InputLayout>( gfx,vbuf.GetLayout().GetD3DLayout(),pvsbc ) );
if( hasSpecularMap )
{
bindablePtrs.push_back( PixelShader::Resolve( gfx,"PhongPSSpecMap.cso" ) );
}
else
{
bindablePtrs.push_back( PixelShader::Resolve( gfx,"PhongPS.cso" ) );
struct PSMaterialConstant
{
DirectX::XMFLOAT3 color = { 0.6f,0.6f,0.8f };
float specularIntensity = 0.6f;
float specularPower = 30.0f;
float padding[3];
float specularIntensity = 0.8f;
float specularPower;
float padding[2];
} pmc;
bindablePtrs.push_back( std::make_unique<Bind::PixelConstantBuffer<PSMaterialConstant>>( gfx,pmc,1u ) );
pmc.specularPower = shininess;
// this is CLEARLY an issue... all meshes will share same mat const, but may have different
// Ns (specular power) specified for each in the material properties... bad conflict
bindablePtrs.push_back( PixelConstantBuffer<PSMaterialConstant>::Resolve( gfx,pmc,1u ) );
}
return std::make_unique<Mesh>( gfx,std::move( bindablePtrs ) );
}
......
#pragma once
#include "DrawableBase.h"
#include "Drawable.h"
#include "BindableCommon.h"
#include "Vertex.h"
#include <optional>
......@@ -20,10 +20,10 @@ private:
std::string note;
};
class Mesh : public DrawableBase<Mesh>
class Mesh : public Drawable
{
public:
Mesh( Graphics& gfx,std::vector<std::unique_ptr<Bind::Bindable>> bindPtrs );
Mesh( Graphics& gfx,std::vector<std::shared_ptr<Bind::Bindable>> bindPtrs );
void Draw( Graphics& gfx,DirectX::FXMMATRIX accumulatedTransform ) const noxnd;
DirectX::XMMATRIX GetTransformXM() const noexcept override;
private:
......@@ -58,7 +58,7 @@ public:
void ShowWindow( const char* windowName = nullptr ) noexcept;
~Model() noexcept;
private:
static std::unique_ptr<Mesh> ParseMesh( Graphics& gfx,const aiMesh& mesh );
static std::unique_ptr<Mesh> ParseMesh( Graphics& gfx,const aiMesh& mesh,const aiMaterial* const* pMaterials );
std::unique_ptr<Node> ParseNode( int& nextId,const aiNode& node ) noexcept;
private:
std::unique_ptr<Node> pRoot;
......
Original Nanosuit model by ForrestPL:
http://tf3dm.com/3d-model/crysis-2-nanosuit-2-97837.html
Slightly modified for use in the LearnOpenGL.com tutorials (by Joey de Vries)
For personal use only
\ No newline at end of file