浏览代码

libobs-metal: Add new Metal renderer.

mntone 8 年之前
父节点
当前提交
e8ea0baede

+ 5 - 0
CMakeLists.txt

@@ -31,6 +31,7 @@ set(SCRIPTING_ENABLED OFF CACHE BOOL "Interal global cmake variable" FORCE)
 include(ObsHelpers)
 include(ObsCpack)
 include(GNUInstallDirs)
+include(MacOSVersion)
 
 # Must be a string in the format of "x.x.x-rcx"
 if(DEFINED RELEASE_CANDIDATE)
@@ -174,6 +175,10 @@ if(NOT INSTALLER_RUN)
 		add_subdirectory(libobs-d3d11)
 	endif()
 
+	if(APPLE AND ${MACOS_VERSION} VERSION_GREATER 10.11)
+		add_subdirectory(libobs-metal)
+	endif()
+
 	add_subdirectory(libobs-opengl)
 	add_subdirectory(libobs)
 	add_subdirectory(plugins)

+ 17 - 2
UI/obs-app.cpp

@@ -52,6 +52,10 @@
 #include <signal.h>
 #endif
 
+#ifdef __APPLE__
+#include <util/mac/mac-version.h>
+#endif
+
 #include <iostream>
 
 #include "ui-config.h"
@@ -382,6 +386,8 @@ bool OBSApp::InitGlobalConfigDefaults()
 #if _WIN32
 	config_set_default_string(globalConfig, "Video", "Renderer",
 			"Direct3D 11");
+#elif defined(__APPLE__) && defined(__MAC_10_11)
+	config_set_default_string(globalConfig, "Video", "Renderer", "Metal");
 #else
 	config_set_default_string(globalConfig, "Video", "Renderer", "OpenGL");
 #endif
@@ -1222,8 +1228,17 @@ const char *OBSApp::GetRenderModule() const
 	const char *renderer = config_get_string(globalConfig, "Video",
 			"Renderer");
 
-	return (astrcmpi(renderer, "Direct3D 11") == 0) ?
-		DL_D3D11 : DL_OPENGL;
+#if defined(_WIN32)
+	return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL;
+#elif defined(__APPLE__) && defined(__MAC_10_12)
+	struct mac_version_info ver;
+	get_mac_ver(&ver);
+	
+	return (ver.identifier >= MACOS_SIERRA &&
+		astrcmpi(renderer, "Metal") == 0) ? DL_METAL : DL_OPENGL;
+#else
+	return DL_OPENGL;
+#endif
 }
 
 static bool StartupOBS(const char *locale, profiler_name_store_t *store)

+ 6 - 8
UI/window-basic-main.cpp

@@ -3460,12 +3460,6 @@ bool OBSBasic::Active() const
 	return outputHandler->Active();
 }
 
-#ifdef _WIN32
-#define IS_WIN32 1
-#else
-#define IS_WIN32 0
-#endif
-
 static inline int AttemptToResetVideo(struct obs_video_info *ovi)
 {
 	return obs_reset_video(ovi);
@@ -3579,14 +3573,15 @@ int OBSBasic::ResetVideo()
 	}
 
 	ret = AttemptToResetVideo(&ovi);
-	if (IS_WIN32 && ret != OBS_VIDEO_SUCCESS) {
+#if defined(_WIN32) || (defined(__APPLE__) && defined(__MAC_10_11))
+	if (ret != OBS_VIDEO_SUCCESS) {
 		if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) {
 			blog(LOG_WARNING, "Tried to reset when "
 			                  "already active");
 			return ret;
 		}
 
-		/* Try OpenGL if DirectX fails on windows */
+		/* Try OpenGL if DirectX/Metal fails on windows/macOS */
 		if (astrcmpi(ovi.graphics_module, DL_OPENGL) != 0) {
 			blog(LOG_WARNING, "Failed to initialize obs video (%d) "
 					  "with graphics_module='%s', retrying "
@@ -3597,6 +3592,9 @@ int OBSBasic::ResetVideo()
 			ret = AttemptToResetVideo(&ovi);
 		}
 	} else if (ret == OBS_VIDEO_SUCCESS) {
+#else
+	if (ret == OBS_VIDEO_SUCCESS) {
+#endif
 		ResizePreview(ovi.base_width, ovi.base_height);
 		if (program)
 			ResizeProgram(ovi.base_width, ovi.base_height);

+ 45 - 11
UI/window-basic-settings.cpp

@@ -50,6 +50,10 @@
 #include <util/platform.h>
 #include "ui-config.h"
 
+#ifdef __APPLE__
+#include <util/mac/mac-version.h>
+#endif
+
 using namespace std;
 
 // Used for QVariant in codec comboboxes
@@ -466,6 +470,26 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 	delete ui->enableAutoUpdates;
 	ui->enableAutoUpdates = nullptr;
 #endif
+	
+#if defined(__APPLE__) && defined(__MAC_10_12)
+	struct mac_version_info ver;
+	get_mac_ver(&ver);
+	
+	if (ver.identifier < MACOS_SIERRA) {
+#endif
+#ifndef _WIN32
+		delete ui->rendererLabel;
+		delete ui->renderer;
+		delete ui->adapterLabel;
+		delete ui->adapter;
+		ui->rendererLabel = nullptr;
+		ui->renderer = nullptr;
+		ui->adapterLabel = nullptr;
+		ui->adapter = nullptr;
+#endif
+#if defined(__APPLE__) && defined(__MAC_10_12)
+	}
+#endif
 
 #if !defined(_WIN32) && !defined(__APPLE__) && !HAVE_PULSEAUDIO
 	delete ui->audioAdvGroupBox;
@@ -506,10 +530,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 		ui->processPriority->addItem(QTStr(pri.name), pri.val);
 
 #else
-	delete ui->rendererLabel;
-	delete ui->renderer;
-	delete ui->adapterLabel;
-	delete ui->adapter;
 	delete ui->processPriorityLabel;
 	delete ui->processPriority;
 	delete ui->advancedGeneralGroupBox;
@@ -520,10 +540,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 #if defined(__APPLE__) || HAVE_PULSEAUDIO
 	delete ui->disableAudioDucking;
 #endif
-	ui->rendererLabel = nullptr;
-	ui->renderer = nullptr;
-	ui->adapterLabel = nullptr;
-	ui->adapter = nullptr;
 	ui->processPriorityLabel = nullptr;
 	ui->processPriority = nullptr;
 	ui->advancedGeneralGroupBox = nullptr;
@@ -1174,12 +1190,28 @@ void OBSBasicSettings::LoadGeneralSettings()
 
 void OBSBasicSettings::LoadRendererList()
 {
-#ifdef _WIN32
+#if defined(_WIN32) || (defined(__APPLE__) && defined(__MAC_10_12))
+#if defined(__APPLE__)
+	struct mac_version_info ver;
+	get_mac_ver(&ver);
+	
+	if (ver.identifier < MACOS_SIERRA)
+		return;
+#endif
+	
 	const char *renderer = config_get_string(GetGlobalConfig(), "Video",
 			"Renderer");
 
+#ifdef _WIN32
 	ui->renderer->addItem(QT_UTF8("Direct3D 11"));
+#endif
+#if defined(__APPLE__) && defined(__MAC_10_12)
+	if (ver.identifier >= MACOS_SIERRA)
+		ui->renderer->addItem(QT_UTF8("Metal"));
+#endif
+#ifdef _WIN32
 	if (opt_allow_opengl || strcmp(renderer, "OpenGL") == 0)
+#endif
 		ui->renderer->addItem(QT_UTF8("OpenGL"));
 
 	int idx = ui->renderer->findText(QT_UTF8(renderer));
@@ -2880,11 +2912,13 @@ void OBSBasicSettings::SaveAdvancedSettings()
 	QString lastMonitoringDevice = config_get_string(main->Config(),
 			"Audio", "MonitoringDeviceId");
 
-#ifdef _WIN32
+#if defined(_WIN32) || defined(__APPLE__)
 	if (WidgetChanged(ui->renderer))
 		config_set_string(App()->GlobalConfig(), "Video", "Renderer",
 				QT_TO_UTF8(ui->renderer->currentText()));
-
+#endif
+	
+#ifdef _WIN32
 	std::string priority =
 		QT_TO_UTF8(ui->processPriority->currentData().toString());
 	config_set_string(App()->GlobalConfig(), "General", "ProcessPriority",

+ 33 - 0
cmake/Modules/MacOSVersion.cmake

@@ -0,0 +1,33 @@
+# Once done these will be defined:
+#
+#   MACOS_VERSION
+
+if(APPLE)
+	# NOTE: CMAKE_SYSTEM_VERSION is Darwin version
+	# - 12.x.x = OS X Mountain Lion (10.8)
+	# - 13.x.x = OS X Mavericks (10.9)
+	# - 14.x.x = OS X Yosemite (10.10)
+	# - 15.x.x = OS X El Capitan (10.11)
+	# - 16.x.x = macOS Sierra (10.12)
+	# - 17.x.x = macOS High Sierra (10.13)
+
+	if(${CMAKE_SYSTEM_VERSION} GREATER 12 AND ${CMAKE_SYSTEM_VERSION} LESS 13)
+		set(MACOS_VERSION 10.8)
+	elseif(${CMAKE_SYSTEM_VERSION} GREATER 13 AND ${CMAKE_SYSTEM_VERSION} LESS 14)
+		set(MACOS_VERSION 10.9)
+	elseif(${CMAKE_SYSTEM_VERSION} GREATER 14 AND ${CMAKE_SYSTEM_VERSION} LESS 15)
+		set(MACOS_VERSION 10.10)
+	elseif(${CMAKE_SYSTEM_VERSION} GREATER 15 AND ${CMAKE_SYSTEM_VERSION} LESS 16)
+		set(MACOS_VERSION 10.11)
+	elseif(${CMAKE_SYSTEM_VERSION} GREATER 16 AND ${CMAKE_SYSTEM_VERSION} LESS 17)
+		set(MACOS_VERSION 10.12)
+	elseif(${CMAKE_SYSTEM_VERSION} GREATER 17 AND ${CMAKE_SYSTEM_VERSION} LESS 18)
+		set(MACOS_VERSION 10.13)
+	else()
+		set(MACOS_VERSION 10.0)
+	endif()
+
+	message(STATUS "macOS Version ${MACOS_VERSION}")
+else()
+	set(MACOS_VERSION 0.0)
+endif()

+ 1 - 1
cmake/Modules/ObsHelpers.cmake

@@ -553,7 +553,7 @@ function(install_obs_plugin_with_data target datadir)
 endfunction()
 
 function(define_graphic_modules target)
-	foreach(dl_lib opengl d3d9 d3d11)
+	foreach(dl_lib opengl d3d9 d3d11 metal)
 		string(TOUPPER ${dl_lib} dl_lib_upper)
 		if(TARGET libobs-${dl_lib})
 			if(UNIX AND UNIX_STRUCTURE)

+ 71 - 0
libobs-metal/CMakeLists.txt

@@ -0,0 +1,71 @@
+project(libobs-metal)
+
+find_library(APPKIT AppKit)
+mark_as_advanced(APPKIT)
+include_directories(${APPKIT})
+
+find_library(QUARTZCORE QuartzCore)
+mark_as_advanced(QUARTZCORE)
+include_directories(${QUARTZCORE})
+
+find_library(METAL Metal)
+mark_as_advanced(METAL)
+include_directories(${METAL})
+
+set(libobs-metal_PLATFORM_DEPS
+	${APPKIT}
+	${QUARTZCORE})
+
+set(libobs-metal_PLATFORM_DEPS_WEAK
+	Metal)
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+
+add_definitions(-DLIBOBS_EXPORTS)
+
+set(libobs-metal_SOURCES
+	metal-device.mm
+	metal-indexbuffer.mm
+	metal-rebuild.mm
+	metal-samplerstate.mm
+	metal-shader.mm
+	metal-shaderbuilder.cpp
+	metal-shaderprocessor.mm
+	metal-stagesurf.mm
+	metal-subsystem.mm
+	metal-swapchain.mm
+	metal-texture2d.mm
+	metal-vertexbuffer.mm
+	metal-zstencilbuffer.mm)
+
+set(libobs-metal_HEADERS
+	metal-shaderprocessor.hpp
+	metal-subsystem.hpp)
+
+add_library(libobs-metal MODULE
+	${libobs-metal_SOURCES}
+	${libobs-metal_HEADERS})
+set_target_properties(libobs-metal
+	PROPERTIES
+		OUTPUT_NAME libobs-metal
+		PREFIX ""
+		XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES)
+target_compile_options(libobs-metal
+	PUBLIC
+		-msse
+		-msse2
+		-msse3
+		-msse4.1
+		-msse4.2)
+target_link_libraries(libobs-metal
+	PRIVATE
+		${libobs-metal_PLATFORM_DEPS}
+	PUBLIC
+		libobs)
+foreach(FRAMEWORK ${libobs-metal_PLATFORM_DEPS_WEAK})
+	target_link_libraries(libobs-metal
+		PRIVATE
+			"-weak_framework ${FRAMEWORK}")
+endforeach()
+
+install_obs_core(libobs-metal)

+ 316 - 0
libobs-metal/metal-device.mm

@@ -0,0 +1,316 @@
+#include <cinttypes>
+#include <util/base.h>
+
+#include "metal-subsystem.hpp"
+
+using namespace std;
+
+void gs_device::InitDevice(uint32_t deviceIdx)
+{
+	NSArray *devices;
+	
+	devIdx  = deviceIdx;
+	devices = MTLCopyAllDevices();
+	if (devices == nil)
+		throw "Failed to create MTLDevice";
+
+	for (size_t i = 0; i < devices.count; i++) {
+		if (i == devIdx) {
+			device = devices[i];
+			break;
+		}
+	}
+
+	blog(LOG_INFO, "Loading up Metal on adapter %s (%" PRIu32 ")",
+			device.name.UTF8String, deviceIdx);
+
+	if ([device supportsFeatureSet:MTLFeatureSet_OSX_GPUFamily1_v2]) {
+		featureSetFamily  = 1;
+		featureSetVersion = 2;
+	} else
+		throw "Failed to initialize Metal";
+
+	blog(LOG_INFO, "Metal loaded successfully, feature set used: %u_v%u",
+			featureSetFamily, featureSetVersion);
+}
+
+void gs_device::SetClear()
+{
+	ClearState state = clearStates.top().second;
+	
+	if (state.flags & GS_CLEAR_COLOR) {
+		MTLRenderPassColorAttachmentDescriptor *colorAttachment =
+				passDesc.colorAttachments[0];
+		colorAttachment.loadAction = MTLLoadActionClear;
+		colorAttachment.clearColor = MTLClearColorMake(
+				state.color.x, state.color.y, state.color.z,
+				state.color.w);
+	} else
+		passDesc.colorAttachments[0].loadAction = MTLLoadActionLoad;
+	
+	if (state.flags & GS_CLEAR_DEPTH) {
+		MTLRenderPassDepthAttachmentDescriptor *depthAttachment =
+				passDesc.depthAttachment;
+		depthAttachment.loadAction = MTLLoadActionClear;
+		depthAttachment.clearDepth = state.depth;
+	} else
+		passDesc.depthAttachment.loadAction = MTLLoadActionLoad;
+	
+	if (state.flags & GS_CLEAR_STENCIL) {
+		MTLRenderPassStencilAttachmentDescriptor *stencilAttachment =
+				passDesc.stencilAttachment;
+		stencilAttachment.loadAction   = MTLLoadActionClear;
+		stencilAttachment.clearStencil = state.stencil;
+	} else
+		passDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
+	
+	clearStates.pop();
+	if (clearStates.size())
+		preserveClearTarget = clearStates.top().first;
+	else
+		preserveClearTarget = nullptr;
+}
+
+void gs_device::UploadVertexBuffer(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	vector<id<MTLBuffer>> buffers;
+	vector<NSUInteger>    offsets;
+	
+	if (curVertexBuffer && curVertexShader) {
+		curVertexBuffer->MakeBufferList(curVertexShader, buffers);
+		if (curVertexBuffer->isDynamic)
+			curVertexBuffer->Release();
+	} else {
+		size_t buffersToClear = curVertexShader ?
+				curVertexShader->NumBuffersExpected() : 0;
+		buffers.resize(buffersToClear);
+	}
+	
+	offsets.resize(buffers.size());
+	
+	[commandEncoder setVertexBuffers:buffers.data()
+			offsets:offsets.data()
+			withRange:NSMakeRange(0, buffers.size())];
+	
+	lastVertexBuffer = curVertexBuffer;
+	lastVertexShader = curVertexShader;
+}
+
+void gs_device::UploadTextures(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	for (size_t i = 0; i < GS_MAX_TEXTURES; i++) {
+		gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(
+				curTextures[i]);
+		if (tex2d == nullptr)
+			break;
+
+		[commandEncoder setFragmentTexture:tex2d->texture atIndex:i];
+	}
+}
+
+void gs_device::UploadSamplers(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	for (size_t i = 0; i < GS_MAX_TEXTURES; i++) {
+		gs_sampler_state *sampler = curSamplers[i];
+		if (sampler == nullptr)
+			break;
+
+		[commandEncoder setFragmentSamplerState:sampler->samplerState
+				atIndex:i];
+	}
+}
+
+void gs_device::LoadRasterState(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	[commandEncoder setViewport:rasterState.mtlViewport];
+	/* use CCW to convert to a right-handed coordinate system */
+	[commandEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
+	[commandEncoder setCullMode:rasterState.mtlCullMode];
+	if (rasterState.scissorEnabled)
+		[commandEncoder setScissorRect:rasterState.mtlScissorRect];
+}
+
+void gs_device::LoadZStencilState(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	if (zstencilState.depthEnabled) {
+		if (depthStencilState == nil) {
+			depthStencilState = [device
+					newDepthStencilStateWithDescriptor:
+					zstencilState.dsd];
+		}
+		[commandEncoder setDepthStencilState:depthStencilState];
+	}
+}
+
+void gs_device::UpdateViewProjMatrix()
+{
+	gs_matrix_get(&curViewMatrix);
+
+	/* negate Z col of the view matrix for right-handed coordinate system */
+	curViewMatrix.x.z = -curViewMatrix.x.z;
+	curViewMatrix.y.z = -curViewMatrix.y.z;
+	curViewMatrix.z.z = -curViewMatrix.z.z;
+	curViewMatrix.t.z = -curViewMatrix.t.z;
+
+	matrix4_mul(&curViewProjMatrix, &curViewMatrix, &curProjMatrix);
+	matrix4_transpose(&curViewProjMatrix, &curViewProjMatrix);
+
+	if (curVertexShader->viewProj)
+		gs_shader_set_matrix4(curVertexShader->viewProj,
+				&curViewProjMatrix);
+}
+
+void gs_device::DrawPrimitives(id<MTLRenderCommandEncoder> commandEncoder,
+		gs_draw_mode drawMode, uint32_t startVert, uint32_t numVerts)
+{
+	MTLPrimitiveType primitive = ConvertGSTopology(drawMode);
+	if (curIndexBuffer) {
+		if (numVerts == 0)
+			numVerts = static_cast<uint32_t>(curIndexBuffer->num);
+		[commandEncoder drawIndexedPrimitives:primitive
+				indexCount:numVerts
+				indexType:curIndexBuffer->indexType
+				indexBuffer:curIndexBuffer->indexBuffer
+				indexBufferOffset:0];
+		if (curIndexBuffer->isDynamic)
+			curIndexBuffer->indexBuffer = nil;
+	} else {
+		if (numVerts == 0)
+			numVerts = static_cast<uint32_t>(
+					curVertexBuffer->vbData->num);
+		[commandEncoder drawPrimitives:primitive
+				vertexStart:startVert vertexCount:numVerts];
+	}
+}
+
+void gs_device::Draw(gs_draw_mode drawMode, uint32_t startVert,
+		uint32_t numVerts)
+{
+	try {
+		if (!curVertexShader)
+			throw "No vertex shader specified";
+		
+		if (!curPixelShader)
+			throw "No pixel shader specified";
+		
+		if (!curVertexBuffer)
+			throw "No vertex buffer specified";
+		
+		if (!curRenderTarget)
+			throw "No render target to render to";
+		
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_draw (Metal): %s", error);
+		return;
+	}
+
+	if (pipelineState == nil || piplineStateChanged) {
+		NSError *error = nil;
+		pipelineState = [device newRenderPipelineStateWithDescriptor:
+				pipelineDesc error:&error];
+		
+		if (pipelineState == nil) {
+			blog(LOG_ERROR, "device_draw (Metal): %s",
+					error.localizedDescription.UTF8String);
+			return;
+		}
+		
+		piplineStateChanged = false;
+	}
+
+	if (preserveClearTarget != curRenderTarget) {
+		passDesc.colorAttachments[0].loadAction = MTLLoadActionLoad;
+		passDesc.depthAttachment.loadAction     = MTLLoadActionLoad;
+		passDesc.stencilAttachment.loadAction   = MTLLoadActionLoad;
+	} else
+		SetClear();
+
+	id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer
+			renderCommandEncoderWithDescriptor:passDesc];
+	[commandEncoder setRenderPipelineState:pipelineState];
+
+	try {
+		gs_effect_t *effect = gs_get_effect();
+		if (effect)
+			gs_effect_update_params(effect);
+
+		LoadRasterState(commandEncoder);
+		LoadZStencilState(commandEncoder);
+		UpdateViewProjMatrix();
+		curVertexShader->UploadParams(commandEncoder);
+		curPixelShader->UploadParams(commandEncoder);
+		UploadVertexBuffer(commandEncoder);
+		UploadTextures(commandEncoder);
+		UploadSamplers(commandEncoder);
+		DrawPrimitives(commandEncoder, drawMode, startVert, numVerts);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_draw (Metal): %s", error);
+	}
+
+	[commandEncoder endEncoding];
+}
+
+static inline id<MTLBuffer> CreateBuffer(id<MTLDevice> device,
+		void *data, size_t length)
+{
+	length = (length + 15) & ~15;
+	
+	MTLResourceOptions options = MTLResourceCPUCacheModeWriteCombined |
+			MTLResourceStorageModeShared;
+	id<MTLBuffer> buffer = [device newBufferWithBytes:data
+			length:length options:options];
+	if (buffer == nil)
+		throw "Failed to create buffer";
+	return buffer;
+}
+
+id<MTLBuffer> gs_device::GetBuffer(void *data, size_t length)
+{
+	lock_guard<mutex> lock(mutexObj);
+	auto target = find_if(unusedBufferPool.begin(), unusedBufferPool.end(),
+		[length](id<MTLBuffer> b) { return b.length >= length; });
+	if (target == unusedBufferPool.end()) {
+		id<MTLBuffer> newBuffer = CreateBuffer(device, data, length);
+		curBufferPool.push_back(newBuffer);
+		return newBuffer;
+	}
+	
+	id<MTLBuffer> targetBuffer = *target;
+	unusedBufferPool.erase(target);
+	curBufferPool.push_back(targetBuffer);
+	memcpy(targetBuffer.contents, data, length);
+	return targetBuffer;
+}
+
+void gs_device::PushResources()
+{
+	lock_guard<mutex> lock(mutexObj);
+	bufferPools.push(curBufferPool);
+	curBufferPool.clear();
+}
+
+void gs_device::ReleaseResources()
+{
+	lock_guard<mutex> lock(mutexObj);
+	auto& pool = bufferPools.front();
+	unusedBufferPool.insert(unusedBufferPool.end(),
+			pool.begin(), pool.end());
+	bufferPools.pop();
+}
+
+gs_device::gs_device(uint32_t adapterIdx)
+{
+	matrix4_identity(&curProjMatrix);
+	matrix4_identity(&curViewMatrix);
+	matrix4_identity(&curViewProjMatrix);
+
+	passDesc = [[MTLRenderPassDescriptor alloc] init];
+	pipelineDesc = [[MTLRenderPipelineDescriptor alloc] init];
+
+	InitDevice(adapterIdx);
+
+	commandQueue = [device newCommandQueue];
+
+	device_set_render_target(this, nullptr, nullptr);
+}

+ 67 - 0
libobs-metal/metal-indexbuffer.mm

@@ -0,0 +1,67 @@
+#include "metal-subsystem.hpp"
+
+static inline MTLIndexType ConvertGSIndexType(gs_index_type type)
+{
+	switch (type) {
+		case GS_UNSIGNED_SHORT: return MTLIndexTypeUInt16;
+		case GS_UNSIGNED_LONG:  return MTLIndexTypeUInt32;
+	}
+	
+	throw "Failed to initialize index buffer";
+}
+
+static inline size_t ConvertGSIndexTypeToSize(gs_index_type type)
+{
+	switch (type) {
+		case GS_UNSIGNED_SHORT: return 2;
+		case GS_UNSIGNED_LONG:  return 4;
+	}
+	
+	throw "Failed to initialize index buffer";
+}
+
+void gs_index_buffer::PrepareBuffer()
+{
+	assert(isDynamic);
+	
+	indexBuffer = device->GetBuffer(indices.get(), len);
+#if _DEBUG
+	indexBuffer.label = @"index";
+#endif
+}
+
+void gs_index_buffer::InitBuffer()
+{
+	NSUInteger         length  = len;
+	MTLResourceOptions options = MTLResourceCPUCacheModeWriteCombined |
+			MTLResourceStorageModeShared;
+	
+	indexBuffer = [device->device newBufferWithBytes:&indices
+			length:length options:options];
+	if (indexBuffer == nil)
+		throw "Failed to create index buffer";
+	
+#ifdef _DEBUG
+	indexBuffer.label = @"index";
+#endif
+}
+
+void gs_index_buffer::Rebuild()
+{
+	if (!isDynamic)
+		InitBuffer();
+}
+
+gs_index_buffer::gs_index_buffer(gs_device_t *device, enum gs_index_type type,
+		void *indices, size_t num, uint32_t flags)
+	: gs_obj    (device, gs_type::gs_index_buffer),
+	  type      (type),
+	  isDynamic ((flags & GS_DYNAMIC) != 0),
+	  indices   (indices, bfree),
+	  num       (num),
+	  len       (ConvertGSIndexTypeToSize(type) * num),
+	  indexType (ConvertGSIndexType(type))
+{
+	if (!isDynamic)
+		InitBuffer();
+}

+ 118 - 0
libobs-metal/metal-rebuild.mm

@@ -0,0 +1,118 @@
+#include "metal-subsystem.hpp"
+
+void gs_device::RebuildDevice()
+try {
+	id<MTLDevice> dev;
+
+	blog(LOG_WARNING, "Device Remove/Reset!  Rebuilding all assets...");
+
+	/* ----------------------------------------------------------------- */
+
+	gs_obj *obj = first_obj;
+
+	while (obj) {
+		switch (obj->obj_type) {
+		case gs_type::gs_vertex_buffer:
+			((gs_vertex_buffer*)obj)->Release();
+			break;
+		case gs_type::gs_index_buffer:
+			((gs_index_buffer*)obj)->Release();
+			break;
+		case gs_type::gs_texture_2d:
+			((gs_texture_2d*)obj)->Release();
+			break;
+		case gs_type::gs_zstencil_buffer:
+			((gs_zstencil_buffer*)obj)->Release();
+			break;
+		case gs_type::gs_stage_surface:
+			((gs_stage_surface*)obj)->Release();
+			break;
+		case gs_type::gs_sampler_state:
+			((gs_sampler_state*)obj)->Release();
+			break;
+		case gs_type::gs_vertex_shader:
+			((gs_vertex_shader*)obj)->Release();
+			break;
+		case gs_type::gs_pixel_shader:
+			((gs_pixel_shader*)obj)->Release();
+			break;
+		case gs_type::gs_swap_chain:
+			((gs_swap_chain*)obj)->Release();
+			break;
+		}
+
+		obj = obj->next;
+	}
+
+	depthStencilState = nil;
+	pipelineState = nil;
+	commandBuffer = nil;
+	commandQueue = nil;
+
+	/* ----------------------------------------------------------------- */
+
+	InitDevice(devIdx);
+
+	dev = device;
+
+	obj = first_obj;
+	while (obj) {
+		switch (obj->obj_type) {
+		case gs_type::gs_vertex_buffer:
+			((gs_vertex_buffer*)obj)->Rebuild();
+			break;
+		case gs_type::gs_index_buffer:
+			((gs_index_buffer*)obj)->Rebuild();
+			break;
+		case gs_type::gs_texture_2d:
+			((gs_texture_2d*)obj)->Rebuild();
+			break;
+		case gs_type::gs_zstencil_buffer:
+			((gs_zstencil_buffer*)obj)->Rebuild();
+			break;
+		case gs_type::gs_stage_surface:
+			((gs_stage_surface*)obj)->Rebuild();
+			break;
+		case gs_type::gs_sampler_state:
+			((gs_sampler_state*)obj)->Rebuild();
+			break;
+		case gs_type::gs_vertex_shader:
+			((gs_vertex_shader*)obj)->Rebuild();
+			break;
+		case gs_type::gs_pixel_shader:
+			((gs_pixel_shader*)obj)->Rebuild();
+			break;
+		case gs_type::gs_swap_chain:
+			((gs_swap_chain*)obj)->Rebuild();
+			break;
+		}
+
+		obj = obj->next;
+	}
+
+	curRenderTarget = nullptr;
+	curRenderSide = 0;
+	curZStencilBuffer = nullptr;
+	memset(&curTextures, 0, sizeof(curTextures));
+	memset(&curSamplers, 0, sizeof(curSamplers));
+	curVertexBuffer = nullptr;
+	curIndexBuffer = nullptr;
+	curVertexShader = nullptr;
+	curPixelShader = nullptr;
+	curSwapChain = nullptr;
+	curStageSurface = nullptr;
+
+	lastVertexBuffer = nullptr;
+	lastVertexShader = nullptr;
+
+	preserveClearTarget = nullptr;
+	while (clearStates.size())
+		clearStates.pop();
+
+	while (projStack.size())
+		projStack.pop();
+
+} catch (const char *error) {
+	bcrash("Failed to recreate Metal: %s", error);
+
+}

+ 136 - 0
libobs-metal/metal-samplerstate.mm

@@ -0,0 +1,136 @@
+#include <graphics/vec4.h>
+
+#include "metal-subsystem.hpp"
+
+using std::min;
+using std::max;
+
+static inline MTLSamplerAddressMode ConvertGSAddressMode(gs_address_mode mode)
+{
+	switch (mode) {
+	case GS_ADDRESS_WRAP:
+		return MTLSamplerAddressModeRepeat;
+	case GS_ADDRESS_CLAMP:
+		return MTLSamplerAddressModeClampToEdge;
+	case GS_ADDRESS_MIRROR:
+		return MTLSamplerAddressModeMirrorRepeat;
+	case GS_ADDRESS_BORDER:
+		return MTLSamplerAddressModeClampToBorderColor;
+	case GS_ADDRESS_MIRRORONCE:
+		return MTLSamplerAddressModeMirrorClampToEdge;
+	}
+
+	return MTLSamplerAddressModeRepeat;
+}
+
+static inline MTLSamplerMinMagFilter ConvertGSMinFilter(gs_sample_filter filter)
+{
+	switch (filter) {
+	case GS_FILTER_POINT:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_LINEAR:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_LINEAR_MAG_MIP_POINT:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_ANISOTROPIC:
+		return MTLSamplerMinMagFilterLinear;
+	}
+
+	return MTLSamplerMinMagFilterNearest;
+}
+
+static inline MTLSamplerMinMagFilter ConvertGSMagFilter(gs_sample_filter filter)
+{
+	switch (filter) {
+	case GS_FILTER_POINT:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_LINEAR:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_MIN_LINEAR_MAG_MIP_POINT:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMinMagFilterNearest;
+	case GS_FILTER_MIN_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMinMagFilterLinear;
+	case GS_FILTER_ANISOTROPIC:
+		return MTLSamplerMinMagFilterLinear;
+	}
+
+	return MTLSamplerMinMagFilterNearest;
+}
+
+static inline MTLSamplerMipFilter ConvertGSMipFilter(gs_sample_filter filter)
+{
+	switch (filter) {
+	case GS_FILTER_POINT:
+		return MTLSamplerMipFilterNearest;
+	case GS_FILTER_LINEAR:
+		return MTLSamplerMipFilterLinear;
+	case GS_FILTER_MIN_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMipFilterLinear;
+	case GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMipFilterNearest;
+	case GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
+		return MTLSamplerMipFilterLinear;
+	case GS_FILTER_MIN_LINEAR_MAG_MIP_POINT:
+		return MTLSamplerMipFilterNearest;
+	case GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR:
+		return MTLSamplerMipFilterLinear;
+	case GS_FILTER_MIN_MAG_LINEAR_MIP_POINT:
+		return MTLSamplerMipFilterNearest;
+	case GS_FILTER_ANISOTROPIC:
+		return MTLSamplerMipFilterLinear;
+	}
+
+	return MTLSamplerMipFilterNearest;
+}
+
+void gs_sampler_state::InitSampler()
+{
+	samplerState = [device->device
+			newSamplerStateWithDescriptor:samplerDesc];
+	if (samplerState == nil)
+		throw "Failed to create sampler state";
+}
+
+gs_sampler_state::gs_sampler_state(gs_device_t *device,
+		const gs_sampler_info *info)
+	: gs_obj (device, gs_type::gs_sampler_state),
+	  info   (*info)
+{
+	samplerDesc = [[MTLSamplerDescriptor alloc] init];
+	samplerDesc.sAddressMode    = ConvertGSAddressMode(info->address_u);
+	samplerDesc.tAddressMode    = ConvertGSAddressMode(info->address_v);
+	samplerDesc.rAddressMode    = ConvertGSAddressMode(info->address_w);
+	samplerDesc.minFilter       = ConvertGSMinFilter(info->filter);
+	samplerDesc.magFilter       = ConvertGSMagFilter(info->filter);
+	samplerDesc.mipFilter       = ConvertGSMipFilter(info->filter);
+	samplerDesc.maxAnisotropy   = min(max(info->max_anisotropy, 1), 16);
+	samplerDesc.compareFunction = MTLCompareFunctionAlways;
+
+	if ((info->border_color & 0x000000FF) == 0)
+		samplerDesc.borderColor = MTLSamplerBorderColorTransparentBlack;
+	else if (info->border_color == 0xFFFFFFFF)
+		samplerDesc.borderColor = MTLSamplerBorderColorOpaqueWhite;
+	else
+		samplerDesc.borderColor = MTLSamplerBorderColorOpaqueBlack;
+
+
+	InitSampler();
+}

+ 354 - 0
libobs-metal/metal-shader.mm

@@ -0,0 +1,354 @@
+#include <graphics/vec2.h>
+#include <graphics/vec3.h>
+#include <graphics/matrix3.h>
+#include <graphics/matrix4.h>
+
+#include "metal-subsystem.hpp"
+#include "metal-shaderprocessor.hpp"
+
+using namespace std;
+
+static MTLCompileOptions *mtlCompileOptions = nil;
+
+gs_vertex_shader::gs_vertex_shader(gs_device_t *device, const char *file,
+		const char *shaderString)
+	: gs_shader   (device, gs_type::gs_vertex_shader, GS_SHADER_VERTEX),
+	  hasNormals  (false),
+	  hasColors   (false),
+	  hasTangents (false),
+	  texUnits    (0)
+{
+	ShaderProcessor     processor(device);
+	ShaderBufferInfo    info;
+	MTLVertexDescriptor *vertdesc;
+
+	vertdesc = [[MTLVertexDescriptor alloc] init];
+
+	processor.Process(shaderString, file);
+	source = processor.BuildString(type);
+	processor.BuildParams(params);
+	processor.BuildParamInfo(info);
+	processor.BuildVertexDesc(vertdesc);
+	BuildConstantBuffer();
+
+	Compile(source);
+
+	hasNormals  = info.normals;
+	hasColors   = info.colors;
+	hasTangents = info.tangents;
+	texUnits    = info.texUnits;
+
+	vertexDesc  = vertdesc;
+
+	viewProj    = gs_shader_get_param_by_name(this, "ViewProj");
+	world       = gs_shader_get_param_by_name(this, "World");
+}
+
+gs_pixel_shader::gs_pixel_shader(gs_device_t *device, const char *file,
+		const char *shaderString)
+	: gs_shader(device, gs_type::gs_pixel_shader, GS_SHADER_PIXEL)
+{
+	ShaderProcessor processor(device);
+
+	processor.Process(shaderString, file);
+	source = processor.BuildString(type);
+	processor.BuildParams(params);
+	processor.BuildSamplers(samplers);
+	BuildConstantBuffer();
+
+	Compile(source);
+}
+
+void gs_shader::BuildConstantBuffer()
+{
+	for (size_t i = 0; i < params.size(); i++) {
+		gs_shader_param &param = params[i];
+		size_t          size   = 0;
+
+		switch (param.type) {
+		case GS_SHADER_PARAM_BOOL:
+		case GS_SHADER_PARAM_INT:
+		case GS_SHADER_PARAM_FLOAT:     size = sizeof(float);     break;
+		case GS_SHADER_PARAM_INT2:
+		case GS_SHADER_PARAM_VEC2:      size = sizeof(vec2);      break;
+		case GS_SHADER_PARAM_INT3:
+		case GS_SHADER_PARAM_VEC3:      size = sizeof(float) * 3; break;
+		case GS_SHADER_PARAM_INT4:
+		case GS_SHADER_PARAM_VEC4:      size = sizeof(vec4);      break;
+		case GS_SHADER_PARAM_MATRIX4X4:
+			size = sizeof(float) * 4 * 4;
+			break;
+		case GS_SHADER_PARAM_TEXTURE:
+		case GS_SHADER_PARAM_STRING:
+		case GS_SHADER_PARAM_UNKNOWN:
+			continue;
+		}
+
+		/* checks to see if this constant needs to start at a new
+		 * register */
+		if (size && (constantSize & 15) != 0) {
+			size_t alignMax = (constantSize + 15) & ~15;
+
+			if ((size + constantSize) > alignMax)
+				constantSize = alignMax;
+		}
+
+		param.pos     = constantSize;
+		constantSize += size;
+	}
+
+	for (gs_shader_param &param : params)
+		gs_shader_set_default(&param);
+
+	data.resize(constantSize);
+}
+
+void gs_shader::Compile(string shaderString)
+{
+	if (mtlCompileOptions == nil) {
+		mtlCompileOptions = [[MTLCompileOptions alloc] init];
+		mtlCompileOptions.languageVersion = MTLLanguageVersion1_2;
+	}
+
+	NSString *nsShaderString = [[NSString alloc]
+			initWithBytesNoCopy:(void*)shaderString.data()
+			length:shaderString.length()
+			encoding:NSUTF8StringEncoding freeWhenDone:NO];
+	NSError *errors;
+	id<MTLLibrary> lib = [device->device newLibraryWithSource:nsShaderString
+			options:mtlCompileOptions error:&errors];
+	if (lib == nil) {
+		blog(LOG_DEBUG, "Converted shader program:\n%s\n------\n",
+				shaderString.c_str());
+
+		if (errors != nil)
+			throw ShaderError(errors);
+		else
+			throw "Failed to compile shader";
+	}
+
+	id<MTLFunction> func = [lib newFunctionWithName:@"_main"];
+	if (func == nil)
+		throw "Failed to create function";
+
+	library  = lib;
+	function = func;
+}
+
+void gs_shader::Rebuild()
+{
+	Compile(source);
+
+	for (gs_shader_param &param : params) {
+		param.nextSampler = nullptr;
+		param.curValue.clear();
+		param.changed = true;
+		gs_shader_set_default(&param);
+	}
+}
+
+inline void gs_shader::UpdateParam(uint8_t *data, gs_shader_param &param)
+{
+	if (param.type != GS_SHADER_PARAM_TEXTURE) {
+		if (!param.curValue.size())
+			throw "Not all shader parameters were set";
+
+		if (param.changed) {
+			memcpy(data + param.pos, param.curValue.data(),
+					param.curValue.size());
+			param.changed = false;
+		}
+
+	} else if (param.curValue.size() == sizeof(gs_texture_t*)) {
+		gs_texture_t *tex;
+		memcpy(&tex, param.curValue.data(), sizeof(gs_texture_t*));
+		device_load_texture(device, tex, param.textureID);
+
+		if (param.nextSampler) {
+			device_load_samplerstate(device, param.nextSampler,
+					param.textureID);
+			param.nextSampler = nullptr;
+		}
+	}
+}
+
+void gs_shader::UploadParams(id<MTLRenderCommandEncoder> commandEncoder)
+{
+	uint8_t *ptr = data.data();
+
+	for (size_t i = 0; i < params.size(); i++)
+		UpdateParam(ptr, params[i]);
+
+	if (!constantSize)
+		return;
+
+	id<MTLBuffer> cnt = device->GetBuffer(ptr, data.size());
+#if _DEBUG
+	cnt.label = @"constants";
+#endif
+
+	if (type == GS_SHADER_VERTEX)
+		[commandEncoder setVertexBuffer:cnt offset:0 atIndex:30];
+	else if (type == GS_SHADER_PIXEL)
+		[commandEncoder setFragmentBuffer:cnt offset:0 atIndex:30];
+	else
+		throw "This is unknown shader type";
+}
+
+void gs_shader_destroy(gs_shader_t *shader)
+{
+	assert(shader != nullptr);
+	assert(shader->obj_type == gs_type::gs_vertex_shader ||
+	       shader->obj_type == gs_type::gs_pixel_shader);
+
+	if (shader->device->lastVertexShader == shader)
+		shader->device->lastVertexShader = nullptr;
+
+	delete shader;
+}
+
+int gs_shader_get_num_params(const gs_shader_t *shader)
+{
+	assert(shader != nullptr);
+	assert(shader->obj_type == gs_type::gs_vertex_shader ||
+	       shader->obj_type == gs_type::gs_pixel_shader);
+
+	return (int)shader->params.size();
+}
+
+gs_sparam_t *gs_shader_get_param_by_idx(gs_shader_t *shader, uint32_t param)
+{
+	assert(shader != nullptr);
+	assert(shader->obj_type == gs_type::gs_vertex_shader ||
+	       shader->obj_type == gs_type::gs_pixel_shader);
+
+	return &shader->params[param];
+}
+
+gs_sparam_t *gs_shader_get_param_by_name(gs_shader_t *shader, const char *name)
+{
+	for (size_t i = 0; i < shader->params.size(); i++) {
+		gs_shader_param &param = shader->params[i];
+		if (strcmp(param.name.c_str(), name) == 0)
+			return &param;
+	}
+
+	return nullptr;
+}
+
+gs_sparam_t *gs_shader_get_viewproj_matrix(const gs_shader_t *shader)
+{
+	assert(shader != nullptr);
+	assert(shader->obj_type == gs_type::gs_vertex_shader ||
+	       shader->obj_type == gs_type::gs_pixel_shader);
+
+	if (shader->type != GS_SHADER_VERTEX)
+		return nullptr;
+
+	return static_cast<const gs_vertex_shader*>(shader)->viewProj;
+}
+
+gs_sparam_t *gs_shader_get_world_matrix(const gs_shader_t *shader)
+{
+	assert(shader != nullptr);
+	assert(shader->obj_type == gs_type::gs_vertex_shader ||
+	       shader->obj_type == gs_type::gs_pixel_shader);
+
+	if (shader->type != GS_SHADER_VERTEX)
+		return nullptr;
+
+	return static_cast<const gs_vertex_shader*>(shader)->world;
+}
+
+void gs_shader_get_param_info(const gs_sparam_t *param,
+		struct gs_shader_param_info *info)
+{
+	if (!param)
+		return;
+
+	info->name = param->name.c_str();
+	info->type = param->type;
+}
+
+static inline void shader_setval_inline(gs_shader_param *param,
+		const void *data, size_t size)
+{
+	assert(param);
+
+	if (!param)
+		return;
+
+	bool size_changed = param->curValue.size() != size;
+	if (size_changed)
+		param->curValue.resize(size);
+
+	if (size_changed || memcmp(param->curValue.data(), data, size) != 0) {
+		memcpy(param->curValue.data(), data, size);
+		param->changed = true;
+	}
+}
+
+void gs_shader_set_bool(gs_sparam_t *param, bool val)
+{
+	int b_val = (int)val;
+	shader_setval_inline(param, &b_val, sizeof(int));
+}
+
+void gs_shader_set_float(gs_sparam_t *param, float val)
+{
+	shader_setval_inline(param, &val, sizeof(float));
+}
+
+void gs_shader_set_int(gs_sparam_t *param, int val)
+{
+	shader_setval_inline(param, &val, sizeof(int));
+}
+
+void gs_shader_set_matrix3(gs_sparam_t *param, const struct matrix3 *val)
+{
+	struct matrix4 mat;
+	matrix4_from_matrix3(&mat, val);
+	shader_setval_inline(param, &mat, sizeof(matrix4));
+}
+
+void gs_shader_set_matrix4(gs_sparam_t *param, const struct matrix4 *val)
+{
+	shader_setval_inline(param, val, sizeof(matrix4));
+}
+
+void gs_shader_set_vec2(gs_sparam_t *param, const struct vec2 *val)
+{
+	shader_setval_inline(param, val, sizeof(vec2));
+}
+
+void gs_shader_set_vec3(gs_sparam_t *param, const struct vec3 *val)
+{
+	shader_setval_inline(param, val, sizeof(float) * 3);
+}
+
+void gs_shader_set_vec4(gs_sparam_t *param, const struct vec4 *val)
+{
+	shader_setval_inline(param, val, sizeof(vec4));
+}
+
+void gs_shader_set_texture(gs_sparam_t *param, gs_texture_t *val)
+{
+	shader_setval_inline(param, &val, sizeof(gs_texture_t*));
+}
+
+void gs_shader_set_val(gs_sparam_t *param, const void *val, size_t size)
+{
+	shader_setval_inline(param, val, size);
+}
+
+void gs_shader_set_default(gs_sparam_t *param)
+{
+	if (param->defaultValue.size())
+		shader_setval_inline(param, param->defaultValue.data(),
+				param->defaultValue.size());
+}
+
+void gs_shader_set_next_sampler(gs_sparam_t *param, gs_samplerstate_t *sampler)
+{
+	param->nextSampler = sampler;
+}

+ 937 - 0
libobs-metal/metal-shaderbuilder.cpp

@@ -0,0 +1,937 @@
+#include "metal-shaderprocessor.hpp"
+
+#include <strstream>
+#include <vector>
+#include <map>
+#include <set>
+
+using namespace std;
+
+#define METAL_VERSION_1_1      ((1 << 16) | 1)
+#define METAL_VERSION_1_2      ((1 << 16) | 2)
+#define COMPILE_METAL_VERSION  METAL_VERSION_1_2
+
+#define USE_PROGRAMMABLE_SAMPLER 1
+
+constexpr const char *UNIFORM_DATA_NAME = "UniformData";
+
+enum class ShaderTextureCallType
+{
+	Sample,
+	SampleBias,
+	SampleGrad,
+	SampleLevel,
+	Load
+};
+
+struct ShaderFunctionInfo
+{
+	bool           useUniform;
+	vector<string> useTextures;
+#if USE_PROGRAMMABLE_SAMPLER
+	vector<string> useSamplers;
+#endif
+};
+
+struct ShaderBuilder
+{
+	const gs_shader_type            type;
+
+	ShaderParser                    *parser;
+	ostrstream                      output;
+
+	set<string>                     constantNames;
+	vector<struct shader_var*>      textureVars;
+	map<string, ShaderFunctionInfo> functionInfo;
+
+	void Build(string &outputString);
+
+	inline ShaderBuilder(gs_shader_type type, ShaderParser *parser)
+		: type(type),
+		  parser(parser)
+	{
+	}
+
+	bool isVertexShader() const {return type == GS_SHADER_VERTEX;}
+	bool isPixelShader() const {return type == GS_SHADER_PIXEL;}
+
+private:
+	struct shader_var *GetVariable(struct cf_token *token);
+
+	bool IsNextCompareOperator(struct cf_token *&token);
+	void AnalysisFunction(struct cf_token *&token, const char *end,
+			ShaderFunctionInfo &info);
+
+	void WriteType(const char *type);
+	bool WriteTypeToken(struct cf_token *token);
+	bool WriteMul(struct cf_token *&token);
+	bool WriteConstantVariable(struct cf_token *token);
+	bool WriteTextureCall(struct cf_token *&token,
+			ShaderTextureCallType type);
+	bool WriteTextureCode(struct cf_token *&token, struct shader_var *var);
+	bool WriteIntrinsic(struct cf_token *&token);
+	void WriteFunctionAdditionalParam(string funcionName);
+	void WriteFunctionContent(struct cf_token *&token, const char *end);
+	void WriteSamplerParamDelimitter(bool &first);
+	void WriteSamplerFilter(enum gs_sample_filter filter, bool &first);
+	void WriteSamplerAddress(enum gs_address_mode address,
+			const char key, bool &first);
+	void WriteSamplerMaxAnisotropy(int maxAnisotropy, bool &first);
+	void WriteSamplerBorderColor(uint32_t borderColor, bool &first);
+
+	void WriteVariable(const shader_var *var);
+	void WriteSampler(struct shader_sampler *sampler);
+	void WriteStruct(const shader_struct *str);
+	void WriteFunction(const shader_func *func);
+
+	void WriteInclude();
+	void WriteVariables();
+	void WriteSamplers();
+	void WriteStructs();
+	void WriteFunctions();
+};
+
+static inline const char *GetType(const string &type)
+{
+	if (type == "texture2d")
+		return "texture2d<float>";
+	else if (type == "texture3d")
+		return "texture3d<float>";
+	else if (type == "texture_cube")
+		return "texturecube<float>";
+	else if (type == "texture_rect")
+		throw "texture_rect is not supported in Metal";
+	else if (type.compare(0, 4, "half") == 0) {
+		switch (*(type.end() - 1)) {
+			case '2': return "float2";
+			case '3': return "float3";
+			case '4': return "float4";
+			case 'f': return "float";
+		}
+		throw "Unknown type";
+	} else if (type.compare(0, 10, "min16float") == 0) {
+		switch (*(type.end() - 1)) {
+			case '2': return "half2";
+			case '3': return "half3";
+			case '4': return "half4";
+			case 'f': return "half";
+		}
+		throw "Unknown type";
+	} else if (type.compare(0, 10, "min10float") == 0)
+		throw "min10float* is not supported in Metal";
+	else if (type.compare(0, 6, "double") == 0)
+		throw "double* is not supported in Metal";
+	else if (type.compare(0, 8, "min16int") == 0) {
+		switch (*(type.end() - 1)) {
+			case '2': return "short2";
+			case '3': return "short3";
+			case '4': return "short4";
+			case 't': return "short";
+		}
+		throw "Unknown type";
+	} else if (type.compare(0, 9, "min16uint") == 0) {
+		switch (*(type.end() - 1)) {
+			case '2': return "ushort2";
+			case '3': return "ushort3";
+			case '4': return "ushort4";
+			case 't': return "ushort";
+		}
+		throw "Unknown type";
+	} else if (type.compare(0, 8, "min12int") == 0)
+		throw "min12int* is not supported in Metal";
+
+	return nullptr;
+}
+
+inline void ShaderBuilder::WriteType(const char *rawType)
+{
+	string type(rawType);
+	const char *newType = GetType(string(rawType));
+	output << (newType != nullptr ? newType : type);
+}
+
+inline bool ShaderBuilder::WriteTypeToken(struct cf_token *token)
+{
+	string type(token->str.array, token->str.len);
+	const char *newType = GetType(type);
+	if (newType == nullptr)
+		return false;
+
+	output << newType;
+	return true;
+}
+
+inline void ShaderBuilder::WriteInclude()
+{
+	output << "#include <metal_stdlib>" << endl
+	       << "using namespace metal;" << endl
+	       << endl;
+}
+
+inline void ShaderBuilder::WriteVariable(const shader_var *var)
+{
+	if (var->var_type == SHADER_VAR_CONST)
+		output << "constant ";
+
+	WriteType(var->type);
+
+	output << ' ' << var->name;
+}
+
+static inline const char *GetMapping(const char *rawMapping)
+{
+	if (rawMapping == nullptr)
+		return nullptr;
+
+	string mapping(rawMapping);
+	if (mapping == "POSITION")
+		return "position";
+	if (mapping == "COLOR")
+		return "color(0)";
+
+	return nullptr;
+}
+
+inline void ShaderBuilder::WriteVariables()
+{
+	if (parser->params.num == 0)
+		return;
+
+	bool isFirst = true;
+	for (struct shader_var *var = parser->params.array;
+	     var != parser->params.array + parser->params.num;
+	     var++) {
+		if (isPixelShader() &&
+		    astrcmp_n("texture", var->type, 7) == 0) {
+			textureVars.push_back(var);
+
+		} else {
+			if (isFirst) {
+				output << "struct " << UNIFORM_DATA_NAME
+				<< " {" << endl;
+				isFirst = false;
+			}
+
+			output << '\t';
+			WriteVariable(var);
+
+			const char* mapping = GetMapping(var->mapping);
+			if (mapping != nullptr)
+				output << " [[" << mapping << "]]";
+
+			output << ';' << endl;
+
+			constantNames.insert(var->name);
+		}
+	}
+	if (!isFirst)
+		output << "};" << endl << endl;
+}
+
+inline void ShaderBuilder::WriteSamplerParamDelimitter(bool &first)
+{
+	if (!first)
+		output << "," << endl;
+	else
+		first = false;
+}
+
+inline void ShaderBuilder::WriteSamplerFilter(enum gs_sample_filter filter,
+		bool &first)
+{
+	if (filter != GS_FILTER_POINT) {
+		WriteSamplerParamDelimitter(first);
+
+		switch (filter) {
+		case GS_FILTER_LINEAR:
+		case GS_FILTER_ANISOTROPIC:
+			output << "\tfilter::linear";
+			break;
+		case GS_FILTER_MIN_MAG_POINT_MIP_LINEAR:
+			output << "\tmag_filter::nearest," << endl
+			       << "\tmin_filter::nearest," << endl
+			       << "\tmip_filter::linear";
+			break;
+		case GS_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT:
+			output << "\tmag_filter::nearest," << endl
+			       << "\tmin_filter::nearest," << endl
+			       << "\tmip_filter::linear";
+			break;
+		case GS_FILTER_MIN_POINT_MAG_MIP_LINEAR:
+			output << "\tmag_filter::linear," << endl
+			       << "\tmin_filter::nearest," << endl
+			       << "\tmip_filter::linear";
+			break;
+		case GS_FILTER_MIN_LINEAR_MAG_MIP_POINT:
+			output << "\tmag_filter::nearest," << endl
+			       << "\tmin_filter::linear," << endl
+			       << "\tmip_filter::nearest";
+			break;
+		case GS_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR:
+			output << "\tmag_filter::nearest," << endl
+			       << "\tmin_filter::linear," << endl
+			       << "\tmip_filter::linear";
+			break;
+		case GS_FILTER_MIN_MAG_LINEAR_MIP_POINT:
+			output << "\tmag_filter::linear," << endl
+			       << "\tmin_filter::linear," << endl
+			       << "\tmip_filter::nearest";
+			break;
+		case GS_FILTER_POINT:
+		default:
+			throw "Unknown error";
+		}
+	}
+}
+
+inline void ShaderBuilder::WriteSamplerAddress(enum gs_address_mode address,
+		const char key, bool &first)
+{
+	if (address != GS_ADDRESS_CLAMP) {
+		WriteSamplerParamDelimitter(first);
+
+		output << "\t" << key << "_address::";
+		switch (address)
+		{
+		case GS_ADDRESS_WRAP:
+			output << "repeat";
+			break;
+		case GS_ADDRESS_MIRROR:
+			output << "mirrored_repeat";
+			break;
+		case GS_ADDRESS_BORDER:
+#if COMPILE_METAL_VERSION >= METAL_VERSION_1_2
+			output << "clamp_to_border";
+			break;
+#else
+			throw "GS_ADDRESS_BORDER is not supported in MSL 1.1";
+#endif
+		case GS_ADDRESS_MIRRORONCE:
+			throw "GS_ADDRESS_MIRRORONCE is not supported in Metal";
+		default:
+			throw "Unknown error";
+		}
+	}
+}
+
+inline void ShaderBuilder::WriteSamplerMaxAnisotropy(int maxAnisotropy,
+		bool &first)
+{
+	if (maxAnisotropy >= 2 && maxAnisotropy <= 16) {
+		WriteSamplerParamDelimitter(first);
+
+		output << "\tmax_anisotropy(" << maxAnisotropy << ")";
+	}
+}
+
+inline void ShaderBuilder::WriteSamplerBorderColor(uint32_t borderColor,
+		bool &first)
+{
+	const bool isNotTransBlack = (borderColor & 0x000000FF) != 0;
+	const bool isOpaqueWhite = borderColor == 0xFFFFFFFF;
+	if (isNotTransBlack || isOpaqueWhite) {
+		WriteSamplerParamDelimitter(first);
+
+		output << "\tborder_color::";
+
+		if (isOpaqueWhite)
+			output << "opaque_white";
+		else if (isNotTransBlack)
+			output << "opaque_black";
+	}
+}
+
+inline void ShaderBuilder::WriteSampler(struct shader_sampler *sampler)
+{
+	gs_sampler_info si;
+	shader_sampler_convert(sampler, &si);
+
+	output << "constexpr sampler " << sampler->name << "(" << endl;
+
+	bool isFirst = true;
+	WriteSamplerFilter(si.filter, isFirst);
+	WriteSamplerAddress(si.address_u, 's', isFirst);
+	WriteSamplerAddress(si.address_v, 't', isFirst);
+	WriteSamplerAddress(si.address_w, 'r', isFirst);
+	WriteSamplerMaxAnisotropy(si.max_anisotropy, isFirst);
+	WriteSamplerBorderColor(si.border_color, isFirst);
+
+	output << ");" << endl << endl;
+}
+
+inline void ShaderBuilder::WriteSamplers()
+{
+	if (isPixelShader()) {
+		for (struct shader_sampler *sampler = parser->samplers.array;
+		     sampler != parser->samplers.array + parser->samplers.num;
+		     sampler++)
+			WriteSampler(sampler);
+	}
+}
+
+inline void ShaderBuilder::WriteStruct(const shader_struct *str)
+{
+	output << "struct " << str->name << " {" << endl;
+
+	size_t attributeId = 0;
+	for (struct shader_var *var = str->vars.array;
+	     var != str->vars.array + str->vars.num;
+	     var++) {
+		output << '\t';
+		WriteVariable(var);
+
+		const char* mapping = GetMapping(var->mapping);
+		if (isVertexShader()) {
+			output << " [[attribute(" << attributeId++ << ")";
+			if (mapping != nullptr)
+				output << ", " << mapping;
+			output << "]]";
+		}
+
+		output << ';' << endl;
+	}
+
+	output << "};" << endl << endl;
+}
+
+inline void ShaderBuilder::WriteStructs()
+{
+	for (struct shader_struct *str = parser->structs.array;
+	     str != parser->structs.array + parser->structs.num;
+	     str++)
+		WriteStruct(str);
+}
+
+/*
+ * NOTE: HLSL -> MSL intrinsic conversions
+ *   clip        -> (unsupported)
+ *   ddx         -> dfdx
+ *   ddy         -> dfdy
+ *   frac        -> fract
+ *   lerp        -> mix
+ *   mul         -> (change to operator)
+ *   Sample      -> sample
+ *   SampleBias  -> sample(.., bias(..))
+ *   SampleGrad  -> sample(.., gradient2d(..))
+ *   SampleLevel -> sample(.., level(..))
+ *   Load        -> read
+ *   A cmp B     -> all(A cmp B)
+ *
+ *   All else can be left as-is
+ */
+
+inline bool ShaderBuilder::WriteMul(struct cf_token *&token)
+{
+	struct cf_parser *cfp = &parser->cfp;
+	cfp->cur_token = token;
+
+	if (!cf_next_token(cfp))    return false;
+	if (!cf_token_is(cfp, "(")) return false;
+
+	output << '(';
+	WriteFunctionContent(cfp->cur_token, ",");
+	output << ") * (";
+	cf_next_token(cfp);
+	WriteFunctionContent(cfp->cur_token, ")");
+	output << "))";
+
+	token = cfp->cur_token;
+	return true;
+}
+
+inline bool ShaderBuilder::WriteConstantVariable(struct cf_token *token)
+{
+	string str(token->str.array, token->str.len);
+	if (constantNames.find(str) != constantNames.end()) {
+		output << "uniforms." << str;
+		return true;
+	}
+	return false;
+}
+
+inline bool ShaderBuilder::WriteTextureCall(struct cf_token *&token,
+		ShaderTextureCallType type)
+{
+	struct cf_parser *cfp = &parser->cfp;
+	cfp->cur_token = token;
+
+	/* ( */
+	if (!cf_next_token(cfp))    return false;
+	if (!cf_token_is(cfp, "(")) return false;
+
+	/* sampler */
+	if (type != ShaderTextureCallType::Load) {
+		output << "sample(";
+
+		if (!cf_next_token(cfp))    return false;
+		if (cfp->cur_token->type != CFTOKEN_NAME) return false;
+		output.write(cfp->cur_token->str.array,
+				cfp->cur_token->str.len);
+
+		if (!cf_next_token(cfp))    return false;
+		if (!cf_token_is(cfp, ",")) return false;
+		output << ", ";
+	} else
+		output << "read((u";
+
+	/* location */
+	if (!cf_next_token(cfp))    return false;
+	if (type != ShaderTextureCallType::Sample &&
+	    type != ShaderTextureCallType::Load) {
+		WriteFunctionContent(cfp->cur_token, ",");
+
+		/* bias, gradient2d, level */
+		switch (type)
+		{
+		case ShaderTextureCallType::SampleBias:
+			output << "bias(";
+			if (!cf_next_token(cfp))    return false;
+			WriteFunctionContent(cfp->cur_token, ")");
+			output << ')';
+			break;
+
+		case ShaderTextureCallType::SampleGrad:
+			output << "gradient2d(";
+			if (!cf_next_token(cfp))    return false;
+			WriteFunctionContent(cfp->cur_token, ",");
+			if (!cf_next_token(cfp))    return false;
+			WriteFunctionContent(cfp->cur_token, ")");
+			output << ')';
+			break;
+
+		case ShaderTextureCallType::SampleLevel:
+			output << "level(";
+			if (!cf_next_token(cfp))    return false;
+			WriteFunctionContent(cfp->cur_token, ")");
+			output << ')';
+			break;
+
+		default:
+			break;
+		}
+	} else
+		WriteFunctionContent(cfp->cur_token, ")");
+
+	/* ) */
+	if (type == ShaderTextureCallType::Load)
+		output << ").xy)";
+	else
+		output << ')';
+
+	return true;
+}
+
+inline bool ShaderBuilder::WriteTextureCode(struct cf_token *&token,
+		struct shader_var *var)
+{
+	struct cf_parser *cfp = &parser->cfp;
+	bool succeeded = false;
+	cfp->cur_token = token;
+
+	if (!cf_next_token(cfp))    return false;
+	if (!cf_token_is(cfp, ".")) return false;
+	output << var->name << ".";
+
+	if (!cf_next_token(cfp))    return false;
+	if (cf_token_is(cfp, "Sample"))
+		succeeded = WriteTextureCall(cfp->cur_token,
+				ShaderTextureCallType::Sample);
+	else if (cf_token_is(cfp, "SampleBias"))
+		succeeded = WriteTextureCall(cfp->cur_token,
+				ShaderTextureCallType::SampleBias);
+	else if (cf_token_is(cfp, "SampleGrad"))
+		succeeded = WriteTextureCall(cfp->cur_token,
+				ShaderTextureCallType::SampleGrad);
+	else if (cf_token_is(cfp, "SampleLevel"))
+		succeeded = WriteTextureCall(cfp->cur_token,
+				ShaderTextureCallType::SampleLevel);
+	else if (cf_token_is(cfp, "Load"))
+		succeeded = WriteTextureCall(cfp->cur_token,
+				ShaderTextureCallType::Load);
+
+	if (!succeeded)
+		throw "Failed to write texture code";
+
+	token = cfp->cur_token;
+	return true;
+}
+
+inline struct shader_var *ShaderBuilder::GetVariable(struct cf_token *token)
+{
+	for (struct shader_var *var = parser->params.array;
+	     var != parser->params.array + parser->params.num;
+	     var++) {
+		if (strref_cmp(&token->str, var->name) == 0)
+			return var;
+	}
+
+	return nullptr;
+}
+
+inline bool ShaderBuilder::WriteIntrinsic(struct cf_token *&token)
+{
+	bool written = true;
+
+	if (strref_cmp(&token->str, "clip") == 0)
+		throw "clip is not supported in Metal";
+	else if (strref_cmp(&token->str, "ddx") == 0)
+		output << "dfdx";
+	else if (strref_cmp(&token->str, "ddy") == 0)
+		output << "dfdy";
+	else if (strref_cmp(&token->str, "frac") == 0)
+		output << "fract";
+	else if (strref_cmp(&token->str, "lerp") == 0)
+		output << "mix";
+	else if (strref_cmp(&token->str, "mul") == 0)
+		written = WriteMul(token);
+	else {
+		struct shader_var *var = GetVariable(token);
+		if (var != nullptr && astrcmp_n(var->type, "texture", 7) == 0)
+			written = WriteTextureCode(token, var);
+		else
+			written = false;
+	}
+
+	return written;
+}
+
+inline void ShaderBuilder::AnalysisFunction(struct cf_token *&token,
+		const char *end, ShaderFunctionInfo &info)
+{
+	while (token->type != CFTOKEN_NONE) {
+		token++;
+
+		if (strref_cmp(&token->str, end) == 0)
+			break;
+
+		if (token->type == CFTOKEN_NAME) {
+			string name(token->str.array, token->str.len);
+
+			/* Check function */
+			const auto fi = functionInfo.find(name);
+			if (fi != functionInfo.end()) {
+				if (fi->second.useUniform)
+					info.useUniform = true;
+				info.useTextures.insert(info.useTextures.end(),
+						fi->second.useTextures.begin(),
+						fi->second.useTextures.end());
+#if USE_PROGRAMMABLE_SAMPLER
+				info.useSamplers.insert(info.useSamplers.end(),
+						fi->second.useSamplers.begin(),
+						fi->second.useSamplers.end());
+#endif
+				continue;
+			}
+
+			/* Check UniformData */
+			if (!info.useUniform &&
+			    constantNames.find(name) != constantNames.end()) {
+				info.useUniform = true;
+				continue;
+			}
+
+			/* Check texture */
+			if (isPixelShader()) {
+				for (auto tex = textureVars.cbegin();
+				     tex != textureVars.cend();
+				     tex++) {
+					if (name == (*tex)->name) {
+						info.useTextures.emplace_back(
+								name);
+						break;
+					}
+				}
+#if USE_PROGRAMMABLE_SAMPLER
+				for (struct shader_sampler *sampler =
+						parser->samplers.array;
+				     sampler != parser->samplers.array +
+						parser->samplers.num;
+				     sampler++) {
+					if (name == sampler->name) {
+						info.useSamplers.emplace_back(
+									name);
+						break;
+					}
+				}
+#endif
+			}
+
+		} else if (token->type == CFTOKEN_OTHER) {
+			if (*token->str.array == '{')
+				AnalysisFunction(token, "}", info);
+			else if (*token->str.array == '(')
+				AnalysisFunction(token, ")", info);
+		}
+	}
+}
+
+inline void ShaderBuilder::WriteFunctionAdditionalParam(string funcionName)
+{
+	auto fi = functionInfo.find(funcionName);
+	if (fi != functionInfo.end()) {
+		if (fi->second.useUniform)
+			output << ", uniforms";
+
+		for (auto var = textureVars.cbegin();
+		     var != textureVars.cend();
+		     var++) {
+			for (auto tex = fi->second.useTextures.cbegin();
+			     tex != fi->second.useTextures.cend();
+			     tex++) {
+				if (*tex == (*var)->name) {
+					output << ", " << *tex;
+					break;
+				}
+			}
+		}
+
+#if USE_PROGRAMMABLE_SAMPLER
+		for (struct shader_sampler *sampler = parser->samplers.array;
+		     sampler != parser->samplers.array + parser->samplers.num;
+		     sampler++) {
+			for (auto s = fi->second.useSamplers.cbegin();
+			     s != fi->second.useSamplers.cend();
+			     s++) {
+				if (*s == sampler->name) {
+					output << ", " << *s;
+					break;
+				}
+			}
+		}
+#endif
+	}
+}
+
+inline bool ShaderBuilder::IsNextCompareOperator(struct cf_token *&token)
+{
+	struct cf_token *token2 = token + 1;
+	if (token2->type != CFTOKEN_OTHER)
+		return false;
+
+	if (astrcmp_n(token2->str.array, "==", token2->str.len) == 0 ||
+	    astrcmp_n(token2->str.array, "!=", token2->str.len) == 0 ||
+	    astrcmp_n(token2->str.array, "<", token2->str.len) == 0 ||
+	    astrcmp_n(token2->str.array, "<=", token2->str.len) == 0 ||
+	    astrcmp_n(token2->str.array, ">", token2->str.len) == 0 ||
+	    astrcmp_n(token2->str.array, ">=", token2->str.len) == 0)
+		return true;
+	return false;
+}
+
+inline void ShaderBuilder::WriteFunctionContent(struct cf_token *&token,
+		const char *end)
+{
+	string temp;
+	if (token->type != CFTOKEN_NAME)
+		output.write(token->str.array, token->str.len);
+
+	else if ((!WriteTypeToken(token) && !WriteIntrinsic(token) &&
+	     !WriteConstantVariable(token))) {
+		temp = string(token->str.array, token->str.len);
+		output << temp;
+	}
+
+	bool dot = false;
+	bool cmp = false;
+	while (token->type != CFTOKEN_NONE) {
+		token++;
+
+		if (strref_cmp(&token->str, end) == 0)
+			break;
+
+		if (token->type == CFTOKEN_NAME) {
+			if (!WriteTypeToken(token) && !WriteIntrinsic(token) &&
+			    (dot || !WriteConstantVariable(token))) {
+				if (dot)
+					dot = false;
+
+				bool cmp2 = IsNextCompareOperator(token);
+				if (cmp2)
+					output << "all(";
+
+				temp = string(token->str.array, token->str.len);
+				output << temp;
+
+				if (cmp) {
+					output << ")";
+					cmp = false;
+				}
+
+				cmp = cmp2;
+			}
+
+		} else if (token->type == CFTOKEN_OTHER) {
+			if (*token->str.array == '{')
+				WriteFunctionContent(token, "}");
+			else if (*token->str.array == '(') {
+				WriteFunctionContent(token, ")");
+				WriteFunctionAdditionalParam(temp);
+			} else if (*token->str.array == '.')
+				dot = true;
+
+			output.write(token->str.array, token->str.len);
+
+		} else
+			output.write(token->str.array, token->str.len);
+	}
+}
+
+inline void ShaderBuilder::WriteFunction(const shader_func *func)
+{
+	string funcName(func->name);
+
+	const bool isMain = funcName == "main";
+	if (isMain) {
+		if (isVertexShader())
+			output << "vertex ";
+		else if (isPixelShader())
+			output << "fragment ";
+		else
+			throw "Failed to add shader prefix";
+
+		funcName = "_main";
+	}
+
+	ShaderFunctionInfo info = {};
+	struct cf_token *token = func->start;
+	AnalysisFunction(token, "}", info);
+	unique(info.useTextures.begin(), info.useTextures.end());
+	unique(info.useSamplers.begin(), info.useSamplers.end());
+	functionInfo.emplace(funcName, info);
+
+	output << func->return_type << ' ' << funcName << '(';
+
+	bool isFirst = true;
+	for (struct shader_var *param = func->params.array;
+	     param != func->params.array + func->params.num;
+	     param++) {
+		if (!isFirst)
+			output << ", ";
+
+		WriteVariable(param);
+
+		if (isMain) {
+			if (!isFirst)
+				throw "Failed to add type";
+			output << " [[stage_in]]";
+
+		}
+
+		if (isFirst)
+			isFirst = false;
+	}
+
+	if (constantNames.size() != 0 && (isMain || info.useUniform))
+	{
+		if (!isFirst)
+			output << ", ";
+
+		output << "constant " << UNIFORM_DATA_NAME << " &uniforms";
+
+		if (isMain)
+			output << " [[buffer(30)]]";
+
+		if (isFirst)
+			isFirst = false;
+	}
+
+	if (isPixelShader())
+	{
+		size_t textureId = 0;
+		for (auto var = textureVars.cbegin();
+		     var != textureVars.cend();
+		     var++) {
+			if (!isMain) {
+				bool additional = false;
+				for (auto tex = info.useTextures.cbegin();
+				     tex != info.useTextures.cend();
+				     tex++) {
+					if (*tex == (*var)->name) {
+						additional = true;
+						break;
+					}
+				}
+				if (!additional)
+					continue;
+			}
+
+			if (!isFirst)
+				output << ", ";
+
+			WriteVariable(*var);
+
+			if (isMain)
+				output << " [[texture(" << textureId++ << ")]]";
+
+			if (isFirst)
+				isFirst = false;
+		}
+
+#if USE_PROGRAMMABLE_SAMPLER
+		size_t samplerId = 0;
+		for (struct shader_sampler *sampler = parser->samplers.array;
+		     sampler != parser->samplers.array + parser->samplers.num;
+		     sampler++) {
+			if (!isMain) {
+				bool additional = false;
+				for (auto s = info.useSamplers.cbegin();
+				     s != info.useSamplers.cend();
+				     s++) {
+					if (*s == sampler->name) {
+						additional = true;
+						break;
+					}
+				}
+				if (!additional)
+					continue;
+			}
+
+			if (!isFirst)
+				output << ", ";
+
+			output << "sampler " << sampler->name;
+
+			if (isMain)
+				output << " [[sampler(" << samplerId++ << ")]]";
+
+			if (isFirst)
+				isFirst = false;
+		}
+#endif
+	}
+
+	output << ")" << endl;
+
+	token = func->start;
+	WriteFunctionContent(token, "}");
+
+	output << '}' << endl << endl;
+}
+
+inline void ShaderBuilder::WriteFunctions()
+{
+	for (struct shader_func *func = parser->funcs.array;
+	     func != parser->funcs.array + parser->funcs.num;
+	     func++)
+		WriteFunction(func);
+}
+
+void ShaderBuilder::Build(string &outputString)
+{
+	WriteInclude();
+#if !USE_PROGRAMMABLE_SAMPLER
+	WriteSamplers();
+#endif
+	WriteVariables();
+	WriteStructs();
+	WriteFunctions();
+	outputString = string(output.str(), output.pcount());
+	output.freeze(false);
+}
+
+string build_shader(gs_shader_type type, ShaderParser *parser)
+{
+	string output;
+	ShaderBuilder(type, parser).Build(output);
+	return output;
+}

+ 38 - 0
libobs-metal/metal-shaderprocessor.hpp

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <string>
+
+#include <graphics/shader-parser.h>
+
+struct ShaderBufferInfo {
+	bool     normals  = false;
+	bool     colors   = false;
+	bool     tangents = false;
+	uint32_t texUnits = 0;
+};
+
+struct ShaderParser : shader_parser {
+	inline ShaderParser()  {shader_parser_init(this);}
+	inline ~ShaderParser() {shader_parser_free(this);}
+};
+
+#ifdef __OBJC__
+struct ShaderProcessor {
+	gs_device_t *device;
+	ShaderParser parser;
+
+	void BuildVertexDesc(__weak MTLVertexDescriptor *vertexDesc);
+	void BuildParamInfo(ShaderBufferInfo &info);
+	void BuildParams(std::vector<gs_shader_param> &params);
+	void BuildSamplers(std::vector<std::unique_ptr<ShaderSampler>>
+			&samplers);
+	std::string BuildString(gs_shader_type type);
+	void Process(const char *shader_string, const char *file);
+
+	inline ShaderProcessor(gs_device_t *device) : device(device)
+	{
+	}
+};
+#endif
+
+extern std::string build_shader(gs_shader_type type, ShaderParser *parser);

+ 186 - 0
libobs-metal/metal-shaderprocessor.mm

@@ -0,0 +1,186 @@
+#include "metal-subsystem.hpp"
+#include "metal-shaderprocessor.hpp"
+
+#include <vector>
+
+using namespace std;
+
+static inline void AddInputLayoutVar(shader_var *var,
+		MTLVertexAttributeDescriptor *vad,
+		MTLVertexBufferLayoutDescriptor *vbld)
+{
+	if (strcmp(var->mapping, "COLOR") == 0) {
+		vad.format = MTLVertexFormatUChar4Normalized;
+		vbld.stride = sizeof(vec4);
+
+	} else if (strcmp(var->mapping, "POSITION") == 0 ||
+	           strcmp(var->mapping, "NORMAL")   == 0 ||
+	           strcmp(var->mapping, "TANGENT")  == 0) {
+		vad.format = MTLVertexFormatFloat4;
+		vbld.stride = sizeof(vec4);
+		
+	} else if (astrcmp_n(var->mapping, "TEXCOORD", 8) == 0) {
+		/* type is always a 'float' type */
+		switch (var->type[5]) {
+		case 0:
+			vad.format = MTLVertexFormatFloat;
+			vbld.stride = sizeof(float);
+			break;
+		
+		case '2':
+			vad.format = MTLVertexFormatFloat2;
+			vbld.stride = sizeof(float) * 2;
+			break;
+				
+		case '3':
+			vad.format = MTLVertexFormatFloat3;
+			vbld.stride = sizeof(vec3);
+			break;
+				
+		case '4':
+			vad.format = MTLVertexFormatFloat4;
+			vbld.stride = sizeof(vec4);
+			break;
+		}
+	}
+}
+
+static inline void BuildVertexDescFromVars(shader_parser *parser, darray *vars,
+		__weak MTLVertexDescriptor *vd, size_t &index)
+{
+	shader_var *array = (shader_var*)vars->array;
+
+	for (size_t i = 0; i < vars->num; i++) {
+		shader_var *var = array + i;
+
+		if (var->mapping) {
+			vd.attributes[index].bufferIndex = index;
+			AddInputLayoutVar(var, vd.attributes[index],
+					vd.layouts[index++]);
+		} else {
+			shader_struct *st = shader_parser_getstruct(parser,
+					var->type);
+			if (st)
+				BuildVertexDescFromVars(parser, &st->vars.da,
+						vd, index);
+		}
+	}
+}
+
+void ShaderProcessor::BuildVertexDesc(__weak MTLVertexDescriptor *vertexDesc)
+{
+	shader_func *func = shader_parser_getfunc(&parser, "main");
+	if (!func)
+		throw "Failed to find 'main' shader function";
+
+	size_t index = 0;
+	BuildVertexDescFromVars(&parser, &func->params.da, vertexDesc, index);
+}
+
+static inline void BuildParamInfoFromVars(shader_parser *parser, darray *vars,
+		ShaderBufferInfo &info)
+{
+	shader_var *array = (shader_var*)vars->array;
+	
+	for (size_t i = 0; i < vars->num; i++) {
+		shader_var *var = array + i;
+		
+		if (var->mapping) {
+			if (strcmp(var->mapping, "NORMAL") == 0)
+				info.normals = true;
+			else if (strcmp(var->mapping, "TANGENT") == 0)
+				info.tangents = true;
+			else if (strcmp(var->mapping, "COLOR") == 0)
+				info.colors = true;
+			else if (astrcmp_n(var->mapping, "TEXCOORD", 8) == 0)
+				info.texUnits++;
+
+		} else {
+			shader_struct *st = shader_parser_getstruct(parser,
+					var->type);
+			if (st)
+				BuildParamInfoFromVars(parser, &st->vars.da,
+						info);
+		}
+	}
+}
+
+void ShaderProcessor::BuildParamInfo(ShaderBufferInfo &info)
+{
+	shader_func *func = shader_parser_getfunc(&parser, "main");
+	if (!func)
+		throw "Failed to find 'main' shader function";
+	
+	BuildParamInfoFromVars(&parser, &func->params.da, info);
+}
+
+gs_shader_param::gs_shader_param(shader_var &var, uint32_t &texCounter)
+	: name       (var.name),
+	  type       (get_shader_param_type(var.type)),
+	  arrayCount (var.array_count),
+	  textureID  (texCounter),
+	  changed    (false)
+{
+	defaultValue.resize(var.default_val.num);
+	memcpy(defaultValue.data(), var.default_val.array, var.default_val.num);
+
+	if (type == GS_SHADER_PARAM_TEXTURE)
+		texCounter++;
+	else
+		textureID = 0;
+}
+
+static inline void AddParam(shader_var &var, vector<gs_shader_param> &params,
+		uint32_t &texCounter)
+{
+	if (var.var_type != SHADER_VAR_UNIFORM ||
+	    strcmp(var.type, "sampler") == 0)
+		return;
+
+	params.push_back(gs_shader_param(var, texCounter));
+}
+
+void ShaderProcessor::BuildParams(vector<gs_shader_param> &params)
+{
+	uint32_t texCounter = 0;
+
+	for (struct shader_var *var = parser.params.array;
+	     var != parser.params.array + parser.params.num;
+	     var++)
+		AddParam(*var, params, texCounter);
+}
+
+static inline void AddSampler(gs_device_t *device, shader_sampler &sampler,
+			vector<unique_ptr<ShaderSampler>> &samplers)
+{
+	gs_sampler_info si;
+	shader_sampler_convert(&sampler, &si);
+	samplers.emplace_back(new ShaderSampler(sampler.name, device, &si));
+}
+
+void ShaderProcessor::BuildSamplers(vector<unique_ptr<ShaderSampler>> &samplers)
+{
+	for (struct shader_sampler *sampler = parser.samplers.array;
+	     sampler != parser.samplers.array + parser.samplers.num;
+	     sampler++)
+		AddSampler(device, *sampler, samplers);
+}
+
+string ShaderProcessor::BuildString(gs_shader_type type)
+{
+	return build_shader(type, &parser);
+}
+
+void ShaderProcessor::Process(const char *shader_string, const char *file)
+{
+	bool success = shader_parse(&parser, shader_string, file);
+	char *str = shader_parser_geterrors(&parser);
+	if (str) {
+		blog(LOG_WARNING, "Shader parser errors/warnings:\n%s\n", str);
+		bfree(str);
+	}
+	
+	if (!success)
+		throw "Failed to parse shader";
+}
+

+ 33 - 0
libobs-metal/metal-stagesurf.mm

@@ -0,0 +1,33 @@
+#include "metal-subsystem.hpp"
+
+void gs_stage_surface::DownloadTexture()
+{
+	MTLRegion from = MTLRegionMake2D(0, 0, width, height);
+	[texture getBytes:data.data() bytesPerRow:bytePerRow
+		fromRegion:from mipmapLevel:0];
+}
+
+void gs_stage_surface::InitTexture()
+{
+	texture = [device->device newTextureWithDescriptor:textureDesc];
+	if (texture == nil)
+		throw "Failed to create staging surface";
+}
+
+gs_stage_surface::gs_stage_surface(gs_device_t *device, uint32_t width,
+		uint32_t height, gs_color_format colorFormat)
+	: gs_obj     (device, gs_type::gs_stage_surface),
+	  width      (width),
+	  height     (height),
+	  bytePerRow (width * gs_get_format_bpp(colorFormat) / 8),
+	  format     (colorFormat)
+{
+	textureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
+			ConvertGSTextureFormat(colorFormat)
+			width:width height:height mipmapped:NO];
+	textureDesc.storageMode = MTLStorageModeManaged;
+
+	data.resize(height * bytePerRow);
+
+	InitTexture();
+}

+ 706 - 0
libobs-metal/metal-subsystem.hpp

@@ -0,0 +1,706 @@
+#pragma once
+
+#include <util/AlignedNew.hpp>
+
+#include <vector>
+#include <stack>
+#include <queue>
+#include <string>
+#include <mutex>
+#include <memory>
+
+#include <util/base.h>
+#include <graphics/matrix4.h>
+#include <graphics/graphics.h>
+#include <graphics/device-exports.h>
+
+#ifdef __OBJC__
+#import <AppKit/NSView.h>
+#import <QuartzCore/CoreAnimation.h>
+#import <Metal/Metal.h>
+
+struct shader_var;
+struct gs_vertex_shader;
+
+
+static inline MTLPixelFormat ConvertGSTextureFormat(gs_color_format format)
+{
+	switch (format) {
+	case GS_UNKNOWN:     return MTLPixelFormatInvalid;
+	case GS_A8:          return MTLPixelFormatA8Unorm;
+	case GS_R8:          return MTLPixelFormatR8Unorm;
+	case GS_RGBA:        return MTLPixelFormatRGBA8Unorm;
+	case GS_BGRX:        return MTLPixelFormatBGRA8Unorm;
+	case GS_BGRA:        return MTLPixelFormatBGRA8Unorm;
+	case GS_R10G10B10A2: return MTLPixelFormatRGB10A2Unorm;
+	case GS_RGBA16:      return MTLPixelFormatRGBA16Unorm;
+	case GS_R16:         return MTLPixelFormatR16Unorm;
+	case GS_RGBA16F:     return MTLPixelFormatRGBA16Float;
+	case GS_RGBA32F:     return MTLPixelFormatRGBA32Float;
+	case GS_RG16F:       return MTLPixelFormatRG16Float;
+	case GS_RG32F:       return MTLPixelFormatRG32Float;
+	case GS_R16F:        return MTLPixelFormatR16Float;
+	case GS_R32F:        return MTLPixelFormatR32Float;
+	case GS_DXT1:        return MTLPixelFormatBC1_RGBA;
+	case GS_DXT3:        return MTLPixelFormatBC2_RGBA;
+	case GS_DXT5:        return MTLPixelFormatBC3_RGBA;
+	}
+
+	return MTLPixelFormatInvalid;
+}
+
+static inline gs_color_format ConvertMTLTextureFormat(MTLPixelFormat format)
+{
+	switch ((NSUInteger)format) {
+	case MTLPixelFormatA8Unorm:       return GS_A8;
+	case MTLPixelFormatR8Unorm:       return GS_R8;
+	case MTLPixelFormatRGBA8Unorm:    return GS_RGBA;
+	case MTLPixelFormatBGRA8Unorm:    return GS_BGRA;
+	case MTLPixelFormatRGB10A2Unorm:  return GS_R10G10B10A2;
+	case MTLPixelFormatRGBA16Unorm:   return GS_RGBA16;
+	case MTLPixelFormatR16Unorm:      return GS_R16;
+	case MTLPixelFormatRGBA16Float:   return GS_RGBA16F;
+	case MTLPixelFormatRGBA32Float:   return GS_RGBA32F;
+	case MTLPixelFormatRG16Float:     return GS_RG16F;
+	case MTLPixelFormatRG32Float:     return GS_RG32F;
+	case MTLPixelFormatR16Float:      return GS_R16F;
+	case MTLPixelFormatR32Float:      return GS_R32F;
+	case MTLPixelFormatBC1_RGBA:      return GS_DXT1;
+	case MTLPixelFormatBC2_RGBA:      return GS_DXT3;
+	case MTLPixelFormatBC3_RGBA:      return GS_DXT5;
+	}
+
+	return GS_UNKNOWN;
+}
+
+static inline gs_color_format ConvertOSTypePixelFormat(OSType format)
+{
+	if (format == 'BGRA') return GS_BGRA;
+	if (format == 'w30r') return GS_R10G10B10A2;
+	return GS_UNKNOWN;
+}
+
+static inline MTLCompareFunction ConvertGSDepthTest(gs_depth_test test)
+{
+	switch (test) {
+	case GS_NEVER:    return MTLCompareFunctionNever;
+	case GS_LESS:     return MTLCompareFunctionLess;
+	case GS_LEQUAL:   return MTLCompareFunctionLessEqual;
+	case GS_EQUAL:    return MTLCompareFunctionEqual;
+	case GS_GEQUAL:   return MTLCompareFunctionGreaterEqual;
+	case GS_GREATER:  return MTLCompareFunctionGreater;
+	case GS_NOTEQUAL: return MTLCompareFunctionNotEqual;
+	case GS_ALWAYS:   return MTLCompareFunctionAlways;
+	}
+
+	return MTLCompareFunctionNever;
+}
+
+static inline MTLStencilOperation ConvertGSStencilOp(gs_stencil_op_type op)
+{
+	switch (op) {
+	case GS_KEEP:    return MTLStencilOperationKeep;
+	case GS_ZERO:    return MTLStencilOperationZero;
+	case GS_REPLACE: return MTLStencilOperationReplace;
+	case GS_INCR:    return MTLStencilOperationIncrementWrap;
+	case GS_DECR:    return MTLStencilOperationDecrementWrap;
+	case GS_INVERT:  return MTLStencilOperationInvert;
+	}
+
+	return MTLStencilOperationKeep;
+}
+
+static inline MTLBlendFactor ConvertGSBlendType(gs_blend_type type)
+{
+	switch (type) {
+	case GS_BLEND_ZERO:
+		return MTLBlendFactorZero;
+	case GS_BLEND_ONE:
+		return MTLBlendFactorOne;
+	case GS_BLEND_SRCCOLOR:
+		return MTLBlendFactorSourceColor;
+	case GS_BLEND_INVSRCCOLOR:
+		return MTLBlendFactorOneMinusSourceColor;
+	case GS_BLEND_SRCALPHA:
+		return MTLBlendFactorSourceAlpha;
+	case GS_BLEND_INVSRCALPHA:
+		return MTLBlendFactorOneMinusSourceAlpha;
+	case GS_BLEND_DSTCOLOR:
+		return MTLBlendFactorDestinationColor;
+	case GS_BLEND_INVDSTCOLOR:
+		return MTLBlendFactorOneMinusDestinationColor;
+	case GS_BLEND_DSTALPHA:
+		return MTLBlendFactorDestinationAlpha;
+	case GS_BLEND_INVDSTALPHA:
+		return MTLBlendFactorOneMinusDestinationAlpha;
+	case GS_BLEND_SRCALPHASAT:
+		return MTLBlendFactorSourceAlphaSaturated;
+	}
+
+	return MTLBlendFactorOne;
+}
+
+static inline MTLCullMode ConvertGSCullMode(gs_cull_mode mode)
+{
+	switch (mode) {
+	case GS_BACK:    return MTLCullModeBack;
+	case GS_FRONT:   return MTLCullModeFront;
+	case GS_NEITHER: return MTLCullModeNone;
+	}
+
+	return MTLCullModeBack;
+}
+
+static inline MTLPrimitiveType ConvertGSTopology(gs_draw_mode mode)
+{
+	switch (mode) {
+	case GS_POINTS:    return MTLPrimitiveTypePoint;
+	case GS_LINES:     return MTLPrimitiveTypeLine;
+	case GS_LINESTRIP: return MTLPrimitiveTypeLineStrip;
+	case GS_TRIS:      return MTLPrimitiveTypeTriangle;
+	case GS_TRISTRIP:  return MTLPrimitiveTypeTriangleStrip;
+	}
+
+	return MTLPrimitiveTypePoint;
+}
+
+static inline MTLViewport ConvertGSRectToMTLViewport(gs_rect rect)
+{
+	MTLViewport ret;
+	ret.originX = rect.x;
+	ret.originY = rect.y;
+	ret.width   = rect.cx;
+	ret.height  = rect.cy;
+	ret.znear   = 0.0;
+	ret.zfar    = 1.0;
+	return ret;
+}
+
+static inline MTLScissorRect ConvertGSRectToMTLScissorRect(gs_rect rect)
+{
+	MTLScissorRect ret;
+	ret.x      = rect.x;
+	ret.y      = rect.y;
+	ret.width  = rect.cx;
+	ret.height = rect.cy;
+	return ret;
+}
+
+enum class gs_type {
+	gs_vertex_buffer,
+	gs_index_buffer,
+	gs_texture_2d,
+	gs_zstencil_buffer,
+	gs_stage_surface,
+	gs_sampler_state,
+	gs_vertex_shader,
+	gs_pixel_shader,
+	gs_swap_chain,
+};
+
+struct gs_obj {
+	gs_device_t *device;
+	gs_type obj_type;
+	gs_obj *next;
+	gs_obj **prev_next;
+
+	inline gs_obj() :
+		device    (nullptr),
+		next      (nullptr),
+		prev_next (nullptr)
+	{
+	}
+
+	gs_obj(gs_device_t *device, gs_type type);
+	virtual ~gs_obj();
+};
+
+struct gs_vertex_buffer : gs_obj {
+	const bool                 isDynamic;
+	const std::unique_ptr<gs_vb_data, decltype(&gs_vbdata_destroy)> vbData;
+
+	id<MTLBuffer>              vertexBuffer;
+	id<MTLBuffer>              normalBuffer;
+	id<MTLBuffer>              colorBuffer;
+	id<MTLBuffer>              tangentBuffer;
+	std::vector<id<MTLBuffer>> uvBuffers;
+
+	inline id<MTLBuffer> PrepareBuffer(void *array, size_t elementSize,
+			__weak NSString *name);
+	void PrepareBuffers();
+
+	void MakeBufferList(gs_vertex_shader *shader,
+			std::vector<id<MTLBuffer>> &buffers);
+
+	inline id<MTLBuffer> InitBuffer(size_t elementSize, void *array,
+			const char *name);
+	void InitBuffers();
+
+	inline void Release()
+	{
+		vertexBuffer = nil;
+		normalBuffer = nil;
+		colorBuffer  = nil;
+		tangentBuffer = nil;
+		uvBuffers.clear();
+	}
+	void Rebuild();
+
+	gs_vertex_buffer(gs_device_t *device, struct gs_vb_data *data,
+			uint32_t flags);
+};
+
+struct gs_index_buffer : gs_obj {
+	const gs_index_type type;
+	const bool          isDynamic;
+	const std::unique_ptr<void, decltype(&bfree)> indices;
+	const size_t        num = 0, len = 0;
+	const MTLIndexType  indexType;
+
+	id<MTLBuffer>       indexBuffer;
+
+	void PrepareBuffer();
+	void InitBuffer();
+
+	inline void Release() {indexBuffer = nil;}
+	void Rebuild();
+
+	gs_index_buffer(gs_device_t *device, enum gs_index_type type,
+			void *indices, size_t num, uint32_t flags);
+};
+
+struct gs_texture : gs_obj {
+	const gs_texture_type type   = GS_TEXTURE_2D;
+	const uint32_t        levels;
+	const gs_color_format format = GS_UNKNOWN;
+
+	inline gs_texture(gs_device *device, gs_type obj_type,
+			gs_texture_type type,
+			uint32_t levels, gs_color_format format)
+		: gs_obj (device, obj_type),
+		  type   (type),
+		  levels (levels),
+		  format (format)
+	{
+	}
+};
+
+struct gs_texture_2d : gs_texture {
+	const uint32_t       width = 0, height = 0, bytePerRow = 0;
+	const bool           isRenderTarget = false;
+	const bool           isDynamic      = false;
+	const bool           genMipmaps     = false;
+	const bool           isShared       = false;
+	const MTLPixelFormat mtlPixelFormat = MTLPixelFormatInvalid;
+
+	MTLTextureDescriptor *textureDesc = nil;
+	id<MTLTexture>       texture = nil;
+
+	std::vector<std::vector<uint8_t>> data;
+
+	void GenerateMipmap();
+	void BackupTexture(const uint8_t **data);
+	void UploadTexture();
+	void InitTexture();
+
+	inline void Release() {texture = nil;}
+	void Rebuild();
+
+	gs_texture_2d(gs_device_t *device, uint32_t width, uint32_t height,
+			gs_color_format colorFormat, uint32_t levels,
+			const uint8_t **data, uint32_t flags,
+			gs_texture_type type);
+
+	gs_texture_2d(gs_device_t *device, id<MTLTexture> texture);
+};
+
+struct gs_zstencil_buffer : gs_obj {
+	const uint32_t           width = 0, height = 0;
+	const gs_zstencil_format format = GS_ZS_NONE;
+
+	MTLTextureDescriptor     *textureDesc;
+	id<MTLTexture>           texture;
+
+	void InitBuffer();
+
+	inline void Release() {texture = nil;}
+	inline void Rebuild() {InitBuffer();}
+
+	gs_zstencil_buffer(gs_device_t *device, uint32_t width,
+			uint32_t height, gs_zstencil_format format);
+};
+
+struct gs_stage_surface : gs_obj {
+	const uint32_t        width = 0, height = 0, bytePerRow = 0;
+	const gs_color_format format = GS_UNKNOWN;
+
+	MTLTextureDescriptor  *textureDesc;
+	id<MTLTexture>        texture;
+
+	std::vector<uint8_t>  data;
+
+	void DownloadTexture();
+	void InitTexture();
+
+	inline void Release() {texture = nil;}
+	inline void Rebuild() {InitTexture();};
+
+	gs_stage_surface(gs_device_t *device, uint32_t width, uint32_t height,
+			gs_color_format colorFormat);
+};
+
+struct gs_sampler_state : gs_obj {
+	const gs_sampler_info info;
+
+	MTLSamplerDescriptor  *samplerDesc;
+	id<MTLSamplerState>   samplerState;
+
+	void InitSampler();
+
+	inline void Release() {samplerState = nil;}
+	inline void Rebuild() {InitSampler();}
+
+	gs_sampler_state(gs_device_t *device, const gs_sampler_info *info);
+};
+
+struct gs_shader_param {
+	const std::string          name;
+	const gs_shader_param_type type;
+	const int                  arrayCount;
+
+	struct gs_sampler_state    *nextSampler = nullptr;
+
+	uint32_t                   textureID;
+	size_t                     pos;
+
+	std::vector<uint8_t>       curValue;
+	std::vector<uint8_t>       defaultValue;
+	bool                       changed;
+
+	gs_shader_param(shader_var &var, uint32_t &texCounter);
+};
+
+struct ShaderError {
+	const std::string error;
+
+	inline ShaderError(NSError *error)
+		: error (error.localizedDescription.UTF8String)
+	{
+	}
+};
+
+struct gs_shader : gs_obj {
+	const gs_shader_type         type;
+	std::string                  source;
+	id<MTLLibrary>               library;
+	id<MTLFunction>              function;
+	std::vector<gs_shader_param> params;
+
+	size_t                       constantSize = 0;
+
+	std::vector<uint8_t>         data;
+
+	inline void UpdateParam(uint8_t *data, gs_shader_param &param);
+	void UploadParams(id<MTLRenderCommandEncoder> commandEncoder);
+
+	void BuildConstantBuffer();
+	void Compile(std::string shaderStr);
+
+	inline void Release()
+	{
+		function = nil;
+		library = nil;
+	}
+	void Rebuild();
+
+	inline gs_shader(gs_device_t *device, gs_type obj_type,
+			gs_shader_type type)
+		: gs_obj       (device, obj_type),
+		  type         (type)
+	{
+	}
+};
+
+struct gs_vertex_shader : gs_shader {
+	MTLVertexDescriptor *vertexDesc;
+
+	bool     hasNormals;
+	bool     hasColors;
+	bool     hasTangents;
+	uint32_t texUnits;
+
+	gs_shader_param *world, *viewProj;
+
+	inline uint32_t NumBuffersExpected() const
+	{
+		uint32_t count = texUnits + 1;
+		if (hasNormals)  count++;
+		if (hasColors)   count++;
+		if (hasTangents) count++;
+
+		return count;
+	}
+
+	gs_vertex_shader(gs_device_t *device, const char *file,
+			const char *shaderString);
+};
+
+struct ShaderSampler {
+	std::string      name;
+	gs_sampler_state sampler;
+
+	inline ShaderSampler(const char *name, gs_device_t *device,
+			gs_sampler_info *info)
+		: name    (name),
+		  sampler (device, info)
+	{
+	}
+};
+
+struct gs_pixel_shader : gs_shader {
+	std::vector<std::unique_ptr<ShaderSampler>> samplers;
+
+	inline void GetSamplerStates(gs_sampler_state **states)
+	{
+		size_t i;
+		for (i = 0; i < samplers.size(); i++)
+			states[i] = &samplers[i]->sampler;
+		for (; i < GS_MAX_TEXTURES; i++)
+			states[i] = nullptr;
+	}
+
+	gs_pixel_shader(gs_device_t *device, const char *file,
+			const char *shaderString);
+};
+
+struct gs_swap_chain : gs_obj {
+	uint32_t            numBuffers;
+	NSView              *view;
+	CAMetalLayer        *metalLayer;
+
+	gs_init_data        initData;
+	id<CAMetalDrawable> nextDrawable;
+	std::unique_ptr<gs_texture_2d> nextTarget;
+
+	gs_texture_2d *GetTarget();
+	gs_texture_2d *NextTarget();
+	void Resize(uint32_t cx, uint32_t cy);
+
+
+	inline void Release()
+	{
+		nextTarget.reset();
+		nextDrawable = nil;
+	}
+	void Rebuild();
+
+	gs_swap_chain(gs_device *device, const gs_init_data *data);
+};
+
+struct ClearState {
+	uint32_t    flags;
+	struct vec4 color;
+	float       depth;
+	uint8_t     stencil;
+
+	inline ClearState()
+		: flags   (0),
+		  color   ({}),
+		  depth   (0.0f),
+		  stencil (0)
+	{
+	}
+};
+
+struct BlendState {
+	bool          blendEnabled;
+	gs_blend_type srcFactorC;
+	gs_blend_type destFactorC;
+	gs_blend_type srcFactorA;
+	gs_blend_type destFactorA;
+
+	bool          redEnabled;
+	bool          greenEnabled;
+	bool          blueEnabled;
+	bool          alphaEnabled;
+
+	inline BlendState()
+		: blendEnabled (true),
+		  srcFactorC   (GS_BLEND_SRCALPHA),
+		  destFactorC  (GS_BLEND_INVSRCALPHA),
+		  srcFactorA   (GS_BLEND_ONE),
+		  destFactorA  (GS_BLEND_ONE),
+		  redEnabled   (true),
+		  greenEnabled (true),
+		  blueEnabled  (true),
+		  alphaEnabled (true)
+	{
+	}
+
+	inline BlendState(const BlendState &state)
+	{
+		memcpy(this, &state, sizeof(BlendState));
+	}
+};
+
+struct RasterState {
+	gs_rect        viewport;
+	gs_cull_mode   cullMode;
+	bool           scissorEnabled;
+	gs_rect        scissorRect;
+
+	MTLViewport    mtlViewport;
+	MTLCullMode    mtlCullMode;
+	MTLScissorRect mtlScissorRect;
+
+	inline RasterState()
+		: viewport       (),
+		  cullMode       (GS_BACK),
+		  scissorEnabled (false),
+		  scissorRect    (),
+		  mtlCullMode    (MTLCullModeBack)
+	{
+	}
+
+	inline RasterState(const RasterState &state)
+	{
+		memcpy(this, &state, sizeof(RasterState));
+	}
+};
+
+struct StencilSide {
+	gs_depth_test test;
+	gs_stencil_op_type fail;
+	gs_stencil_op_type zfail;
+	gs_stencil_op_type zpass;
+
+	inline StencilSide()
+		: test  (GS_ALWAYS),
+		  fail  (GS_KEEP),
+		  zfail (GS_KEEP),
+		  zpass (GS_KEEP)
+	{
+	}
+};
+
+struct ZStencilState {
+	bool          depthEnabled;
+	bool          depthWriteEnabled;
+	gs_depth_test depthFunc;
+
+	bool          stencilEnabled;
+	bool          stencilWriteEnabled;
+	StencilSide   stencilFront;
+	StencilSide   stencilBack;
+
+	MTLDepthStencilDescriptor *dsd;
+
+	inline ZStencilState()
+		: depthEnabled        (true),
+		  depthWriteEnabled   (true),
+		  depthFunc           (GS_LESS),
+		  stencilEnabled      (false),
+		  stencilWriteEnabled (true)
+	{
+		dsd = [[MTLDepthStencilDescriptor alloc] init];
+	}
+
+	inline ZStencilState(const ZStencilState &state)
+	{
+		memcpy(this, &state, sizeof(ZStencilState));
+	}
+};
+
+struct gs_device {
+	uint32_t                    devIdx = 0;
+	uint16_t                    featureSetFamily = 0;
+	uint16_t                    featureSetVersion = 0;
+
+	MTLRenderPassDescriptor     *passDesc;
+	MTLRenderPipelineDescriptor *pipelineDesc;
+	
+	id<MTLDevice>               device;
+	id<MTLCommandQueue>         commandQueue;
+	id<MTLCommandBuffer>        commandBuffer;
+	id<MTLRenderPipelineState>  pipelineState;
+	id<MTLDepthStencilState>    depthStencilState;
+
+	gs_texture_2d               *curRenderTarget = nullptr;
+	int                         curRenderSide = 0;
+	gs_zstencil_buffer          *curZStencilBuffer = nullptr;
+	gs_texture                  *curTextures[GS_MAX_TEXTURES] = {};
+	gs_sampler_state            *curSamplers[GS_MAX_TEXTURES] = {};
+	gs_vertex_buffer            *curVertexBuffer = nullptr;
+	gs_index_buffer             *curIndexBuffer = nullptr;
+	gs_vertex_shader            *curVertexShader = nullptr;
+	gs_pixel_shader             *curPixelShader = nullptr;
+	gs_swap_chain               *curSwapChain = nullptr;
+	gs_stage_surface            *curStageSurface = nullptr;
+
+	gs_vertex_buffer            *lastVertexBuffer = nullptr;
+	gs_vertex_shader            *lastVertexShader = nullptr;
+
+	gs_texture_2d               *preserveClearTarget = nullptr;
+	std::stack<std::pair<gs_texture_2d *, ClearState>> clearStates;
+
+	bool                        piplineStateChanged = false;
+	BlendState                  blendState;
+	RasterState                 rasterState;
+	ZStencilState               zstencilState;
+
+	std::stack<matrix4>         projStack;
+
+	matrix4                     curProjMatrix;
+	matrix4                     curViewMatrix;
+	matrix4                     curViewProjMatrix;
+
+	gs_obj                      *first_obj = nullptr;
+
+	std::mutex                  mutexObj;
+	std::vector<id<MTLBuffer>>  curBufferPool;
+	std::queue<std::vector<id<MTLBuffer>>> bufferPools;
+	std::vector<id<MTLBuffer>>  unusedBufferPool;
+
+	void InitDevice(uint32_t adapterIdx);
+
+	/* Create Draw Command */
+	void SetClear();
+	void LoadSamplers(id<MTLRenderCommandEncoder> commandEncoder);
+	void LoadRasterState(id<MTLRenderCommandEncoder> commandEncoder);
+	void LoadZStencilState(id<MTLRenderCommandEncoder> commandEncoder);
+	void UpdateViewProjMatrix();
+	void UploadVertexBuffer(id<MTLRenderCommandEncoder> commandEncoder);
+	void UploadTextures(id<MTLRenderCommandEncoder> commandEncoder);
+	void UploadSamplers(id<MTLRenderCommandEncoder> commandEncoder);
+	void DrawPrimitives(id<MTLRenderCommandEncoder> commandEncoder,
+			gs_draw_mode drawMode,
+			uint32_t startVert, uint32_t numVerts);
+	void Draw(gs_draw_mode drawMode, uint32_t startVert, uint32_t numVerts);
+
+	/* Buffer Management */
+	id<MTLBuffer> GetBuffer(void *data, size_t length);
+	void PushResources();
+	void ReleaseResources();
+
+	/* Other */
+	void RebuildDevice();
+	inline void CopyTex(id<MTLTexture> dst,
+			uint32_t dst_x, uint32_t dst_y,
+			gs_texture_t *src, uint32_t src_x, uint32_t src_y,
+			uint32_t src_w, uint32_t src_h);
+
+	gs_device(uint32_t adapterIdx);
+};
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef libobs_metal_EXPORTS
+void device_clear_textures(gs_device_t *device);
+#endif
+
+#ifdef __cplusplus
+}
+#endif

+ 1448 - 0
libobs-metal/metal-subsystem.mm

@@ -0,0 +1,1448 @@
+#include <cinttypes>
+#include <util/base.h>
+#include <graphics/matrix3.h>
+
+#include "metal-subsystem.hpp"
+
+using namespace std;
+
+gs_obj::gs_obj(gs_device_t *device_, gs_type type) :
+	device   (device_),
+	obj_type (type)
+{
+	prev_next = &device->first_obj;
+	next = device->first_obj;
+	device->first_obj = this;
+	if (next)
+		next->prev_next = &next;
+}
+
+gs_obj::~gs_obj()
+{
+	if (prev_next)
+		*prev_next = next;
+	if (next)
+		next->prev_next = prev_next;
+}
+
+const char *device_get_name(void)
+{
+	return "Metal";
+}
+
+int device_get_type(void)
+{
+	return GS_DEVICE_METAL;
+}
+
+const char *device_preprocessor_name(void)
+{
+	return "_Metal";
+}
+
+static inline void EnumMetalAdapters(
+		bool (*callback)(void*, const char*, uint32_t),
+		void *param)
+{
+	uint32_t i = 0;
+	NSArray *devices = MTLCopyAllDevices();
+	if (devices == nil)
+		return;
+
+	for (id<MTLDevice> device in devices) {
+		if (!callback(param, [device name].UTF8String, i++))
+			break;
+	}
+}
+
+bool device_enum_adapters(
+		bool (*callback)(void *param, const char *name, uint32_t id),
+		void *param)
+{
+	EnumMetalAdapters(callback, param);
+	return true;
+}
+
+static inline void CheckMetalSupport()
+{
+	if (NSProtocolFromString(@"MTLDevice") == nil)
+		throw "This device doesn't support Metal.";
+}
+
+static inline void LogMetalAdapters()
+{
+	blog(LOG_INFO, "Available Video Adapters: ");
+
+	NSArray *devices = MTLCopyAllDevices();
+	if (devices == nil)
+		return;
+
+	for (size_t i = 0; i < devices.count; i++) {
+		id<MTLDevice> device = devices[i];
+		blog(LOG_INFO, "\tAdapter %zu: %s", i,
+				[device name].UTF8String);
+	}
+}
+
+int device_create(gs_device_t **p_device, uint32_t adapter)
+{
+	int errorcode = GS_SUCCESS;
+
+	gs_device *device = nullptr;
+	try {
+		CheckMetalSupport();
+
+		blog(LOG_INFO, "---------------------------------");
+		blog(LOG_INFO, "Initializing Metal...");
+		LogMetalAdapters();
+
+		device = new gs_device(adapter);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_create (Metal): %s", error);
+		errorcode = GS_ERROR_NOT_SUPPORTED;
+	}
+
+	*p_device = device;
+	return errorcode;
+}
+
+void device_destroy(gs_device_t *device)
+{
+	delete device;
+}
+
+void device_enter_context(gs_device_t *device)
+{
+	/* does nothing */
+	UNUSED_PARAMETER(device);
+}
+
+void device_leave_context(gs_device_t *device)
+{
+	/* does nothing */
+	UNUSED_PARAMETER(device);
+}
+
+gs_swapchain_t *device_swapchain_create(gs_device_t *device,
+		const struct gs_init_data *data)
+{
+	gs_swap_chain *swap = nullptr;
+	try {
+		swap = new gs_swap_chain(device, data);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_swapchain_create (Metal): %s", error);
+	}
+
+	return swap;
+}
+
+void device_resize(gs_device_t *device, uint32_t cx, uint32_t cy)
+{
+	if (device->curSwapChain == nullptr) {
+		blog(LOG_WARNING, "device_resize (Metal): No active swap");
+		return;
+	}
+
+	try {
+		id<MTLTexture> renderTarget = nil;
+		id<MTLTexture> zstencilTarget = nil;
+
+		device->passDesc.colorAttachments[0].texture = nil;
+		device->passDesc.depthAttachment.texture     = nil;
+		device->passDesc.stencilAttachment.texture   = nil;
+		device->curSwapChain->Resize(cx, cy);
+
+		if (device->curRenderTarget)
+			renderTarget = device->curRenderTarget->texture;
+		if (device->curZStencilBuffer)
+			zstencilTarget  = device->curZStencilBuffer->texture;
+
+		device->passDesc.colorAttachments[0].texture = renderTarget;
+		device->passDesc.depthAttachment.texture     = zstencilTarget;
+		device->passDesc.stencilAttachment.texture   = zstencilTarget;
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_resize (Metal): %s", error);
+	}
+}
+
+void device_get_size(const gs_device_t *device, uint32_t *cx, uint32_t *cy)
+{
+	if (device->curSwapChain) {
+		CGSize curSize = device->curSwapChain->metalLayer.drawableSize;
+		*cx = curSize.width;
+		*cy = curSize.height;
+	} else {
+		blog(LOG_ERROR, "device_get_size (Metal): No active swap");
+		*cx = 0;
+		*cy = 0;
+	}
+}
+
+uint32_t device_get_width(const gs_device_t *device)
+{
+	if (device->curSwapChain) {
+		CGSize curSize = device->curSwapChain->metalLayer.drawableSize;
+		return curSize.width;
+	} else {
+		blog(LOG_ERROR, "device_get_size (Metal): No active swap");
+		return 0;
+	}
+}
+
+uint32_t device_get_height(const gs_device_t *device)
+{
+	if (device->curSwapChain) {
+		CGSize curSize = device->curSwapChain->metalLayer.drawableSize;
+		return curSize.height;
+	} else {
+		blog(LOG_ERROR, "device_get_size (Metal): No active swap");
+		return 0;
+	}
+}
+
+gs_texture_t *device_texture_create(gs_device_t *device, uint32_t width,
+		uint32_t height, enum gs_color_format color_format,
+		uint32_t levels, const uint8_t **data, uint32_t flags)
+{
+	gs_texture *texture = nullptr;
+	try {
+		texture = new gs_texture_2d(device, width, height, color_format,
+				levels, data, flags, GS_TEXTURE_2D);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_texture_create (Metal): %s", error);
+	}
+
+	return texture;
+}
+
+gs_texture_t *device_cubetexture_create(gs_device_t *device, uint32_t size,
+		enum gs_color_format color_format, uint32_t levels,
+		const uint8_t **data, uint32_t flags)
+{
+	gs_texture *texture = nullptr;
+	try {
+		texture = new gs_texture_2d(device, size, size, color_format,
+				levels, data, flags, GS_TEXTURE_CUBE);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_cubetexture_create (Metal): %s", error);
+	}
+
+	return texture;
+}
+
+gs_texture_t *device_voltexture_create(gs_device_t *device, uint32_t width,
+		uint32_t height, uint32_t depth,
+		enum gs_color_format color_format, uint32_t levels,
+		const uint8_t **data, uint32_t flags)
+{
+	/* TODO */
+	UNUSED_PARAMETER(device);
+	UNUSED_PARAMETER(width);
+	UNUSED_PARAMETER(height);
+	UNUSED_PARAMETER(depth);
+	UNUSED_PARAMETER(color_format);
+	UNUSED_PARAMETER(levels);
+	UNUSED_PARAMETER(data);
+	UNUSED_PARAMETER(flags);
+	return NULL;
+}
+
+gs_zstencil_t *device_zstencil_create(gs_device_t *device, uint32_t width,
+		uint32_t height, enum gs_zstencil_format format)
+{
+	gs_zstencil_buffer *zstencil = nullptr;
+	try {
+		zstencil = new gs_zstencil_buffer(device, width, height,
+				format);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_zstencil_create (Metal): %s", error);
+	}
+
+	return zstencil;
+}
+
+gs_stagesurf_t *device_stagesurface_create(gs_device_t *device, uint32_t width,
+		uint32_t height, enum gs_color_format color_format)
+{
+	gs_stage_surface *surf = nullptr;
+	try {
+		surf = new gs_stage_surface(device, width, height,
+				color_format);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_stagesurface_create (Metal): %s",
+				error);
+	}
+
+	return surf;
+}
+
+gs_samplerstate_t *device_samplerstate_create(gs_device_t *device,
+		const struct gs_sampler_info *info)
+{
+	gs_sampler_state *ss = nullptr;
+	try {
+		ss = new gs_sampler_state(device, info);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_samplerstate_create (Metal): %s",
+				error);
+	}
+
+	return ss;
+}
+
+gs_shader_t *device_vertexshader_create(gs_device_t *device,
+		const char *shader_string, const char *file,
+		char **error_string)
+{
+	gs_vertex_shader *shader = nullptr;
+	try {
+		shader = new gs_vertex_shader(device, file, shader_string);
+
+	} catch (ShaderError error) {
+		blog(LOG_ERROR, "device_vertexshader_create (Metal): "
+		                "Compile warnings/errors for %s:\n%s",
+		                file,
+		                error.error.c_str());
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_vertexshader_create (Metal): %s",
+				error);
+	}
+
+	return shader;
+
+	UNUSED_PARAMETER(error_string);
+}
+
+gs_shader_t *device_pixelshader_create(gs_device_t *device,
+		const char *shader_string, const char *file,
+		char **error_string)
+{
+	gs_pixel_shader *shader = nullptr;
+	try {
+		shader = new gs_pixel_shader(device, file, shader_string);
+
+	} catch (ShaderError error) {
+		blog(LOG_ERROR, "device_pixelshader_create (Metal): "
+		                "Compile warnings/errors for %s:\n%s",
+		                file,
+		                error.error.c_str());
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_pixelshader_create (Metal): %s", error);
+	}
+
+	return shader;
+
+	UNUSED_PARAMETER(error_string);
+}
+
+gs_vertbuffer_t *device_vertexbuffer_create(gs_device_t *device,
+		struct gs_vb_data *data, uint32_t flags)
+{
+	gs_vertex_buffer *buffer = nullptr;
+	try {
+		buffer = new gs_vertex_buffer(device, data, flags);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_vertexbuffer_create (Metal): %s",
+				error);
+	}
+
+	return buffer;
+}
+
+gs_indexbuffer_t *device_indexbuffer_create(gs_device_t *device,
+		enum gs_index_type type, void *indices, size_t num,
+		uint32_t flags)
+{
+	gs_index_buffer *buffer = nullptr;
+	try {
+		buffer = new gs_index_buffer(device, type, indices, num, flags);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_indexbuffer_create (Metal): %s", error);
+	}
+
+	return buffer;
+}
+
+enum gs_texture_type device_get_texture_type(const gs_texture_t *texture)
+{
+	return texture->type;
+}
+
+void device_load_vertexbuffer(gs_device_t *device, gs_vertbuffer_t *vertbuffer)
+{
+	if (device->curVertexBuffer == vertbuffer)
+		return;
+
+	device->curVertexBuffer = vertbuffer;
+}
+
+void device_load_indexbuffer(gs_device_t *device, gs_indexbuffer_t *indexbuffer)
+{
+	if (device->curIndexBuffer == indexbuffer)
+		return;
+
+	device->curIndexBuffer = indexbuffer;
+}
+
+void device_load_texture(gs_device_t *device, gs_texture_t *tex, int unit)
+{
+	if (device->curTextures[unit] == tex)
+		return;
+
+	device->curTextures[unit] = tex;
+}
+
+void device_load_samplerstate(gs_device_t *device,
+		gs_samplerstate_t *samplerstate, int unit)
+{
+	if (device->curSamplers[unit] == samplerstate)
+		return;
+
+	device->curSamplers[unit] = samplerstate;
+}
+
+void device_load_vertexshader(gs_device_t *device, gs_shader_t *vertshader)
+{
+	id<MTLFunction>     function  = nil;
+	MTLVertexDescriptor *vertDesc = nil;
+
+	if (device->curVertexShader == vertshader)
+		return;
+
+	gs_vertex_shader *vs = static_cast<gs_vertex_shader*>(vertshader);
+	if (vertshader) {
+		if (vertshader->type != GS_SHADER_VERTEX) {
+			blog(LOG_ERROR, "device_load_vertexshader (Metal): "
+			                "Specified shader is not a vertex "
+			                "shader");
+			return;
+		}
+
+		function = vs->function;
+		vertDesc = vs->vertexDesc;
+	}
+
+	device->curVertexShader = vs;
+
+	device->pipelineDesc.vertexFunction = function;
+	device->pipelineDesc.vertexDescriptor = vertDesc;
+
+	device->piplineStateChanged = true;
+}
+
+void device_clear_textures(gs_device_t *device)
+{
+	memset(device->curTextures, 0, sizeof(device->curTextures));
+}
+
+void device_load_pixelshader(gs_device_t *device, gs_shader_t *pixelshader)
+{
+	id<MTLFunction> function = nil;
+	gs_sampler_state *states[GS_MAX_TEXTURES];
+
+	if (device->curPixelShader == pixelshader)
+		return;
+
+	gs_pixel_shader *ps = static_cast<gs_pixel_shader*>(pixelshader);
+	if (pixelshader) {
+		if (pixelshader->type != GS_SHADER_PIXEL) {
+			blog(LOG_ERROR, "device_load_pixelshader (Metal): "
+			                "Specified shader is not a pixel "
+			                "shader");
+			return;
+		}
+
+		function  = ps->function;
+		ps->GetSamplerStates(states);
+	}
+
+	device_clear_textures(device);
+
+	device->curPixelShader = ps;
+	for (size_t i = 0; i < GS_MAX_TEXTURES; i++)
+		device->curSamplers[i] = states[i];
+
+	device->pipelineDesc.fragmentFunction = function;
+
+	device->piplineStateChanged = true;
+}
+
+void device_load_default_samplerstate(gs_device_t *device, bool b_3d, int unit)
+{
+	/* TODO */
+	UNUSED_PARAMETER(device);
+	UNUSED_PARAMETER(b_3d);
+	UNUSED_PARAMETER(unit);
+}
+
+gs_shader_t *device_get_vertex_shader(const gs_device_t *device)
+{
+	return device->curVertexShader;
+}
+
+gs_shader_t *device_get_pixel_shader(const gs_device_t *device)
+{
+	return device->curPixelShader;
+}
+
+gs_texture_t *device_get_render_target(const gs_device_t *device)
+{
+	if (device->curSwapChain &&
+	    device->curRenderTarget == device->curSwapChain->GetTarget())
+		return nullptr;
+
+	return device->curRenderTarget;
+}
+
+gs_zstencil_t *device_get_zstencil_target(const gs_device_t *device)
+{
+	return device->curZStencilBuffer;
+}
+
+void device_set_render_target(gs_device_t *device, gs_texture_t *tex,
+		gs_zstencil_t *zstencil)
+{
+	if (device->curSwapChain) {
+		if (!tex)
+			tex = device->curSwapChain->GetTarget();
+	}
+
+	if (device->curRenderTarget   == tex &&
+	    device->curZStencilBuffer == zstencil)
+		return;
+
+	if (tex && tex->type != GS_TEXTURE_2D) {
+		blog(LOG_ERROR, "device_set_render_target (Metal): "
+		                "texture is not a 2D texture");
+		return;
+	}
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(tex);
+	if (tex2d && tex2d->texture == nil) {
+		blog(LOG_ERROR, "device_set_render_target (Metal): "
+		                "texture is null");
+		return;
+	}
+
+	device->curRenderTarget   = tex2d;
+	device->curRenderSide     = 0;
+	device->curZStencilBuffer = zstencil;
+
+	if (tex2d) {
+		device->passDesc.colorAttachments[0].texture = tex2d->texture;
+		device->pipelineDesc.colorAttachments[0].pixelFormat =
+				tex2d->mtlPixelFormat;
+	} else
+		device->passDesc.colorAttachments[0].texture = nil;
+
+	if (zstencil) {
+		device->passDesc.depthAttachment.texture   = zstencil->texture;
+		device->passDesc.stencilAttachment.texture = zstencil->texture;
+		device->pipelineDesc.depthAttachmentPixelFormat =
+				zstencil->textureDesc.pixelFormat;
+		device->pipelineDesc.stencilAttachmentPixelFormat =
+				zstencil->textureDesc.pixelFormat;
+	} else {
+		device->passDesc.depthAttachment.texture   = nil;
+		device->passDesc.stencilAttachment.texture = nil;
+	}
+
+	device->piplineStateChanged = true;
+}
+
+void device_set_cube_render_target(gs_device_t *device, gs_texture_t *tex,
+		int side, gs_zstencil_t *zstencil)
+{
+	if (device->curSwapChain) {
+		if (!tex)
+			tex = device->curSwapChain->GetTarget();
+	}
+
+	if (device->curRenderTarget   == tex  &&
+	    device->curRenderSide     == side &&
+	    device->curZStencilBuffer == zstencil)
+		return;
+
+	if (tex->type != GS_TEXTURE_CUBE) {
+		blog(LOG_ERROR, "device_set_cube_render_target (Metal): "
+		                "texture is not a cube texture");
+		return;
+	}
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(tex);
+	if (tex2d && tex2d->texture == nil) {
+		blog(LOG_ERROR, "device_set_cube_render_target (Metal): "
+				"texture is null");
+		return;
+	}
+
+	device->curRenderTarget   = tex2d;
+	device->curRenderSide     = side;
+	device->curZStencilBuffer = zstencil;
+
+	if (tex2d) {
+		device->passDesc.colorAttachments[0].texture = tex2d->texture;
+		device->pipelineDesc.colorAttachments[0].pixelFormat =
+				tex2d->mtlPixelFormat;
+	} else
+		device->passDesc.colorAttachments[0].texture = nil;
+
+	if (zstencil) {
+		device->passDesc.depthAttachment.texture   = zstencil->texture;
+		device->passDesc.stencilAttachment.texture = zstencil->texture;
+		device->pipelineDesc.depthAttachmentPixelFormat =
+				zstencil->textureDesc.pixelFormat;
+		device->pipelineDesc.stencilAttachmentPixelFormat =
+				zstencil->textureDesc.pixelFormat;
+	} else {
+		device->passDesc.depthAttachment.texture   = nil;
+		device->passDesc.stencilAttachment.texture = nil;
+	}
+
+	device->piplineStateChanged = true;
+}
+
+inline void gs_device::CopyTex(id<MTLTexture> dst,
+		uint32_t dst_x, uint32_t dst_y,
+		gs_texture_t *src, uint32_t src_x, uint32_t src_y,
+		uint32_t src_w, uint32_t src_h)
+{
+	assert(commandBuffer != nil);
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(src);
+	if (src_w == 0)
+		src_w = tex2d->width;
+	if (src_h == 0)
+		src_h = tex2d->height;
+
+	@autoreleasepool {
+		id<MTLBlitCommandEncoder> commandEncoder =
+				[commandBuffer blitCommandEncoder];
+		MTLOrigin sourceOrigin      = MTLOriginMake(src_x, src_y, 0);
+		MTLSize   sourceSize        = MTLSizeMake(src_w, src_h, 1);
+		MTLOrigin destinationOrigin = MTLOriginMake(dst_x, dst_y, 0);
+		[commandEncoder copyFromTexture:tex2d->texture
+				sourceSlice:0
+				sourceLevel:0
+				sourceOrigin:sourceOrigin
+				sourceSize:sourceSize
+				toTexture:dst
+				destinationSlice:0
+				destinationLevel:0
+				destinationOrigin:destinationOrigin];
+		[commandEncoder endEncoding];
+	}
+}
+
+void device_copy_texture_region(gs_device_t *device,
+		gs_texture_t *dst, uint32_t dst_x, uint32_t dst_y,
+		gs_texture_t *src, uint32_t src_x, uint32_t src_y,
+		uint32_t src_w, uint32_t src_h)
+{
+	try {
+		gs_texture_2d *src2d = static_cast<gs_texture_2d*>(src);
+		gs_texture_2d *dst2d = static_cast<gs_texture_2d*>(dst);
+
+		if (!src)
+			throw "Source texture is null";
+		if (!dst)
+			throw "Destination texture is null";
+		if (src->type != GS_TEXTURE_2D || dst->type != GS_TEXTURE_2D)
+			throw "Source and destination textures must be a 2D "
+			      "textures";
+		if (dst->format != src->format)
+			throw "Source and destination formats do not match";
+
+		uint32_t copyWidth  = src_w ? src_w : (src2d->width - src_x);
+		uint32_t copyHeight = src_h ? src_h : (src2d->height - src_y);
+
+		uint32_t dstWidth  = dst2d->width  - dst_x;
+		uint32_t dstHeight = dst2d->height - dst_y;
+
+		if (dstWidth < copyWidth || dstHeight < copyHeight)
+			throw "Destination texture region is not big "
+			      "enough to hold the source region";
+
+		if (dst_x == 0 && dst_y == 0 &&
+		    src_x == 0 && src_y == 0 &&
+		    src_w == 0 && src_h == 0) {
+			copyWidth  = 0;
+			copyHeight = 0;
+		}
+
+		device->CopyTex(dst2d->texture, dst_x, dst_y,
+				src, src_x, src_y, copyWidth, copyHeight);
+
+	} catch(const char *error) {
+		blog(LOG_ERROR, "device_copy_texture (Metal): %s", error);
+	}
+}
+
+void device_copy_texture(gs_device_t *device, gs_texture_t *dst,
+		gs_texture_t *src)
+{
+	device_copy_texture_region(device, dst, 0, 0, src, 0, 0, 0, 0);
+}
+
+void device_stage_texture(gs_device_t *device, gs_stagesurf_t *dst,
+		gs_texture_t *src)
+{
+	try {
+		gs_texture_2d *src2d = static_cast<gs_texture_2d*>(src);
+
+		if (!src)
+			throw "Source texture is null";
+		if (src->type != GS_TEXTURE_2D)
+			throw "Source texture must be a 2D texture";
+		if (!dst)
+			throw "Destination surface is null";
+		if (dst->format != src->format)
+			throw "Source and destination formats do not match";
+		if (dst->width  != src2d->width ||
+		    dst->height != src2d->height)
+			throw "Source and destination must have the same "
+			      "dimensions";
+
+		device->CopyTex(dst->texture, 0, 0, src, 0, 0, 0, 0);
+
+	} catch (const char *error) {
+		blog(LOG_ERROR, "device_stage_texture (Metal): %s", error);
+	}
+}
+
+void device_begin_scene(gs_device_t *device)
+{
+	device_clear_textures(device);
+
+	device->commandBuffer = [device->commandQueue commandBuffer];
+}
+
+void device_draw(gs_device_t *device, enum gs_draw_mode draw_mode,
+		uint32_t start_vert, uint32_t num_verts)
+{
+	/*
+	 * Do not remove autorelease pool.
+	 * Add MTLRenderCommandEncoder to autorelease pool.
+	 */
+	@autoreleasepool {
+		device->Draw(draw_mode, start_vert, num_verts);
+	}
+}
+
+void device_end_scene(gs_device_t *device)
+{
+	/* does nothing in Metal */
+	UNUSED_PARAMETER(device);
+}
+
+void device_load_swapchain(gs_device_t *device, gs_swapchain_t *swapchain)
+{
+	if (device->curSwapChain == swapchain)
+		return;
+
+	if (swapchain) {
+		device->curSwapChain      = swapchain;
+		device->curRenderTarget   = swapchain->GetTarget();
+		device->curRenderSide     = 0;
+		device->curZStencilBuffer = nullptr;
+
+		device->passDesc.colorAttachments[0].texture =
+				device->curSwapChain->nextDrawable.texture;
+		device->passDesc.depthAttachment.texture     = nil;
+		device->passDesc.stencilAttachment.texture   = nil;
+
+		device->pipelineDesc.colorAttachments[0].pixelFormat =
+				device->curSwapChain->metalLayer.pixelFormat;
+	} else {
+		device->curSwapChain      = nullptr;
+		device->curRenderTarget   = nullptr;
+		device->curRenderSide     = 0;
+		device->curZStencilBuffer = nullptr;
+
+		device->passDesc.colorAttachments[0].texture = nil;
+		device->passDesc.depthAttachment.texture     = nil;
+		device->passDesc.stencilAttachment.texture   = nil;
+	}
+
+	device->piplineStateChanged = true;
+}
+
+void device_clear(gs_device_t *device, uint32_t clear_flags,
+		const struct vec4 *color, float depth, uint8_t stencil)
+{
+	device->preserveClearTarget = device->curRenderTarget;
+
+	ClearState state;
+	state.flags   = clear_flags;
+	state.color   = *color;
+	state.depth   = depth;
+	state.stencil = stencil;
+	device->clearStates.emplace(device->curRenderTarget, state);
+}
+
+void device_present(gs_device_t *device)
+{
+	if (device->curSwapChain) {
+		[device->commandBuffer presentDrawable:
+				device->curSwapChain->nextDrawable];
+	} else {
+		blog(LOG_WARNING, "device_present (Metal): No active swap");
+	}
+
+	device->PushResources();
+
+	[device->commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buf) {
+		device->ReleaseResources();
+
+		UNUSED_PARAMETER(buf);
+	}];
+	[device->commandBuffer commit];
+	device->commandBuffer = nil;
+
+	if (device->curSwapChain)
+		device->curSwapChain->NextTarget();
+
+}
+
+void device_flush(gs_device_t *device)
+{
+	if (device->commandBuffer != nil) {
+		device->PushResources();
+
+		[device->commandBuffer commit];
+		[device->commandBuffer waitUntilCompleted];
+		device->commandBuffer = nil;
+
+		device->ReleaseResources();
+
+		if (device->curStageSurface) {
+			device->curStageSurface->DownloadTexture();
+			device->curStageSurface = nullptr;
+		}
+	}
+}
+
+void device_set_cull_mode(gs_device_t *device, enum gs_cull_mode mode)
+{
+	if (device->rasterState.cullMode == mode)
+		return;
+
+	device->rasterState.cullMode = mode;
+
+	device->rasterState.mtlCullMode = ConvertGSCullMode(mode);
+}
+
+enum gs_cull_mode device_get_cull_mode(const gs_device_t *device)
+{
+	return device->rasterState.cullMode;
+}
+
+void device_enable_blending(gs_device_t *device, bool enable)
+{
+	if (device->blendState.blendEnabled == enable)
+		return;
+
+	device->blendState.blendEnabled = enable;
+
+	device->pipelineDesc.colorAttachments[0].blendingEnabled =
+			enable ? YES : NO;
+
+	device->piplineStateChanged = true;
+}
+
+void device_enable_depth_test(gs_device_t *device, bool enable)
+{
+	if (device->zstencilState.depthEnabled == enable)
+		return;
+
+	device->zstencilState.depthEnabled = enable;
+
+	device->depthStencilState = nil;
+}
+
+void device_enable_stencil_test(gs_device_t *device, bool enable)
+{
+	ZStencilState &state = device->zstencilState;
+
+	if (state.stencilEnabled == enable)
+		return;
+
+	state.stencilEnabled = enable;
+
+	state.dsd.frontFaceStencil.readMask = enable ? 1 : 0;
+	state.dsd.backFaceStencil.readMask  = enable ? 1 : 0;
+
+	device->depthStencilState = nil;
+}
+
+void device_enable_stencil_write(gs_device_t *device, bool enable)
+{
+	ZStencilState &state = device->zstencilState;
+
+	if (state.stencilWriteEnabled == enable)
+		return;
+
+	state.stencilWriteEnabled = enable;
+
+	state.dsd.frontFaceStencil.writeMask = enable ? 1 : 0;
+	state.dsd.backFaceStencil.writeMask  = enable ? 1 : 0;
+
+	device->depthStencilState = nil;
+}
+
+void device_enable_color(gs_device_t *device, bool red, bool green,
+		bool blue, bool alpha)
+{
+	BlendState &state = device->blendState;
+
+	if (state.redEnabled   == red   &&
+	    state.greenEnabled == green &&
+	    state.blueEnabled  == blue  &&
+	    state.alphaEnabled == alpha)
+		return;
+
+	state.redEnabled   = red;
+	state.greenEnabled = green;
+	state.blueEnabled  = blue;
+	state.alphaEnabled = alpha;
+
+	MTLRenderPipelineColorAttachmentDescriptor *cad =
+			device->pipelineDesc.colorAttachments[0];
+	cad.writeMask = MTLColorWriteMaskNone;
+	if (red)   cad.writeMask |= MTLColorWriteMaskRed;
+	if (green) cad.writeMask |= MTLColorWriteMaskGreen;
+	if (blue)  cad.writeMask |= MTLColorWriteMaskBlue;
+	if (alpha) cad.writeMask |= MTLColorWriteMaskAlpha;
+
+	device->piplineStateChanged = true;
+}
+
+void device_blend_function(gs_device_t *device, enum gs_blend_type src,
+		enum gs_blend_type dest)
+{
+	BlendState &state = device->blendState;
+
+	if (state.srcFactorC == src && state.destFactorC == dest &&
+	    state.srcFactorA == src && state.destFactorA == dest)
+		return;
+
+	state.srcFactorC  = src;
+	state.destFactorC = dest;
+	state.srcFactorA  = src;
+	state.destFactorA = dest;
+
+	MTLRenderPipelineColorAttachmentDescriptor *cad =
+			device->pipelineDesc.colorAttachments[0];
+	cad.sourceRGBBlendFactor        = ConvertGSBlendType(src);
+	cad.destinationRGBBlendFactor   = ConvertGSBlendType(dest);
+	cad.sourceAlphaBlendFactor      = ConvertGSBlendType(src);
+	cad.destinationAlphaBlendFactor = ConvertGSBlendType(dest);
+
+	device->piplineStateChanged = true;
+}
+
+void device_blend_function_separate(gs_device_t *device,
+		enum gs_blend_type src_c, enum gs_blend_type dest_c,
+		enum gs_blend_type src_a, enum gs_blend_type dest_a)
+{
+	BlendState &state = device->blendState;
+
+	if (state.srcFactorC == src_c && state.destFactorC == dest_c &&
+	    state.srcFactorA == src_a && state.destFactorA == dest_a)
+		return;
+
+	state.srcFactorC  = src_c;
+	state.destFactorC = dest_c;
+	state.srcFactorA  = src_a;
+	state.destFactorA = dest_a;
+
+	MTLRenderPipelineColorAttachmentDescriptor *cad =
+			device->pipelineDesc.colorAttachments[0];
+	cad.sourceRGBBlendFactor        = ConvertGSBlendType(src_c);
+	cad.destinationRGBBlendFactor   = ConvertGSBlendType(dest_c);
+	cad.sourceAlphaBlendFactor      = ConvertGSBlendType(src_a);
+	cad.destinationAlphaBlendFactor = ConvertGSBlendType(dest_a);
+
+	device->piplineStateChanged = true;
+}
+
+void device_depth_function(gs_device_t *device, enum gs_depth_test test)
+{
+	if (device->zstencilState.depthFunc == test)
+		return;
+
+	device->zstencilState.depthFunc = test;
+
+	device->zstencilState.dsd.depthCompareFunction =
+			ConvertGSDepthTest(test);
+
+	device->depthStencilState = nil;
+}
+
+static inline void update_stencilside_test(gs_device_t *device,
+		StencilSide &side, MTLStencilDescriptor *desc,
+		gs_depth_test test)
+{
+	if (side.test == test)
+		return;
+
+	side.test = test;
+
+	desc.stencilCompareFunction = ConvertGSDepthTest(test);
+
+	device->depthStencilState = nil;
+}
+
+void device_stencil_function(gs_device_t *device, enum gs_stencil_side side,
+		enum gs_depth_test test)
+{
+	int sideVal = static_cast<int>(side);
+	if (sideVal & GS_STENCIL_FRONT)
+		update_stencilside_test(device,
+				device->zstencilState.stencilFront,
+				device->zstencilState.dsd.frontFaceStencil,
+				test);
+	if (sideVal & GS_STENCIL_BACK)
+		update_stencilside_test(device,
+				device->zstencilState.stencilBack,
+				device->zstencilState.dsd.backFaceStencil,
+				test);
+}
+
+static inline void update_stencilside_op(gs_device_t *device,
+		StencilSide &side, MTLStencilDescriptor *desc,
+		enum gs_stencil_op_type fail, enum gs_stencil_op_type zfail,
+		enum gs_stencil_op_type zpass)
+{
+	if (side.fail == fail && side.zfail == zfail && side.zpass == zpass)
+		return;
+
+	side.fail  = fail;
+	side.zfail = zfail;
+	side.zpass = zpass;
+
+	desc.stencilFailureOperation   = ConvertGSStencilOp(fail);
+	desc.depthFailureOperation     = ConvertGSStencilOp(zfail);
+	desc.depthStencilPassOperation = ConvertGSStencilOp(zpass);
+
+	device->depthStencilState = nil;
+}
+
+void device_stencil_op(gs_device_t *device, enum gs_stencil_side side,
+		enum gs_stencil_op_type fail, enum gs_stencil_op_type zfail,
+		enum gs_stencil_op_type zpass)
+{
+	int sideVal = static_cast<int>(side);
+
+	if (sideVal & GS_STENCIL_FRONT)
+		update_stencilside_op(device,
+				device->zstencilState.stencilFront,
+				device->zstencilState.dsd.frontFaceStencil,
+				fail, zfail, zpass);
+	if (sideVal & GS_STENCIL_BACK)
+		update_stencilside_op(device,
+				device->zstencilState.stencilBack,
+				device->zstencilState.dsd.backFaceStencil,
+				fail, zfail, zpass);
+}
+
+void device_set_viewport(gs_device_t *device, int x, int y, int width,
+		int height)
+{
+	RasterState &state = device->rasterState;
+
+	if (state.viewport.x == x &&
+	    state.viewport.y == y &&
+	    state.viewport.cx == width &&
+	    state.viewport.cy == height)
+		return;
+
+	state.viewport.x  = x;
+	state.viewport.y  = y;
+	state.viewport.cx = width;
+	state.viewport.cy = height;
+
+	state.mtlViewport = ConvertGSRectToMTLViewport(state.viewport);
+}
+
+void device_get_viewport(const gs_device_t *device, struct gs_rect *rect)
+{
+	memcpy(rect, &device->rasterState.viewport, sizeof(gs_rect));
+}
+
+void device_set_scissor_rect(gs_device_t *device, const struct gs_rect *rect)
+{
+	if (rect != nullptr) {
+		device->rasterState.scissorEnabled = true;
+		device->rasterState.scissorRect    = *rect;
+		device->rasterState.mtlScissorRect =
+				ConvertGSRectToMTLScissorRect(*rect);
+	} else {
+		device->rasterState.scissorEnabled = false;
+	}
+}
+
+void device_ortho(gs_device_t *device, float left, float right, float top,
+		float bottom, float zNear, float zFar)
+{
+	matrix4 &dst = device->curProjMatrix;
+
+	float rml = right - left;
+	float bmt = bottom - top;
+	float fmn = zFar - zNear;
+
+	vec4_zero(&dst.x);
+	vec4_zero(&dst.y);
+	vec4_zero(&dst.z);
+	vec4_zero(&dst.t);
+
+	dst.x.x =           2.0f /  rml;
+	dst.t.x = (left + right) / -rml;
+
+	dst.y.y =           2.0f / -bmt;
+	dst.t.y = (bottom + top) /  bmt;
+
+	dst.z.z =           1.0f /  fmn;
+	dst.t.z =          zNear / -fmn;
+
+	dst.t.w = 1.0f;
+}
+
+void device_frustum(gs_device_t *device, float left, float right, float top,
+		float bottom, float zNear, float zFar)
+{
+	matrix4 &dst = device->curProjMatrix;
+
+	float rml    = right - left;
+	float bmt    = bottom - top;
+	float fmn    = zFar - zNear;
+	float nearx2 = 2.0f * zNear;
+
+	vec4_zero(&dst.x);
+	vec4_zero(&dst.y);
+	vec4_zero(&dst.z);
+	vec4_zero(&dst.t);
+
+	dst.x.x =         nearx2 /  rml;
+	dst.z.x = (left + right) / -rml;
+
+	dst.y.y =         nearx2 / -bmt;
+	dst.z.y = (bottom + top) /  bmt;
+
+	dst.z.z =           zFar /  fmn;
+	dst.t.z = (zNear * zFar) / -fmn;
+
+	dst.z.w = 1.0f;
+}
+
+void device_projection_push(gs_device_t *device)
+{
+	device->projStack.push(device->curProjMatrix);
+}
+
+void device_projection_pop(gs_device_t *device)
+{
+	if (!device->projStack.size())
+		return;
+
+	device->curProjMatrix = device->projStack.top();
+	device->projStack.pop();
+}
+
+void gs_swapchain_destroy(gs_swapchain_t *swapchain)
+{
+	assert(swapchain->obj_type == gs_type::gs_swap_chain);
+
+	if (swapchain->device->curSwapChain == swapchain)
+		device_load_swapchain(swapchain->device, nullptr);
+
+	delete swapchain;
+}
+
+void gs_texture_destroy(gs_texture_t *tex)
+{
+	delete tex;
+}
+
+uint32_t gs_texture_get_width(const gs_texture_t *tex)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return 0;
+
+	return static_cast<const gs_texture_2d*>(tex)->width;
+}
+
+uint32_t gs_texture_get_height(const gs_texture_t *tex)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return 0;
+
+	return static_cast<const gs_texture_2d*>(tex)->height;
+}
+
+enum gs_color_format gs_texture_get_color_format(const gs_texture_t *tex)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return GS_UNKNOWN;
+
+	return static_cast<const gs_texture_2d*>(tex)->format;
+}
+
+bool gs_texture_map(gs_texture_t *tex, uint8_t **ptr, uint32_t *linesize)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return false;
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(tex);
+	uint32_t texSizeBytes = tex2d->height * tex2d->bytePerRow;
+
+	tex2d->data.resize(1);
+	tex2d->data[0].resize(texSizeBytes);
+
+	*ptr      = (uint8_t *)tex2d->data[0].data();
+	*linesize = tex2d->bytePerRow;
+	return true;
+}
+
+void gs_texture_unmap(gs_texture_t *tex)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return;
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(tex);
+	tex2d->UploadTexture();
+}
+
+void *gs_texture_get_obj(gs_texture_t *tex)
+{
+	if (tex->type != GS_TEXTURE_2D)
+		return nullptr;
+
+	gs_texture_2d *tex2d = static_cast<gs_texture_2d*>(tex);
+	return (__bridge void*)tex2d->texture;
+}
+
+
+void gs_cubetexture_destroy(gs_texture_t *cubetex)
+{
+	delete cubetex;
+}
+
+uint32_t gs_cubetexture_get_size(const gs_texture_t *cubetex)
+{
+	if (cubetex->type != GS_TEXTURE_CUBE)
+		return 0;
+
+	const gs_texture_2d *tex = static_cast<const gs_texture_2d*>(cubetex);
+	return tex->width;
+}
+
+enum gs_color_format gs_cubetexture_get_color_format(
+		const gs_texture_t *cubetex)
+{
+	if (cubetex->type != GS_TEXTURE_CUBE)
+		return GS_UNKNOWN;
+
+	const gs_texture_2d *tex = static_cast<const gs_texture_2d*>(cubetex);
+	return tex->format;
+}
+
+
+void gs_voltexture_destroy(gs_texture_t *voltex)
+{
+	delete voltex;
+}
+
+uint32_t gs_voltexture_get_width(const gs_texture_t *voltex)
+{
+	/* TODO */
+	UNUSED_PARAMETER(voltex);
+	return 0;
+}
+
+uint32_t gs_voltexture_get_height(const gs_texture_t *voltex)
+{
+	/* TODO */
+	UNUSED_PARAMETER(voltex);
+	return 0;
+}
+
+uint32_t gs_voltexture_get_depth(const gs_texture_t *voltex)
+{
+	/* TODO */
+	UNUSED_PARAMETER(voltex);
+	return 0;
+}
+
+enum gs_color_format gs_voltexture_get_color_format(const gs_texture_t *voltex)
+{
+	/* TODO */
+	UNUSED_PARAMETER(voltex);
+	return GS_UNKNOWN;
+}
+
+
+void gs_stagesurface_destroy(gs_stagesurf_t *stagesurf)
+{
+	assert(stagesurf->obj_type == gs_type::gs_stage_surface);
+
+	delete stagesurf;
+}
+
+uint32_t gs_stagesurface_get_width(const gs_stagesurf_t *stagesurf)
+{
+	assert(stagesurf->obj_type == gs_type::gs_stage_surface);
+
+	return stagesurf->width;
+}
+
+uint32_t gs_stagesurface_get_height(const gs_stagesurf_t *stagesurf)
+{
+	assert(stagesurf->obj_type == gs_type::gs_stage_surface);
+
+	return stagesurf->height;
+}
+
+enum gs_color_format gs_stagesurface_get_color_format(
+		const gs_stagesurf_t *stagesurf)
+{
+	assert(stagesurf->obj_type == gs_type::gs_stage_surface);
+
+	return stagesurf->format;
+}
+
+bool gs_stagesurface_map(gs_stagesurf_t *stagesurf, uint8_t **data,
+		uint32_t *linesize)
+{
+	assert(stagesurf->obj_type == gs_type::gs_stage_surface);
+	assert(stagesurf->device->commandBuffer != nil);
+
+	@autoreleasepool {
+		id<MTLBlitCommandEncoder> commandEncoder =
+				[stagesurf->device->commandBuffer
+				blitCommandEncoder];
+		[commandEncoder synchronizeTexture:stagesurf->texture
+				slice:0 level:0];
+		[commandEncoder endEncoding];
+	}
+
+	*data     = (uint8_t *)stagesurf->data.data();
+	*linesize = stagesurf->bytePerRow;
+
+	stagesurf->device->curStageSurface = stagesurf;
+	return true;
+}
+
+void gs_stagesurface_unmap(gs_stagesurf_t *stagesurf)
+{
+	/* does nothing in Metal */
+	UNUSED_PARAMETER(stagesurf);
+}
+
+
+void gs_zstencil_destroy(gs_zstencil_t *zstencil)
+{
+	assert(zstencil->obj_type == gs_type::gs_zstencil_buffer);
+
+	delete zstencil;
+}
+
+
+void gs_samplerstate_destroy(gs_samplerstate_t *samplerstate)
+{
+	assert(samplerstate->obj_type == gs_type::gs_sampler_state);
+
+	if (samplerstate->device) {
+		for (size_t i = 0; i < GS_MAX_TEXTURES; i++)
+			if (samplerstate->device->curSamplers[i] ==
+			    samplerstate)
+				samplerstate->device->curSamplers[i] = nullptr;
+	}
+
+	delete samplerstate;
+}
+
+
+void gs_vertexbuffer_destroy(gs_vertbuffer_t *vertbuffer)
+{
+	assert(vertbuffer->obj_type == gs_type::gs_vertex_buffer);
+
+	if (vertbuffer->device->lastVertexBuffer == vertbuffer)
+		vertbuffer->device->lastVertexBuffer = nullptr;
+
+	delete vertbuffer;
+}
+
+void gs_vertexbuffer_flush(gs_vertbuffer_t *vertbuffer)
+{
+	assert(vertbuffer->obj_type == gs_type::gs_vertex_buffer);
+
+	if (!vertbuffer->isDynamic) {
+		blog(LOG_ERROR, "gs_vertexbuffer_flush: vertex buffer is not "
+		                "dynamic");
+		return;
+	}
+
+	vertbuffer->PrepareBuffers();
+}
+
+struct gs_vb_data *gs_vertexbuffer_get_data(const gs_vertbuffer_t *vertbuffer)
+{
+	assert(vertbuffer->obj_type == gs_type::gs_vertex_buffer);
+
+	return vertbuffer->vbData.get();
+}
+
+
+void gs_indexbuffer_destroy(gs_indexbuffer_t *indexbuffer)
+{
+	assert(indexbuffer->obj_type == gs_type::gs_index_buffer);
+
+	delete indexbuffer;
+}
+
+void gs_indexbuffer_flush(gs_indexbuffer_t *indexbuffer)
+{
+	assert(indexbuffer->obj_type == gs_type::gs_index_buffer);
+
+	if (!indexbuffer->isDynamic) {
+		blog(LOG_ERROR, "gs_indexbuffer_flush: index buffer is not "
+		                "dynamic");
+		return;
+	}
+
+	indexbuffer->PrepareBuffer();
+}
+
+void *gs_indexbuffer_get_data(const gs_indexbuffer_t *indexbuffer)
+{
+	assert(indexbuffer->obj_type == gs_type::gs_index_buffer);
+
+	return indexbuffer->indices.get();
+}
+
+size_t gs_indexbuffer_get_num_indices(const gs_indexbuffer_t *indexbuffer)
+{
+	assert(indexbuffer->obj_type == gs_type::gs_index_buffer);
+
+	return indexbuffer->num;
+}
+
+enum gs_index_type gs_indexbuffer_get_type(const gs_indexbuffer_t *indexbuffer)
+{
+	assert(indexbuffer->obj_type == gs_type::gs_index_buffer);
+
+	return indexbuffer->type;
+}

+ 61 - 0
libobs-metal/metal-swapchain.mm

@@ -0,0 +1,61 @@
+#include "metal-subsystem.hpp"
+
+#import <QuartzCore/QuartzCore.h>
+
+using namespace std;
+
+gs_texture_2d *gs_swap_chain::GetTarget()
+{
+	if (!nextTarget)
+		return NextTarget();
+
+	return nextTarget.get();
+}
+
+gs_texture_2d *gs_swap_chain::NextTarget()
+{
+	nextDrawable = metalLayer.nextDrawable;
+	if (nextDrawable != nil)
+		nextTarget.reset(new gs_texture_2d(device,
+				nextDrawable.texture));
+	else
+		nextTarget.reset();
+
+	return nextTarget.get();
+}
+
+void gs_swap_chain::Resize(uint32_t cx, uint32_t cy)
+{
+	initData.cx = cx;
+	initData.cy = cy;
+
+	if (cx == 0 || cy == 0) {
+		NSRect clientRect = view.layer.frame;
+		if (cx == 0) cx = clientRect.size.width - clientRect.origin.x;
+		if (cy == 0) cy = clientRect.size.height - clientRect.origin.y;
+	}
+
+	metalLayer.drawableSize = CGSizeMake(cx, cy);
+}
+
+void gs_swap_chain::Rebuild()
+{
+	metalLayer.device = device->device;
+}
+
+gs_swap_chain::gs_swap_chain(gs_device *device, const gs_init_data *data)
+	: gs_obj     (device, gs_type::gs_swap_chain),
+	  numBuffers (data->num_backbuffers),
+	  view       (data->window.view),
+	  initData   (*data)
+{
+	if (metalLayer.pixelFormat != ConvertGSTextureFormat(data->format))
+		blog(LOG_WARNING, "device_stage_texture (Metal): "
+			          "pixel format is not matched (RGBA only)");
+
+	metalLayer = [CAMetalLayer layer];
+	metalLayer.device = device->device;
+	metalLayer.drawableSize = CGSizeMake(initData.cx, initData.cy);
+	view.wantsLayer = YES;
+	view.layer = metalLayer;
+}

+ 159 - 0
libobs-metal/metal-texture2d.mm

@@ -0,0 +1,159 @@
+#include <util/base.h>
+
+#include "metal-subsystem.hpp"
+
+void gs_texture_2d::GenerateMipmap()
+{
+	assert(device->commandBuffer == nil);
+
+	if (levels == 1)
+		return;
+
+	@autoreleasepool {
+		id<MTLCommandBuffer> buf = [device->commandQueue commandBuffer];
+		id<MTLBlitCommandEncoder> blit = [buf blitCommandEncoder];
+		[blit generateMipmapsForTexture:texture];
+		[blit endEncoding];
+		[buf commit];
+		[buf waitUntilCompleted];
+	}
+}
+
+void gs_texture_2d::BackupTexture(const uint8_t **data)
+{
+	this->data.resize(levels);
+
+	uint32_t w = width;
+	uint32_t h = height;
+	uint32_t bpp = gs_get_format_bpp(format);
+
+	for (uint32_t i = 0; i < levels; i++) {
+		if (!data[i])
+			break;
+
+		uint32_t texSize = bpp * w * h / 8;
+		this->data[i].resize(texSize);
+
+		auto &subData = this->data[i];
+		memcpy(&subData[0], data[i], texSize);
+
+		w /= 2;
+		h /= 2;
+	}
+}
+
+void gs_texture_2d::UploadTexture()
+{
+	const uint32_t bpp = gs_get_format_bpp(format) / 8;
+	uint32_t w = width;
+	uint32_t h = height;
+
+	for (uint32_t i = 0; i < levels; i++) {
+		if (i >= data.size())
+			break;
+
+		const uint32_t rowSizeBytes = w * bpp;
+		const uint32_t texSizeBytes = h * rowSizeBytes;
+		MTLRegion region = MTLRegionMake2D(0, 0, w, h);
+		[texture replaceRegion:region mipmapLevel:i slice:0
+				withBytes:data[i].data()
+				bytesPerRow:rowSizeBytes
+				bytesPerImage:texSizeBytes];
+
+		w /= 2;
+		h /= 2;
+	}
+}
+
+void gs_texture_2d::InitTexture()
+{
+	assert(!isShared);
+
+	texture = [device->device newTextureWithDescriptor:textureDesc];
+	if (texture == nil)
+		throw "Failed to create 2D texture";
+}
+
+void gs_texture_2d::Rebuild()
+{
+	if (isShared) {
+		texture = nil;
+		return;
+	}
+
+	InitTexture();
+}
+
+gs_texture_2d::gs_texture_2d(gs_device_t *device, uint32_t width,
+		uint32_t height, gs_color_format colorFormat, uint32_t levels,
+		const uint8_t **data, uint32_t flags, gs_texture_type type)
+	: gs_texture      (device, gs_type::gs_texture_2d, type, levels,
+	                   colorFormat),
+	  width           (width),
+	  height          (height),
+	  bytePerRow      (width * gs_get_format_bpp(colorFormat) / 8),
+	  isRenderTarget  ((flags & GS_RENDER_TARGET) != 0),
+	  isDynamic       ((flags & GS_DYNAMIC) != 0),
+	  genMipmaps      ((flags & GS_BUILD_MIPMAPS) != 0),
+	  isShared        (false),
+	  mtlPixelFormat  (ConvertGSTextureFormat(format))
+{
+	if (type == GS_TEXTURE_CUBE) {
+		NSUInteger size = 6 * width * height;
+		textureDesc = [MTLTextureDescriptor
+				textureCubeDescriptorWithPixelFormat:
+				mtlPixelFormat size:size
+				mipmapped:genMipmaps ? YES : NO];
+	} else {
+		textureDesc = [MTLTextureDescriptor
+				texture2DDescriptorWithPixelFormat:
+				mtlPixelFormat width:width height:height
+				mipmapped:genMipmaps ? YES : NO];
+	}
+
+	switch (type) {
+	case GS_TEXTURE_3D:
+		textureDesc.textureType = MTLTextureType3D;
+		break;
+	case GS_TEXTURE_CUBE:
+		textureDesc.textureType = MTLTextureTypeCube;
+		break;
+	case GS_TEXTURE_2D:
+	default:
+		break;
+	}
+	if (genMipmaps)
+		textureDesc.mipmapLevelCount = levels;
+	textureDesc.arrayLength              = type == GS_TEXTURE_CUBE ? 6 : 1;
+	textureDesc.cpuCacheMode             = MTLCPUCacheModeWriteCombined;
+	textureDesc.storageMode              = MTLStorageModeManaged;
+	textureDesc.usage                    = MTLTextureUsageShaderRead;
+	if (isRenderTarget)
+		textureDesc.usage |= MTLTextureUsageRenderTarget;
+
+	InitTexture();
+
+	if (data) {
+		BackupTexture(data);
+		UploadTexture();
+		if (genMipmaps)
+			GenerateMipmap();
+	}
+}
+
+gs_texture_2d::gs_texture_2d(gs_device_t *device, id<MTLTexture> texture)
+	: gs_texture      (device, gs_type::gs_texture_2d,
+			   GS_TEXTURE_2D,
+			   texture.mipmapLevelCount,
+			   ConvertMTLTextureFormat(texture.pixelFormat)),
+	  width           (texture.width),
+	  height          (texture.height),
+	  bytePerRow      (width * gs_get_format_bpp(format) / 8),
+	  isRenderTarget  (false),
+	  isDynamic       (false),
+	  genMipmaps      (false),
+	  isShared        (true),
+	  mtlPixelFormat  (texture.pixelFormat),
+	  texture         (texture)
+{
+}

+ 136 - 0
libobs-metal/metal-vertexbuffer.mm

@@ -0,0 +1,136 @@
+#include <util/base.h>
+#include <graphics/vec3.h>
+
+#include "metal-subsystem.hpp"
+
+using namespace std;
+
+inline id<MTLBuffer> gs_vertex_buffer::PrepareBuffer(
+		void *array, size_t elementSize, __weak NSString *name)
+{
+	id<MTLBuffer> b = device->GetBuffer(array, elementSize * vbData->num);
+#if _DEBUG
+	b.label = name;
+#endif
+	return b;
+}
+
+void gs_vertex_buffer::PrepareBuffers()
+{
+	assert(isDynamic);
+
+	vertexBuffer = PrepareBuffer(vbData->points, sizeof(vec3), @"point");
+	if (vbData->normals)
+		normalBuffer = PrepareBuffer(vbData->normals, sizeof(vec3),
+				@"normal");
+	if (vbData->tangents)
+		tangentBuffer = PrepareBuffer(vbData->tangents, sizeof(vec3),
+				@"color");
+	if (vbData->colors)
+		colorBuffer = PrepareBuffer(vbData->colors, sizeof(uint32_t),
+				@"tangent");
+
+	for (size_t i = 0; i < vbData->num_tex; i++) {
+		gs_tvertarray &tv = vbData->tvarray[i];
+		uvBuffers.push_back(PrepareBuffer(tv.array,
+				tv.width * sizeof(float), @"texcoord"));
+	}
+}
+
+static inline void PushBuffer(vector<id<MTLBuffer>> &buffers,
+		id<MTLBuffer> buffer, const char *name)
+{
+	if (buffer != nil) {
+		buffers.push_back(buffer);
+	} else {
+		blog(LOG_ERROR, "This vertex shader requires a %s buffer",
+				name);
+	}
+}
+
+void gs_vertex_buffer::MakeBufferList(gs_vertex_shader *shader,
+		vector<id<MTLBuffer>> &buffers)
+{
+	PushBuffer(buffers, vertexBuffer, "point");
+	if (shader->hasNormals)
+		PushBuffer(buffers, normalBuffer, "normal");
+	if (shader->hasColors)
+		PushBuffer(buffers, colorBuffer, "color");
+	if (shader->hasTangents)
+		PushBuffer(buffers, tangentBuffer, "tangent");
+	if (shader->texUnits <= uvBuffers.size()) {
+		for (size_t i = 0; i < shader->texUnits; i++)
+			buffers.push_back(uvBuffers[i]);
+	} else {
+		blog(LOG_ERROR, "This vertex shader requires at least %u "
+		                "texture buffers.",
+		                (uint32_t)shader->texUnits);
+	}
+}
+
+inline id<MTLBuffer> gs_vertex_buffer::InitBuffer(size_t elementSize,
+		void *array, const char *name)
+{
+	NSUInteger         length  = elementSize * vbData->num;
+	MTLResourceOptions options = MTLResourceCPUCacheModeWriteCombined |
+			(isDynamic ? MTLResourceStorageModeShared :
+			MTLResourceStorageModeManaged);
+
+	id<MTLBuffer> buffer = [device->device newBufferWithBytes:array
+			length:length options:options];
+	if (buffer == nil)
+		throw "Failed to create buffer";
+
+#ifdef _DEBUG
+	buffer.label = [[NSString alloc] initWithUTF8String:name];
+#endif
+
+	return buffer;
+}
+
+void gs_vertex_buffer::InitBuffers()
+{
+	vertexBuffer = InitBuffer(sizeof(vec3), vbData->points, "point");
+	if (vbData->normals)
+		normalBuffer = InitBuffer(sizeof(vec3), vbData->normals,
+				"normal");
+	if (vbData->tangents)
+		tangentBuffer = InitBuffer(sizeof(vec3), vbData->tangents,
+				"color");
+	if (vbData->colors)
+		colorBuffer = InitBuffer(sizeof(uint32_t), vbData->colors,
+				"tangent");
+	for (struct gs_tvertarray *tverts = vbData->tvarray;
+	     tverts != vbData->tvarray + vbData->num_tex;
+	     tverts++) {
+		if (tverts->width != 2 && tverts->width != 4)
+			throw "Invalid texture vertex size specified";
+		if (!tverts->array)
+			throw "No texture vertices specified";
+
+		id<MTLBuffer> buffer = InitBuffer(tverts->width * sizeof(float),
+				tverts->array, "texcoord");
+		uvBuffers.emplace_back(buffer);
+	}
+}
+
+void gs_vertex_buffer::Rebuild()
+{
+	if (!isDynamic)
+		InitBuffers();
+}
+
+gs_vertex_buffer::gs_vertex_buffer(gs_device_t *device, struct gs_vb_data *data,
+		uint32_t flags)
+	: gs_obj    (device, gs_type::gs_vertex_buffer),
+	  isDynamic ((flags & GS_DYNAMIC) != 0),
+	  vbData    (data, gs_vbdata_destroy)
+{
+	if (!data->num)
+		throw "Cannot initialize vertex buffer with 0 vertices";
+	if (!data->points)
+		throw "No points specified for vertex buffer";
+
+	if (!isDynamic)
+		InitBuffers();
+}

+ 43 - 0
libobs-metal/metal-zstencilbuffer.mm

@@ -0,0 +1,43 @@
+#include "metal-subsystem.hpp"
+
+static inline MTLPixelFormat ConvertGSZStencilFormat(gs_zstencil_format format)
+{
+	switch (format) {
+	case GS_ZS_NONE:    return MTLPixelFormatInvalid;
+	case GS_Z16:        return MTLPixelFormatDepth16Unorm;
+	case GS_Z24_S8:     return MTLPixelFormatDepth24Unorm_Stencil8;
+	case GS_Z32F:       return MTLPixelFormatDepth32Float;
+	case GS_Z32F_S8X24: return MTLPixelFormatDepth32Float_Stencil8;
+	default:            throw "Failed to initialize zstencil buffer";
+	}
+
+	return MTLPixelFormatInvalid;
+}
+
+void gs_zstencil_buffer::InitBuffer()
+{
+	texture = [device->device newTextureWithDescriptor:textureDesc];
+	if (texture == nil)
+		throw "Failed to create depth stencil texture";
+
+#if _DEBUG
+	texture.label = @"zstencil";
+#endif
+}
+
+gs_zstencil_buffer::gs_zstencil_buffer(gs_device_t *device,
+		uint32_t width, uint32_t height,
+		gs_zstencil_format format)
+	: gs_obj (device, gs_type::gs_zstencil_buffer),
+	  width  (width),
+	  height (height),
+	  format (format)
+{
+	textureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
+			ConvertGSZStencilFormat(format)
+			width:width height:height mipmapped:NO];
+	textureDesc.cpuCacheMode = MTLCPUCacheModeWriteCombined;
+	textureDesc.storageMode  = MTLStorageModeManaged;
+
+	InitBuffer();
+}

+ 2 - 1
libobs/CMakeLists.txt

@@ -117,7 +117,8 @@ elseif(APPLE)
 		util/platform-cocoa.m)
 	set(libobs_PLATFORM_HEADERS
 		util/threading-posix.h
-		util/apple/cfstring-utils.h)
+		util/apple/cfstring-utils.h
+		util/mac/mac-version.h)
 	set(libobs_audio_monitoring_SOURCES
 		audio-monitoring/osx/coreaudio-enum-devices.c
 		audio-monitoring/osx/coreaudio-output.c

+ 1 - 0
libobs/graphics/graphics.h

@@ -479,6 +479,7 @@ struct gs_init_data {
 
 #define GS_DEVICE_OPENGL      1
 #define GS_DEVICE_DIRECT3D_11 2
+#define GS_DEVICE_METAL       3
 
 EXPORT const char *gs_get_device_name(void);
 EXPORT int gs_get_device_type(void);

+ 35 - 0
libobs/util/mac/mac-version.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include "../c99defs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct mac_version_info {
+	union {
+		struct {
+			uint8_t _;
+			uint8_t bug_fix;
+			uint8_t minor;
+			uint8_t major;
+		};
+		uint32_t identifier;
+	};
+};
+	
+#define MACOSX_LEOPARD       ((10 << 24) | ( 5 << 16))
+#define MACOSX_SNOW_LEOPARD  ((10 << 24) | ( 6 << 16))
+#define MACOSX_LION          ((10 << 24) | ( 7 << 16))
+#define OSX_MOUNTAIN_LION    ((10 << 24) | ( 8 << 16))
+#define OSX_MAVERICKS        ((10 << 24) | ( 9 << 16))
+#define OSX_YOSEMITE         ((10 << 24) | (10 << 16))
+#define OSX_EL_CAPITAN       ((10 << 24) | (11 << 16))
+#define MACOS_SIERRA         ((10 << 24) | (12 << 16))
+#define MACOS_HIGH_SIERRA    ((10 << 24) | (13 << 16))
+
+EXPORT void get_mac_ver(struct mac_version_info *info);
+
+#ifdef __cplusplus
+}
+#endif

+ 36 - 0
libobs/util/platform-cocoa.m

@@ -19,6 +19,7 @@
 #include "base.h"
 #include "platform.h"
 #include "dstr.h"
+#include "mac/mac-version.h"
 
 #include <dlfcn.h>
 #include <time.h>
@@ -538,3 +539,38 @@ bool cfstr_copy_dstr(CFStringRef cfstring,
 
 	return (bool)success;
 }
+
+void get_mac_ver(struct mac_version_info *info)
+{
+	static struct mac_version_info ver = {0};
+	static bool got_version = false;
+
+	if (!info)
+		return;
+
+	if (!got_version) {
+		@autoreleasepool {
+			NSDictionary *dict = [NSDictionary
+					      dictionaryWithContentsOfFile:
+					      @"/System/Library/CoreServices/"
+					      "SystemVersion.plist"];
+			NSString *version =
+					[dict objectForKey:@"ProductVersion"];
+			const char *str = version.UTF8String;
+		
+			const char *p = str;
+			ver.major = atoi(p);
+			if ((p = strchr(p, '.')) != NULL) {
+				p++;
+				ver.minor = atoi(p);
+			}
+			if ((p = strchr(p, '.')) != NULL) {
+				p++;
+				ver.bug_fix = atoi(p);
+			}
+		}
+		got_version = true;
+	}
+
+	*info = ver;
+}