Prechádzať zdrojové kódy

Vulkan: Work on resource group. Uniforms seem to work

Panagiotis Christopoulos Charitos 9 rokov pred
rodič
commit
5c5eeb703e

+ 6 - 2
include/anki/gr/Common.h

@@ -23,6 +23,7 @@ class SamplerInitInfo;
 class GrManagerInitInfo;
 class PipelineInitInfo;
 class FramebufferInitInfo;
+class ResourceGroupInitInfo;
 
 /// @addtogroup graphics
 /// @{
@@ -109,7 +110,7 @@ public:
 };
 
 // Some constants
-// WARNING: If you change those update the shaders
+// WARNING: If you change those you may need to update the shaders.
 const U MAX_VERTEX_ATTRIBUTES = 8;
 const U MAX_COLOR_ATTACHMENTS = 4;
 const U MAX_MIPMAPS = 16;
@@ -119,7 +120,10 @@ const U MAX_UNIFORM_BUFFER_BINDINGS = 4;
 const U MAX_STORAGE_BUFFER_BINDINGS = 4;
 const U MAX_ATOMIC_BUFFER_BINDINGS = 1;
 const U MAX_FRAMES_IN_FLIGHT = 3; ///< Triple buffering.
-const U MAX_RESOURCE_GROUPS = 2; ///< Groups that can be bound at the same time.
+/// Groups that can be bound at the same time.
+const U MAX_BOUND_RESOURCE_GROUPS = 2;
+/// An anoying limit for Vulkan.
+const U MAX_RESOURCE_GROUPS = 1024;
 
 /// The life expectancy of a TransientMemoryToken.
 enum class TransientMemoryTokenLifetime : U8

+ 6 - 0
include/anki/gr/vulkan/BufferImpl.h

@@ -41,6 +41,12 @@ public:
 #endif
 	}
 
+	VkBuffer getHandle() const
+	{
+		ANKI_ASSERT(isCreated());
+		return m_handle;
+	}
+
 private:
 	VkBuffer m_handle = VK_NULL_HANDLE;
 	GpuMemoryAllocationHandle m_memHandle;

+ 5 - 1
include/anki/gr/vulkan/CommandBufferImpl.h

@@ -74,6 +74,9 @@ public:
 
 	void endRenderPass();
 
+	void bindResourceGroup(
+		ResourceGroupPtr rc, U slot, const TransientMemoryInfo* dynInfo);
+
 	void drawArrays(U32 count, U32 instanceCount, U32 first, U32 baseInstance)
 	{
 		drawcallCommon();
@@ -97,10 +100,11 @@ private:
 	Bool m_firstRpassDrawcall = true; ///< First drawcall in a renderpass.
 	FramebufferPtr m_activeFb;
 
-	/// @name cleanup_lists
+	/// @name cleanup_references
 	/// @{
 	List<PipelinePtr> m_pplineList;
 	List<FramebufferPtr> m_fbList;
+	List<ResourceGroupPtr> m_rcList;
 /// @}
 
 #if ANKI_ASSERTIONS

+ 32 - 0
include/anki/gr/vulkan/GrManagerImpl.h

@@ -61,6 +61,34 @@ public:
 		return m_globalPipelineLayout;
 	}
 
+	ANKI_USE_RESULT Error allocateDescriptorSet(VkDescriptorSet& out)
+	{
+		out = VK_NULL_HANDLE;
+		VkDescriptorSetAllocateInfo ci = {};
+		ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
+		ci.descriptorPool = m_globalDescriptorPool;
+		ci.descriptorSetCount = 1;
+		ci.pSetLayouts = &m_globalDescriptorSetLayout;
+
+		LockGuard<Mutex> lock(m_globalDescriptorPoolMtx);
+		if(++m_descriptorSetAllocationCount > MAX_RESOURCE_GROUPS)
+		{
+			ANKI_LOGE("Exceeded the MAX_RESOURCE_GROUPS");
+			return ErrorCode::OUT_OF_MEMORY;
+		}
+		ANKI_VK_CHECK(vkAllocateDescriptorSets(m_device, &ci, &out));
+		return ErrorCode::NONE;
+	}
+
+	void freeDescriptorSet(VkDescriptorSet ds)
+	{
+		ANKI_ASSERT(ds);
+		LockGuard<Mutex> lock(m_globalDescriptorPoolMtx);
+		--m_descriptorSetAllocationCount;
+		ANKI_VK_CHECKF(
+			vkFreeDescriptorSets(m_device, m_globalDescriptorPool, 1, &ds));
+	}
+
 	VkCommandBuffer newCommandBuffer(Thread::Id tid, Bool secondLevel);
 
 	void deleteCommandBuffer(
@@ -169,6 +197,9 @@ private:
 	/// @}
 
 	VkDescriptorSetLayout m_globalDescriptorSetLayout = VK_NULL_HANDLE;
+	VkDescriptorPool m_globalDescriptorPool = VK_NULL_HANDLE;
+	Mutex m_globalDescriptorPoolMtx;
+	U32 m_descriptorSetAllocationCount = 0;
 	VkPipelineLayout m_globalPipelineLayout = VK_NULL_HANDLE;
 
 	/// Map for compatible render passes.
@@ -228,6 +259,7 @@ private:
 	ANKI_USE_RESULT Error initSwapchain(const GrManagerInitInfo& init);
 	ANKI_USE_RESULT Error initFramebuffers(const GrManagerInitInfo& init);
 	ANKI_USE_RESULT Error initGlobalDsetLayout();
+	ANKI_USE_RESULT Error initGlobalDsetPool();
 	ANKI_USE_RESULT Error initGlobalPplineLayout();
 	void initMemory();
 

+ 16 - 1
include/anki/gr/vulkan/ResourceGroupImpl.h

@@ -22,9 +22,24 @@ public:
 	{
 	}
 
-	~ResourceGroupImpl()
+	~ResourceGroupImpl();
+
+	ANKI_USE_RESULT Error init(const ResourceGroupInitInfo& init);
+
+	VkDescriptorSet getHandle() const
 	{
+		ANKI_ASSERT(m_handle);
+		return m_handle;
 	}
+
+private:
+	VkDescriptorSet m_handle = VK_NULL_HANDLE;
+
+	/// Holds the references to the resources. Used to release the references
+	/// gracefully
+	DynamicArray<GrObjectPtr<GrObject>> m_refs;
+
+	static U calcRefCount(const ResourceGroupInitInfo& init, Bool& hasUploaded);
 };
 /// @}
 

+ 2 - 3
shaders/Common.glsl

@@ -36,10 +36,9 @@ const uint UBO_MAX_SIZE = 16384;
 #define UBO_BINDING(set_, binding_) binding = set_ * 4 + binding_
 #define SS_BINDING(set_, binding_) binding = set_ * 4 + binding_
 #define TEX_BINDING(set_, binding_) binding = set_ * 10 + binding_
-#define ATOMIC_BINDING(set_, binding_) binding = set_ * 1 + binding_
 #elif defined(ANKI_VK)
-#define UBO_BINDING(set_, binding_) set = set_, binding = binding_
-#define SS_BINDING(set_, binding_) set = set_, binding = binding_
+#define UBO_BINDING(set_, binding_) set = set_, binding = 10 + binding_
+#define SS_BINDING(set_, binding_) set = set_, binding = 14 + binding_
 #define TEX_BINDING(set_, binding_) set = set_, binding = binding_
 #else
 #error Missing define

+ 1 - 1
src/gr/gl/ResourceGroupImpl.cpp

@@ -235,7 +235,7 @@ void ResourceGroupImpl::initResourceReferences(
 void ResourceGroupImpl::bind(
 	U slot, const TransientMemoryInfo& transientInfo, GlState& state)
 {
-	ANKI_ASSERT(slot < MAX_RESOURCE_GROUPS);
+	ANKI_ASSERT(slot < MAX_BOUND_RESOURCE_GROUPS);
 
 	// Bind textures
 	if(m_textureNamesCount)

+ 2 - 3
src/gr/vulkan/BufferImpl.cpp

@@ -60,9 +60,8 @@ Error BufferImpl::init(
 		// Fallback
 		if(m_memIdx == MAX_U32)
 		{
-			m_memIdx = getGrManagerImpl().findMemoryType(req.memoryTypeBits,
-				VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
-				0);
+			m_memIdx = getGrManagerImpl().findMemoryType(
+				req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 0);
 		}
 	}
 	else

+ 2 - 0
src/gr/vulkan/CommandBuffer.cpp

@@ -7,6 +7,7 @@
 #include <anki/gr/vulkan/CommandBufferImpl.h>
 
 #include <anki/gr/Pipeline.h>
+#include <anki/gr/ResourceGroup.h>
 
 namespace anki
 {
@@ -85,6 +86,7 @@ void CommandBuffer::endRenderPass()
 void CommandBuffer::bindResourceGroup(
 	ResourceGroupPtr rc, U slot, const TransientMemoryInfo* dynInfo)
 {
+	m_impl->bindResourceGroup(rc, slot, dynInfo);
 }
 
 //==============================================================================

+ 25 - 0
src/gr/vulkan/CommandBufferImpl.cpp

@@ -11,6 +11,8 @@
 #include <anki/gr/vulkan/PipelineImpl.h>
 #include <anki/gr/Framebuffer.h>
 #include <anki/gr/vulkan/FramebufferImpl.h>
+#include <anki/gr/ResourceGroup.h>
+#include <anki/gr/vulkan/ResourceGroupImpl.h>
 
 namespace anki
 {
@@ -41,6 +43,7 @@ CommandBufferImpl::~CommandBufferImpl()
 
 	m_pplineList.destroy(m_alloc);
 	m_fbList.destroy(m_alloc);
+	m_rcList.destroy(m_alloc);
 }
 
 //==============================================================================
@@ -225,4 +228,26 @@ void CommandBufferImpl::flush(CommandBuffer* cmdb)
 	m_flushed = true;
 }
 
+//==============================================================================
+void CommandBufferImpl::bindResourceGroup(
+	ResourceGroupPtr rc, U slot, const TransientMemoryInfo* dynInfo)
+{
+	commandCommon();
+	// TODO set the correct binding point
+	Array<U32, MAX_UNIFORM_BUFFER_BINDINGS + MAX_STORAGE_BUFFER_BINDINGS>
+		dynOffsets = {{}};
+
+	VkDescriptorSet dset = rc->getImplementation().getHandle();
+	vkCmdBindDescriptorSets(m_handle,
+		VK_PIPELINE_BIND_POINT_GRAPHICS,
+		getGrManagerImpl().getGlobalPipelineLayout(),
+		slot,
+		1,
+		&dset,
+		dynOffsets.getSize(),
+		&dynOffsets[0]);
+
+	m_rcList.pushBack(m_alloc, rc);
+}
+
 } // end namespace anki

+ 32 - 2
src/gr/vulkan/GrManagerImpl.cpp

@@ -108,6 +108,11 @@ GrManagerImpl::~GrManagerImpl()
 		vkDestroyPipelineLayout(m_device, m_globalPipelineLayout, nullptr);
 	}
 
+	if(m_globalDescriptorPool)
+	{
+		vkDestroyDescriptorPool(m_device, m_globalDescriptorPool, nullptr);
+	}
+
 	if(m_globalDescriptorSetLayout)
 	{
 		vkDestroyDescriptorSetLayout(
@@ -199,6 +204,7 @@ Error GrManagerImpl::initInternal(const GrManagerInitInfo& init)
 
 	initMemory();
 	ANKI_CHECK(initGlobalDsetLayout());
+	ANKI_CHECK(initGlobalDsetPool());
 	ANKI_CHECK(initGlobalPplineLayout());
 
 	m_renderPasses = getAllocator().newInstance<CompatibleRenderPassHashMap>();
@@ -522,17 +528,41 @@ Error GrManagerImpl::initGlobalDsetLayout()
 	return ErrorCode::NONE;
 }
 
+//==============================================================================
+Error GrManagerImpl::initGlobalDsetPool()
+{
+	Array<VkDescriptorPoolSize, 3> pools = {{}};
+	pools[0] = VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+		MAX_TEXTURE_BINDINGS * MAX_RESOURCE_GROUPS};
+	pools[1] = VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
+		MAX_UNIFORM_BUFFER_BINDINGS * MAX_RESOURCE_GROUPS};
+	pools[2] = VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC,
+		MAX_STORAGE_BUFFER_BINDINGS * MAX_RESOURCE_GROUPS};
+
+	VkDescriptorPoolCreateInfo ci = {};
+	ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
+	ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
+	ci.maxSets = MAX_RESOURCE_GROUPS;
+	ci.poolSizeCount = pools.getSize();
+	ci.pPoolSizes = &pools[0];
+
+	ANKI_VK_CHECK(vkCreateDescriptorPool(
+		m_device, &ci, nullptr, &m_globalDescriptorPool));
+
+	return ErrorCode::NONE;
+}
+
 //==============================================================================
 Error GrManagerImpl::initGlobalPplineLayout()
 {
-	Array<VkDescriptorSetLayout, MAX_RESOURCE_GROUPS> sets = {
+	Array<VkDescriptorSetLayout, MAX_BOUND_RESOURCE_GROUPS> sets = {
 		{m_globalDescriptorSetLayout, m_globalDescriptorSetLayout}};
 
 	VkPipelineLayoutCreateInfo ci;
 	ci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
 	ci.pNext = nullptr;
 	ci.flags = 0;
-	ci.setLayoutCount = MAX_RESOURCE_GROUPS;
+	ci.setLayoutCount = MAX_BOUND_RESOURCE_GROUPS;
 	ci.pSetLayouts = &sets[0];
 	ci.pushConstantRangeCount = 0;
 	ci.pPushConstantRanges = nullptr;

+ 6 - 0
src/gr/vulkan/ResourceGroup.cpp

@@ -23,6 +23,12 @@ ResourceGroup::~ResourceGroup()
 //==============================================================================
 void ResourceGroup::init(const ResourceGroupInitInfo& init)
 {
+	m_impl.reset(getAllocator().newInstance<ResourceGroupImpl>(&getManager()));
+
+	if(m_impl->init(init))
+	{
+		ANKI_LOGF("Cannot recover");
+	}
 }
 
 } // end namespace anki

+ 108 - 0
src/gr/vulkan/ResourceGroupImpl.cpp

@@ -4,3 +4,111 @@
 // http://www.anki3d.org/LICENSE
 
 #include <anki/gr/vulkan/ResourceGroupImpl.h>
+#include <anki/gr/ResourceGroup.h>
+#include <anki/gr/vulkan/GrManagerImpl.h>
+#include <anki/gr/vulkan/BufferImpl.h>
+
+namespace anki
+{
+
+//==============================================================================
+ResourceGroupImpl::~ResourceGroupImpl()
+{
+	if(m_handle)
+	{
+		getGrManagerImpl().freeDescriptorSet(m_handle);
+	}
+
+	m_refs.destroy(getAllocator());
+}
+
+//==============================================================================
+U ResourceGroupImpl::calcRefCount(
+	const ResourceGroupInitInfo& init, Bool& hasUploaded)
+{
+	U count = 0;
+	hasUploaded = false;
+
+	for(U i = 0; i < MAX_UNIFORM_BUFFER_BINDINGS; ++i)
+	{
+		if(init.m_uniformBuffers[i].m_buffer)
+		{
+			++count;
+		}
+		else if(init.m_uniformBuffers[i].m_uploadedMemory)
+		{
+			hasUploaded = true;
+		}
+	}
+
+	// TODO: The rest of the resources
+
+	return count;
+}
+
+//==============================================================================
+Error ResourceGroupImpl::init(const ResourceGroupInitInfo& init)
+{
+	// Create
+	//
+	ANKI_CHECK(getGrManagerImpl().allocateDescriptorSet(m_handle));
+
+	// Store the references
+	//
+	Bool hasUploaded = false;
+	U refCount = calcRefCount(init, hasUploaded);
+	ANKI_ASSERT(refCount > 0 || hasUploaded);
+	if(refCount)
+	{
+		m_refs.create(getAllocator(), refCount);
+	}
+
+	// Update
+	//
+	Array<VkDescriptorBufferInfo, MAX_UNIFORM_BUFFER_BINDINGS> unis = {{}};
+	U uniCount = 0;
+	Array<VkWriteDescriptorSet, 1> write = {{}};
+	U writeCount = 0;
+	refCount = 0;
+
+	// 1st the textures
+	// TODO
+
+	// 2nd the uniform buffers
+	for(U i = 0; i < MAX_UNIFORM_BUFFER_BINDINGS; ++i)
+	{
+		if(init.m_uniformBuffers[i].m_buffer)
+		{
+			VkDescriptorBufferInfo& inf = unis[uniCount++];
+			inf.buffer = init.m_uniformBuffers[i]
+							 .m_buffer->getImplementation()
+							 .getHandle();
+			inf.offset = init.m_uniformBuffers[i].m_offset;
+			inf.range = init.m_uniformBuffers[i].m_range;
+
+			m_refs[refCount++] = init.m_uniformBuffers[i].m_buffer;
+		}
+		else if(init.m_uniformBuffers[i].m_uploadedMemory)
+		{
+			ANKI_ASSERT(0 && "TODO");
+		}
+	}
+
+	if(uniCount)
+	{
+		VkWriteDescriptorSet& w = write[writeCount++];
+		w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
+		w.descriptorCount = uniCount;
+		w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
+		w.dstBinding = MAX_TEXTURE_BINDINGS;
+		w.pBufferInfo = &unis[0];
+		w.dstSet = m_handle;
+	}
+
+	ANKI_ASSERT(writeCount > 0);
+	vkUpdateDescriptorSets(getDevice(), writeCount, &write[0], 0, nullptr);
+
+	return ErrorCode::NONE;
+}
+
+} // end namespace anki

+ 14 - 1
src/gr/vulkan/ShaderImpl.cpp

@@ -148,6 +148,16 @@ static TBuiltInResource setLimits()
 
 static const TBuiltInResource GLSLANG_LIMITS = setLimits();
 
+static const char* SHADER_HEADER = R"(#version 450 core
+#define ANKI_VK 1
+#define %s
+#define gl_VertexID gl_VertexIndex
+#define ANKI_UBO_BINDING(set_, binding_) set = set_, binding = %u + binding_
+#define ANKI_SS_BINDING(set_, binding_) set = set_, binding = %u + binding_
+#define ANKI_TEX_BINDING(set_, binding_) set = set_, binding = %u + binding_
+
+%s)";
+
 //==============================================================================
 // ShaderImpl                                                                  =
 //==============================================================================
@@ -213,8 +223,11 @@ Error ShaderImpl::init(ShaderType shaderType, const CString& source)
 		"FRAGMENT_SHADER",
 		"COMPUTE_SHADER"}};
 
-	fullSrc.sprintf("#version 450 core\n#define ANKI_VK\n#define %s\n%s\n",
+	fullSrc.sprintf(SHADER_HEADER,
 		shaderName[shaderType],
+		MAX_TEXTURE_BINDINGS,
+		MAX_TEXTURE_BINDINGS + MAX_UNIFORM_BUFFER_BINDINGS,
+		0,
 		&source[0]);
 
 	std::vector<unsigned int> spirv;

+ 146 - 9
tests/gr/Gr.cpp

@@ -16,26 +16,43 @@ const U WIDTH = 1024;
 const U HEIGHT = 768;
 
 static const char* VERT_SRC = R"(
+out gl_PerVertex
+{
+	vec4 gl_Position;
+};
 
-#if defined(ANKI_GL)
-#define gl_VertexIndex gl_VertexID
-#elif defined(ANKI_VK)
-// Do nothing
-#else
-#error See file
+void main()
+{
+	const vec2 POSITIONS[3] =
+		vec2[](vec2(-1.0, 1.0), vec2(0.0, -1.0), vec2(1.0, 1.0));
+
+	gl_Position = vec4(POSITIONS[gl_VertexID % 3], 0.0, 1.0);
+#if defined(ANKI_VK)
+	gl_Position.y = -gl_Position.y;
 #endif
+})";
 
+static const char* VERT_UBO_SRC = R"(
 out gl_PerVertex
 {
 	vec4 gl_Position;
 };
 
+layout(ANKI_UBO_BINDING(0, 0)) uniform u0_
+{
+	vec4 u_color[3];
+};
+
+out vec3 out_color;
+
 void main()
 {
 	const vec2 POSITIONS[3] =
 		vec2[](vec2(-1.0, 1.0), vec2(0.0, -1.0), vec2(1.0, 1.0));
+		
+	out_color = u_color[gl_VertexID].rgb;
 
-	gl_Position = vec4(POSITIONS[gl_VertexIndex % 3], 0.0, 1.0);
+	gl_Position = vec4(POSITIONS[gl_VertexID % 3], 0.0, 1.0);
 #if defined(ANKI_VK)
 	gl_Position.y = -gl_Position.y;
 #endif
@@ -48,6 +65,15 @@ void main()
 	out_color = vec4(0.5);
 })";
 
+static const char* FRAG_UBO_SRC = R"(layout (location = 0) out vec4 out_color;
+
+in vec3 in_color;
+
+void main()
+{
+	out_color = vec4(in_color, 1.0);
+})";
+
 #define COMMON_BEGIN()                                                         \
 	NativeWindow* win = nullptr;                                               \
 	GrManager* gr = nullptr;                                                   \
@@ -201,12 +227,123 @@ ANKI_TEST(Gr, Buffer)
 		BufferPtr a = gr->newInstance<Buffer>(
 			512, BufferUsageBit::UNIFORM, BufferAccessBit::NONE);
 
-		BufferPtr b = gr->newInstance<Buffer>(
-			64, BufferUsageBit::STORAGE, BufferAccessBit::CLIENT_MAP_WRITE);
+		BufferPtr b = gr->newInstance<Buffer>(64,
+			BufferUsageBit::STORAGE,
+			BufferAccessBit::CLIENT_MAP_WRITE
+				| BufferAccessBit::CLIENT_MAP_READ);
 
 		void* ptr = b->map(0, 64, BufferAccessBit::CLIENT_MAP_WRITE);
 		ANKI_TEST_EXPECT_NEQ(ptr, nullptr);
+		U8 ptr2[64];
+		memset(ptr, 0xCC, 64);
+		memset(ptr2, 0xCC, 64);
+		b->unmap();
+
+		ptr = b->map(0, 64, BufferAccessBit::CLIENT_MAP_READ);
+		ANKI_TEST_EXPECT_NEQ(ptr, nullptr);
+		ANKI_TEST_EXPECT_EQ(memcmp(ptr, ptr2, 64), 0);
+		b->unmap();
+	}
+
+	COMMON_END();
+}
+
+//==============================================================================
+ANKI_TEST(Gr, ResourceGroup)
+{
+	COMMON_BEGIN();
+
+	{
+		BufferPtr b = gr->newInstance<Buffer>(sizeof(F32) * 4,
+			BufferUsageBit::UNIFORM,
+			BufferAccessBit::CLIENT_MAP_WRITE);
+
+		ResourceGroupInitInfo rcinit;
+		rcinit.m_uniformBuffers[0].m_buffer = b;
+		ResourceGroupPtr rc = gr->newInstance<ResourceGroup>(rcinit);
+	}
+
+	COMMON_END();
+}
+
+//==============================================================================
+ANKI_TEST(Gr, DrawWithUniforms)
+{
+	COMMON_BEGIN();
+
+	{
+		// The buffer
+		BufferPtr b = gr->newInstance<Buffer>(sizeof(Vec4) * 3,
+			BufferUsageBit::UNIFORM,
+			BufferAccessBit::CLIENT_MAP_WRITE);
+
+		Vec4* ptr = static_cast<Vec4*>(
+			b->map(0, sizeof(Vec4) * 3, BufferAccessBit::CLIENT_MAP_WRITE));
+		ANKI_TEST_EXPECT_NEQ(ptr, nullptr);
+		ptr[0] = Vec4(1.0, 0.0, 0.0, 0.0);
+		ptr[1] = Vec4(0.0, 1.0, 0.0, 0.0);
+		ptr[2] = Vec4(0.0, 0.0, 1.0, 0.0);
 		b->unmap();
+
+		// Resource group
+		ResourceGroupInitInfo rcinit;
+		rcinit.m_uniformBuffers[0].m_buffer = b;
+		ResourceGroupPtr rc = gr->newInstance<ResourceGroup>(rcinit);
+
+		// Shaders
+		ShaderPtr vert =
+			gr->newInstance<Shader>(ShaderType::VERTEX, VERT_UBO_SRC);
+		ShaderPtr frag =
+			gr->newInstance<Shader>(ShaderType::FRAGMENT, FRAG_UBO_SRC);
+
+		// Ppline
+		PipelineInitInfo init;
+		init.m_shaders[ShaderType::VERTEX] = vert;
+		init.m_shaders[ShaderType::FRAGMENT] = frag;
+		init.m_color.m_drawsToDefaultFramebuffer = true;
+		init.m_color.m_attachmentCount = 1;
+		init.m_depthStencil.m_depthWriteEnabled = false;
+
+		PipelinePtr ppline = gr->newInstance<Pipeline>(init);
+
+		// FB
+		FramebufferInitInfo fbinit;
+		fbinit.m_colorAttachmentCount = 1;
+		fbinit.m_colorAttachments[0].m_clearValue.m_colorf = {
+			1.0, 0.0, 1.0, 1.0};
+		FramebufferPtr fb = gr->newInstance<Framebuffer>(fbinit);
+
+		U iterations = 100;
+		while(--iterations)
+		{
+			HighRezTimer timer;
+			timer.start();
+
+			gr->beginFrame();
+
+			CommandBufferInitInfo cinit;
+			cinit.m_frameFirstCommandBuffer = true;
+			cinit.m_frameLastCommandBuffer = true;
+			CommandBufferPtr cmdb = gr->newInstance<CommandBuffer>(cinit);
+
+			cmdb->setViewport(0, 0, WIDTH, HEIGHT);
+			cmdb->setPolygonOffset(0.0, 0.0);
+			cmdb->bindPipeline(ppline);
+			cmdb->beginRenderPass(fb);
+			cmdb->bindResourceGroup(rc, 0, nullptr);
+			cmdb->drawArrays(3);
+			cmdb->endRenderPass();
+			cmdb->flush();
+
+			gr->swapBuffers();
+
+			timer.stop();
+			const F32 TICK = 1.0 / 30.0;
+			if(timer.getElapsedTime() < TICK)
+			{
+				HighRezTimer::sleep(TICK - timer.getElapsedTime());
+			}
+		}
 	}
 
 	COMMON_END();