#pragma once

#include <string>
#include <functional>
#include <vector>
#include <memory>
#include <unordered_map>
#include <algorithm>

#include "GLExecutionState.h"
#include "../GCM.h"
#include "../Common/TextureUtils.h"
#include "../Common/GLSLTypes.h"

#include "Emu/system_config.h"
#include "Utilities/mutex.h"
#include "Utilities/geometry.h"
#include "Utilities/File.h"
#include "util/logs.hpp"
#include "util/asm.hpp"

#define GL_FRAGMENT_TEXTURES_START 0
#define GL_VERTEX_TEXTURES_START   (GL_FRAGMENT_TEXTURES_START + 16)
#define GL_STENCIL_MIRRORS_START   (GL_VERTEX_TEXTURES_START + 4)
#define GL_STREAM_BUFFER_START     (GL_STENCIL_MIRRORS_START + 16)

#define GL_VERTEX_PARAMS_BIND_SLOT 0
#define GL_VERTEX_LAYOUT_BIND_SLOT 1
#define GL_VERTEX_CONSTANT_BUFFERS_BIND_SLOT 2
#define GL_FRAGMENT_CONSTANT_BUFFERS_BIND_SLOT 3
#define GL_FRAGMENT_STATE_BIND_SLOT 4
#define GL_FRAGMENT_TEXTURE_PARAMS_BIND_SLOT 5
#define GL_RASTERIZER_STATE_BIND_SLOT 6
#define GL_INTERPRETER_VERTEX_BLOCK 7
#define GL_INTERPRETER_FRAGMENT_BLOCK 8
#define GL_COMPUTE_BUFFER_SLOT(index) (index + 9)

// Noop keyword outside of Windows (used in log_debug)
#if !defined(_WIN32) && !defined(APIENTRY)
#define APIENTRY
#endif

inline static void _SelectTexture(int unit) { glActiveTexture(GL_TEXTURE0 + unit); }

//using enum rsx::format_class;
using namespace ::rsx::format_class_;

namespace gl
{
	//Function call wrapped in ARB_DSA vs EXT_DSA compat check
#define DSA_CALL(func, texture_name, target, ...)\
	if (::gl::get_driver_caps().ARB_dsa_supported)\
		gl##func(texture_name, __VA_ARGS__);\
	else\
		gl##func##EXT(texture_name, target, __VA_ARGS__);

	class fence;

	void enable_debugging();
	bool is_primitive_native(rsx::primitive_type in);
	GLenum draw_mode(rsx::primitive_type in);

	void set_primary_context_thread(bool = true);
	bool is_primary_context_thread();
	void flush_command_queue(fence& fence_obj);

	// Texture helpers
	std::array<GLenum, 4> apply_swizzle_remap(const std::array<GLenum, 4>& swizzle_remap, const std::pair<std::array<u8, 4>, std::array<u8, 4>>& decoded_remap);

	class exception : public std::exception
	{
	protected:
		std::string m_what;

	public:
		const char* what() const noexcept override
		{
			return m_what.c_str();
		}
	};

	class fence
	{
		GLsync m_value = nullptr;
		mutable GLenum flags = GL_SYNC_FLUSH_COMMANDS_BIT;
		mutable bool signaled = false;

	public:

		fence() = default;
		~fence() = default;

		void create()
		{
			m_value = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
			flags = GL_SYNC_FLUSH_COMMANDS_BIT;
		}

		void destroy()
		{
			glDeleteSync(m_value);
			m_value = nullptr;
		}

		void reset()
		{
			if (m_value != nullptr)
				destroy();

			create();
		}

		bool is_empty() const
		{
			return (m_value == nullptr);
		}

		bool check_signaled() const
		{
			ensure(m_value);

			if (signaled)
				return true;

			if (flags)
			{
				GLenum err = glClientWaitSync(m_value, flags, 0);
				flags = 0;

				if (!(err == GL_ALREADY_SIGNALED || err == GL_CONDITION_SATISFIED))
					return false;
			}
			else
			{
				GLint status = GL_UNSIGNALED;
				GLint tmp;

				glGetSynciv(m_value, GL_SYNC_STATUS, 4, &tmp, &status);

				if (status != GL_SIGNALED)
					return false;
			}

			signaled = true;
			return true;
		}

		bool wait_for_signal()
		{
			ensure(m_value);

			if (signaled == GL_FALSE)
			{
				GLenum err = GL_WAIT_FAILED;
				bool done = false;

				while (!done)
				{
					if (flags)
					{
						err = glClientWaitSync(m_value, flags, 0);
						flags = 0;

						switch (err)
						{
						default:
							rsx_log.error("gl::fence sync returned unknown error 0x%X", err);
							[[fallthrough]];
						case GL_ALREADY_SIGNALED:
						case GL_CONDITION_SATISFIED:
							done = true;
							break;
						case GL_TIMEOUT_EXPIRED:
							continue;
						}
					}
					else
					{
						GLint status = GL_UNSIGNALED;
						GLint tmp;

						glGetSynciv(m_value, GL_SYNC_STATUS, 4, &tmp, &status);

						if (status == GL_SIGNALED)
							break;
					}
				}

				signaled = (err == GL_ALREADY_SIGNALED || err == GL_CONDITION_SATISFIED);
			}

			glDeleteSync(m_value);
			m_value = nullptr;

			return signaled;
		}

		void server_wait_sync() const
		{
			ensure(m_value != nullptr);
			glWaitSync(m_value, 0, GL_TIMEOUT_IGNORED);
		}
	};

	template<typename Type, uint BindId, uint GetStateId>
	class save_binding_state_base
	{
		GLint m_last_binding;

	public:
		save_binding_state_base(const Type& new_state) : save_binding_state_base()
		{
			new_state.bind();
		}

		save_binding_state_base()
		{
			glGetIntegerv(GetStateId, &m_last_binding);
		}

		~save_binding_state_base()
		{
			glBindBuffer(BindId, m_last_binding);
		}
	};

	enum class filter
	{
		nearest = GL_NEAREST,
		linear = GL_LINEAR
	};

	enum class min_filter
	{
		nearest = GL_NEAREST,
		linear = GL_LINEAR,
		nearest_mipmap_nearest = GL_NEAREST_MIPMAP_NEAREST,
		nearest_mipmap_linear = GL_NEAREST_MIPMAP_LINEAR,
		linear_mipmap_nearest = GL_LINEAR_MIPMAP_NEAREST,
		linear_mipmap_linear = GL_LINEAR_MIPMAP_LINEAR
	};

	enum class buffers
	{
		none = 0,
		color = GL_COLOR_BUFFER_BIT,
		depth = GL_DEPTH_BUFFER_BIT,
		stencil = GL_STENCIL_BUFFER_BIT,

		color_depth = color | depth,
		color_depth_stencil = color | depth | stencil,
		color_stencil = color | stencil,

		depth_stencil = depth | stencil
	};

	class pixel_pack_settings
	{
		bool m_swap_bytes = false;
		bool m_lsb_first = false;
		int m_row_length = 0;
		int m_image_height = 0;
		int m_skip_rows = 0;
		int m_skip_pixels = 0;
		int m_skip_images = 0;
		int m_alignment = 4;

	public:
		void apply() const
		{
			glPixelStorei(GL_PACK_SWAP_BYTES, m_swap_bytes ? GL_TRUE : GL_FALSE);
			glPixelStorei(GL_PACK_LSB_FIRST, m_lsb_first ? GL_TRUE : GL_FALSE);
			glPixelStorei(GL_PACK_ROW_LENGTH, m_row_length);
			glPixelStorei(GL_PACK_IMAGE_HEIGHT, m_image_height);
			glPixelStorei(GL_PACK_SKIP_ROWS, m_skip_rows);
			glPixelStorei(GL_PACK_SKIP_PIXELS, m_skip_pixels);
			glPixelStorei(GL_PACK_SKIP_IMAGES, m_skip_images);
			glPixelStorei(GL_PACK_ALIGNMENT, m_alignment);
		}

		pixel_pack_settings& swap_bytes(bool value = true)
		{
			m_swap_bytes = value;
			return *this;
		}
		pixel_pack_settings& lsb_first(bool value = true)
		{
			m_lsb_first = value;
			return *this;
		}
		pixel_pack_settings& row_length(int value)
		{
			m_row_length = value;
			return *this;
		}
		pixel_pack_settings& image_height(int value)
		{
			m_image_height = value;
			return *this;
		}
		pixel_pack_settings& skip_rows(int value)
		{
			m_skip_rows = value;
			return *this;
		}
		pixel_pack_settings& skip_pixels(int value)
		{
			m_skip_pixels = value;
			return *this;
		}
		pixel_pack_settings& skip_images(int value)
		{
			m_skip_images = value;
			return *this;
		}
		pixel_pack_settings& alignment(int value)
		{
			m_alignment = value;
			return *this;
		}
	};

	class pixel_unpack_settings
	{
		bool m_swap_bytes = false;
		bool m_lsb_first = false;
		int m_row_length = 0;
		int m_image_height = 0;
		int m_skip_rows = 0;
		int m_skip_pixels = 0;
		int m_skip_images = 0;
		int m_alignment = 4;

	public:
		void apply() const
		{
			glPixelStorei(GL_UNPACK_SWAP_BYTES, m_swap_bytes ? GL_TRUE : GL_FALSE);
			glPixelStorei(GL_UNPACK_LSB_FIRST, m_lsb_first ? GL_TRUE : GL_FALSE);
			glPixelStorei(GL_UNPACK_ROW_LENGTH, m_row_length);
			glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, m_image_height);
			glPixelStorei(GL_UNPACK_SKIP_ROWS, m_skip_rows);
			glPixelStorei(GL_UNPACK_SKIP_PIXELS, m_skip_pixels);
			glPixelStorei(GL_UNPACK_SKIP_IMAGES, m_skip_images);
			glPixelStorei(GL_UNPACK_ALIGNMENT, m_alignment);
		}

		pixel_unpack_settings& swap_bytes(bool value = true)
		{
			m_swap_bytes = value;
			return *this;
		}
		pixel_unpack_settings& lsb_first(bool value = true)
		{
			m_lsb_first = value;
			return *this;
		}
		pixel_unpack_settings& row_length(int value)
		{
			m_row_length = value;
			return *this;
		}
		pixel_unpack_settings& image_height(int value)
		{
			m_image_height = value;
			return *this;
		}
		pixel_unpack_settings& skip_rows(int value)
		{
			m_skip_rows = value;
			return *this;
		}
		pixel_unpack_settings& skip_pixels(int value)
		{
			m_skip_pixels = value;
			return *this;
		}
		pixel_unpack_settings& skip_images(int value)
		{
			m_skip_images = value;
			return *this;
		}
		pixel_unpack_settings& alignment(int value)
		{
			m_alignment = value;
			return *this;
		}
	};

	class vao;
	class attrib_t;

	class buffer_pointer
	{
	public:
		enum class type
		{
			s8 = GL_BYTE,
			u8 = GL_UNSIGNED_BYTE,
			s16 = GL_SHORT,
			u16 = GL_UNSIGNED_SHORT,
			s32 = GL_INT,
			u32 = GL_UNSIGNED_INT,
			f16 = GL_HALF_FLOAT,
			f32 = GL_FLOAT,
			f64 = GL_DOUBLE,
			fixed = GL_FIXED,
			s32_2_10_10_10_rev = GL_INT_2_10_10_10_REV,
			u32_2_10_10_10_rev = GL_UNSIGNED_INT_2_10_10_10_REV,
			u32_10f_11f_11f_rev = GL_UNSIGNED_INT_10F_11F_11F_REV
		};

	private:
		vao* m_vao;
		u32 m_offset;
		u32 m_stride;
		u32 m_size = 4;
		type m_type = type::f32;
		bool m_normalize = false;

	public:
		buffer_pointer(vao* vao, u32 offset = 0, u32 stride = 0)
			: m_vao(vao)
			, m_offset(offset)
			, m_stride(stride)
		{
		}

		const class ::gl::vao& get_vao() const
		{
			return *m_vao;
		}

		class ::gl::vao& get_vao()
		{
			return *m_vao;
		}

		buffer_pointer& offset(u32 value)
		{
			m_offset = value;
			return *this;
		}

		u32 offset() const
		{
			return m_offset;
		}

		buffer_pointer& stride(u32 value)
		{
			m_stride = value;
			return *this;
		}

		u32 stride() const
		{
			return m_stride;
		}

		buffer_pointer& size(u32 value)
		{
			m_size = value;
			return *this;
		}

		u32 size() const
		{
			return m_size;
		}

		buffer_pointer& set_type(type value)
		{
			m_type = value;
			return *this;
		}

		type get_type() const
		{
			return m_type;
		}

		buffer_pointer& normalize(bool value)
		{
			m_normalize = value;
			return *this;
		}

		bool normalize() const
		{
			return m_normalize;
		}

		buffer_pointer& operator >> (u32 value)
		{
			return stride(value);
		}

		buffer_pointer& config(type type_ = type::f32, u32 size_ = 4, bool normalize_ = false)
		{
			return set_type(type_).size(size_).normalize(normalize_);
		}
	};

	class buffer
	{
	public:
		enum class target
		{
			pixel_pack = GL_PIXEL_PACK_BUFFER,
			pixel_unpack = GL_PIXEL_UNPACK_BUFFER,
			array = GL_ARRAY_BUFFER,
			element_array = GL_ELEMENT_ARRAY_BUFFER,
			uniform = GL_UNIFORM_BUFFER,
			texture = GL_TEXTURE_BUFFER,
			ssbo = GL_SHADER_STORAGE_BUFFER
		};

		enum class access
		{
			read = GL_READ_ONLY,
			write = GL_WRITE_ONLY,
			read_write = GL_READ_WRITE
		};

		enum class memory_type
		{
			undefined = 0,
			local = 1,
			host_visible = 2
		};

		class save_binding_state
		{
			GLint m_last_binding;
			GLenum m_target;

		public:
			save_binding_state(target target_, const buffer& new_state) : save_binding_state(target_)
			{
				new_state.bind(target_);
			}

			save_binding_state(target target_)
			{
				GLenum pname;
				switch (target_)
				{
				case target::pixel_pack: pname = GL_PIXEL_PACK_BUFFER_BINDING; break;
				case target::pixel_unpack: pname = GL_PIXEL_UNPACK_BUFFER_BINDING; break;
				case target::array: pname = GL_ARRAY_BUFFER_BINDING; break;
				case target::element_array: pname = GL_ELEMENT_ARRAY_BUFFER_BINDING; break;
				case target::uniform: pname = GL_UNIFORM_BUFFER_BINDING; break;
				case target::texture: pname = GL_TEXTURE_BUFFER_BINDING; break;
				case target::ssbo: pname = GL_SHADER_STORAGE_BUFFER_BINDING; break;
				}

				glGetIntegerv(pname, &m_last_binding);
				m_target = static_cast<GLenum>(target_);
			}

			~save_binding_state()
			{
				glBindBuffer(m_target, m_last_binding);
			}
		};

	protected:
		GLuint m_id = GL_NONE;
		GLsizeiptr m_size = 0;
		target m_target = target::array;
		memory_type m_memory_type = memory_type::undefined;

		void allocate(GLsizeiptr size, const void* data_, memory_type type, GLenum usage)
		{
			if (const auto& caps = get_driver_caps();
				caps.ARB_buffer_storage_supported)
			{
				target target_ = current_target();
				save_binding_state save(target_, *this);
				GLenum flags = 0;

				if (type == memory_type::host_visible)
				{
					switch (usage)
					{
					case GL_STREAM_DRAW:
					case GL_STATIC_DRAW:
					case GL_DYNAMIC_DRAW:
						flags |= GL_MAP_WRITE_BIT;
						break;
					case GL_STREAM_READ:
					case GL_STATIC_READ:
					case GL_DYNAMIC_READ:
						flags |= GL_MAP_READ_BIT;
						break;
					default:
						fmt::throw_exception("Unsupported buffer usage 0x%x", usage);
					}
				}

				if ((flags & GL_MAP_READ_BIT) && !caps.vendor_AMD)
				{
					// This flag stops NVIDIA from allocating read-only memory in VRAM.
					// NOTE: On AMD, allocating client-side memory via CLIENT_STORAGE_BIT or
					// making use of GL_AMD_pinned_memory brings everything down to a crawl.
					// Afaict there is no reason for this; disabling pixel pack/unpack operations does not alleviate the problem.
					// The driver seems to eventually figure out the optimal storage location by itself.
					flags |= GL_CLIENT_STORAGE_BIT;
				}

				glBufferStorage(static_cast<GLenum>(target_), size, data_, flags);
				m_size = size;
			}
			else
			{
				data(size, data_, usage);
			}

			m_memory_type = type;
		}

	public:
		buffer() = default;
		buffer(const buffer&) = delete;

		buffer(GLuint id)
		{
			set_id(id);
		}

		~buffer()
		{
			if (created())
				remove();
		}

		void recreate()
		{
			if (created())
			{
				remove();
			}

			create();
		}

		void recreate(GLsizeiptr size, const void* data = nullptr)
		{
			if (created())
			{
				remove();
			}

			create(size, data);
		}

		void create()
		{
			glGenBuffers(1, &m_id);
		}

		void create(GLsizeiptr size, const void* data_ = nullptr, memory_type type = memory_type::local, GLenum usage = GL_STREAM_DRAW)
		{
			create();
			allocate(size, data_, type, usage);
		}

		void create(target target_, GLsizeiptr size, const void* data_ = nullptr, memory_type type = memory_type::local, GLenum usage = GL_STREAM_DRAW)
		{
			create();
			m_target = target_;
			allocate(size, data_, type, usage);
		}

		void bind(target target_) const
		{
			glBindBuffer(static_cast<GLenum>(target_), m_id);
		}

		void bind() const
		{
			bind(current_target());
		}

		target current_target() const
		{
			return m_target;
		}

		void remove()
		{
			if (m_id != GL_NONE)
			{
				glDeleteBuffers(1, &m_id);
				m_id = GL_NONE;
			}
		}

		GLsizeiptr size() const
		{
			return m_size;
		}

		uint id() const
		{
			return m_id;
		}

		void set_id(uint id)
		{
			m_id = id;
		}

		bool created() const
		{
			return m_id != GL_NONE;
		}

		explicit operator bool() const
		{
			return created();
		}

		void data(GLsizeiptr size, const void* data_ = nullptr, GLenum usage = GL_STREAM_DRAW)
		{
			ensure(m_memory_type != memory_type::local);

			target target_ = current_target();
			save_binding_state save(target_, *this);
			glBufferData(static_cast<GLenum>(target_), size, data_, usage);
			m_size = size;
		}

		GLubyte* map(access access_)
		{
			ensure(m_memory_type == memory_type::host_visible);

			bind(current_target());
			return reinterpret_cast<GLubyte*>(glMapBuffer(static_cast<GLenum>(current_target()), static_cast<GLenum>(access_)));
		}

		void unmap()
		{
			ensure(m_memory_type == memory_type::host_visible);
			glUnmapBuffer(static_cast<GLenum>(current_target()));
		}

		void bind_range(u32 index, u32 offset, u32 size) const
		{
			glBindBufferRange(static_cast<GLenum>(current_target()), index, id(), offset, size);
		}

		void bind_range(target target_, u32 index, u32 offset, u32 size) const
		{
			glBindBufferRange(static_cast<GLenum>(target_), index, id(), offset, size);
		}

		void copy_to(buffer* other, u64 src_offset, u64 dst_offset, u64 size)
		{
			if (get_driver_caps().ARB_dsa_supported)
			{
				glCopyNamedBufferSubData(this->id(), other->id(), src_offset, dst_offset, size);
			}
			else
			{
				glNamedCopyBufferSubDataEXT(this->id(), other->id(), src_offset, dst_offset, size);
			}
		}
	};

	class ring_buffer : public buffer
	{
	protected:

		u32 m_data_loc = 0;
		void *m_memory_mapping = nullptr;

		fence m_fence;

	public:

		virtual void recreate(GLsizeiptr size, const void* data = nullptr)
		{
			if (m_id)
			{
				m_fence.wait_for_signal();
				remove();
			}

			buffer::create();

			GLbitfield buffer_storage_flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
			if (gl::get_driver_caps().vendor_MESA) buffer_storage_flags |= GL_CLIENT_STORAGE_BIT;

			glBindBuffer(static_cast<GLenum>(m_target), m_id);
			glBufferStorage(static_cast<GLenum>(m_target), size, data, buffer_storage_flags);
			m_memory_mapping = glMapBufferRange(static_cast<GLenum>(m_target), 0, size, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);

			ensure(m_memory_mapping != nullptr);
			m_data_loc = 0;
			m_size = ::narrow<u32>(size);
		}

		void create(target target_, GLsizeiptr size, const void* data_ = nullptr)
		{
			m_target = target_;
			recreate(size, data_);
		}

		virtual std::pair<void*, u32> alloc_from_heap(u32 alloc_size, u16 alignment)
		{
			u32 offset = m_data_loc;
			if (m_data_loc) offset = utils::align(offset, alignment);

			if ((offset + alloc_size) > m_size)
			{
				if (!m_fence.is_empty())
				{
					m_fence.wait_for_signal();
				}
				else
				{
					rsx_log.error("OOM Error: Ring buffer was likely being used without notify() being called");
					glFinish();
				}

				m_data_loc = 0;
				offset = 0;
			}

			//Align data loc to 256; allows some "guard" region so we dont trample our own data inadvertently
			m_data_loc = utils::align(offset + alloc_size, 256);
			return std::make_pair(static_cast<char*>(m_memory_mapping) + offset, offset);
		}

		virtual void remove()
		{
			if (m_memory_mapping)
			{
				glBindBuffer(static_cast<GLenum>(m_target), m_id);
				glUnmapBuffer(static_cast<GLenum>(m_target));

				m_memory_mapping = nullptr;
				m_data_loc = 0;
				m_size = 0;
			}


			if (m_id != GL_NONE)
			{
				glDeleteBuffers(1, &m_id);
				m_id = GL_NONE;
			}
		}

		virtual void reserve_storage_on_heap(u32 /*alloc_size*/) {}

		virtual void unmap() {}

		//Notification of a draw command
		virtual void notify()
		{
			//Insert fence about 25% into the buffer
			if (m_fence.is_empty() && (m_data_loc > (m_size >> 2)))
				m_fence.reset();
		}
	};

	class legacy_ring_buffer : public ring_buffer
	{
		u32 m_mapped_bytes = 0;
		u32 m_mapping_offset = 0;
		u32 m_alignment_offset = 0;

	public:

		void recreate(GLsizeiptr size, const void* data = nullptr) override
		{
			if (m_id)
				remove();

			buffer::create();
			buffer::data(size, data, GL_DYNAMIC_DRAW);

			m_memory_type = memory_type::host_visible;
			m_memory_mapping = nullptr;
			m_data_loc = 0;
			m_size = ::narrow<u32>(size);
		}

		void create(target target_, GLsizeiptr size, const void* data_ = nullptr)
		{
			m_target = target_;
			recreate(size, data_);
		}

		void reserve_storage_on_heap(u32 alloc_size) override
		{
			ensure(m_memory_mapping == nullptr);

			u32 offset = m_data_loc;
			if (m_data_loc) offset = utils::align(offset, 256);

			const u32 block_size = utils::align(alloc_size + 16, 256);	//Overallocate just in case we need to realign base

			if ((offset + block_size) > m_size)
			{
				buffer::data(m_size, nullptr, GL_DYNAMIC_DRAW);
				m_data_loc = 0;
			}

			glBindBuffer(static_cast<GLenum>(m_target), m_id);
			m_memory_mapping = glMapBufferRange(static_cast<GLenum>(m_target), m_data_loc, block_size, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
			m_mapped_bytes = block_size;
			m_mapping_offset = m_data_loc;
			m_alignment_offset = 0;

			//When using debugging tools, the mapped base might not be aligned as expected
			const u64 mapped_address_base = reinterpret_cast<u64>(m_memory_mapping);
			if (mapped_address_base & 0xF)
			{
				//Unaligned result was returned. We have to modify the base address a bit
				//We lose some memory here, but the 16 byte overallocation above makes up for it
				const u64 new_base = (mapped_address_base & ~0xF) + 16;
				const u64 diff_bytes = new_base - mapped_address_base;

				m_memory_mapping = reinterpret_cast<void*>(new_base);
				m_mapped_bytes -= ::narrow<u32>(diff_bytes);
				m_alignment_offset = ::narrow<u32>(diff_bytes);
			}

			ensure(m_mapped_bytes >= alloc_size);
		}

		std::pair<void*, u32> alloc_from_heap(u32 alloc_size, u16 alignment) override
		{
			u32 offset = m_data_loc;
			if (m_data_loc) offset = utils::align(offset, alignment);

			u32 padding = (offset - m_data_loc);
			u32 real_size = utils::align(padding + alloc_size, alignment);	//Ensures we leave the loc pointer aligned after we exit

			if (real_size > m_mapped_bytes)
			{
				//Missed allocation. We take a performance hit on doing this.
				//Overallocate slightly for the next allocation if requested size is too small
				unmap();
				reserve_storage_on_heap(std::max(real_size, 4096U));

				offset = m_data_loc;
				if (m_data_loc) offset = utils::align(offset, alignment);

				padding = (offset - m_data_loc);
				real_size = utils::align(padding + alloc_size, alignment);
			}

			m_data_loc = offset + real_size;
			m_mapped_bytes -= real_size;

			u32 local_offset = (offset - m_mapping_offset);
			return std::make_pair(static_cast<char*>(m_memory_mapping) + local_offset, offset + m_alignment_offset);
		}

		void remove() override
		{
			ring_buffer::remove();
			m_mapped_bytes = 0;
		}

		void unmap() override
		{
			buffer::bind();
			buffer::unmap();

			m_memory_mapping = nullptr;
			m_mapped_bytes = 0;
			m_mapping_offset = 0;
		}

		void notify() override {}
	};

	class buffer_view
	{
		buffer* m_buffer = nullptr;
		u32 m_offset = 0;
		u32 m_range = 0;
		GLenum m_format = GL_R8UI;

	public:
		buffer_view(buffer *_buffer, u32 offset, u32 range, GLenum format = GL_R8UI)
			: m_buffer(_buffer), m_offset(offset), m_range(range), m_format(format)
		{}

		buffer_view() = default;

		void update(buffer *_buffer, u32 offset, u32 range, GLenum format = GL_R8UI)
		{
			ensure(_buffer->size() >= (offset + range));
			m_buffer = _buffer;
			m_offset = offset;
			m_range = range;
			m_format = format;
		}

		u32 offset() const
		{
			return m_offset;
		}

		u32 range() const
		{
			return m_range;
		}

		u32 format() const
		{
			return m_format;
		}

		buffer* value() const
		{
			return m_buffer;
		}

		bool in_range(u32 address, u32 size, u32& new_offset) const
		{
			if (address < m_offset)
				return false;

			const u32 _offset = address - m_offset;
			if (m_range < _offset)
				return false;

			const auto remaining = m_range - _offset;
			if (size <= remaining)
			{
				new_offset = _offset;
				return true;
			}

			return false;
		}
	};

	class vao
	{
		template<buffer::target BindId, uint GetStateId>
		class entry
		{
			vao& m_parent;

		public:
			using save_binding_state = save_binding_state_base<entry, (static_cast<GLuint>(BindId)), GetStateId>;

			entry(vao* parent) noexcept : m_parent(*parent)
			{
			}

			entry& operator = (const buffer& buf) noexcept
			{
				m_parent.bind();
				buf.bind(BindId);

				return *this;
			}
		};

		GLuint m_id = GL_NONE;

	public:
		entry<buffer::target::pixel_pack, GL_PIXEL_PACK_BUFFER_BINDING> pixel_pack_buffer{ this };
		entry<buffer::target::pixel_unpack, GL_PIXEL_UNPACK_BUFFER_BINDING> pixel_unpack_buffer{ this };
		entry<buffer::target::array, GL_ARRAY_BUFFER_BINDING> array_buffer{ this };
		entry<buffer::target::element_array, GL_ELEMENT_ARRAY_BUFFER_BINDING> element_array_buffer{ this };

		vao() = default;
		vao(const vao&) = delete;

		vao(vao&& vao_) noexcept
		{
			swap(vao_);
		}
		vao(GLuint id) noexcept
		{
			set_id(id);
		}

		~vao() noexcept
		{
			if (created())
				remove();
		}

		void swap(vao& vao_) noexcept
		{
			auto my_old_id = id();
			set_id(vao_.id());
			vao_.set_id(my_old_id);
		}

		vao& operator = (const vao& rhs) = delete;
		vao& operator = (vao&& rhs) noexcept
		{
			swap(rhs);
			return *this;
		}

		void bind() const noexcept
		{
			glBindVertexArray(m_id);
		}

		void create() noexcept
		{
			glGenVertexArrays(1, &m_id);
		}

		void remove() noexcept
		{
			if (m_id != GL_NONE)
			{
				glDeleteVertexArrays(1, &m_id);
				m_id = GL_NONE;
			}
		}

		uint id() const noexcept
		{
			return m_id;
		}

		void set_id(uint id) noexcept
		{
			m_id = id;
		}

		bool created() const noexcept
		{
			return m_id != GL_NONE;
		}

		explicit operator bool() const noexcept
		{
			return created();
		}

		void enable_for_attributes(std::initializer_list<GLuint> indexes) noexcept
		{
			for (auto &index : indexes)
			{
				glEnableVertexAttribArray(index);
			}
		}

		void disable_for_attributes(std::initializer_list<GLuint> indexes) noexcept
		{
			for (auto &index : indexes)
			{
				glDisableVertexAttribArray(index);
			}
		}

		void enable_for_attribute(GLuint index) noexcept
		{
			enable_for_attributes({ index });
		}

		void disable_for_attribute(GLuint index) noexcept
		{
			disable_for_attributes({ index });
		}

		buffer_pointer operator + (u32 offset) noexcept
		{
			return{ this, offset };
		}

		buffer_pointer operator >> (u32 stride) noexcept
		{
			return{ this, {}, stride };
		}

		operator buffer_pointer() noexcept
		{
			return{ this };
		}

		attrib_t operator [] (u32 index) const noexcept;
	};

	class attrib_t
	{
		GLint m_location;

	public:
		attrib_t(GLint location)
			: m_location(location)
		{
		}

		GLint location() const
		{
			return m_location;
		}

		void operator = (float rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs); }
		void operator = (double rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs); }

		void operator = (const color1f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs.r); }
		void operator = (const color1d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs.r); }
		void operator = (const color2f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2f(location(), rhs.r, rhs.g); }
		void operator = (const color2d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2d(location(), rhs.r, rhs.g); }
		void operator = (const color3f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3f(location(), rhs.r, rhs.g, rhs.b); }
		void operator = (const color3d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3d(location(), rhs.r, rhs.g, rhs.b); }
		void operator = (const color4f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4f(location(), rhs.r, rhs.g, rhs.b, rhs.a); }
		void operator = (const color4d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4d(location(), rhs.r, rhs.g, rhs.b, rhs.a); }

		void operator = (buffer_pointer& pointer) const
		{
			pointer.get_vao().enable_for_attribute(m_location);
			glVertexAttribPointer(location(), pointer.size(), static_cast<GLenum>(pointer.get_type()), pointer.normalize(),
				pointer.stride(), reinterpret_cast<const void*>(u64{pointer.offset()}));
		}
	};

	enum image_aspect : u32
	{
		color = 1,
		depth = 2,
		stencil = 4
	};

	class texture
	{
	public:
		enum class type
		{
			ubyte = GL_UNSIGNED_BYTE,
			ushort = GL_UNSIGNED_SHORT,
			uint = GL_UNSIGNED_INT,

			ubyte_3_3_2 = GL_UNSIGNED_BYTE_3_3_2,
			ubyte_2_3_3_rev = GL_UNSIGNED_BYTE_2_3_3_REV,

			ushort_5_6_5 = GL_UNSIGNED_SHORT_5_6_5,
			ushort_5_6_5_rev = GL_UNSIGNED_SHORT_5_6_5_REV,
			ushort_4_4_4_4 = GL_UNSIGNED_SHORT_4_4_4_4,
			ushort_4_4_4_4_rev = GL_UNSIGNED_SHORT_4_4_4_4_REV,
			ushort_5_5_5_1 = GL_UNSIGNED_SHORT_5_5_5_1,
			ushort_1_5_5_5_rev = GL_UNSIGNED_SHORT_1_5_5_5_REV,

			uint_8_8_8_8 = GL_UNSIGNED_INT_8_8_8_8,
			uint_8_8_8_8_rev = GL_UNSIGNED_INT_8_8_8_8_REV,
			uint_10_10_10_2 = GL_UNSIGNED_INT_10_10_10_2,
			uint_2_10_10_10_rev = GL_UNSIGNED_INT_2_10_10_10_REV,
			uint_24_8 = GL_UNSIGNED_INT_24_8,
			float32_uint8 = GL_FLOAT_32_UNSIGNED_INT_24_8_REV,

			sbyte = GL_BYTE,
			sshort = GL_SHORT,
			sint = GL_INT,
			f16 = GL_HALF_FLOAT,
			f32 = GL_FLOAT,
			f64 = GL_DOUBLE,
		};

		enum class channel
		{
			zero = GL_ZERO,
			one = GL_ONE,
			r = GL_RED,
			g = GL_GREEN,
			b = GL_BLUE,
			a = GL_ALPHA,
		};

		enum class format
		{
			r = GL_RED,
			rg = GL_RG,
			rgb = GL_RGB,
			rgba = GL_RGBA,

			bgr = GL_BGR,
			bgra = GL_BGRA,

			stencil = GL_STENCIL_INDEX,
			depth = GL_DEPTH_COMPONENT,
			depth_stencil = GL_DEPTH_STENCIL
		};

		enum class internal_format
		{
			stencil8 = GL_STENCIL_INDEX8,
			depth16 = GL_DEPTH_COMPONENT16,
			depth32f = GL_DEPTH_COMPONENT32F,
			depth24_stencil8 = GL_DEPTH24_STENCIL8,
			depth32f_stencil8 = GL_DEPTH32F_STENCIL8,

			compressed_rgb_s3tc_dxt1 = GL_COMPRESSED_RGB_S3TC_DXT1_EXT,
			compressed_rgba_s3tc_dxt1 = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT,
			compressed_rgba_s3tc_dxt3 = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT,
			compressed_rgba_s3tc_dxt5 = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT,

			//Sized internal formats, see opengl spec document on glTexImage2D, table 3
			rgba8 = GL_RGBA8,
			rgb565 = GL_RGB565,
			rgb5a1 = GL_RGB5_A1,
			rgba4 = GL_RGBA4,
			r8 = GL_R8,
			r16 = GL_R16,
			r32f = GL_R32F,
			rg8 = GL_RG8,
			rg16 = GL_RG16,
			rg16f = GL_RG16F,
			rgba16f = GL_RGBA16F,
			rgba32f = GL_RGBA32F
		};

		enum class wrap
		{
			repeat = GL_REPEAT,
			mirrored_repeat = GL_MIRRORED_REPEAT,
			clamp_to_edge = GL_CLAMP_TO_EDGE,
			clamp_to_border = GL_CLAMP_TO_BORDER,
			mirror_clamp = GL_MIRROR_CLAMP_EXT,
			//mirror_clamp_to_edge = GL_MIRROR_CLAMP_TO_EDGE,
			mirror_clamp_to_border = GL_MIRROR_CLAMP_TO_BORDER_EXT
		};

		enum class compare_mode
		{
			none = GL_NONE,
			ref_to_texture = GL_COMPARE_REF_TO_TEXTURE
		};

		enum class target
		{
			texture1D = GL_TEXTURE_1D,
			texture2D = GL_TEXTURE_2D,
			texture3D = GL_TEXTURE_3D,
			textureCUBE = GL_TEXTURE_CUBE_MAP,
			textureBuffer = GL_TEXTURE_BUFFER
		};

	protected:
		GLuint m_id = GL_NONE;
		GLuint m_width = 0;
		GLuint m_height = 0;
		GLuint m_depth = 0;
		GLuint m_mipmaps = 0;
		GLuint m_pitch = 0;
		GLuint m_compressed = GL_FALSE;
		GLuint m_aspect_flags = 0;

		target m_target = target::texture2D;
		internal_format m_internal_format = internal_format::rgba8;
		std::array<GLenum, 4> m_component_layout;

		rsx::format_class m_format_class = RSX_FORMAT_CLASS_UNDEFINED;

	private:
		class save_binding_state
		{
			GLenum target = GL_NONE;
			GLuint old_binding = GL_NONE;

		public:
			save_binding_state(GLenum target)
			{
				this->target = target;
				switch (target)
				{
				case GL_TEXTURE_1D:
					glGetIntegerv(GL_TEXTURE_BINDING_1D, reinterpret_cast<GLint*>(&old_binding));
					break;
				case GL_TEXTURE_2D:
					glGetIntegerv(GL_TEXTURE_BINDING_2D, reinterpret_cast<GLint*>(&old_binding));
					break;
				case GL_TEXTURE_3D:
					glGetIntegerv(GL_TEXTURE_BINDING_3D, reinterpret_cast<GLint*>(&old_binding));
					break;
				case GL_TEXTURE_CUBE_MAP:
					glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, reinterpret_cast<GLint*>(&old_binding));
					break;
				case GL_TEXTURE_2D_ARRAY:
					glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, reinterpret_cast<GLint*>(&old_binding));
					break;
				case GL_TEXTURE_BUFFER:
					glGetIntegerv(GL_TEXTURE_BINDING_BUFFER, reinterpret_cast<GLint*>(&old_binding));
					break;
				}
			}

			~save_binding_state()
			{
				glBindTexture(target, old_binding);
			}
		};
	public:
		texture(const texture&) = delete;
		texture(texture&& texture_) = delete;

		texture(GLenum target, GLuint width, GLuint height, GLuint depth, GLuint mipmaps, GLenum sized_format,
				rsx::format_class format_class = rsx::RSX_FORMAT_CLASS_UNDEFINED)
		{
			save_binding_state save(target);
			glGenTextures(1, &m_id);
			glBindTexture(target, m_id); //Must bind to initialize the new texture

			switch (target)
			{
			default:
				fmt::throw_exception("Invalid image target 0x%X", target);
			case GL_TEXTURE_1D:
				glTexStorage1D(target, mipmaps, sized_format, width);
				height = depth = 1;
				break;
			case GL_TEXTURE_2D:
			case GL_TEXTURE_CUBE_MAP:
				glTexStorage2D(target, mipmaps, sized_format, width, height);
				depth = 1;
				break;
			case GL_TEXTURE_3D:
			case GL_TEXTURE_2D_ARRAY:
				glTexStorage3D(target, mipmaps, sized_format, width, height, depth);
				break;
			case GL_TEXTURE_BUFFER:
				break;
			}

			if (target != GL_TEXTURE_BUFFER)
			{
				glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT);
				glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT);
				glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_REPEAT);
				glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, 0);
				glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, mipmaps - 1);

				m_width = width;
				m_height = height;
				m_depth = depth;
				m_mipmaps = mipmaps;
				m_aspect_flags = image_aspect::color;

				switch (sized_format)
				{
				case GL_DEPTH_COMPONENT16:
				{
					m_pitch = width * 2;
					m_aspect_flags = image_aspect::depth;
					break;
				}
				case GL_DEPTH_COMPONENT32F:
				{
					m_pitch = width * 4;
					m_aspect_flags = image_aspect::depth;
					break;
				}
				case GL_DEPTH24_STENCIL8:
				case GL_DEPTH32F_STENCIL8:
				{
					m_pitch = width * 4;
					m_aspect_flags = image_aspect::depth | image_aspect::stencil;
					break;
				}
				case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
				{
					m_compressed = true;
					m_pitch = width / 2;
					break;
				}
				case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
				case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
				{
					m_compressed = true;
					m_pitch = width;
					break;
				}
				default:
				{
					GLenum query_target = (target == GL_TEXTURE_CUBE_MAP) ? GL_TEXTURE_CUBE_MAP_POSITIVE_X : target;
					GLint r, g, b, a;

					glGetTexLevelParameteriv(query_target, 0, GL_TEXTURE_RED_SIZE, &r);
					glGetTexLevelParameteriv(query_target, 0, GL_TEXTURE_GREEN_SIZE, &g);
					glGetTexLevelParameteriv(query_target, 0, GL_TEXTURE_BLUE_SIZE, &b);
					glGetTexLevelParameteriv(query_target, 0, GL_TEXTURE_ALPHA_SIZE, &a);

					m_pitch = width * (r + g + b + a) / 8;
					break;
				}
				}

				if (!m_pitch)
				{
					fmt::throw_exception("Unhandled GL format 0x%X", sized_format);
				}

				if (format_class == RSX_FORMAT_CLASS_UNDEFINED)
				{
					if (m_aspect_flags != image_aspect::color)
					{
						rsx_log.error("Undefined format class for depth texture is not allowed");
					}
					else
					{
						format_class = RSX_FORMAT_CLASS_COLOR;
					}
				}
			}

			m_target = static_cast<texture::target>(target);
			m_internal_format = static_cast<internal_format>(sized_format);
			m_component_layout = { GL_ALPHA, GL_RED, GL_GREEN, GL_BLUE };
			m_format_class = format_class;
		}

		virtual ~texture()
		{
			if (m_id != GL_NONE)
			{
				glDeleteTextures(1, &m_id);
				m_id = GL_NONE;
			}
		}

		void set_native_component_layout(const std::array<GLenum, 4>& layout)
		{
			m_component_layout[0] = layout[0];
			m_component_layout[1] = layout[1];
			m_component_layout[2] = layout[2];
			m_component_layout[3] = layout[3];
		}

		target get_target() const noexcept
		{
			return m_target;
		}

		static bool compressed_format(internal_format format_) noexcept
		{
			switch (format_)
			{
			case internal_format::compressed_rgb_s3tc_dxt1:
			case internal_format::compressed_rgba_s3tc_dxt1:
			case internal_format::compressed_rgba_s3tc_dxt3:
			case internal_format::compressed_rgba_s3tc_dxt5:
				return true;
			default:
				return false;
			}
		}

		uint id() const noexcept
		{
			return m_id;
		}

		explicit operator bool() const noexcept
		{
			return (m_id != GL_NONE);
		}

		GLuint width() const
		{
			return m_width;
		}

		GLuint height() const
		{
			return m_height;
		}

		GLuint depth() const
		{
			return m_depth;
		}

		GLuint levels() const
		{
			return m_mipmaps;
		}

		GLuint pitch() const
		{
			return m_pitch;
		}

		GLboolean compressed() const
		{
			return m_compressed;
		}

		GLuint aspect() const
		{
			return m_aspect_flags;
		}

		rsx::format_class format_class() const
		{
			return m_format_class;
		}

		sizeu size2D() const
		{
			return{ m_width, m_height };
		}

		size3u size3D() const
		{
			const auto depth = (m_target == target::textureCUBE) ? 6 : m_depth;
			return{ m_width, m_height, depth };
		}

		texture::internal_format get_internal_format() const
		{
			return m_internal_format;
		}

		std::array<GLenum, 4> get_native_component_layout() const
		{
			return m_component_layout;
		}

		void copy_from(const void* src, texture::format format, texture::type type, int level, const coord3u region, const pixel_unpack_settings& pixel_settings)
		{
			pixel_settings.apply();

			switch (const auto target_ =static_cast<GLenum>(m_target))
			{
			case GL_TEXTURE_1D:
			{
				DSA_CALL(TextureSubImage1D, m_id, GL_TEXTURE_1D, level, region.x, region.width, static_cast<GLenum>(format), static_cast<GLenum>(type), src);
				break;
			}
			case GL_TEXTURE_2D:
			{
				DSA_CALL(TextureSubImage2D, m_id, GL_TEXTURE_2D, level, region.x, region.y, region.width, region.height, static_cast<GLenum>(format), static_cast<GLenum>(type), src);
				break;
			}
			case GL_TEXTURE_3D:
			case GL_TEXTURE_2D_ARRAY:
			{
				DSA_CALL(TextureSubImage3D, m_id, target_, level, region.x, region.y, region.z, region.width, region.height, region.depth, static_cast<GLenum>(format), static_cast<GLenum>(type), src);
				break;
			}
			case GL_TEXTURE_CUBE_MAP:
			{
				if (get_driver_caps().ARB_dsa_supported)
				{
					glTextureSubImage3D(m_id, level, region.x, region.y, region.z, region.width, region.height, region.depth, static_cast<GLenum>(format), static_cast<GLenum>(type), src);
				}
				else
				{
					rsx_log.warning("Cubemap upload via texture::copy_from is halfplemented!");
					auto ptr = static_cast<const u8*>(src);
					const auto end = std::min(6u, region.z + region.depth);
					for (unsigned face = region.z; face < end; ++face)
					{
						glTextureSubImage2DEXT(m_id, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, level, region.x, region.y, region.width, region.height, static_cast<GLenum>(format), static_cast<GLenum>(type), ptr);
						ptr += (region.width * region.height * 4); //TODO
					}
				}
				break;
			}
			}
		}

		void copy_from(const void* src, texture::format format, texture::type type, const pixel_unpack_settings& pixel_settings)
		{
			const coord3u region = { {}, size3D() };
			copy_from(src, format, type, 0, region, pixel_settings);
		}

		void copy_from(buffer &buf, u32 gl_format_type, u32 offset, u32 length)
		{
			if (get_target() != target::textureBuffer)
				fmt::throw_exception("OpenGL error: texture cannot copy from buffer");

			DSA_CALL(TextureBufferRange, m_id, GL_TEXTURE_BUFFER, gl_format_type, buf.id(), offset, length);
		}

		void copy_from(buffer_view &view)
		{
			copy_from(*view.value(), view.format(), view.offset(), view.range());
		}

		void copy_to(void* dst, texture::format format, texture::type type, int level, const coord3u& region, const pixel_pack_settings& pixel_settings) const
		{
			pixel_settings.apply();
			const auto& caps = get_driver_caps();

			if (!region.x && !region.y && !region.z &&
				region.width == m_width && region.height == m_height && region.depth == m_depth)
			{
				if (caps.ARB_dsa_supported)
					glGetTextureImage(m_id, level, static_cast<GLenum>(format), static_cast<GLenum>(type), INT32_MAX, dst);
				else
					glGetTextureImageEXT(m_id, static_cast<GLenum>(m_target), level, static_cast<GLenum>(format), static_cast<GLenum>(type), dst);
			}
			else if (caps.ARB_dsa_supported)
			{
				glGetTextureSubImage(m_id, level, region.x, region.y, region.z, region.width, region.height, region.depth,
					static_cast<GLenum>(format), static_cast<GLenum>(type), INT32_MAX, dst);
			}
			else
			{
				// Worst case scenario. For some reason, EXT_dsa does not have glGetTextureSubImage
				const auto target_ = static_cast<GLenum>(m_target);
				texture tmp{ target_, region.width, region.height, region.depth, 1, static_cast<GLenum>(m_internal_format) };
				glCopyImageSubData(m_id, target_, level, region.x, region.y, region.z, tmp.id(), target_, 0, 0, 0, 0,
					region.width, region.height, region.depth);

				const coord3u region2 = { {0, 0, 0}, region.size };
				tmp.copy_to(dst, format, type, 0, region2, pixel_settings);
			}
		}

		void copy_to(void* dst, texture::format format, texture::type type, const pixel_pack_settings& pixel_settings) const
		{
			const coord3u region = { {}, size3D() };
			copy_to(dst, format, type, 0, region, pixel_settings);
		}
	};

	class texture_view
	{
		GLuint m_id = GL_NONE;
		GLenum m_target = 0;
		GLenum m_format = 0;
		GLenum m_aspect_flags = 0;
		texture *m_image_data = nullptr;

		GLenum component_swizzle[4];

		void create(texture* data, GLenum target, GLenum sized_format, GLenum aspect_flags, const GLenum* argb_swizzle = nullptr)
		{
			m_target = target;
			m_format = sized_format;
			m_image_data = data;
			m_aspect_flags = aspect_flags;

			u32 num_layers;
			switch (target)
			{
			default:
				num_layers = 1; break;
			case GL_TEXTURE_CUBE_MAP:
				num_layers = 6; break;
			case GL_TEXTURE_2D_ARRAY:
				num_layers = data->depth(); break;
			}

			glGenTextures(1, &m_id);
			glTextureView(m_id, target, data->id(), sized_format, 0, data->levels(), 0, num_layers);

			if (argb_swizzle)
			{
				component_swizzle[0] = argb_swizzle[1];
				component_swizzle[1] = argb_swizzle[2];
				component_swizzle[2] = argb_swizzle[3];
				component_swizzle[3] = argb_swizzle[0];

				glBindTexture(m_target, m_id);
				glTexParameteriv(m_target, GL_TEXTURE_SWIZZLE_RGBA, reinterpret_cast<GLint*>(component_swizzle));
			}
			else
			{
				component_swizzle[0] = GL_RED;
				component_swizzle[1] = GL_GREEN;
				component_swizzle[2] = GL_BLUE;
				component_swizzle[3] = GL_ALPHA;
			}

			if (aspect_flags & image_aspect::stencil)
			{
				constexpr u32 depth_stencil_mask = (image_aspect::depth | image_aspect::stencil);
				ensure((aspect_flags & depth_stencil_mask) != depth_stencil_mask); // "Invalid aspect mask combination"

				glBindTexture(m_target, m_id);
				glTexParameteri(m_target, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
			}
		}

	public:
		texture_view(const texture_view&) = delete;
		texture_view(texture_view&&) = delete;

		texture_view(texture* data, GLenum target, GLenum sized_format,
			const GLenum* argb_swizzle = nullptr,
			GLenum aspect_flags = image_aspect::color | image_aspect::depth)
		{
			create(data, target, sized_format, aspect_flags, argb_swizzle);
		}

		texture_view(texture* data, const GLenum* argb_swizzle = nullptr,
			GLenum aspect_flags = image_aspect::color | image_aspect::depth)
		{
			GLenum target = static_cast<GLenum>(data->get_target());
			GLenum sized_format = static_cast<GLenum>(data->get_internal_format());
			create(data, target, sized_format, aspect_flags, argb_swizzle);
		}

		~texture_view()
		{
			if (m_id != GL_NONE)
			{
				glDeleteTextures(1, &m_id);
				m_id = GL_NONE;
			}
		}

		GLuint id() const
		{
			return m_id;
		}

		GLenum target() const
		{
			return m_target;
		}

		GLenum internal_format() const
		{
			return m_format;
		}

		GLenum aspect() const
		{
			return m_aspect_flags;
		}

		bool compare_swizzle(const GLenum* argb_swizzle) const
		{
			return (argb_swizzle[0] == component_swizzle[3] &&
				argb_swizzle[1] == component_swizzle[0] &&
				argb_swizzle[2] == component_swizzle[1] &&
				argb_swizzle[3] == component_swizzle[2]);
		}

		void bind() const
		{
			glBindTexture(m_target, m_id);
		}

		texture* image() const
		{
			return m_image_data;
		}

		std::array<GLenum, 4> component_mapping() const
		{
			return{ component_swizzle[3], component_swizzle[0], component_swizzle[1], component_swizzle[2] };
		}

		u32 encoded_component_map() const
		{
			// Unused, OGL supports proper component swizzles
			return 0u;
		}
	};

	class viewable_image : public texture
	{
		std::unordered_multimap<u32, std::unique_ptr<texture_view>> views;

public:
		using texture::texture;

		texture_view* get_view(u32 remap_encoding, const std::pair<std::array<u8, 4>, std::array<u8, 4>>& remap, GLenum aspect_flags = image_aspect::color | image_aspect::depth)
		{
			auto found = views.equal_range(remap_encoding);
			for (auto It = found.first; It != found.second; ++It)
			{
				if (It->second->aspect() & aspect_flags)
				{
					return It->second.get();
				}
			}

			ensure(aspect() & aspect_flags);
			auto mapping = apply_swizzle_remap(get_native_component_layout(), remap);
			auto view = std::make_unique<texture_view>(this, mapping.data(), aspect_flags);
			auto result = view.get();
			views.emplace(remap_encoding, std::move(view));
			return result;
		}

		void set_native_component_layout(const std::array<GLenum, 4>& layout)
		{
			if (m_component_layout[0] != layout[0] ||
				m_component_layout[1] != layout[1] ||
				m_component_layout[2] != layout[2] ||
				m_component_layout[3] != layout[3])
			{
				texture::set_native_component_layout(layout);
				views.clear();
			}
		}
	};

	class rbo
	{
		GLuint m_id = GL_NONE;

	public:
		rbo() = default;

		rbo(GLuint id)
		{
			set_id(id);
		}

		~rbo()
		{
			if (created())
				remove();
		}

		class save_binding_state
		{
			GLint m_old_value;

		public:
			save_binding_state(const rbo& new_state)
			{
				glGetIntegerv(GL_RENDERBUFFER_BINDING, &m_old_value);
				new_state.bind();
			}

			~save_binding_state()
			{
				glBindRenderbuffer(GL_RENDERBUFFER, m_old_value);
			}
		};

		void recreate()
		{
			if (created())
				remove();

			create();
		}

		void recreate(texture::format format, u32 width, u32 height)
		{
			if (created())
				remove();

			create(format, width, height);
		}

		void create()
		{
			glGenRenderbuffers(1, &m_id);
		}

		void create(texture::format format, u32 width, u32 height)
		{
			create();
			storage(format, width, height);
		}

		void bind() const
		{
			glBindRenderbuffer(GL_RENDERBUFFER, m_id);
		}

		void storage(texture::format format, u32 width, u32 height)
		{
			save_binding_state save(*this);
			glRenderbufferStorage(GL_RENDERBUFFER, static_cast<GLenum>(format), width, height);
		}

		void remove()
		{
			if (m_id != GL_NONE)
			{
				glDeleteRenderbuffers(1, &m_id);
				m_id = GL_NONE;
			}
		}

		uint id() const
		{
			return m_id;
		}

		void set_id(uint id)
		{
			m_id = id;
		}

		bool created() const
		{
			return m_id != GL_NONE;
		}

		explicit operator bool() const
		{
			return created();
		}
	};

	enum class indices_type
	{
		ubyte = GL_UNSIGNED_BYTE,
		ushort = GL_UNSIGNED_SHORT,
		uint = GL_UNSIGNED_INT
	};

	class fbo
	{
		GLuint m_id = GL_NONE;
		size2i m_size;

	protected:
		std::unordered_map<GLenum, GLuint> m_resource_bindings;

	public:
		fbo() = default;

		fbo(GLuint id)
		{
			set_id(id);
		}

		~fbo()
		{
			if (created())
				remove();
		}

		class save_binding_state
		{
			GLint m_last_binding;
			bool reset = true;

		public:
			save_binding_state(const fbo& new_binding)
			{
				glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_last_binding);

				if (m_last_binding + 0u != new_binding.id())
					new_binding.bind();
				else
					reset = false;
			}

			~save_binding_state()
			{
				if (reset)
					glBindFramebuffer(GL_FRAMEBUFFER, m_last_binding);
			}
		};

		class attachment
		{
		public:
			enum class type
			{
				color = GL_COLOR_ATTACHMENT0,
				depth = GL_DEPTH_ATTACHMENT,
				stencil = GL_STENCIL_ATTACHMENT,
				depth_stencil = GL_DEPTH_STENCIL_ATTACHMENT
			};

		protected:
			GLuint m_id = GL_NONE;
			fbo &m_parent;

		public:
			attachment(fbo& parent, type type)
				: m_id(static_cast<int>(type))
				, m_parent(parent)
			{
			}

			void set_id(uint id)
			{
				m_id = id;
			}

			uint id() const
			{
				return m_id;
			}

			GLuint resource_id() const
			{
				const auto found = m_parent.m_resource_bindings.find(m_id);
				if (found != m_parent.m_resource_bindings.end())
				{
					return found->second;
				}

				return 0;
			}

			void operator = (const rbo& rhs)
			{
				save_binding_state save(m_parent);
				m_parent.m_resource_bindings[m_id] = rhs.id();
				glFramebufferRenderbuffer(GL_FRAMEBUFFER, m_id, GL_RENDERBUFFER, rhs.id());
			}

			void operator = (const texture& rhs)
			{
				save_binding_state save(m_parent);

				ensure(rhs.get_target() == texture::target::texture2D);
				m_parent.m_resource_bindings[m_id] = rhs.id();
				glFramebufferTexture2D(GL_FRAMEBUFFER, m_id, GL_TEXTURE_2D, rhs.id(), 0);
			}

			void operator = (const GLuint rhs)
			{
				save_binding_state save(m_parent);
				m_parent.m_resource_bindings[m_id] = rhs;
				glFramebufferTexture2D(GL_FRAMEBUFFER, m_id, GL_TEXTURE_2D, rhs, 0);
			}
		};

		class indexed_attachment : public attachment
		{
		public:
			indexed_attachment(fbo& parent, type type) : attachment(parent, type)
			{
			}

			attachment operator[](int index) const
			{
				return{ m_parent, type(id() + index) };
			}

			std::vector<attachment> range(int from, int count) const
			{
				std::vector<attachment> result;

				for (int i = from; i < from + count; ++i)
					result.push_back((*this)[i]);

				return result;
			}

			using attachment::operator =;
		};

		indexed_attachment color{ *this, attachment::type::color };
		attachment depth{ *this, attachment::type::depth };
		attachment stencil{ *this, attachment::type::stencil };
		attachment depth_stencil{ *this, attachment::type::depth_stencil };

		enum class target
		{
			read_frame_buffer = GL_READ_FRAMEBUFFER,
			draw_frame_buffer = GL_DRAW_FRAMEBUFFER
		};

		void create();
		void bind() const;
		void blit(const fbo& dst, areai src_area, areai dst_area, buffers buffers_ = buffers::color, filter filter_ = filter::nearest) const;
		void bind_as(target target_) const;
		void remove();
		bool created() const;
		bool check() const;

		void recreate();
		void draw_buffer(const attachment& buffer) const;
		void draw_buffers(const std::initializer_list<attachment>& indexes) const;

		void read_buffer(const attachment& buffer) const;

		void draw_arrays(rsx::primitive_type mode, GLsizei count, GLint first = 0) const;
		void draw_arrays(const buffer& buffer, rsx::primitive_type mode, GLsizei count, GLint first = 0) const;
		void draw_arrays(const vao& buffer, rsx::primitive_type mode, GLsizei count, GLint first = 0) const;

		void draw_elements(rsx::primitive_type mode, GLsizei count, indices_type type, const GLvoid *indices) const;
		void draw_elements(const buffer& buffer, rsx::primitive_type mode, GLsizei count, indices_type type, const GLvoid *indices) const;
		void draw_elements(rsx::primitive_type mode, GLsizei count, indices_type type, const buffer& indices, usz indices_buffer_offset = 0) const;
		void draw_elements(const buffer& buffer_, rsx::primitive_type mode, GLsizei count, indices_type type, const buffer& indices, usz indices_buffer_offset = 0) const;
		void draw_elements(rsx::primitive_type mode, GLsizei count, const GLubyte *indices) const;
		void draw_elements(const buffer& buffer, rsx::primitive_type mode, GLsizei count, const GLubyte *indices) const;
		void draw_elements(rsx::primitive_type mode, GLsizei count, const GLushort *indices) const;
		void draw_elements(const buffer& buffer, rsx::primitive_type mode, GLsizei count, const GLushort *indices) const;
		void draw_elements(rsx::primitive_type mode, GLsizei count, const GLuint *indices) const;
		void draw_elements(const buffer& buffer, rsx::primitive_type mode, GLsizei count, const GLuint *indices) const;

		void clear(buffers buffers_) const;
		void clear(buffers buffers_, color4f color_value, double depth_value, u8 stencil_value) const;

		void copy_from(const void* pixels, const sizei& size, gl::texture::format format_, gl::texture::type type_, class pixel_unpack_settings pixel_settings = pixel_unpack_settings()) const;
		void copy_from(const buffer& buf, const sizei& size, gl::texture::format format_, gl::texture::type type_, class pixel_unpack_settings pixel_settings = pixel_unpack_settings()) const;

		void copy_to(void* pixels, coordi coord, gl::texture::format format_, gl::texture::type type_, class pixel_pack_settings pixel_settings = pixel_pack_settings()) const;
		void copy_to(const buffer& buf, coordi coord, gl::texture::format format_, gl::texture::type type_, class pixel_pack_settings pixel_settings = pixel_pack_settings()) const;

		static fbo get_bound_draw_buffer();
		static fbo get_bound_read_buffer();
		static fbo get_bound_buffer();

		GLuint id() const;
		void set_id(GLuint id);

		void set_extents(const size2i& extents);
		size2i get_extents() const;

		bool matches(const std::array<GLuint, 4>& color_targets, GLuint depth_stencil_target) const;
		bool references_any(const std::vector<GLuint>& resources) const;

		explicit operator bool() const
		{
			return created();
		}
	};

	extern const fbo screen;

	namespace glsl
	{
		class shader
		{
			std::string source;
			::glsl::program_domain type;
			GLuint m_id = GL_NONE;

			fence m_compiled_fence;
			fence m_init_fence;

			shared_mutex m_compile_lock;
			atomic_t<bool> m_is_compiled{};

			void precompile()
			{
				const char* str = source.c_str();
				const GLint length = ::narrow<GLint>(source.length());

				if (g_cfg.video.log_programs)
				{
					std::string base_name;
					switch (type)
					{
					case ::glsl::program_domain::glsl_vertex_program:
						base_name = "shaderlog/VertexProgram";
						break;
					case ::glsl::program_domain::glsl_fragment_program:
						base_name = "shaderlog/FragmentProgram";
						break;
					case ::glsl::program_domain::glsl_compute_program:
						base_name = "shaderlog/ComputeProgram";
						break;
					}

					fs::file(fs::get_cache_dir() + base_name + std::to_string(m_id) + ".glsl", fs::rewrite).write(str, length);
				}

				glShaderSource(m_id, 1, &str, &length);

				m_init_fence.create();
				flush_command_queue(m_init_fence);
			}

		public:
			shader() = default;

			shader(GLuint id)
			{
				set_id(id);
			}

			shader(::glsl::program_domain type_, const std::string& src)
			{
				create(type_, src);
			}

			~shader()
			{
				if (created())
					remove();
			}

			void create(::glsl::program_domain type_, const std::string& src)
			{
				type = type_;
				source = src;

				GLenum shader_type;
				switch (type)
				{
				case ::glsl::program_domain::glsl_vertex_program:
					shader_type = GL_VERTEX_SHADER;
					break;
				case ::glsl::program_domain::glsl_fragment_program:
					shader_type = GL_FRAGMENT_SHADER;
					break;
				case ::glsl::program_domain::glsl_compute_program:
					shader_type = GL_COMPUTE_SHADER;
					break;
				default:
					rsx_log.fatal("gl::glsl::shader::compile(): Unhandled shader type");
				}

				m_id = glCreateShader(shader_type);
				precompile();
			}

			shader& compile()
			{
				std::lock_guard lock(m_compile_lock);
				if (m_is_compiled)
				{
					// Another thread compiled this already
					return *this;
				}

				ensure(!m_init_fence.is_empty()); // Do not attempt to compile a shader_view!!
				m_init_fence.server_wait_sync();

				glCompileShader(m_id);

				GLint status = GL_FALSE;
				glGetShaderiv(m_id, GL_COMPILE_STATUS, &status);

				if (status == GL_FALSE)
				{
					GLint length = 0;
					glGetShaderiv(m_id, GL_INFO_LOG_LENGTH, &length);

					std::string error_msg;
					if (length)
					{
						std::unique_ptr<GLchar[]> buf(new char[length + 1]);
						glGetShaderInfoLog(m_id, length, nullptr, buf.get());
						error_msg = buf.get();
					}

					rsx_log.fatal("Compilation failed: %s", error_msg);
				}

				m_compiled_fence.create();
				flush_command_queue(m_compiled_fence);

				m_is_compiled = true;
				return *this;
			}

			void remove()
			{
				if (m_id != GL_NONE)
				{
					glDeleteShader(m_id);
					m_id = GL_NONE;
				}
			}

			uint id() const
			{
				return m_id;
			}

			const std::string& get_source() const
			{
				return source;
			}

			fence get_compile_fence_sync() const
			{
				return m_compiled_fence;
			}

			void set_id(uint id)
			{
				m_id = id;
			}

			bool created() const
			{
				return m_id != GL_NONE;
			}

			bool compiled() const
			{
				return m_is_compiled;
			}

			explicit operator bool() const
			{
				return created();
			}
		};

		class shader_view : public shader
		{
		public:
			shader_view(GLuint id) : shader(id)
			{
			}

			~shader_view()
			{
				set_id(0);
			}
		};

		class program
		{
			GLuint m_id = GL_NONE;
			fence m_fence;

		public:
			class uniform_t
			{
				program& m_program;
				GLint m_location;

			public:
				uniform_t(program& program, GLint location)
					: m_program(program)
					, m_location(location)
				{
				}

				GLint location() const
				{
					return m_location;
				}

				void operator = (int rhs) const { glProgramUniform1i(m_program.id(), location(), rhs); }
				void operator = (unsigned rhs) const { glProgramUniform1ui(m_program.id(), location(), rhs); }
				void operator = (float rhs) const { glProgramUniform1f(m_program.id(), location(), rhs); }
				void operator = (const color1i& rhs) const { glProgramUniform1i(m_program.id(), location(), rhs.r); }
				void operator = (const color1f& rhs) const { glProgramUniform1f(m_program.id(), location(), rhs.r); }
				void operator = (const color2i& rhs) const { glProgramUniform2i(m_program.id(), location(), rhs.r, rhs.g); }
				void operator = (const color2f& rhs) const { glProgramUniform2f(m_program.id(), location(), rhs.r, rhs.g); }
				void operator = (const color3i& rhs) const { glProgramUniform3i(m_program.id(), location(), rhs.r, rhs.g, rhs.b); }
				void operator = (const color3f& rhs) const { glProgramUniform3f(m_program.id(), location(), rhs.r, rhs.g, rhs.b); }
				void operator = (const color4i& rhs) const { glProgramUniform4i(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a); }
				void operator = (const color4f& rhs) const { glProgramUniform4f(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a); }
				void operator = (const areaf& rhs) const { glProgramUniform4f(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2); }
				void operator = (const areai& rhs) const { glProgramUniform4i(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2); }
				void operator = (const std::vector<int>& rhs) const { glProgramUniform1iv(m_program.id(), location(), ::size32(rhs), rhs.data()); }
			};

			class uniforms_t
			{
				program& m_program;
				std::unordered_map<std::string, GLint> locations;
				int active_texture = 0;

			public:
				uniforms_t(program* program) : m_program(*program)
				{
				}

				void clear()
				{
					locations.clear();
					active_texture = 0;
				}

				bool has_location(const std::string &name, int *location = nullptr)
				{
					auto found = locations.find(name);
					if (found != locations.end())
					{
						if (location)
						{
							*location = found->second;
						}

						return (found->second >= 0);
					}

					auto result = glGetUniformLocation(m_program.id(), name.c_str());
					locations[name] = result;

					if (location)
					{
						*location = result;
					}

					return (result >= 0);
				}

				GLint location(const std::string &name)
				{
					auto found = locations.find(name);
					if (found != locations.end())
					{
						if (found->second >= 0)
						{
							return found->second;
						}
						else
						{
							rsx_log.fatal("%s not found.", name);
							return -1;
						}
					}

					auto result = glGetUniformLocation(m_program.id(), name.c_str());

					if (result < 0)
					{
						rsx_log.fatal("%s not found.", name);
						return result;
					}

					locations[name] = result;
					return result;
				}

				int texture(GLint location, int active_texture, const gl::texture_view& texture)
				{
					glActiveTexture(GL_TEXTURE0 + active_texture);
					texture.bind();
					(*this)[location] = active_texture;

					return active_texture;
				}

				uniform_t operator[](GLint location)
				{
					return{ m_program, location };
				}

				uniform_t operator[](const std::string &name)
				{
					return{ m_program, location(name) };
				}

				void swap(uniforms_t& uniforms)
				{
					locations.swap(uniforms.locations);
					std::swap(active_texture, uniforms.active_texture);
				}
			} uniforms{ this };

			program& recreate()
			{
				if (created())
					remove();

				return create();
			}

			program& create()
			{
				m_id = glCreateProgram();
				return *this;
			}

			void remove()
			{
				if (m_id != GL_NONE)
				{
					glDeleteProgram(m_id);
					m_id = GL_NONE;
				}
				uniforms.clear();
			}

			static program get_current_program()
			{
				GLint id;
				glGetIntegerv(GL_CURRENT_PROGRAM, &id);
				return{ static_cast<GLuint>(id) };
			}

			void use()
			{
				glUseProgram(m_id);
			}

			void link(std::function<void(program*)> init_func = {})
			{
				glLinkProgram(m_id);

				GLint status = GL_FALSE;
				glGetProgramiv(m_id, GL_LINK_STATUS, &status);

				if (status == GL_FALSE)
				{
					GLint length = 0;
					glGetProgramiv(m_id, GL_INFO_LOG_LENGTH, &length);

					std::string error_msg;
					if (length)
					{
						std::unique_ptr<GLchar[]> buf(new char[length + 1]);
						glGetProgramInfoLog(m_id, length, nullptr, buf.get());
						error_msg = buf.get();
					}

					rsx_log.fatal("Linkage failed: %s", error_msg);
				}
				else
				{
					if (init_func)
					{
						init_func(this);
					}

					m_fence.create();
					flush_command_queue(m_fence);
				}
			}

			void validate()
			{
				glValidateProgram(m_id);

				GLint status = GL_FALSE;
				glGetProgramiv(m_id, GL_VALIDATE_STATUS, &status);

				if (status == GL_FALSE)
				{
					GLint length = 0;
					glGetProgramiv(m_id, GL_INFO_LOG_LENGTH, &length);

					std::string error_msg;
					if (length)
					{
						std::unique_ptr<GLchar[]> buf(new char[length + 1]);
						glGetProgramInfoLog(m_id, length, nullptr, buf.get());
						error_msg = buf.get();
					}

					rsx_log.error("Validation failed: %s", error_msg.c_str());
				}
			}

			GLuint id() const
			{
				return m_id;
			}

			void set_id(uint id)
			{
				uniforms.clear();
				m_id = id;
			}

			bool created() const
			{
				return m_id != GL_NONE;
			}

			void sync()
			{
				if (!m_fence.check_signaled())
				{
					m_fence.server_wait_sync();
				}
			}

			explicit operator bool() const
			{
				return created();
			}

			program& attach(const shader& shader_)
			{
				glAttachShader(m_id, shader_.id());
				return *this;
			}

			program& bind_attribute_location(const std::string& name, int index)
			{
				glBindAttribLocation(m_id, index, name.c_str());
				return *this;
			}

			program& bind_fragment_data_location(const std::string& name, int color_number)
			{
				glBindFragDataLocation(m_id, color_number, name.c_str());
				return *this;
			}

			int attribute_location(const std::string& name)
			{
				return glGetAttribLocation(m_id, name.c_str());
			}

			int uniform_location(const std::string& name)
			{
				return glGetUniformLocation(m_id, name.c_str());
			}

			program() = default;
			program(const program&) = delete;
			program(program&& program_)
			{
				swap(program_);
			}

			program(GLuint id)
			{
				set_id(id);
			}

			~program()
			{
				if (created())
					remove();
			}

			void swap(program& program_)
			{
				auto my_old_id = id();
				set_id(program_.id());
				program_.set_id(my_old_id);
				uniforms.swap(program_.uniforms);
			}

			program& operator = (const program& rhs) = delete;
			program& operator = (program&& rhs)
			{
				swap(rhs);
				return *this;
			}
		};

		class program_view : public program
		{
		public:
			program_view(GLuint id) : program(id)
			{
			}

			~program_view()
			{
				set_id(0);
			}
		};
	}

	class blitter
	{
		struct save_binding_state
		{
			GLuint old_fbo;

			save_binding_state()
			{
				glGetIntegerv(GL_FRAMEBUFFER_BINDING, reinterpret_cast<GLint*>(&old_fbo));
			}

			~save_binding_state()
			{
				glBindFramebuffer(GL_FRAMEBUFFER, old_fbo);
			}
		};

		fbo blit_src;
		fbo blit_dst;

	public:

		void init()
		{
			blit_src.create();
			blit_dst.create();
		}

		void destroy()
		{
			blit_dst.remove();
			blit_src.remove();
		}

		void scale_image(gl::command_context& cmd, const texture* src, texture* dst, areai src_rect, areai dst_rect, bool linear_interpolation,
			const rsx::typeless_xfer& xfer_info);

		void fast_clear_image(gl::command_context& cmd, const texture* dst, const color4f& color);
		void fast_clear_image(gl::command_context& cmd, const texture* dst, float depth, u8 stencil);
	};
}
