Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
File last commit:
Show/Diff file:
Action:
FNA/src/FNAPlatform/MetalDevice.cs
4698 lines | 107.4 KiB | text/x-csharp | CSharpLexer
#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
* Copyright 2009-2020 Ethan Lee and the MonoGame Team
*
* Released under the Microsoft Public License.
* See LICENSE for details.
*/
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SDL2;
#endregion
namespace Microsoft.Xna.Framework.Graphics
{
internal partial class MetalDevice : IGLDevice
{
#region Metal Texture Container Class
private class MetalTexture : IGLTexture
{
public IntPtr Handle
{
get;
private set;
}
public bool HasMipmaps
{
get;
private set;
}
public int Width
{
get;
private set;
}
public int Height
{
get;
private set;
}
public bool IsPrivate
{
get;
private set;
}
public SurfaceFormat Format;
public TextureAddressMode WrapS;
public TextureAddressMode WrapT;
public TextureAddressMode WrapR;
public TextureFilter Filter;
public float Anisotropy;
public int MaxMipmapLevel;
public float LODBias;
public MetalTexture(
IntPtr handle,
int width,
int height,
SurfaceFormat format,
int levelCount,
bool isPrivate
) {
Handle = handle;
Width = width;
Height = height;
Format = format;
HasMipmaps = levelCount > 1;
IsPrivate = isPrivate;
WrapS = TextureAddressMode.Wrap;
WrapT = TextureAddressMode.Wrap;
WrapR = TextureAddressMode.Wrap;
Filter = TextureFilter.Linear;
Anisotropy = 4.0f;
MaxMipmapLevel = 0;
LODBias = 0.0f;
}
private MetalTexture()
{
Handle = IntPtr.Zero;
}
public static readonly MetalTexture NullTexture = new MetalTexture();
public void Dispose()
{
objc_release(Handle);
Handle = IntPtr.Zero;
}
}
#endregion
#region Metal Renderbuffer Container Class
private class MetalRenderbuffer : IGLRenderbuffer
{
public IntPtr Handle
{
get;
private set;
}
public IntPtr MultiSampleHandle
{
get;
private set;
}
public MTLPixelFormat PixelFormat
{
get;
private set;
}
public int MultiSampleCount
{
get;
private set;
}
public MetalRenderbuffer(
IntPtr handle,
MTLPixelFormat pixelFormat,
int multiSampleCount,
IntPtr multiSampleHandle
) {
Handle = handle;
PixelFormat = pixelFormat;
MultiSampleCount = multiSampleCount;
MultiSampleHandle = multiSampleHandle;
}
public void Dispose()
{
if (MultiSampleHandle == IntPtr.Zero)
{
objc_release(Handle);
Handle = IntPtr.Zero;
}
else
{
objc_release(MultiSampleHandle);
MultiSampleHandle = IntPtr.Zero;
/* Don't release the regular Handle since
* it's owned by the associated IGLTexture.
*/
Handle = IntPtr.Zero;
}
}
}
#endregion
#region Metal Buffer Container Class
private class MetalBuffer : IGLBuffer
{
public IntPtr Handle
{
get;
private set;
}
public IntPtr Contents
{
get;
private set;
}
public IntPtr BufferSize
{
get;
private set;
}
public int InternalOffset
{
get;
private set;
}
private MetalDevice device;
private IntPtr mtlDevice = IntPtr.Zero;
private int internalBufferSize = 0;
private int prevDataLength = 0;
private int prevInternalOffset;
private BufferUsage usage;
private bool boundThisFrame;
public MetalBuffer(
MetalDevice device,
bool dynamic,
BufferUsage usage,
IntPtr bufferSize
) {
this.device = device;
this.mtlDevice = device.device;
this.usage = usage;
BufferSize = bufferSize;
internalBufferSize = (int) bufferSize;
CreateBackingBuffer(-1);
}
private void CreateBackingBuffer(int prevSize)
{
IntPtr oldBuffer = Handle;
IntPtr oldContents = Contents;
Handle = mtlNewBufferWithLength(
mtlDevice,
internalBufferSize,
usage == BufferUsage.WriteOnly ?
MTLResourceOptions.CPUCacheModeWriteCombined :
MTLResourceOptions.CPUCacheModeDefaultCache
);
Contents = mtlGetBufferContentsPtr(Handle);
// Copy over data from old buffer
if (oldBuffer != IntPtr.Zero)
{
SDL.SDL_memcpy(
Contents,
oldContents,
(IntPtr) prevSize
);
objc_release(oldBuffer);
}
}
public void SetData(
int offsetInBytes,
IntPtr data,
int dataLength,
SetDataOptions options
) {
if (options == SetDataOptions.None && boundThisFrame)
{
device.Stall();
boundThisFrame = true;
}
else if (options == SetDataOptions.Discard && boundThisFrame)
{
InternalOffset += (int) BufferSize;
if (InternalOffset + dataLength > internalBufferSize)
{
// Expand!
int prevSize = internalBufferSize;
internalBufferSize *= 2;
CreateBackingBuffer(prevSize);
}
}
// Copy previous contents, if needed
if (prevInternalOffset != InternalOffset && dataLength < (int) BufferSize)
{
SDL.SDL_memcpy(
Contents + InternalOffset,
Contents + prevInternalOffset,
BufferSize
);
}
// Copy the data into the buffer
SDL.SDL_memcpy(
Contents + InternalOffset + offsetInBytes,
data,
(IntPtr) dataLength
);
prevInternalOffset = InternalOffset;
prevDataLength = (int) BufferSize;
}
public void SetData(
int offsetInBytes,
IntPtr data,
int dataLength
) {
InternalOffset += prevDataLength;
if (InternalOffset + dataLength > internalBufferSize)
{
// Expand!
int prevSize = internalBufferSize;
internalBufferSize = Math.Max(
internalBufferSize * 2,
internalBufferSize + dataLength
);
CreateBackingBuffer(prevSize);
}
// Copy the data into the buffer
SDL.SDL_memcpy(
Contents + InternalOffset,
data + offsetInBytes,
(IntPtr) dataLength
);
prevDataLength = dataLength;
}
public void Bound()
{
boundThisFrame = true;
}
public void Reset()
{
InternalOffset = 0;
boundThisFrame = false;
prevDataLength = 0;
}
public void Dispose()
{
objc_release(Handle);
Handle = IntPtr.Zero;
}
}
#endregion
#region Metal Effect Container Class
private class MetalEffect : IGLEffect
{
public IntPtr EffectData
{
get;
private set;
}
public IntPtr MTLEffectData
{
get;
private set;
}
public MetalEffect(IntPtr effect, IntPtr mtlEffect)
{
EffectData = effect;
MTLEffectData = mtlEffect;
}
}
#endregion
#region Metal Query Container Class
private class MetalQuery : IGLQuery
{
public IntPtr Handle
{
get;
private set;
}
public MetalQuery(IntPtr handle)
{
Handle = handle;
}
public void Dispose()
{
objc_release(Handle);
Handle = IntPtr.Zero;
}
}
#endregion
#region Blending State Variables
private Color blendColor = Color.Transparent;
public Color BlendFactor
{
get
{
return blendColor;
}
set
{
if (value != blendColor)
{
blendColor = value;
SetEncoderBlendColor();
}
}
}
private int multisampleMask = -1; // AKA 0xFFFFFFFF
public int MultiSampleMask
{
get
{
return multisampleMask;
}
set
{
multisampleMask = value;
// FIXME: Metal does not support multisample masks. Workarounds...?
}
}
#endregion
#region Stencil State Variables
private int stencilRef = 0;
public int ReferenceStencil
{
get
{
return stencilRef;
}
set
{
if (value != stencilRef)
{
stencilRef = value;
SetEncoderStencilReferenceValue();
}
}
}
#endregion
#region Rasterizer State Variables
private bool scissorTestEnable = false;
private CullMode cullFrontFace = CullMode.None;
private FillMode fillMode = FillMode.Solid;
private float depthBias = 0.0f;
private float slopeScaleDepthBias = 0.0f;
private bool multiSampleEnable = true;
#endregion
#region Viewport State Variables
private Rectangle scissorRectangle = new Rectangle();
private Rectangle viewport = new Rectangle();
private float depthRangeMin = 0.0f;
private float depthRangeMax = 1.0f;
/* Used for resetting scissor rectangle */
private int currentAttachmentWidth;
private int currentAttachmentHeight;
#endregion
#region Sampler State Variables
private MetalTexture[] Textures;
private IntPtr[] Samplers;
private bool[] textureNeedsUpdate;
private bool[] samplerNeedsUpdate;
#endregion
#region Depth Stencil State Variables
private DepthStencilState depthStencilState;
private IntPtr defaultDepthStencilState; // MTLDepthStencilState*
private IntPtr ldDepthStencilState; // MTLDepthStencilState*
private MTLPixelFormat D16Format;
private MTLPixelFormat D24Format;
private MTLPixelFormat D24S8Format;
#endregion
#region Buffer Binding Cache Variables
private List<MetalBuffer> Buffers = new List<MetalBuffer>();
private MetalBuffer userVertexBuffer = null;
private MetalBuffer userIndexBuffer = null;
private int userVertexStride = 0;
// Some vertex declarations may have overlapping attributes :/
private bool[,] attrUse = new bool[(int) MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_TOTAL, 16];
#endregion
#region Render Target Cache Variables
private readonly IntPtr[] currentAttachments;
private readonly MTLPixelFormat[] currentColorFormats;
private readonly IntPtr[] currentMSAttachments;
private readonly CubeMapFace[] currentAttachmentSlices;
private IntPtr currentDepthStencilBuffer;
private DepthFormat currentDepthFormat;
private int currentSampleCount;
#endregion
#region Clear Cache Variables
private Vector4 clearColor = new Vector4(0, 0, 0, 0);
private float clearDepth = 1.0f;
private int clearStencil = 0;
private bool shouldClearColor = false;
private bool shouldClearDepth = false;
private bool shouldClearStencil = false;
#endregion
#region Private Metal State Variables
private IntPtr view; // SDL_MetalView*
private IntPtr layer; // CAMetalLayer*
private IntPtr device; // MTLDevice*
private IntPtr queue; // MTLCommandQueue*
private IntPtr commandBuffer; // MTLCommandBuffer*
private IntPtr renderCommandEncoder; // MTLRenderCommandEncoder*
private IntPtr currentVertexDescriptor; // MTLVertexDescriptor*
private IntPtr currentVisibilityBuffer; // MTLBuffer*
private bool needNewRenderPass;
#endregion
#region Operating System Variables
private bool isMac;
#endregion
#region Frame Tracking Variables
/* FIXME:
* In theory, double- or even triple-buffering could
* significantly help performance by reducing CPU idle
* time. The trade-off is that buffer synchronization
* becomes much more complicated and error-prone.
*
* I've attempted a few implementations of multi-
* buffering, but they all had serious issues and
* typically performed worse than single buffering.
*
* I'm leaving these variables here in case any brave
* souls want to attempt a multi-buffer implementation.
* This could be a huge win for performance, but it'll
* take someone smarter than me to figure this out. ;)
*
* -caleb
*/
private const int MAX_FRAMES_IN_FLIGHT = 1;
private Queue<IntPtr> committedCommandBuffers = new Queue<IntPtr>();
private bool frameInProgress = false;
#endregion
#region Objective-C Memory Management Variables
private IntPtr pool; // NSAutoreleasePool*
#endregion
#region Faux-Backbuffer Variables
public IGLBackbuffer Backbuffer
{
get;
private set;
}
private MTLSamplerMinMagFilter backbufferScaleMode;
// Cached data for rendering the faux-backbuffer
private Rectangle fauxBackbufferDestBounds;
private IntPtr fauxBackbufferDrawBuffer;
private IntPtr fauxBackbufferRenderPipeline;
private IntPtr fauxBackbufferSamplerState;
private bool fauxBackbufferSizeChanged;
#endregion
#region Metal Device Capabilities
public bool SupportsDxt1
{
get;
private set;
}
public bool SupportsS3tc
{
get;
private set;
}
public bool SupportsHardwareInstancing
{
get
{
return true;
}
}
public bool SupportsNoOverwrite
{
get
{
return true;
}
}
public int MaxTextureSlots
{
get
{
return 16;
}
}
public int MaxMultiSampleCount
{
get;
private set;
}
private bool supportsOcclusionQueries;
#endregion
#region Private State Object Caches
private Dictionary<ulong, IntPtr> VertexDescriptorCache =
new Dictionary<ulong, IntPtr>();
private Dictionary<PipelineHash, IntPtr> PipelineStateCache =
new Dictionary<PipelineHash, IntPtr>();
private Dictionary<StateHash, IntPtr> DepthStencilStateCache =
new Dictionary<StateHash, IntPtr>();
private Dictionary<StateHash, IntPtr> SamplerStateCache =
new Dictionary<StateHash, IntPtr>();
private List<MetalTexture> transientTextures =
new List<MetalTexture>();
#endregion
#region Private Render Pipeline State Variables
private BlendState blendState;
private IntPtr ldPipelineState = IntPtr.Zero;
#endregion
#region Private Buffer Binding Cache
private const int MAX_BOUND_VERTEX_BUFFERS = 16;
private IntPtr ldVertUniformBuffer = IntPtr.Zero;
private IntPtr ldFragUniformBuffer = IntPtr.Zero;
private int ldVertUniformOffset = 0;
private int ldFragUniformOffset = 0;
private IntPtr[] ldVertexBuffers;
private int[] ldVertexBufferOffsets;
#endregion
#region Private MojoShader Interop
private IntPtr currentEffect = IntPtr.Zero;
private IntPtr currentTechnique = IntPtr.Zero;
private uint currentPass = 0;
private IntPtr prevEffect = IntPtr.Zero;
private MojoShader.MOJOSHADER_mtlShaderState shaderState = new MojoShader.MOJOSHADER_mtlShaderState();
private MojoShader.MOJOSHADER_mtlShaderState prevShaderState;
#endregion
#region Public Constructor
public MetalDevice(PresentationParameters presentationParameters)
{
device = MTLCreateSystemDefaultDevice();
queue = mtlNewCommandQueue(device);
// Create the Metal view
view = SDL.SDL_Metal_CreateView(
presentationParameters.DeviceWindowHandle
);
// Get the layer from the view
layer = mtlGetLayer(view);
// Set up the CAMetalLayer
mtlSetLayerDevice(layer, device);
mtlSetLayerFramebufferOnly(layer, true);
mtlSetLayerMagnificationFilter(layer, UTF8ToNSString("nearest"));
// Log GLDevice info
FNALoggerEXT.LogInfo("IGLDevice: MetalDevice");
FNALoggerEXT.LogInfo("Device Name: " + mtlGetDeviceName(device));
FNALoggerEXT.LogInfo("MojoShader Profile: metal");
// Some users might want pixely upscaling...
backbufferScaleMode = Environment.GetEnvironmentVariable(
"FNA_GRAPHICS_BACKBUFFER_SCALE_NEAREST"
) == "1" ? MTLSamplerMinMagFilter.Nearest : MTLSamplerMinMagFilter.Linear;
// Set device properties
isMac = SDL.SDL_GetPlatform().Equals("Mac OS X");
SupportsS3tc = SupportsDxt1 = isMac;
MaxMultiSampleCount = mtlSupportsSampleCount(device, 8) ? 8 : 4;
supportsOcclusionQueries = isMac || HasModernAppleGPU();
// Determine supported depth formats
D16Format = MTLPixelFormat.Depth32Float;
D24Format = MTLPixelFormat.Depth32Float;
D24S8Format = MTLPixelFormat.Depth32Float_Stencil8;
if (isMac)
{
bool supportsD24S8 = mtlSupportsDepth24Stencil8(device);
if (supportsD24S8)
{
D24S8Format = MTLPixelFormat.Depth24Unorm_Stencil8;
// Gross, but at least it's a unorm format! -caleb
D24Format = MTLPixelFormat.Depth24Unorm_Stencil8;
D16Format = MTLPixelFormat.Depth24Unorm_Stencil8;
}
// Depth16Unorm requires macOS 10.12+
if (OperatingSystemAtLeast(10, 12, 0))
{
D16Format = MTLPixelFormat.Depth16Unorm;
}
}
else
{
// Depth16Unorm requires iOS 13+
if (OperatingSystemAtLeast(13, 0, 0))
{
D16Format = MTLPixelFormat.Depth16Unorm;
}
}
// Add fallbacks for missing texture formats on macOS
if (isMac)
{
XNAToMTL.TextureFormat[(int) SurfaceFormat.Bgr565]
= MTLPixelFormat.BGRA8Unorm;
XNAToMTL.TextureFormat[(int) SurfaceFormat.Bgra5551]
= MTLPixelFormat.BGRA8Unorm;
XNAToMTL.TextureFormat[(int) SurfaceFormat.Bgra4444]
= MTLPixelFormat.BGRA8Unorm;
}
// Initialize texture and sampler collections
Textures = new MetalTexture[MaxTextureSlots];
Samplers = new IntPtr[MaxTextureSlots];
for (int i = 0; i < MaxTextureSlots; i += 1)
{
Textures[i] = MetalTexture.NullTexture;
Samplers[i] = IntPtr.Zero;
}
textureNeedsUpdate = new bool[MaxTextureSlots];
samplerNeedsUpdate = new bool[MaxTextureSlots];
// Initialize attachment arrays
int numAttachments = GraphicsDevice.MAX_RENDERTARGET_BINDINGS;
currentAttachments = new IntPtr[numAttachments];
currentColorFormats = new MTLPixelFormat[numAttachments];
currentMSAttachments = new IntPtr[numAttachments];
currentAttachmentSlices = new CubeMapFace[numAttachments];
// Initialize vertex buffer cache
ldVertexBuffers = new IntPtr[MAX_BOUND_VERTEX_BUFFERS];
ldVertexBufferOffsets = new int[MAX_BOUND_VERTEX_BUFFERS];
// Create a default depth stencil state
IntPtr defDS = mtlNewDepthStencilDescriptor();
defaultDepthStencilState = mtlNewDepthStencilStateWithDescriptor(device, defDS);
objc_release(defDS);
// Create and setup the faux-backbuffer
InitializeFauxBackbuffer(presentationParameters);
}
#endregion
#region Dispose Method
public void Dispose()
{
// Stop rendering
EndPass();
// Release vertex descriptors
foreach (IntPtr vdesc in VertexDescriptorCache.Values)
{
objc_release(vdesc);
}
VertexDescriptorCache.Clear();
VertexDescriptorCache = null;
// Release depth stencil states
foreach (IntPtr ds in DepthStencilStateCache.Values)
{
objc_release(ds);
}
DepthStencilStateCache.Clear();
DepthStencilStateCache = null;
// Release pipeline states
foreach (IntPtr pso in PipelineStateCache.Values)
{
objc_release(pso);
}
PipelineStateCache.Clear();
PipelineStateCache = null;
// Release sampler states
foreach (IntPtr ss in SamplerStateCache.Values)
{
objc_release(ss);
}
SamplerStateCache.Clear();
SamplerStateCache = null;
// Release transient textures
foreach (MetalTexture tex in transientTextures)
{
objc_release(tex.Handle);
}
transientTextures.Clear();
transientTextures = null;
// Dispose the backbuffer
(Backbuffer as MetalBackbuffer).Dispose();
// Destroy the view
SDL.SDL_Metal_DestroyView(view);
}
#endregion
#region GetDrawableSize Methods
public static void GetDrawableSize(
IntPtr layer,
out int w,
out int h
) {
CGSize size = mtlGetDrawableSize(layer);
w = (int) size.width;
h = (int) size.height;
}
public static void GetDrawableSizeFromView(
IntPtr view,
out int w,
out int h
) {
GetDrawableSize(mtlGetLayer(view), out w, out h);
}
#endregion
#region Window Backbuffer Reset Method
public void ResetBackbuffer(PresentationParameters presentationParameters)
{
Backbuffer.ResetFramebuffer(presentationParameters);
}
#endregion
#region BeginFrame Method
public void BeginFrame()
{
if (frameInProgress) return;
// Wait for command buffers to complete...
while (committedCommandBuffers.Count >= MAX_FRAMES_IN_FLIGHT)
{
IntPtr cmdbuf = committedCommandBuffers.Dequeue();
mtlCommandBufferWaitUntilCompleted(cmdbuf);
objc_release(cmdbuf);
}
// The cycle begins anew!
frameInProgress = true;
pool = objc_autoreleasePoolPush();
commandBuffer = mtlMakeCommandBuffer(queue);
}
#endregion
#region Window SwapBuffers Method
public void SwapBuffers(
Rectangle? sourceRectangle,
Rectangle? destinationRectangle,
IntPtr overrideWindowHandle
) {
/* Just in case Present() is called
* before any rendering happens...
*/
BeginFrame();
// Bind the backbuffer and finalize rendering
SetRenderTargets(null, null, DepthFormat.None);
EndPass();
// Determine the regions to present
int srcX, srcY, srcW, srcH;
int dstX, dstY, dstW, dstH;
if (sourceRectangle.HasValue)
{
srcX = sourceRectangle.Value.X;
srcY = sourceRectangle.Value.Y;
srcW = sourceRectangle.Value.Width;
srcH = sourceRectangle.Value.Height;
}
else
{
srcX = 0;
srcY = 0;
srcW = Backbuffer.Width;
srcH = Backbuffer.Height;
}
if (destinationRectangle.HasValue)
{
dstX = destinationRectangle.Value.X;
dstY = destinationRectangle.Value.Y;
dstW = destinationRectangle.Value.Width;
dstH = destinationRectangle.Value.Height;
}
else
{
dstX = 0;
dstY = 0;
GetDrawableSize(
layer,
out dstW,
out dstH
);
}
// Get the next drawable
IntPtr drawable = mtlNextDrawable(layer);
// "Blit" the backbuffer to the drawable
BlitFramebuffer(
currentAttachments[0],
new Rectangle(srcX, srcY, srcW, srcH),
mtlGetTextureFromDrawable(drawable),
new Rectangle(dstX, dstY, dstW, dstH)
);
// Commit the command buffer for presentation
mtlPresentDrawable(commandBuffer, drawable);
mtlCommitCommandBuffer(commandBuffer);
// Enqueue the command buffer for tracking
objc_retain(commandBuffer);
committedCommandBuffers.Enqueue(commandBuffer);
commandBuffer = IntPtr.Zero;
// Release allocations from the past frame
objc_autoreleasePoolPop(pool);
// Reset buffers
for (int i = 0; i < Buffers.Count; i += 1)
{
Buffers[i].Reset();
}
MojoShader.MOJOSHADER_mtlEndFrame();
// We're done here.
frameInProgress = false;
}
private void BlitFramebuffer(
IntPtr srcTex,
Rectangle srcRect,
IntPtr dstTex,
Rectangle dstRect
) {
if ( srcRect.Width == 0 ||
srcRect.Height == 0 ||
dstRect.Width == 0 ||
dstRect.Height == 0 )
{
// Enjoy that bright red window!
return;
}
// Update cached vertex buffer if needed
if (fauxBackbufferDestBounds != dstRect || fauxBackbufferSizeChanged)
{
fauxBackbufferDestBounds = dstRect;
fauxBackbufferSizeChanged = false;
// Scale the coordinates to (-1, 1)
int dw, dh;
GetDrawableSize(layer, out dw, out dh);
float sx = -1 + (dstRect.X / (float) dw);
float sy = -1 + (dstRect.Y / (float) dh);
float sw = (dstRect.Width / (float) dw) * 2;
float sh = (dstRect.Height / (float) dh) * 2;
// Update the vertex buffer contents
float[] data = new float[]
{
sx, sy, 0, 0,
sx + sw, sy, 1, 0,
sx + sw, sy + sh, 1, 1,
sx, sy + sh, 0, 1
};
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
SDL.SDL_memcpy(
mtlGetBufferContentsPtr(fauxBackbufferDrawBuffer),
handle.AddrOfPinnedObject(),
(IntPtr) (16 * sizeof(float))
);
handle.Free();
}
// Render the source texture to the destination texture
IntPtr backbufferRenderPass = mtlMakeRenderPassDescriptor();
mtlSetAttachmentTexture(
mtlGetColorAttachment(backbufferRenderPass, 0),
dstTex
);
IntPtr rce = mtlMakeRenderCommandEncoder(
commandBuffer,
backbufferRenderPass
);
mtlSetRenderPipelineState(rce, fauxBackbufferRenderPipeline);
mtlSetVertexBuffer(rce, fauxBackbufferDrawBuffer, 0, 0);
mtlSetFragmentTexture(rce, srcTex, 0);
mtlSetFragmentSamplerState(rce, fauxBackbufferSamplerState, 0);
mtlDrawIndexedPrimitives(
rce,
MTLPrimitiveType.Triangle,
6,
MTLIndexType.UInt16,
fauxBackbufferDrawBuffer,
16 * sizeof(float),
1
);
mtlEndEncoding(rce);
}
#endregion
#region Render Command Encoder Methods
private void EndPass()
{
if (renderCommandEncoder != IntPtr.Zero)
{
mtlEndEncoding(renderCommandEncoder);
renderCommandEncoder = IntPtr.Zero;
}
}
private void UpdateRenderPass()
{
if (!needNewRenderPass) return;
/* Normally the frame begins in BeginDraw(),
* but some games perform drawing outside
* of the Draw method (e.g. initializing
* render targets in LoadContent). This call
* ensures that we catch any unexpected draws.
* -caleb
*/
BeginFrame();
// Wrap up rendering with the old encoder
EndPass();
// Generate the descriptor
IntPtr passDesc = mtlMakeRenderPassDescriptor();
// Bind color attachments
for (int i = 0; i < currentAttachments.Length; i += 1)
{
if (currentAttachments[i] == IntPtr.Zero)
{
continue;
}
IntPtr colorAttachment = mtlGetColorAttachment(passDesc, i);
mtlSetAttachmentTexture(
colorAttachment,
currentAttachments[i]
);
mtlSetAttachmentSlice(
colorAttachment,
(int) currentAttachmentSlices[i]
);
// Multisample?
if (currentSampleCount > 0)
{
mtlSetAttachmentTexture(
colorAttachment,
currentMSAttachments[i]
);
mtlSetAttachmentSlice(
colorAttachment,
0
);
mtlSetAttachmentResolveTexture(
colorAttachment,
currentAttachments[i]
);
mtlSetAttachmentStoreAction(
colorAttachment,
MTLStoreAction.MultisampleResolve
);
mtlSetAttachmentResolveSlice(
colorAttachment,
(int) currentAttachmentSlices[i]
);
}
// Clear color
if (shouldClearColor)
{
mtlSetAttachmentLoadAction(
colorAttachment,
MTLLoadAction.Clear
);
mtlSetColorAttachmentClearColor(
colorAttachment,
clearColor.X,
clearColor.Y,
clearColor.Z,
clearColor.W
);
}
else
{
mtlSetAttachmentLoadAction(
colorAttachment,
MTLLoadAction.Load
);
}
}
// Bind depth attachment
if (currentDepthFormat != DepthFormat.None)
{
IntPtr depthAttachment = mtlGetDepthAttachment(passDesc);
mtlSetAttachmentTexture(
depthAttachment,
currentDepthStencilBuffer
);
mtlSetAttachmentStoreAction(
depthAttachment,
MTLStoreAction.Store
);
// Clear?
if (shouldClearDepth)
{
mtlSetAttachmentLoadAction(
depthAttachment,
MTLLoadAction.Clear
);
mtlSetDepthAttachmentClearDepth(
depthAttachment,
clearDepth
);
}
else
{
mtlSetAttachmentLoadAction(
depthAttachment,
MTLLoadAction.Load
);
}
}
// Bind stencil buffer
if (currentDepthFormat == DepthFormat.Depth24Stencil8)
{
IntPtr stencilAttachment = mtlGetStencilAttachment(passDesc);
mtlSetAttachmentTexture(
stencilAttachment,
currentDepthStencilBuffer
);
mtlSetAttachmentStoreAction(
stencilAttachment,
MTLStoreAction.Store
);
// Clear?
if (shouldClearStencil)
{
mtlSetAttachmentLoadAction(
stencilAttachment,
MTLLoadAction.Clear
);
mtlSetStencilAttachmentClearStencil(
stencilAttachment,
clearStencil
);
}
else
{
mtlSetAttachmentLoadAction(
stencilAttachment,
MTLLoadAction.Load
);
}
}
// Get attachment size
currentAttachmentWidth = (int) mtlGetTextureWidth(
currentAttachments[0]
);
currentAttachmentHeight = (int) mtlGetTextureHeight(
currentAttachments[0]
);
// Attach the visibility buffer, if needed
if (currentVisibilityBuffer != IntPtr.Zero)
{
mtlSetVisibilityResultBuffer(
passDesc,
currentVisibilityBuffer
);
}
// Make a new encoder
renderCommandEncoder = mtlMakeRenderCommandEncoder(
commandBuffer,
passDesc
);
// Reset the flags
needNewRenderPass = false;
shouldClearColor = false;
shouldClearDepth = false;
shouldClearStencil = false;
// Apply the dynamic state
SetEncoderViewport();
SetEncoderScissorRect();
SetEncoderBlendColor();
SetEncoderStencilReferenceValue();
SetEncoderCullMode();
SetEncoderFillMode();
SetEncoderDepthBias();
// Start visibility buffer counting
if (currentVisibilityBuffer != IntPtr.Zero)
{
mtlSetVisibilityResultMode(
renderCommandEncoder,
MTLVisibilityResultMode.Counting,
0
);
}
// Reset the bindings
for (int i = 0; i < MaxTextureSlots; i += 1)
{
if (Textures[i] != MetalTexture.NullTexture)
{
textureNeedsUpdate[i] = true;
}
if (Samplers[i] != IntPtr.Zero)
{
samplerNeedsUpdate[i] = true;
}
}
ldDepthStencilState = IntPtr.Zero;
ldFragUniformBuffer = IntPtr.Zero;
ldFragUniformOffset = 0;
ldVertUniformBuffer = IntPtr.Zero;
ldVertUniformOffset = 0;
ldPipelineState = IntPtr.Zero;
for (int i = 0; i < MAX_BOUND_VERTEX_BUFFERS; i += 1)
{
ldVertexBuffers[i] = IntPtr.Zero;
ldVertexBufferOffsets[i] = 0;
}
}
private void SetEncoderStencilReferenceValue()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetStencilReferenceValue(
renderCommandEncoder,
(uint) stencilRef
);
}
}
private void SetEncoderBlendColor()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetBlendColor(
renderCommandEncoder,
blendColor.R / 255f,
blendColor.G / 255f,
blendColor.B / 255f,
blendColor.A / 255f
);
}
}
private void SetEncoderViewport()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetViewport(
renderCommandEncoder,
viewport.X,
viewport.Y,
viewport.Width,
viewport.Height,
(double) depthRangeMin,
(double) depthRangeMax
);
}
}
private void SetEncoderCullMode()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetCullMode(
renderCommandEncoder,
XNAToMTL.CullingEnabled[(int) cullFrontFace]
);
}
}
private void SetEncoderFillMode()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetTriangleFillMode(
renderCommandEncoder,
XNAToMTL.FillMode[(int) fillMode]
);
}
}
private void SetEncoderDepthBias()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
mtlSetDepthBias(
renderCommandEncoder,
depthBias,
slopeScaleDepthBias,
0.0f // no clamp
);
}
}
private void SetEncoderScissorRect()
{
if (renderCommandEncoder != IntPtr.Zero && !needNewRenderPass)
{
if (!scissorTestEnable)
{
// Set to the default scissor rect
mtlSetScissorRect(
renderCommandEncoder,
0,
0,
currentAttachmentWidth,
currentAttachmentHeight
);
}
else
{
mtlSetScissorRect(
renderCommandEncoder,
scissorRectangle.X,
scissorRectangle.Y,
scissorRectangle.Width,
scissorRectangle.Height
);
}
}
}
#endregion
#region Metal Object Disposal Wrappers
public void AddDisposeEffect(IGLEffect effect)
{
DeleteEffect(effect);
}
public void AddDisposeIndexBuffer(IGLBuffer buffer)
{
DeleteBuffer(buffer);
}
public void AddDisposeQuery(IGLQuery query)
{
DeleteQuery(query);
}
public void AddDisposeRenderbuffer(IGLRenderbuffer renderbuffer)
{
DeleteRenderbuffer(renderbuffer);
}
public void AddDisposeTexture(IGLTexture texture)
{
DeleteTexture(texture);
}
public void AddDisposeVertexBuffer(IGLBuffer buffer)
{
DeleteBuffer(buffer);
}
#endregion
#region Pipeline Stall Method
private void Stall()
{
EndPass();
mtlCommitCommandBuffer(commandBuffer);
mtlCommandBufferWaitUntilCompleted(commandBuffer);
commandBuffer = mtlMakeCommandBuffer(queue);
needNewRenderPass = true;
committedCommandBuffers.Clear();
for (int i = 0; i < Buffers.Count; i += 1)
{
Buffers[i].Reset();
}
}
#endregion
#region String Marker Method
public void SetStringMarker(string text)
{
#if DEBUG
if (renderCommandEncoder != IntPtr.Zero)
{
mtlInsertDebugSignpost(renderCommandEncoder, text);
}
#endif
}
#endregion
#region Drawing Methods
public void DrawIndexedPrimitives(
PrimitiveType primitiveType,
int baseVertex,
int minVertexIndex,
int numVertices,
int startIndex,
int primitiveCount,
IGLBuffer indices,
IndexElementSize indexElementSize
) {
MetalBuffer indexBuffer = indices as MetalBuffer;
indexBuffer.Bound();
int totalIndexOffset = (
(startIndex * XNAToMTL.IndexSize[(int) indexElementSize]) +
indexBuffer.InternalOffset
);
mtlDrawIndexedPrimitives(
renderCommandEncoder,
XNAToMTL.Primitive[(int) primitiveType],
XNAToMTL.PrimitiveVerts(primitiveType, primitiveCount),
XNAToMTL.IndexType[(int) indexElementSize],
indexBuffer.Handle,
totalIndexOffset,
1
);
}
public void DrawInstancedPrimitives(
PrimitiveType primitiveType,
int baseVertex,
int minVertexIndex,
int numVertices,
int startIndex,
int primitiveCount,
int instanceCount,
IGLBuffer indices,
IndexElementSize indexElementSize
) {
MetalBuffer indexBuffer = indices as MetalBuffer;
indexBuffer.Bound();
int totalIndexOffset = (
(startIndex * XNAToMTL.IndexSize[(int) indexElementSize]) +
indexBuffer.InternalOffset
);
mtlDrawIndexedPrimitives(
renderCommandEncoder,
XNAToMTL.Primitive[(int) primitiveType],
XNAToMTL.PrimitiveVerts(primitiveType, primitiveCount),
XNAToMTL.IndexType[(int) indexElementSize],
indexBuffer.Handle,
totalIndexOffset,
instanceCount
);
}
public void DrawPrimitives(
PrimitiveType primitiveType,
int vertexStart,
int primitiveCount
) {
mtlDrawPrimitives(
renderCommandEncoder,
XNAToMTL.Primitive[(int) primitiveType],
vertexStart,
XNAToMTL.PrimitiveVerts(primitiveType, primitiveCount)
);
}
private void BindUserVertexBuffer(
IntPtr vertexData,
int vertexCount,
int vertexOffset
) {
// Update the buffer contents
int len = vertexCount * userVertexStride;
if (userVertexBuffer == null)
{
userVertexBuffer = new MetalBuffer(
this,
true,
BufferUsage.WriteOnly,
(IntPtr) len
);
Buffers.Add(userVertexBuffer);
}
userVertexBuffer.SetData(
vertexOffset * userVertexStride,
vertexData,
len
);
// Bind the buffer
int offset = userVertexBuffer.InternalOffset;
IntPtr handle = userVertexBuffer.Handle;
if (ldVertexBuffers[0] != handle)
{
mtlSetVertexBuffer(
renderCommandEncoder,
handle,
offset,
0
);
ldVertexBuffers[0] = handle;
ldVertexBufferOffsets[0] = offset;
}
else if (ldVertexBufferOffsets[0] != offset)
{
mtlSetVertexBufferOffset(
renderCommandEncoder,
offset,
0
);
ldVertexBufferOffsets[0] = offset;
}
}
public void DrawUserIndexedPrimitives(
PrimitiveType primitiveType,
IntPtr vertexData,
int vertexOffset,
int numVertices,
IntPtr indexData,
int indexOffset,
IndexElementSize indexElementSize,
int primitiveCount
) {
// Bind the vertex buffer
BindUserVertexBuffer(
vertexData,
numVertices,
vertexOffset
);
// Prepare the index buffer
int numIndices = XNAToMTL.PrimitiveVerts(
primitiveType,
primitiveCount
);
int indexSize = XNAToMTL.IndexSize[(int) indexElementSize];
int len = (int) numIndices * indexSize;
if (userIndexBuffer == null)
{
userIndexBuffer = new MetalBuffer(
this,
true,
BufferUsage.WriteOnly,
(IntPtr) len
);
Buffers.Add(userIndexBuffer);
}
userIndexBuffer.SetData(
indexOffset * indexSize,
indexData,
len
);
// Draw!
mtlDrawIndexedPrimitives(
renderCommandEncoder,
XNAToMTL.Primitive[(int) primitiveType],
numIndices,
XNAToMTL.IndexType[(int) indexElementSize],
userIndexBuffer.Handle,
userIndexBuffer.InternalOffset,
1
);
}
public void DrawUserPrimitives(
PrimitiveType primitiveType,
IntPtr vertexData,
int vertexOffset,
int primitiveCount
) {
// Bind the vertex buffer
int numVerts = XNAToMTL.PrimitiveVerts(
primitiveType,
primitiveCount
);
BindUserVertexBuffer(
vertexData,
numVerts,
vertexOffset
);
// Draw!
mtlDrawPrimitives(
renderCommandEncoder,
XNAToMTL.Primitive[(int) primitiveType],
0,
numVerts
);
}
#endregion
#region State Management Methods
public void SetPresentationInterval(PresentInterval interval)
{
// Toggling vsync is only supported on macOS 10.13+
if (!RespondsToSelector(layer, selDisplaySyncEnabled))
{
FNALoggerEXT.LogWarn(
"Cannot set presentation interval! " +
"Only vsync is supported."
);
return;
}
if (interval == PresentInterval.Default || interval == PresentInterval.One)
{
mtlSetDisplaySyncEnabled(layer, true);
}
else if (interval == PresentInterval.Immediate)
{
mtlSetDisplaySyncEnabled(layer, false);
}
else if (interval == PresentInterval.Two)
{
/* FIXME:
* There is no built-in support for
* present-every-other-frame in Metal.
* We could work around this, but do
* any games actually use this mode...?
* -caleb
*/
mtlSetDisplaySyncEnabled(layer, true);
}
else
{
throw new NotSupportedException("Unrecognized PresentInterval!");
}
}
public void SetViewport(Viewport vp)
{
if ( vp.Bounds != viewport ||
vp.MinDepth != depthRangeMin ||
vp.MaxDepth != depthRangeMax )
{
viewport = vp.Bounds;
depthRangeMin = vp.MinDepth;
depthRangeMax = vp.MaxDepth;
SetEncoderViewport(); // Dynamic state!
}
}
public void SetScissorRect(Rectangle scissorRect)
{
if (scissorRectangle != scissorRect)
{
scissorRectangle = scissorRect;
SetEncoderScissorRect(); // Dynamic state!
}
}
public void ApplyRasterizerState(RasterizerState rasterizerState)
{
if (rasterizerState.ScissorTestEnable != scissorTestEnable)
{
scissorTestEnable = rasterizerState.ScissorTestEnable;
SetEncoderScissorRect(); // Dynamic state!
}
if (rasterizerState.CullMode != cullFrontFace)
{
cullFrontFace = rasterizerState.CullMode;
SetEncoderCullMode(); // Dynamic state!
}
if (rasterizerState.FillMode != fillMode)
{
fillMode = rasterizerState.FillMode;
SetEncoderFillMode(); // Dynamic state!
}
float realDepthBias = rasterizerState.DepthBias;
realDepthBias *= XNAToMTL.DepthBiasScale(
GetDepthFormat(currentDepthFormat)
);
if ( realDepthBias != depthBias ||
rasterizerState.SlopeScaleDepthBias != slopeScaleDepthBias )
{
depthBias = realDepthBias;
slopeScaleDepthBias = rasterizerState.SlopeScaleDepthBias;
SetEncoderDepthBias(); // Dynamic state!
}
if (rasterizerState.MultiSampleAntiAlias != multiSampleEnable)
{
multiSampleEnable = rasterizerState.MultiSampleAntiAlias;
// FIXME: Metal does not support toggling MSAA. Workarounds...?
}
}
public void VerifySampler(int index, Texture texture, SamplerState sampler)
{
if (texture == null)
{
if (Textures[index] != MetalTexture.NullTexture)
{
Textures[index] = MetalTexture.NullTexture;
textureNeedsUpdate[index] = true;
}
if (Samplers[index] == IntPtr.Zero)
{
/* Some shaders require non-null samplers
* even if they aren't actually used.
* -caleb
*/
Samplers[index] = FetchSamplerState(sampler, false);
samplerNeedsUpdate[index] = true;
}
return;
}
MetalTexture tex = texture.texture as MetalTexture;
if ( tex == Textures[index] &&
sampler.AddressU == tex.WrapS &&
sampler.AddressV == tex.WrapT &&
sampler.AddressW == tex.WrapR &&
sampler.Filter == tex.Filter &&
sampler.MaxAnisotropy == tex.Anisotropy &&
sampler.MaxMipLevel == tex.MaxMipmapLevel &&
sampler.MipMapLevelOfDetailBias == tex.LODBias )
{
// Nothing's changing, forget it.
return;
}
// Bind the correct texture
if (tex != Textures[index])
{
Textures[index] = tex;
textureNeedsUpdate[index] = true;
}
// Update the texture sampler info
tex.WrapS = sampler.AddressU;
tex.WrapT = sampler.AddressV;
tex.WrapR = sampler.AddressW;
tex.Filter = sampler.Filter;
tex.Anisotropy = sampler.MaxAnisotropy;
tex.MaxMipmapLevel = sampler.MaxMipLevel;
tex.LODBias = sampler.MipMapLevelOfDetailBias;
// Update the sampler state, if needed
IntPtr ss = FetchSamplerState(sampler, tex.HasMipmaps);
if (ss != Samplers[index])
{
Samplers[index] = ss;
samplerNeedsUpdate[index] = true;
}
}
public void SetBlendState(BlendState blendState)
{
this.blendState = blendState;
BlendFactor = blendState.BlendFactor; // Dynamic state!
}
public void SetDepthStencilState(DepthStencilState depthStencilState)
{
this.depthStencilState = depthStencilState;
ReferenceStencil = depthStencilState.ReferenceStencil; // Dynamic state!
}
#endregion
#region State Creation/Retrieval Methods
private struct PipelineHash : IEquatable<PipelineHash>
{
readonly ulong a;
readonly ulong b;
readonly ulong c;
readonly ulong d;
public PipelineHash(
ulong vertexShader,
ulong fragmentShader,
ulong vertexDescriptor,
MTLPixelFormat[] formats,
DepthFormat depthFormat,
int sampleCount,
BlendState blendState
) {
this.a = vertexShader;
this.b = fragmentShader;
this.c = vertexDescriptor;
unchecked
{
this.d = (
((ulong) blendState.GetHashCode() << 32) |
((ulong) sampleCount << 22) |
((ulong) depthFormat << 20) |
((ulong) HashFormat(formats[3]) << 15) |
((ulong) HashFormat(formats[2]) << 10) |
((ulong) HashFormat(formats[1]) << 5) |
((ulong) HashFormat(formats[0]))
);
}
}
private static uint HashFormat(MTLPixelFormat format)
{
switch (format)
{
case MTLPixelFormat.Invalid:
return 0;
case MTLPixelFormat.R16Float:
return 1;
case MTLPixelFormat.R32Float:
return 2;
case MTLPixelFormat.RG16Float:
return 3;
case MTLPixelFormat.RG16Snorm:
return 4;
case MTLPixelFormat.RG16Unorm:
return 5;
case MTLPixelFormat.RG32Float:
return 6;
case MTLPixelFormat.RG8Snorm:
return 7;
case MTLPixelFormat.RGB10A2Unorm:
return 8;
case MTLPixelFormat.RGBA16Float:
return 9;
case MTLPixelFormat.RGBA16Unorm:
return 10;
case MTLPixelFormat.RGBA32Float:
return 11;
case MTLPixelFormat.RGBA8Unorm:
return 12;
case MTLPixelFormat.A8Unorm:
return 13;
case MTLPixelFormat.ABGR4Unorm:
return 14;
case MTLPixelFormat.B5G6R5Unorm:
return 15;
case MTLPixelFormat.BC1_RGBA:
return 16;
case MTLPixelFormat.BC2_RGBA:
return 17;
case MTLPixelFormat.BC3_RGBA:
return 18;
case MTLPixelFormat.BGR5A1Unorm:
return 19;
case MTLPixelFormat.BGRA8Unorm:
return 20;
}
throw new NotSupportedException();
}
public override int GetHashCode()
{
unchecked
{
int i1 = (int) (a ^ (a >> 32));
int i2 = (int) (b ^ (b >> 32));
int i3 = (int) (c ^ (c >> 32));
int i4 = (int) (d ^ (d >> 32));
return i1 + i2 + i3 + i4;
}
}
public bool Equals(PipelineHash other)
{
return (
a == other.a &&
b == other.b &&
c == other.c &&
d == other.d
);
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
PipelineHash hash = (PipelineHash) obj;
return (
a == hash.a &&
b == hash.b &&
c == hash.c &&
d == hash.d
);
}
}
private IntPtr FetchRenderPipeline()
{
// Can we just reuse an existing pipeline?
PipelineHash hash = new PipelineHash(
(ulong) shaderState.vertexShader,
(ulong) shaderState.fragmentShader,
(ulong) currentVertexDescriptor,
currentColorFormats,
currentDepthFormat,
currentSampleCount,
blendState
);
IntPtr pipeline = IntPtr.Zero;
if (PipelineStateCache.TryGetValue(hash, out pipeline))
{
// We have this state already cached!
return pipeline;
}
// We have to make a new pipeline...
IntPtr pipelineDesc = mtlNewRenderPipelineDescriptor();
IntPtr vertHandle = MojoShader.MOJOSHADER_mtlGetFunctionHandle(
shaderState.vertexShader
);
IntPtr fragHandle = MojoShader.MOJOSHADER_mtlGetFunctionHandle(
shaderState.fragmentShader
);
mtlSetPipelineVertexFunction(
pipelineDesc,
vertHandle
);
mtlSetPipelineFragmentFunction(
pipelineDesc,
fragHandle
);
mtlSetPipelineVertexDescriptor(
pipelineDesc,
currentVertexDescriptor
);
mtlSetDepthAttachmentPixelFormat(
pipelineDesc,
GetDepthFormat(currentDepthFormat)
);
if (currentDepthFormat == DepthFormat.Depth24Stencil8)
{
mtlSetStencilAttachmentPixelFormat(
pipelineDesc,
GetDepthFormat(currentDepthFormat)
);
}
mtlSetPipelineSampleCount(
pipelineDesc,
Math.Max(1, currentSampleCount)
);
// Apply the blend state
bool alphaBlendEnable = !(
blendState.ColorSourceBlend == Blend.One &&
blendState.ColorDestinationBlend == Blend.Zero &&
blendState.AlphaSourceBlend == Blend.One &&
blendState.AlphaDestinationBlend == Blend.Zero
);
for (int i = 0; i < currentAttachments.Length; i += 1)
{
if (currentAttachments[i] == IntPtr.Zero)
{
// There's no attachment bound at this index.
continue;
}
IntPtr colorAttachment = mtlGetColorAttachment(
pipelineDesc,
i
);
mtlSetAttachmentPixelFormat(
colorAttachment,
currentColorFormats[i]
);
mtlSetAttachmentBlendingEnabled(
colorAttachment,
alphaBlendEnable
);
if (alphaBlendEnable)
{
mtlSetAttachmentSourceRGBBlendFactor(
colorAttachment,
XNAToMTL.BlendMode[
(int) blendState.ColorSourceBlend
]
);
mtlSetAttachmentDestinationRGBBlendFactor(
colorAttachment,
XNAToMTL.BlendMode[
(int) blendState.ColorDestinationBlend
]
);
mtlSetAttachmentSourceAlphaBlendFactor(
colorAttachment,
XNAToMTL.BlendMode[
(int) blendState.AlphaSourceBlend
]
);
mtlSetAttachmentDestinationAlphaBlendFactor(
colorAttachment,
XNAToMTL.BlendMode[
(int) blendState.AlphaDestinationBlend
]
);
mtlSetAttachmentRGBBlendOperation(
colorAttachment,
XNAToMTL.BlendOperation[
(int) blendState.ColorBlendFunction
]
);
mtlSetAttachmentAlphaBlendOperation(
colorAttachment,
XNAToMTL.BlendOperation[
(int) blendState.AlphaBlendFunction
]
);
}
/* FIXME: So how exactly do we factor in
* COLORWRITEENABLE for buffer 0? Do we just assume that
* the default is just buffer 0, and all other calls
* update the other write masks?
*/
if (i == 0)
{
mtlSetAttachmentWriteMask(
colorAttachment,
XNAToMTL.ColorWriteMask(blendState.ColorWriteChannels)
);
}
else if (i == 1)
{
mtlSetAttachmentWriteMask(
mtlGetColorAttachment(pipelineDesc, 1),
XNAToMTL.ColorWriteMask(blendState.ColorWriteChannels1)
);
}
else if (i == 2)
{
mtlSetAttachmentWriteMask(
mtlGetColorAttachment(pipelineDesc, 2),
XNAToMTL.ColorWriteMask(blendState.ColorWriteChannels2)
);
}
else if (i == 3)
{
mtlSetAttachmentWriteMask(
mtlGetColorAttachment(pipelineDesc, 3),
XNAToMTL.ColorWriteMask(blendState.ColorWriteChannels3)
);
}
}
// Bake the render pipeline!
IntPtr pipelineState = mtlNewRenderPipelineStateWithDescriptor(
device,
pipelineDesc
);
PipelineStateCache[hash] = pipelineState;
// Clean up
objc_release(pipelineDesc);
objc_release(vertHandle);
objc_release(fragHandle);
// Return the pipeline!
return pipelineState;
}
private IntPtr FetchDepthStencilState()
{
/* Just use the default depth-stencil state
* if depth and stencil testing are disabled,
* or if there is no bound depth attachment.
* This wards off Metal validation errors.
* -caleb
*/
bool zEnable = depthStencilState.DepthBufferEnable;
bool sEnable = depthStencilState.StencilEnable;
bool zFormat = (currentDepthFormat != DepthFormat.None);
if ((!zEnable && !sEnable) || (!zFormat))
{
return defaultDepthStencilState;
}
// Can we just reuse an existing state?
StateHash hash = PipelineCache.GetDepthStencilHash(depthStencilState);
IntPtr state = IntPtr.Zero;
if (DepthStencilStateCache.TryGetValue(hash, out state))
{
// This state has already been cached!
return state;
}
// We have to make a new DepthStencilState...
IntPtr dsDesc = mtlNewDepthStencilDescriptor();
if (zEnable)
{
mtlSetDepthCompareFunction(
dsDesc,
XNAToMTL.CompareFunc[(int) depthStencilState.DepthBufferFunction]
);
mtlSetDepthWriteEnabled(
dsDesc,
depthStencilState.DepthBufferWriteEnable
);
}
// Create stencil descriptors
IntPtr front = IntPtr.Zero;
IntPtr back = IntPtr.Zero;
if (sEnable)
{
front = mtlNewStencilDescriptor();
mtlSetStencilFailureOperation(
front,
XNAToMTL.StencilOp[(int) depthStencilState.StencilFail]
);
mtlSetDepthFailureOperation(
front,
XNAToMTL.StencilOp[(int) depthStencilState.StencilDepthBufferFail]
);
mtlSetDepthStencilPassOperation(
front,
XNAToMTL.StencilOp[(int) depthStencilState.StencilPass]
);
mtlSetStencilCompareFunction(
front,
XNAToMTL.CompareFunc[(int) depthStencilState.StencilFunction]
);
mtlSetStencilReadMask(
front,
(uint) depthStencilState.StencilMask
);
mtlSetStencilWriteMask(
front,
(uint) depthStencilState.StencilWriteMask
);
if (!depthStencilState.TwoSidedStencilMode)
{
back = front;
}
}
if (front != back)
{
back = mtlNewStencilDescriptor();
mtlSetStencilFailureOperation(
back,
XNAToMTL.StencilOp[(int) depthStencilState.CounterClockwiseStencilFail]
);
mtlSetDepthFailureOperation(
back,
XNAToMTL.StencilOp[(int) depthStencilState.CounterClockwiseStencilDepthBufferFail]
);
mtlSetDepthStencilPassOperation(
back,
XNAToMTL.StencilOp[(int) depthStencilState.CounterClockwiseStencilPass]
);
mtlSetStencilCompareFunction(
back,
XNAToMTL.CompareFunc[(int) depthStencilState.CounterClockwiseStencilFunction]
);
mtlSetStencilReadMask(
back,
(uint) depthStencilState.StencilMask
);
mtlSetStencilWriteMask(
back,
(uint) depthStencilState.StencilWriteMask
);
}
mtlSetFrontFaceStencil(
dsDesc,
front
);
mtlSetBackFaceStencil(
dsDesc,
back
);
// Bake the state!
state = mtlNewDepthStencilStateWithDescriptor(
device,
dsDesc
);
DepthStencilStateCache[hash] = state;
// Clean up
objc_release(dsDesc);
// Return the state!
return state;
}
private IntPtr FetchSamplerState(SamplerState samplerState, bool hasMipmaps)
{
// Can we just reuse an existing state?
StateHash hash = PipelineCache.GetSamplerHash(samplerState);
IntPtr state = IntPtr.Zero;
if (SamplerStateCache.TryGetValue(hash, out state))
{
// The value is already cached!
return state;
}
// We have to make a new sampler state...
IntPtr samplerDesc = mtlNewSamplerDescriptor();
mtlSetSampler_sAddressMode(
samplerDesc,
XNAToMTL.Wrap[(int) samplerState.AddressU]
);
mtlSetSampler_tAddressMode(
samplerDesc,
XNAToMTL.Wrap[(int) samplerState.AddressV]
);
mtlSetSampler_rAddressMode(
samplerDesc,
XNAToMTL.Wrap[(int) samplerState.AddressW]
);
mtlSetSamplerMagFilter(
samplerDesc,
XNAToMTL.MagFilter[(int) samplerState.Filter]
);
mtlSetSamplerMinFilter(
samplerDesc,
XNAToMTL.MinFilter[(int) samplerState.Filter]
);
if (hasMipmaps)
{
mtlSetSamplerMipFilter(
samplerDesc,
XNAToMTL.MipFilter[(int) samplerState.Filter]
);
}
mtlSetSamplerLodMinClamp(
samplerDesc,
samplerState.MaxMipLevel
);
mtlSetSamplerMaxAnisotropy(
samplerDesc,
(samplerState.Filter == TextureFilter.Anisotropic) ?
Math.Max(1, samplerState.MaxAnisotropy) :
1
);
/* FIXME:
* The only way to set lod bias in metal is via the MSL
* bias() function in a shader. So we can't do:
*
* mtlSetSamplerLodBias(
* samplerDesc,
* samplerState.MipMapLevelOfDetailBias
* );
*
* What should we do instead?
*
* -caleb
*/
// Bake the sampler state!
state = mtlNewSamplerStateWithDescriptor(
device,
samplerDesc
);
SamplerStateCache[hash] = state;
// Clean up
objc_release(samplerDesc);
// Return the sampler state!
return state;
}
private IntPtr FetchVertexDescriptor(
VertexBufferBinding[] bindings,
int numBindings
) {
// Can we just reuse an existing descriptor?
ulong hash = PipelineCache.GetVertexBindingHash(
bindings,
numBindings,
(ulong) shaderState.vertexShader
);
IntPtr descriptor;
if (VertexDescriptorCache.TryGetValue(hash, out descriptor))
{
// The value is already cached!
return descriptor;
}
// We have to make a new vertex descriptor...
descriptor = mtlMakeVertexDescriptor();
objc_retain(descriptor);
/* There's this weird case where you can have overlapping
* vertex usage/index combinations. It seems like the first
* attrib gets priority, so whenever a duplicate attribute
* exists, give it the next available index. If that fails, we
* have to crash :/
* -flibit
*/
Array.Clear(attrUse, 0, attrUse.Length);
for (int i = 0; i < numBindings; i += 1)
{
// Describe vertex attributes
VertexDeclaration vertexDeclaration = bindings[i].VertexBuffer.VertexDeclaration;
foreach (VertexElement element in vertexDeclaration.elements)
{
int usage = (int) element.VertexElementUsage;
int index = element.UsageIndex;
if (attrUse[usage, index])
{
index = -1;
for (int j = 0; j < 16; j += 1)
{
if (!attrUse[usage, j])
{
index = j;
break;
}
}
if (index < 0)
{
throw new InvalidOperationException("Vertex usage collision!");
}
}
attrUse[usage, index] = true;
int attribLoc = MojoShader.MOJOSHADER_mtlGetVertexAttribLocation(
shaderState.vertexShader,
XNAToMTL.VertexAttribUsage[usage],
index
);
if (attribLoc == -1)
{
// Stream not in use!
continue;
}
IntPtr attrib = mtlGetVertexAttributeDescriptor(
descriptor,
attribLoc
);
mtlSetVertexAttributeFormat(
attrib,
XNAToMTL.VertexAttribType[(int) element.VertexElementFormat]
);
mtlSetVertexAttributeOffset(
attrib,
element.Offset
);
mtlSetVertexAttributeBufferIndex(
attrib,
i
);
}
// Describe vertex buffer layout
IntPtr layout = mtlGetVertexBufferLayoutDescriptor(
descriptor,
i
);
mtlSetVertexBufferLayoutStride(
layout,
vertexDeclaration.VertexStride
);
if (bindings[i].InstanceFrequency > 0)
{
mtlSetVertexBufferLayoutStepFunction(
layout,
MTLVertexStepFunction.PerInstance
);
mtlSetVertexBufferLayoutStepRate(
layout,
bindings[i].InstanceFrequency
);
}
}
VertexDescriptorCache[hash] = descriptor;
return descriptor;
}
private IntPtr FetchVertexDescriptor(
VertexDeclaration vertexDeclaration,
int vertexOffset
) {
// Can we just reuse an existing descriptor?
ulong hash = PipelineCache.GetVertexDeclarationHash(
vertexDeclaration,
(ulong) shaderState.vertexShader
);
IntPtr descriptor;
if (VertexDescriptorCache.TryGetValue(hash, out descriptor))
{
// The value is already cached!
return descriptor;
}
// We have to make a new vertex descriptor...
descriptor = mtlMakeVertexDescriptor();
objc_retain(descriptor);
/* There's this weird case where you can have overlapping
* vertex usage/index combinations. It seems like the first
* attrib gets priority, so whenever a duplicate attribute
* exists, give it the next available index. If that fails, we
* have to crash :/
* -flibit
*/
Array.Clear(attrUse, 0, attrUse.Length);
foreach (VertexElement element in vertexDeclaration.elements)
{
int usage = (int) element.VertexElementUsage;
int index = element.UsageIndex;
if (attrUse[usage, index])
{
index = -1;
for (int j = 0; j < 16; j += 1)
{
if (!attrUse[usage, j])
{
index = j;
break;
}
}
if (index < 0)
{
throw new InvalidOperationException("Vertex usage collision!");
}
}
attrUse[usage, index] = true;
int attribLoc = MojoShader.MOJOSHADER_mtlGetVertexAttribLocation(
shaderState.vertexShader,
XNAToMTL.VertexAttribUsage[usage],
index
);
if (attribLoc == -1)
{
// Stream not in use!
continue;
}
IntPtr attrib = mtlGetVertexAttributeDescriptor(
descriptor,
attribLoc
);
mtlSetVertexAttributeFormat(
attrib,
XNAToMTL.VertexAttribType[(int) element.VertexElementFormat]
);
mtlSetVertexAttributeOffset(
attrib,
element.Offset
);
mtlSetVertexAttributeBufferIndex(
attrib,
0
);
}
// Describe vertex buffer layout
IntPtr layout = mtlGetVertexBufferLayoutDescriptor(
descriptor,
0
);
mtlSetVertexBufferLayoutStride(
layout,
vertexDeclaration.VertexStride
);
VertexDescriptorCache[hash] = descriptor;
return descriptor;
}
private IntPtr FetchTransientTexture(MetalTexture fromTexture)
{
// Can we just reuse an existing texture?
for (int i = 0; i < transientTextures.Count; i += 1)
{
MetalTexture tex = transientTextures[i];
if ( tex.Format == fromTexture.Format &&
tex.Width == fromTexture.Width &&
tex.Height == fromTexture.Height &&
tex.HasMipmaps == fromTexture.HasMipmaps )
{
mtlSetPurgeableState(
tex.Handle,
MTLPurgeableState.NonVolatile
);
return tex.Handle;
}
}
// We have to make a new texture...
IntPtr texDesc = mtlMakeTexture2DDescriptor(
XNAToMTL.TextureFormat[(int) fromTexture.Format],
fromTexture.Width,
fromTexture.Height,
fromTexture.HasMipmaps
);
MetalTexture ret = new MetalTexture(
mtlNewTextureWithDescriptor(device, texDesc),
fromTexture.Width,
fromTexture.Height,
fromTexture.Format,
fromTexture.HasMipmaps ? 2 : 0,
false
);
transientTextures.Add(ret);
return ret.Handle;
}
#endregion
#region DepthFormat Conversion Method
private MTLPixelFormat GetDepthFormat(DepthFormat format)
{
switch (format)
{
case DepthFormat.Depth16: return D16Format;
case DepthFormat.Depth24: return D24Format;
case DepthFormat.Depth24Stencil8: return D24S8Format;
default: return MTLPixelFormat.Invalid;
}
}
#endregion
#region Effect Methods
public IGLEffect CreateEffect(byte[] effectCode)
{
IntPtr effect = IntPtr.Zero;
IntPtr mtlEffect = IntPtr.Zero;
effect = MojoShader.MOJOSHADER_parseEffect(
"metal",
effectCode,
(uint) effectCode.Length,
null,
0,
null,
0,
null,
null,
IntPtr.Zero
);
#if DEBUG
unsafe
{
MojoShader.MOJOSHADER_effect *effectPtr = (MojoShader.MOJOSHADER_effect*) effect;
MojoShader.MOJOSHADER_error* err = (MojoShader.MOJOSHADER_error*) effectPtr->errors;
for (int i = 0; i < effectPtr->error_count; i += 1)
{
// From the SDL2# LPToUtf8StringMarshaler
byte* endPtr = (byte*) err[i].error;
while (*endPtr != 0)
{
endPtr++;
}
byte[] bytes = new byte[endPtr - (byte*) err[i].error];
Marshal.Copy(err[i].error, bytes, 0, bytes.Length);
FNALoggerEXT.LogError(
"MOJOSHADER_parseEffect Error: " +
System.Text.Encoding.UTF8.GetString(bytes)
);
}
}
#endif
mtlEffect = MojoShader.MOJOSHADER_mtlCompileEffect(
effect,
device,
MAX_FRAMES_IN_FLIGHT
);
if (mtlEffect == IntPtr.Zero)
{
throw new InvalidOperationException(
MojoShader.MOJOSHADER_mtlGetError()
);
}
return new MetalEffect(effect, mtlEffect);
}
private void DeleteEffect(IGLEffect effect)
{
IntPtr mtlEffectData = (effect as MetalEffect).MTLEffectData;
if (mtlEffectData == currentEffect)
{
MojoShader.MOJOSHADER_mtlEffectEndPass(currentEffect);
MojoShader.MOJOSHADER_mtlEffectEnd(
currentEffect,
ref shaderState
);
currentEffect = IntPtr.Zero;
currentTechnique = IntPtr.Zero;
currentPass = 0;
shaderState = new MojoShader.MOJOSHADER_mtlShaderState();
}
MojoShader.MOJOSHADER_mtlDeleteEffect(mtlEffectData);
MojoShader.MOJOSHADER_freeEffect(effect.EffectData);
}
public IGLEffect CloneEffect(IGLEffect cloneSource)
{
IntPtr effect = IntPtr.Zero;
IntPtr mtlEffect = IntPtr.Zero;
effect = MojoShader.MOJOSHADER_cloneEffect(cloneSource.EffectData);
mtlEffect = MojoShader.MOJOSHADER_mtlCompileEffect(
effect,
device,
1
);
if (mtlEffect == IntPtr.Zero)
{
throw new InvalidOperationException(
MojoShader.MOJOSHADER_mtlGetError()
);
}
return new MetalEffect(effect, mtlEffect);
}
public void ApplyEffect(
IGLEffect effect,
IntPtr technique,
uint pass,
IntPtr stateChanges
) {
/* If a frame isn't already in progress,
* wait until one begins to avoid overwriting
* the previous frame's uniform buffers.
*/
BeginFrame();
IntPtr mtlEffectData = (effect as MetalEffect).MTLEffectData;
if (mtlEffectData == currentEffect)
{
if (technique == currentTechnique && pass == currentPass)
{
MojoShader.MOJOSHADER_mtlEffectCommitChanges(
currentEffect,
ref shaderState
);
return;
}
MojoShader.MOJOSHADER_mtlEffectEndPass(currentEffect);
MojoShader.MOJOSHADER_mtlEffectBeginPass(
currentEffect,
pass,
ref shaderState
);
currentTechnique = technique;
currentPass = pass;
return;
}
else if (currentEffect != IntPtr.Zero)
{
MojoShader.MOJOSHADER_mtlEffectEndPass(currentEffect);
MojoShader.MOJOSHADER_mtlEffectEnd(
currentEffect,
ref shaderState
);
}
uint whatever;
MojoShader.MOJOSHADER_mtlEffectBegin(
mtlEffectData,
out whatever,
0,
stateChanges
);
MojoShader.MOJOSHADER_mtlEffectBeginPass(
mtlEffectData,
pass,
ref shaderState
);
currentEffect = mtlEffectData;
currentTechnique = technique;
currentPass = pass;
}
public void BeginPassRestore(IGLEffect effect, IntPtr stateChanges)
{
/* If a frame isn't already in progress,
* wait until one begins to avoid overwriting
* the previous frame's uniform buffers.
*/
BeginFrame();
// Store the current data
prevEffect = currentEffect;
prevShaderState = shaderState;
IntPtr mtlEffectData = (effect as MetalEffect).MTLEffectData;
uint whatever;
MojoShader.MOJOSHADER_mtlEffectBegin(
mtlEffectData,
out whatever,
1,
stateChanges
);
MojoShader.MOJOSHADER_mtlEffectBeginPass(
mtlEffectData,
0,
ref shaderState
);
currentEffect = mtlEffectData;
}
public void EndPassRestore(IGLEffect effect)
{
IntPtr mtlEffectData = (effect as MetalEffect).MTLEffectData;
MojoShader.MOJOSHADER_mtlEffectEndPass(mtlEffectData);
MojoShader.MOJOSHADER_mtlEffectEnd(
mtlEffectData,
ref shaderState
);
// Restore the old data
shaderState = prevShaderState;
currentEffect = prevEffect;
}
#endregion
#region Resource Binding Method
private void BindResources()
{
// Bind textures and their sampler states
for (int i = 0; i < Textures.Length; i += 1)
{
if (textureNeedsUpdate[i])
{
mtlSetFragmentTexture(
renderCommandEncoder,
Textures[i].Handle,
i
);
textureNeedsUpdate[i] = false;
}
if (samplerNeedsUpdate[i])
{
mtlSetFragmentSamplerState(
renderCommandEncoder,
Samplers[i],
i
);
samplerNeedsUpdate[i] = false;
}
}
// Bind the uniform buffers
const int UNIFORM_REG = 16; // In MojoShader output it's always 16
IntPtr vUniform = shaderState.vertexUniformBuffer;
int vOff = shaderState.vertexUniformOffset;
if (vUniform != ldVertUniformBuffer)
{
mtlSetVertexBuffer(
renderCommandEncoder,
vUniform,
vOff,
UNIFORM_REG
);
ldVertUniformBuffer = vUniform;
ldVertUniformOffset = vOff;
}
else if (vOff != ldVertUniformOffset)
{
mtlSetVertexBufferOffset(
renderCommandEncoder,
vOff,
UNIFORM_REG
);
ldVertUniformOffset = vOff;
}
IntPtr fUniform = shaderState.fragmentUniformBuffer;
int fOff = shaderState.fragmentUniformOffset;
if (fUniform != ldFragUniformBuffer)
{
mtlSetFragmentBuffer(
renderCommandEncoder,
fUniform,
fOff,
UNIFORM_REG
);
ldFragUniformBuffer = fUniform;
ldFragUniformOffset = fOff;
}
else if (fOff != ldFragUniformOffset)
{
mtlSetFragmentBufferOffset(
renderCommandEncoder,
fOff,
UNIFORM_REG
);
ldFragUniformOffset = fOff;
}
// Bind the depth-stencil state
IntPtr depthStencilState = FetchDepthStencilState();
if (depthStencilState != ldDepthStencilState)
{
mtlSetDepthStencilState(
renderCommandEncoder,
depthStencilState
);
ldDepthStencilState = depthStencilState;
}
// Finally, bind the pipeline state
IntPtr pipelineState = FetchRenderPipeline();
if (pipelineState != ldPipelineState)
{
mtlSetRenderPipelineState(
renderCommandEncoder,
pipelineState
);
ldPipelineState = pipelineState;
}
}
#endregion
#region ApplyVertexAttributes Methods
public void ApplyVertexAttributes(
VertexBufferBinding[] bindings,
int numBindings,
bool bindingsUpdated,
int baseVertex
) {
// Translate the bindings array into a descriptor
currentVertexDescriptor = FetchVertexDescriptor(
bindings,
numBindings
);
// Prepare for rendering
UpdateRenderPass();
BindResources();
// Bind the vertex buffers
for (int i = 0; i < bindings.Length; i += 1)
{
VertexBuffer vertexBuffer = bindings[i].VertexBuffer;
if (vertexBuffer != null)
{
int stride = bindings[i].VertexBuffer.VertexDeclaration.VertexStride;
int offset = (
((bindings[i].VertexOffset + baseVertex) * stride) +
(vertexBuffer.buffer as MetalBuffer).InternalOffset
);
IntPtr handle = (vertexBuffer.buffer as MetalBuffer).Handle;
(vertexBuffer.buffer as MetalBuffer).Bound();
if (ldVertexBuffers[i] != handle)
{
mtlSetVertexBuffer(
renderCommandEncoder,
handle,
offset,
i
);
ldVertexBuffers[i] = handle;
ldVertexBufferOffsets[i] = offset;
}
else if (ldVertexBufferOffsets[i] != offset)
{
mtlSetVertexBufferOffset(
renderCommandEncoder,
offset,
i
);
ldVertexBufferOffsets[i] = offset;
}
}
}
}
public void ApplyVertexAttributes(
VertexDeclaration vertexDeclaration,
IntPtr ptr,
int vertexOffset
) {
// Translate the declaration into a descriptor
currentVertexDescriptor = FetchVertexDescriptor(
vertexDeclaration,
vertexOffset
);
userVertexStride = vertexDeclaration.VertexStride;
// Prepare for rendering
UpdateRenderPass();
BindResources();
// The rest happens in DrawUser[Indexed]Primitives.
}
#endregion
#region GenBuffers Methods
public IGLBuffer GenIndexBuffer(
bool dynamic,
BufferUsage usage,
int indexCount,
IndexElementSize indexElementSize
) {
int elementSize = XNAToMTL.IndexSize[(int) indexElementSize];
MetalBuffer newbuf = new MetalBuffer(
this,
dynamic,
usage,
(IntPtr) (indexCount * elementSize)
);
Buffers.Add(newbuf);
return newbuf;
}
public IGLBuffer GenVertexBuffer(
bool dynamic,
BufferUsage usage,
int vertexCount,
int vertexStride
) {
MetalBuffer newbuf = new MetalBuffer(
this,
dynamic,
usage,
(IntPtr) (vertexCount * vertexStride)
);
Buffers.Add(newbuf);
return newbuf;
}
#endregion
#region Renderbuffer Methods
public IGLRenderbuffer GenRenderbuffer(
int width,
int height,
SurfaceFormat format,
int multiSampleCount,
IGLTexture texture
) {
MTLPixelFormat pixelFormat = XNAToMTL.TextureFormat[(int) format];
int sampleCount = GetCompatibleSampleCount(multiSampleCount);
// Generate a multisample texture
IntPtr desc = mtlMakeTexture2DDescriptor(
pixelFormat,
width,
height,
false
);
mtlSetStorageMode(
desc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
desc,
MTLTextureUsage.RenderTarget
);
mtlSetTextureType(
desc,
MTLTextureType.Multisample2D
);
mtlSetTextureSampleCount(
desc,
sampleCount
);
IntPtr multisampleTexture = mtlNewTextureWithDescriptor(
device,
desc
);
// We're done!
return new MetalRenderbuffer(
(texture as MetalTexture).Handle,
pixelFormat,
sampleCount,
multisampleTexture
);
}
public IGLRenderbuffer GenRenderbuffer(
int width,
int height,
DepthFormat format,
int multiSampleCount
) {
MTLPixelFormat pixelFormat = GetDepthFormat(format);
int sampleCount = GetCompatibleSampleCount(multiSampleCount);
// Generate a depth texture
IntPtr desc = mtlMakeTexture2DDescriptor(
pixelFormat,
width,
height,
false
);
mtlSetStorageMode(
desc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
desc,
MTLTextureUsage.RenderTarget
);
if (multiSampleCount > 0)
{
mtlSetTextureType(
desc,
MTLTextureType.Multisample2D
);
mtlSetTextureSampleCount(
desc,
sampleCount
);
}
IntPtr handle = mtlNewTextureWithDescriptor(
device,
desc
);
// We're done!
return new MetalRenderbuffer(
handle,
pixelFormat,
sampleCount,
IntPtr.Zero
);
}
private void DeleteRenderbuffer(IGLRenderbuffer renderbuffer)
{
MetalRenderbuffer rb = renderbuffer as MetalRenderbuffer;
bool isDepthStencil = rb.MultiSampleHandle == IntPtr.Zero;
if (isDepthStencil)
{
if (rb.Handle == currentDepthStencilBuffer)
{
currentDepthStencilBuffer = IntPtr.Zero;
}
}
else
{
for (int i = 0; i < currentAttachments.Length; i += 1)
{
if (rb.MultiSampleHandle == currentMSAttachments[i])
{
currentMSAttachments[i] = IntPtr.Zero;
}
}
}
rb.Dispose();
}
#endregion
#region SetBufferData Methods
public void SetIndexBufferData(
IGLBuffer buffer,
int offsetInBytes,
IntPtr data,
int dataLength,
SetDataOptions options
) {
(buffer as MetalBuffer).SetData(
offsetInBytes,
data,
dataLength,
options
);
}
public void SetVertexBufferData(
IGLBuffer buffer,
int offsetInBytes,
IntPtr data,
int dataLength,
SetDataOptions options
) {
(buffer as MetalBuffer).SetData(
offsetInBytes,
data,
dataLength,
options
);
}
#endregion
#region GetBufferData Methods
public void GetIndexBufferData(
IGLBuffer buffer,
int offsetInBytes,
IntPtr data,
int startIndex,
int elementCount,
int elementSizeInBytes
) {
SDL.SDL_memcpy(
data + (startIndex * elementSizeInBytes),
(buffer as MetalBuffer).Contents + offsetInBytes,
(IntPtr) (elementCount * elementSizeInBytes)
);
}
public void GetVertexBufferData(
IGLBuffer buffer,
int offsetInBytes,
IntPtr data,
int startIndex,
int elementCount,
int elementSizeInBytes,
int vertexStride
) {
IntPtr cpy;
bool useStagingBuffer = elementSizeInBytes < vertexStride;
if (useStagingBuffer)
{
cpy = Marshal.AllocHGlobal(elementCount * vertexStride);
}
else
{
cpy = data + (startIndex * elementSizeInBytes);
}
SDL.SDL_memcpy(
cpy,
(buffer as MetalBuffer).Contents + offsetInBytes,
(IntPtr) (elementCount * vertexStride)
);
if (useStagingBuffer)
{
IntPtr src = cpy;
IntPtr dst = data + (startIndex * elementSizeInBytes);
for (int i = 0; i < elementCount; i += 1)
{
SDL.SDL_memcpy(dst, src, (IntPtr) elementSizeInBytes);
dst += elementSizeInBytes;
src += vertexStride;
}
Marshal.FreeHGlobal(cpy);
}
}
#endregion
#region DeleteBuffer Methods
private void DeleteBuffer(IGLBuffer buffer)
{
Buffers.Remove(buffer as MetalBuffer);
(buffer as MetalBuffer).Dispose();
}
#endregion
#region CreateTexture Methods
public IGLTexture CreateTexture2D(
SurfaceFormat format,
int width,
int height,
int levelCount,
bool isRenderTarget
) {
IntPtr texDesc = mtlMakeTexture2DDescriptor(
XNAToMTL.TextureFormat[(int) format],
width,
height,
levelCount > 1
);
if (isRenderTarget)
{
mtlSetStorageMode(
texDesc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
texDesc,
MTLTextureUsage.RenderTarget | MTLTextureUsage.ShaderRead
);
}
return new MetalTexture(
mtlNewTextureWithDescriptor(device, texDesc),
width,
height,
format,
levelCount,
isRenderTarget
);
}
public IGLTexture CreateTexture3D(
SurfaceFormat format,
int width,
int height,
int depth,
int levelCount
) {
IntPtr texDesc = mtlMakeTexture2DDescriptor(
XNAToMTL.TextureFormat[(int) format],
width,
height,
levelCount > 1
);
// Make it 3D!
mtlSetTextureDepth(texDesc, depth);
mtlSetTextureType(texDesc, MTLTextureType.Texture3D);
return new MetalTexture(
mtlNewTextureWithDescriptor(device, texDesc),
width,
height,
format,
levelCount,
false
);
}
public IGLTexture CreateTextureCube(
SurfaceFormat format,
int size,
int levelCount,
bool isRenderTarget
) {
IntPtr texDesc = mtlMakeTextureCubeDescriptor(
XNAToMTL.TextureFormat[(int) format],
size,
levelCount > 1
);
if (isRenderTarget)
{
mtlSetStorageMode(
texDesc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
texDesc,
MTLTextureUsage.RenderTarget | MTLTextureUsage.ShaderRead
);
}
return new MetalTexture(
mtlNewTextureWithDescriptor(device, texDesc),
size,
size,
format,
levelCount,
isRenderTarget
);
}
#endregion
#region DeleteTexture Method
private void DeleteTexture(IGLTexture texture)
{
MetalTexture tex = texture as MetalTexture;
for (int i = 0; i < currentAttachments.Length; i += 1)
{
if (tex.Handle == currentAttachments[i])
{
currentAttachments[i] = IntPtr.Zero;
}
}
for (int i = 0; i < Textures.Length; i += 1)
{
if (tex.Handle == Textures[i].Handle)
{
Textures[i] = MetalTexture.NullTexture;
textureNeedsUpdate[i] = true;
}
}
tex.Dispose();
}
#endregion
#region Texture Data Helper Methods
private int BytesPerRow(int width, SurfaceFormat format)
{
int blocksPerRow = width;
if ( format == SurfaceFormat.Dxt1 ||
format == SurfaceFormat.Dxt3 ||
format == SurfaceFormat.Dxt5 )
{
blocksPerRow = (width + 3) / 4;
}
return blocksPerRow * Texture.GetFormatSize(format);
}
private int BytesPerImage(int width, int height, SurfaceFormat format)
{
int blocksPerRow = width;
int blocksPerColumn = height;
int formatSize = Texture.GetFormatSize(format);
if ( format == SurfaceFormat.Dxt1 ||
format == SurfaceFormat.Dxt3 ||
format == SurfaceFormat.Dxt5 )
{
blocksPerRow = (width + 3) / 4;
blocksPerColumn = (height + 3) / 4;
}
return blocksPerRow * blocksPerColumn * formatSize;
}
private int GetCompatibleSampleCount(int sampleCount)
{
/* If the device does not support the requested
* multisample count, halve it until we find a
* value that is supported.
*/
while (sampleCount > 0 && !mtlSupportsSampleCount(device, sampleCount))
{
sampleCount = MathHelper.ClosestMSAAPower(
sampleCount / 2
);
}
return sampleCount;
}
#endregion
#region SetTextureData Methods
public void SetTextureData2D(
IGLTexture texture,
SurfaceFormat format,
int x,
int y,
int w,
int h,
int level,
IntPtr data,
int dataLength
) {
MetalTexture tex = texture as MetalTexture;
IntPtr handle = tex.Handle;
MTLOrigin origin = new MTLOrigin(x, y, 0);
MTLSize size = new MTLSize(w, h, 1);
if (tex.IsPrivate)
{
// We need an active command buffer
BeginFrame();
// Fetch a CPU-accessible texture
handle = FetchTransientTexture(tex);
}
// Write the data
mtlReplaceRegion(
handle,
new MTLRegion(origin, size),
level,
0,
data,
BytesPerRow(w, format),
0
);
// Blit the temp texture to the actual texture
if (tex.IsPrivate)
{
// End the render pass
EndPass();
// Blit!
IntPtr blit = mtlMakeBlitCommandEncoder(commandBuffer);
mtlBlitTextureToTexture(
blit,
handle,
0,
level,
origin,
size,
tex.Handle,
0,
level,
origin
);
// Submit the blit command to the GPU and wait...
mtlEndEncoding(blit);
Stall();
// We're done with the temp texture
mtlSetPurgeableState(
handle,
MTLPurgeableState.Empty
);
}
}
public void SetTextureDataYUV(Texture2D[] textures, IntPtr ptr)
{
for (int i = 0; i < 3; i += 1)
{
Texture2D tex = textures[i];
MTLRegion region = new MTLRegion(
MTLOrigin.Zero,
new MTLSize(tex.Width, tex.Height, 1)
);
mtlReplaceRegion(
(tex.texture as MetalTexture).Handle,
region,
0,
0,
ptr,
tex.Width,
0
);
ptr += tex.Width * tex.Height;
}
}
public void SetTextureData3D(
IGLTexture texture,
SurfaceFormat format,
int level,
int left,
int top,
int right,
int bottom,
int front,
int back,
IntPtr data,
int dataLength
) {
int w = right - left;
int h = bottom - top;
int d = back - front;
MTLRegion region = new MTLRegion(
new MTLOrigin(left, top, front),
new MTLSize(w, h, d)
);
mtlReplaceRegion(
(texture as MetalTexture).Handle,
region,
level,
0,
data,
BytesPerRow(w, format),
BytesPerImage(w, h, format)
);
}
public void SetTextureDataCube(
IGLTexture texture,
SurfaceFormat format,
int xOffset,
int yOffset,
int width,
int height,
CubeMapFace cubeMapFace,
int level,
IntPtr data,
int dataLength
) {
MetalTexture tex = texture as MetalTexture;
IntPtr handle = tex.Handle;
MTLOrigin origin = new MTLOrigin(xOffset, yOffset, 0);
MTLSize size = new MTLSize(width, height, 1);
int slice = (int) cubeMapFace;
if (tex.IsPrivate)
{
// We need an active command buffer
BeginFrame();
// Fetch a CPU-accessible texture
handle = FetchTransientTexture(tex);
// Transient textures have no slices
slice = 0;
}
// Write the data
mtlReplaceRegion(
handle,
new MTLRegion(origin, size),
level,
slice,
data,
BytesPerRow(width, format),
0
);
// Blit the temp texture to the actual texture
if (tex.IsPrivate)
{
// End the render pass
EndPass();
// Blit!
IntPtr blit = mtlMakeBlitCommandEncoder(commandBuffer);
mtlBlitTextureToTexture(
blit,
handle,
slice,
level,
origin,
size,
tex.Handle,
slice,
level,
origin
);
// Submit the blit command to the GPU and wait...
mtlEndEncoding(blit);
Stall();
// We're done with the temp texture
mtlSetPurgeableState(
handle,
MTLPurgeableState.Empty
);
}
}
#endregion
#region GetTextureData Methods
public void GetTextureData2D(
IGLTexture texture,
SurfaceFormat format,
int width,
int height,
int level,
int subX,
int subY,
int subW,
int subH,
IntPtr data,
int startIndex,
int elementCount,
int elementSizeInBytes
) {
MetalTexture tex = texture as MetalTexture;
IntPtr handle = tex.Handle;
MTLSize size = new MTLSize(subW, subH, 1);
MTLOrigin origin = new MTLOrigin(subX, subY, 0);
if (tex.IsPrivate)
{
// We need an active command buffer
BeginFrame();
// Fetch a CPU-accessible texture
handle = FetchTransientTexture(tex);
// End the render pass
EndPass();
// Blit the actual texture to a CPU-accessible texture
IntPtr blit = mtlMakeBlitCommandEncoder(commandBuffer);
mtlBlitTextureToTexture(
blit,
tex.Handle,
0,
level,
origin,
size,
handle,
0,
level,
origin
);
// Managed resources require explicit synchronization
if (isMac)
{
mtlSynchronizeResource(blit, handle);
}
// Submit the blit command to the GPU and wait...
mtlEndEncoding(blit);
Stall();
}
mtlGetTextureBytes(
handle,
data,
BytesPerRow(subW, format),
0,
new MTLRegion(origin, size),
level,
0
);
if (tex.IsPrivate)
{
// We're done with the temp texture
mtlSetPurgeableState(
handle,
MTLPurgeableState.Empty
);
}
}
public void GetTextureData3D(
IGLTexture texture,
SurfaceFormat format,
int left,
int top,
int front,
int right,
int bottom,
int back,
int level,
IntPtr data,
int startIndex,
int elementCount,
int elementSizeInBytes
) {
int w = right - left;
int h = bottom - top;
int d = back - front;
MTLRegion region = new MTLRegion(
new MTLOrigin(left, top, front),
new MTLSize(w, h, d)
);
mtlGetTextureBytes(
(texture as MetalTexture).Handle,
data,
BytesPerRow(w, format),
BytesPerImage(w, h, format),
region,
level,
0
);
}
public void GetTextureDataCube(
IGLTexture texture,
SurfaceFormat format,
int size,
CubeMapFace cubeMapFace,
int level,
int subX,
int subY,
int subW,
int subH,
IntPtr data,
int startIndex,
int elementCount,
int elementSizeInBytes
) {
MetalTexture tex = texture as MetalTexture;
IntPtr handle = tex.Handle;
MTLSize regionSize = new MTLSize(subW, subH, 1);
MTLOrigin origin = new MTLOrigin(subX, subY, 0);
int slice = (int) cubeMapFace;
if (tex.IsPrivate)
{
// We need an active command buffer
BeginFrame();
// Fetch a CPU-accessible texture
handle = FetchTransientTexture(tex);
// Transient textures have no slices
slice = 0;
// End the render pass
EndPass();
// Blit the actual texture to a CPU-accessible texture
IntPtr blit = mtlMakeBlitCommandEncoder(commandBuffer);
mtlBlitTextureToTexture(
blit,
tex.Handle,
(int) cubeMapFace,
level,
origin,
regionSize,
handle,
slice,
level,
origin
);
// Managed resources require explicit synchronization
if (isMac)
{
mtlSynchronizeResource(blit, handle);
}
// Submit the blit command to the GPU and wait...
mtlEndEncoding(blit);
Stall();
}
mtlGetTextureBytes(
handle,
data,
BytesPerRow(subW, format),
0,
new MTLRegion(origin, regionSize),
level,
slice
);
if (tex.IsPrivate)
{
// We're done with the temp texture
mtlSetPurgeableState(
handle,
MTLPurgeableState.Empty
);
}
}
#endregion
#region ReadBackbuffer Method
public void ReadBackbuffer(
IntPtr data,
int dataLen,
int startIndex,
int elementCount,
int elementSizeInBytes,
int subX,
int subY,
int subW,
int subH
) {
/* FIXME: Right now we're expecting one of the following:
* - byte[]
* - int[]
* - uint[]
* - Color[]
* Anything else will freak out because we're using
* color backbuffers. Maybe check this out when adding
* support for more backbuffer types!
* -flibit
*/
if (startIndex > 0 || elementCount != (dataLen / elementSizeInBytes))
{
throw new NotImplementedException(
"ReadBackbuffer startIndex/elementCount"
);
}
GetTextureData2D(
(Backbuffer as MetalBackbuffer).Texture,
SurfaceFormat.Color,
Backbuffer.Width,
Backbuffer.Height,
0,
subX,
subY,
subW,
subH,
data,
0,
dataLen,
1
);
}
#endregion
#region RenderTarget->Texture Method
public void ResolveTarget(RenderTargetBinding target)
{
// The target is resolved at the end of each render pass.
// If the target has mipmaps, regenerate them now
if (target.RenderTarget.LevelCount > 1)
{
EndPass();
IntPtr blit = mtlMakeBlitCommandEncoder(commandBuffer);
mtlGenerateMipmapsForTexture(
blit,
(target.RenderTarget.texture as MetalTexture).Handle
);
mtlEndEncoding(blit);
needNewRenderPass = true;
}
}
#endregion
#region Clear Method
public void Clear(
ClearOptions options,
Vector4 color,
float depth,
int stencil
) {
bool clearTarget = (options & ClearOptions.Target) == ClearOptions.Target;
bool clearDepth = (options & ClearOptions.DepthBuffer) == ClearOptions.DepthBuffer;
bool clearStencil = (options & ClearOptions.Stencil) == ClearOptions.Stencil;
if (clearTarget)
{
clearColor = color;
shouldClearColor = true;
}
if (clearDepth)
{
this.clearDepth = depth;
shouldClearDepth = true;
}
if (clearStencil)
{
this.clearStencil = stencil;
shouldClearStencil = true;
}
needNewRenderPass |= clearTarget | clearDepth | clearStencil;
}
#endregion
#region SetRenderTargets Methods
public void SetRenderTargets(
RenderTargetBinding[] renderTargets,
IGLRenderbuffer renderbuffer,
DepthFormat depthFormat
) {
// Perform any pending clears before switching render targets
if (shouldClearColor || shouldClearDepth || shouldClearStencil)
{
UpdateRenderPass();
}
// Force an update to the render pass
needNewRenderPass = true;
// Bind the correct framebuffer
ResetAttachments();
if (renderTargets == null)
{
BindBackbuffer();
return;
}
// Update color buffers
int i;
for (i = 0; i < renderTargets.Length; i += 1)
{
IRenderTarget rt = renderTargets[i].RenderTarget as IRenderTarget;
currentAttachmentSlices[i] = renderTargets[i].CubeMapFace;
if (rt.ColorBuffer != null)
{
MetalRenderbuffer rb = rt.ColorBuffer as MetalRenderbuffer;
currentAttachments[i] = rb.Handle;
currentColorFormats[i] = rb.PixelFormat;
currentSampleCount = rb.MultiSampleCount;
currentMSAttachments[i] = rb.MultiSampleHandle;
}
else
{
MetalTexture tex = renderTargets[i].RenderTarget.texture as MetalTexture;
currentAttachments[i] = tex.Handle;
currentColorFormats[i] = XNAToMTL.TextureFormat[(int) tex.Format];
currentSampleCount = 0;
}
}
// Update depth stencil buffer
IntPtr handle = IntPtr.Zero;
if (renderbuffer != null)
{
handle = (renderbuffer as MetalRenderbuffer).Handle;
}
currentDepthStencilBuffer = handle;
currentDepthFormat = (
(currentDepthStencilBuffer == IntPtr.Zero) ?
DepthFormat.None :
depthFormat
);
}
private void ResetAttachments()
{
for (int i = 0; i < currentAttachments.Length; i += 1)
{
currentAttachments[i] = IntPtr.Zero;
currentColorFormats[i] = MTLPixelFormat.Invalid;
currentMSAttachments[i] = IntPtr.Zero;
currentAttachmentSlices[i] = (CubeMapFace) 0;
}
currentDepthStencilBuffer = IntPtr.Zero;
currentDepthFormat = DepthFormat.None;
currentSampleCount = 0;
}
private void BindBackbuffer()
{
MetalBackbuffer bb = Backbuffer as MetalBackbuffer;
currentAttachments[0] = bb.ColorBuffer;
currentColorFormats[0] = bb.PixelFormat;
currentDepthStencilBuffer = bb.DepthStencilBuffer;
currentDepthFormat = bb.DepthFormat;
currentSampleCount = bb.MultiSampleCount;
currentMSAttachments[0] = bb.MultiSampleColorBuffer;
currentAttachmentSlices[0] = (CubeMapFace) 0;
}
#endregion
#region Query Object Methods
public IGLQuery CreateQuery()
{
if (!supportsOcclusionQueries)
{
throw new NotSupportedException(
"Occlusion queries are not supported on this device!"
);
}
IntPtr buf = mtlNewBufferWithLength(device, sizeof(ulong), 0);
return new MetalQuery(buf);
}
private void DeleteQuery(IGLQuery query)
{
(query as MetalQuery).Dispose();
}
public void QueryBegin(IGLQuery query)
{
// Stop the current pass
EndPass();
// Attach the visibility buffer to a new render pass
currentVisibilityBuffer = (query as MetalQuery).Handle;
needNewRenderPass = true;
}
public bool QueryComplete(IGLQuery query)
{
/* FIXME:
* There's no easy way to check for completion
* of the query. The only accurate way would be
* to monitor the completion of the command buffer
* associated with each query, but that gets tricky
* since we can't use completion callbacks.
* (Thank Objective-C and its stupid "block" lambdas.)
*
* Futhermore, I don't know how visibility queries
* work across command buffers, in the event of a
* stalled buffer overwrite or something similar.
*
* The below code is obviously wrong, but it happens
* to work for the Lens Flare XNA sample. Maybe it'll
* work for your game too?
*
* (Although if you're making a new game with FNA,
* you really shouldn't be using queries anyway...)
*
* -caleb
*/
return true;
}
public void QueryEnd(IGLQuery query)
{
if (renderCommandEncoder != IntPtr.Zero)
{
// Stop counting.
mtlSetVisibilityResultMode(
renderCommandEncoder,
MTLVisibilityResultMode.Disabled,
0
);
}
currentVisibilityBuffer = IntPtr.Zero;
}
public int QueryPixelCount(IGLQuery query)
{
IntPtr contents = mtlGetBufferContentsPtr(
(query as MetalQuery).Handle
);
ulong result;
unsafe
{
result = *((ulong *) contents);
}
return (int) result;
}
#endregion
#region XNA->GL Enum Conversion Class
private static class XNAToMTL
{
public static readonly MTLPixelFormat[] TextureFormat = new MTLPixelFormat[]
{
MTLPixelFormat.RGBA8Unorm, // SurfaceFormat.Color
MTLPixelFormat.B5G6R5Unorm, // SurfaceFormat.Bgr565
MTLPixelFormat.BGR5A1Unorm, // SurfaceFormat.Bgra5551
MTLPixelFormat.ABGR4Unorm, // SurfaceFormat.Bgra4444
MTLPixelFormat.BC1_RGBA, // SurfaceFormat.Dxt1
MTLPixelFormat.BC2_RGBA, // SurfaceFormat.Dxt3
MTLPixelFormat.BC3_RGBA, // SurfaceFormat.Dxt5
MTLPixelFormat.RG8Snorm, // SurfaceFormat.NormalizedByte2
MTLPixelFormat.RG16Snorm, // SurfaceFormat.NormalizedByte4
MTLPixelFormat.RGB10A2Unorm, // SurfaceFormat.Rgba1010102
MTLPixelFormat.RG16Unorm, // SurfaceFormat.Rg32
MTLPixelFormat.RGBA16Unorm, // SurfaceFormat.Rgba64
MTLPixelFormat.A8Unorm, // SurfaceFormat.Alpha8
MTLPixelFormat.R32Float, // SurfaceFormat.Single
MTLPixelFormat.RG32Float, // SurfaceFormat.Vector2
MTLPixelFormat.RGBA32Float, // SurfaceFormat.Vector4
MTLPixelFormat.R16Float, // SurfaceFormat.HalfSingle
MTLPixelFormat.RG16Float, // SurfaceFormat.HalfVector2
MTLPixelFormat.RGBA16Float, // SurfaceFormat.HalfVector4
MTLPixelFormat.RGBA16Float, // SurfaceFormat.HdrBlendable
MTLPixelFormat.BGRA8Unorm // SurfaceFormat.ColorBgraEXT
};
public static readonly MojoShader.MOJOSHADER_usage[] VertexAttribUsage = new MojoShader.MOJOSHADER_usage[]
{
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_POSITION, // VertexElementUsage.Position
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_COLOR, // VertexElementUsage.Color
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_TEXCOORD, // VertexElementUsage.TextureCoordinate
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_NORMAL, // VertexElementUsage.Normal
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_BINORMAL, // VertexElementUsage.Binormal
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_TANGENT, // VertexElementUsage.Tangent
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_BLENDINDICES, // VertexElementUsage.BlendIndices
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_BLENDWEIGHT, // VertexElementUsage.BlendWeight
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_DEPTH, // VertexElementUsage.Depth
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_FOG, // VertexElementUsage.Fog
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_POINTSIZE, // VertexElementUsage.PointSize
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_SAMPLE, // VertexElementUsage.Sample
MojoShader.MOJOSHADER_usage.MOJOSHADER_USAGE_TESSFACTOR // VertexElementUsage.TessellateFactor
};
public static readonly MTLVertexFormat[] VertexAttribType = new MTLVertexFormat[]
{
MTLVertexFormat.Float, // VertexElementFormat.Single
MTLVertexFormat.Float2, // VertexElementFormat.Vector2
MTLVertexFormat.Float3, // VertexElementFormat.Vector3
MTLVertexFormat.Float4, // VertexElementFormat.Vector4
MTLVertexFormat.UChar4Normalized, // VertexElementFormat.Color
MTLVertexFormat.UChar4, // VertexElementFormat.Byte4
MTLVertexFormat.Short2, // VertexElementFormat.Short2
MTLVertexFormat.Short4, // VertexElementFormat.Short4
MTLVertexFormat.Short2Normalized, // VertexElementFormat.NormalizedShort2
MTLVertexFormat.Short4Normalized, // VertexElementFormat.NormalizedShort4
MTLVertexFormat.Half2, // VertexElementFormat.HalfVector2
MTLVertexFormat.Half4 // VertexElementFormat.HalfVector4
};
public static readonly MTLIndexType[] IndexType = new MTLIndexType[]
{
MTLIndexType.UInt16, // IndexElementSize.SixteenBits
MTLIndexType.UInt32 // IndexElementSize.ThirtyTwoBits
};
public static readonly int[] IndexSize = new int[]
{
2, // IndexElementSize.SixteenBits
4 // IndexElementSize.ThirtyTwoBits
};
public static readonly MTLBlendFactor[] BlendMode = new MTLBlendFactor[]
{
MTLBlendFactor.One, // Blend.One
MTLBlendFactor.Zero, // Blend.Zero
MTLBlendFactor.SourceColor, // Blend.SourceColor
MTLBlendFactor.OneMinusSourceColor, // Blend.InverseSourceColor
MTLBlendFactor.SourceAlpha, // Blend.SourceAlpha
MTLBlendFactor.OneMinusSourceAlpha, // Blend.InverseSourceAlpha
MTLBlendFactor.DestinationColor, // Blend.DestinationColor
MTLBlendFactor.OneMinusDestinationColor,// Blend.InverseDestinationColor
MTLBlendFactor.DestinationAlpha, // Blend.DestinationAlpha
MTLBlendFactor.OneMinusDestinationAlpha,// Blend.InverseDestinationAlpha
MTLBlendFactor.BlendColor, // Blend.BlendFactor
MTLBlendFactor.OneMinusBlendColor, // Blend.InverseBlendFactor
MTLBlendFactor.SourceAlphaSaturated // Blend.SourceAlphaSaturation
};
public static readonly MTLBlendOperation[] BlendOperation = new MTLBlendOperation[]
{
MTLBlendOperation.Add, // BlendFunction.Add
MTLBlendOperation.Subtract, // BlendFunction.Subtract
MTLBlendOperation.ReverseSubtract, // BlendFunction.ReverseSubtract
MTLBlendOperation.Max, // BlendFunction.Max
MTLBlendOperation.Min // BlendFunction.Min
};
public static int ColorWriteMask(ColorWriteChannels channels)
{
if (channels == ColorWriteChannels.None)
{
return 0x0;
}
if (channels == ColorWriteChannels.All)
{
return 0xf;
}
int ret = 0;
if ((channels & ColorWriteChannels.Red) != 0)
{
ret |= (0x1 << 3);
}
if ((channels & ColorWriteChannels.Green) != 0)
{
ret |= (0x1 << 2);
}
if ((channels & ColorWriteChannels.Blue) != 0)
{
ret |= (0x1 << 1);
}
if ((channels & ColorWriteChannels.Alpha) != 0)
{
ret |= (0x1 << 0);
}
return ret;
}
public static readonly MTLCompareFunction[] CompareFunc = new MTLCompareFunction[]
{
MTLCompareFunction.Always, // CompareFunction.Always
MTLCompareFunction.Never, // CompareFunction.Never
MTLCompareFunction.Less, // CompareFunction.Less
MTLCompareFunction.LessEqual, // CompareFunction.LessEqual
MTLCompareFunction.Equal, // CompareFunction.Equal
MTLCompareFunction.GreaterEqual,// CompareFunction.GreaterEqual
MTLCompareFunction.Greater, // CompareFunction.Greater
MTLCompareFunction.NotEqual // CompareFunction.NotEqual
};
public static readonly MTLStencilOperation[] StencilOp = new MTLStencilOperation[]
{
MTLStencilOperation.Keep, // StencilOperation.Keep
MTLStencilOperation.Zero, // StencilOperation.Zero
MTLStencilOperation.Replace, // StencilOperation.Replace
MTLStencilOperation.IncrementWrap, // StencilOperation.Increment
MTLStencilOperation.DecrementWrap, // StencilOperation.Decrement
MTLStencilOperation.IncrementClamp, // StencilOperation.IncrementSaturation
MTLStencilOperation.DecrementClamp, // StencilOperation.DecrementSaturation
MTLStencilOperation.Invert // StencilOperation.Invert
};
public static readonly MTLTriangleFillMode[] FillMode = new MTLTriangleFillMode[]
{
MTLTriangleFillMode.Fill, // FillMode.Solid
MTLTriangleFillMode.Lines // FillMode.WireFrame
};
public static float DepthBiasScale(MTLPixelFormat format)
{
switch (format)
{
case MTLPixelFormat.Depth16Unorm:
return (float) ((1 << 16) - 1);
case MTLPixelFormat.Depth24Unorm_Stencil8:
return (float) ((1 << 24) - 1);
case MTLPixelFormat.Depth32Float:
case MTLPixelFormat.Depth32Float_Stencil8:
return (float) ((1 << 23) - 1);
}
return 0.0f;
}
public static readonly MTLCullMode[] CullingEnabled = new MTLCullMode[]
{
MTLCullMode.None, // CullMode.None
MTLCullMode.Front, // CullMode.CullClockwiseFace
MTLCullMode.Back // CullMode.CullCounterClockwiseFace
};
public static readonly MTLSamplerAddressMode[] Wrap = new MTLSamplerAddressMode[]
{
MTLSamplerAddressMode.Repeat, // TextureAddressMode.Wrap
MTLSamplerAddressMode.ClampToEdge, // TextureAddressMode.Clamp
MTLSamplerAddressMode.MirrorRepeat // TextureAddressMode.Mirror
};
public static readonly MTLSamplerMinMagFilter[] MagFilter = new MTLSamplerMinMagFilter[]
{
MTLSamplerMinMagFilter.Linear, // TextureFilter.Linear
MTLSamplerMinMagFilter.Nearest, // TextureFilter.Point
MTLSamplerMinMagFilter.Linear, // TextureFilter.Anisotropic
MTLSamplerMinMagFilter.Linear, // TextureFilter.LinearMipPoint
MTLSamplerMinMagFilter.Nearest, // TextureFilter.PointMipLinear
MTLSamplerMinMagFilter.Nearest, // TextureFilter.MinLinearMagPointMipLinear
MTLSamplerMinMagFilter.Nearest, // TextureFilter.MinLinearMagPointMipPoint
MTLSamplerMinMagFilter.Linear, // TextureFilter.MinPointMagLinearMipLinear
MTLSamplerMinMagFilter.Linear // TextureFilter.MinPointMagLinearMipPoint
};
public static readonly MTLSamplerMipFilter[] MipFilter = new MTLSamplerMipFilter[]
{
MTLSamplerMipFilter.Linear, // TextureFilter.Linear
MTLSamplerMipFilter.Nearest, // TextureFilter.Point
MTLSamplerMipFilter.Linear, // TextureFilter.Anisotropic
MTLSamplerMipFilter.Nearest, // TextureFilter.LinearMipPoint
MTLSamplerMipFilter.Linear, // TextureFilter.PointMipLinear
MTLSamplerMipFilter.Linear, // TextureFilter.MinLinearMagPointMipLinear
MTLSamplerMipFilter.Nearest, // TextureFilter.MinLinearMagPointMipPoint
MTLSamplerMipFilter.Linear, // TextureFilter.MinPointMagLinearMipLinear
MTLSamplerMipFilter.Nearest // TextureFilter.MinPointMagLinearMipPoint
};
public static readonly MTLSamplerMinMagFilter[] MinFilter = new MTLSamplerMinMagFilter[]
{
MTLSamplerMinMagFilter.Linear, // TextureFilter.Linear
MTLSamplerMinMagFilter.Nearest, // TextureFilter.Point
MTLSamplerMinMagFilter.Linear, // TextureFilter.Anisotropic
MTLSamplerMinMagFilter.Linear, // TextureFilter.LinearMipPoint
MTLSamplerMinMagFilter.Nearest, // TextureFilter.PointMipLinear
MTLSamplerMinMagFilter.Linear, // TextureFilter.MinLinearMagPointMipLinear
MTLSamplerMinMagFilter.Linear, // TextureFilter.MinLinearMagPointMipPoint
MTLSamplerMinMagFilter.Nearest, // TextureFilter.MinPointMagLinearMipLinear
MTLSamplerMinMagFilter.Nearest // TextureFilter.MinPointMagLinearMipPoint
};
public static readonly MTLPrimitiveType[] Primitive = new MTLPrimitiveType[]
{
MTLPrimitiveType.Triangle, // PrimitiveType.TriangleList
MTLPrimitiveType.TriangleStrip, // PrimitiveType.TriangleStrip
MTLPrimitiveType.Line, // PrimitiveType.LineList
MTLPrimitiveType.LineStrip, // PrimitiveType.LineStrip
MTLPrimitiveType.Point // PrimitiveType.PointListEXT
};
public static int PrimitiveVerts(PrimitiveType primitiveType, int primitiveCount)
{
switch (primitiveType)
{
case PrimitiveType.TriangleList:
return primitiveCount * 3;
case PrimitiveType.TriangleStrip:
return primitiveCount + 2;
case PrimitiveType.LineList:
return primitiveCount * 2;
case PrimitiveType.LineStrip:
return primitiveCount + 1;
case PrimitiveType.PointListEXT:
return primitiveCount;
}
throw new NotSupportedException();
}
}
#endregion
#region The Faux-Backbuffer
private class MetalBackbuffer : IGLBackbuffer
{
public int Width
{
get;
private set;
}
public int Height
{
get;
private set;
}
public MTLPixelFormat PixelFormat
{
get;
private set;
}
public DepthFormat DepthFormat
{
get;
private set;
}
public int MultiSampleCount
{
get;
private set;
}
public MetalTexture Texture
{
get;
private set;
}
public IntPtr ColorBuffer = IntPtr.Zero;
public IntPtr MultiSampleColorBuffer = IntPtr.Zero;
public IntPtr DepthStencilBuffer = IntPtr.Zero;
private MetalDevice mtlDevice;
public MetalBackbuffer(
MetalDevice device,
PresentationParameters presentationParameters
) {
mtlDevice = device;
PixelFormat = MTLPixelFormat.RGBA8Unorm;
/* Set these now to prevent a changed event in Create!
* The rest will be set and don't have checks anywhere.
* -flibit
*/
Width = presentationParameters.BackBufferWidth;
Height = presentationParameters.BackBufferHeight;
}
public void Dispose()
{
objc_release(ColorBuffer);
ColorBuffer = IntPtr.Zero;
objc_release(MultiSampleColorBuffer);
MultiSampleColorBuffer = IntPtr.Zero;
objc_release(DepthStencilBuffer);
DepthStencilBuffer = IntPtr.Zero;
}
public void ResetFramebuffer(
PresentationParameters presentationParameters
) {
// Just destroy and recreate from scratch
Dispose();
CreateFramebuffer(presentationParameters);
}
public void CreateFramebuffer(
PresentationParameters presentationParameters
) {
// Update the backbuffer size
int newWidth = presentationParameters.BackBufferWidth;
int newHeight = presentationParameters.BackBufferHeight;
if (Width != newWidth || Height != newHeight)
{
mtlDevice.fauxBackbufferSizeChanged = true;
}
Width = newWidth;
Height = newHeight;
// Update other presentation parameters
DepthFormat = presentationParameters.DepthStencilFormat;
MultiSampleCount = mtlDevice.GetCompatibleSampleCount(
presentationParameters.MultiSampleCount
);
// Update color buffer to the new resolution.
IntPtr colorBufferDesc = mtlMakeTexture2DDescriptor(
PixelFormat,
Width,
Height,
false
);
mtlSetStorageMode(
colorBufferDesc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
colorBufferDesc,
MTLTextureUsage.RenderTarget | MTLTextureUsage.ShaderRead
);
ColorBuffer = mtlNewTextureWithDescriptor(
mtlDevice.device,
colorBufferDesc
);
if (MultiSampleCount > 0)
{
mtlSetTextureType(
colorBufferDesc,
MTLTextureType.Multisample2D
);
mtlSetTextureSampleCount(
colorBufferDesc,
MultiSampleCount
);
mtlSetTextureUsage(
colorBufferDesc,
MTLTextureUsage.RenderTarget
);
MultiSampleColorBuffer = mtlNewTextureWithDescriptor(
mtlDevice.device,
colorBufferDesc
);
}
// Update the depth/stencil buffer, if applicable
if (DepthFormat != DepthFormat.None)
{
IntPtr depthStencilBufferDesc = mtlMakeTexture2DDescriptor(
mtlDevice.GetDepthFormat(DepthFormat),
Width,
Height,
false
);
mtlSetStorageMode(
depthStencilBufferDesc,
MTLStorageMode.Private
);
mtlSetTextureUsage(
depthStencilBufferDesc,
MTLTextureUsage.RenderTarget
);
if (MultiSampleCount > 0)
{
mtlSetTextureType(
depthStencilBufferDesc,
MTLTextureType.Multisample2D
);
mtlSetTextureSampleCount(
depthStencilBufferDesc,
MultiSampleCount
);
}
DepthStencilBuffer = mtlNewTextureWithDescriptor(
mtlDevice.device,
depthStencilBufferDesc
);
}
// Update the Texture representation
Texture = new MetalTexture(
ColorBuffer,
Width,
Height,
SurfaceFormat.Color,
1,
true
);
// This is the default render target
mtlDevice.SetRenderTargets(null, null, DepthFormat.None);
}
}
private void InitializeFauxBackbuffer(
PresentationParameters presentationParameters
) {
MetalBackbuffer mtlBackbuffer = new MetalBackbuffer(
this,
presentationParameters
);
Backbuffer = mtlBackbuffer;
mtlBackbuffer.CreateFramebuffer(presentationParameters);
/* Create a combined vertex/index buffer
* for rendering the faux-backbuffer.
*/
fauxBackbufferDrawBuffer = mtlNewBufferWithLength(
device,
(16 * sizeof(float)) + (6 * sizeof(ushort)),
MTLResourceOptions.CPUCacheModeWriteCombined
);
ushort[] indices = new ushort[]
{
0, 1, 3,
1, 2, 3
};
GCHandle indicesPinned = GCHandle.Alloc(indices, GCHandleType.Pinned);
SDL.SDL_memcpy(
mtlGetBufferContentsPtr(fauxBackbufferDrawBuffer) + (16 * sizeof(float)),
indicesPinned.AddrOfPinnedObject(),
(IntPtr) (6 * sizeof(ushort))
);
indicesPinned.Free();
// Create vertex and fragment shaders for the faux-backbuffer pipeline
string shaderSource =
@"
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
packed_float2 position;
packed_float2 texCoord;
};
struct VertexOut {
float4 position [[ position ]];
float2 texCoord;
};
vertex VertexOut
vertexShader(
uint vertexID [[ vertex_id ]],
constant VertexIn *vertexArray [[ buffer(0) ]]
) {
VertexOut out;
out.position = float4(vertexArray[vertexID].position, 0.0, 1.0);
out.position.y *= -1;
out.texCoord = vertexArray[vertexID].texCoord;
return out;
}
fragment float4
fragmentShader(
VertexOut in [[stage_in]],
texture2d<half> colorTexture [[ texture(0) ]],
sampler s0 [[sampler(0)]]
) {
const half4 colorSample = colorTexture.sample(s0, in.texCoord);
return float4(colorSample);
}
";
IntPtr nsShaderSource = UTF8ToNSString(shaderSource);
IntPtr nsVertexShader = UTF8ToNSString("vertexShader");
IntPtr nsFragmentShader = UTF8ToNSString("fragmentShader");
IntPtr library = mtlNewLibraryWithSource(
device,
nsShaderSource,
IntPtr.Zero
);
IntPtr vertexFunc = mtlNewFunctionWithName(
library,
nsVertexShader
);
IntPtr fragFunc = mtlNewFunctionWithName(
library,
nsFragmentShader
);
objc_release(nsShaderSource);
objc_release(nsVertexShader);
objc_release(nsFragmentShader);
// Create a sampler state
IntPtr samplerDescriptor = mtlNewSamplerDescriptor();
mtlSetSamplerMinFilter(samplerDescriptor, backbufferScaleMode);
mtlSetSamplerMagFilter(samplerDescriptor, backbufferScaleMode);
fauxBackbufferSamplerState = mtlNewSamplerStateWithDescriptor(
device,
samplerDescriptor
);
objc_release(samplerDescriptor);
// Create a render pipeline for rendering the backbuffer
IntPtr pipelineDesc = mtlNewRenderPipelineDescriptor();
mtlSetPipelineVertexFunction(pipelineDesc, vertexFunc);
mtlSetPipelineFragmentFunction(pipelineDesc, fragFunc);
mtlSetAttachmentPixelFormat(
mtlGetColorAttachment(pipelineDesc, 0),
mtlGetLayerPixelFormat(layer)
);
fauxBackbufferRenderPipeline = mtlNewRenderPipelineStateWithDescriptor(
device,
pipelineDesc
);
objc_release(pipelineDesc);
objc_release(vertexFunc);
objc_release(fragFunc);
}
#endregion
}
}