Metal Effect support!
authorCaleb Cornett <caleb.cornett@outlook.com>
Sun, 12 Jan 2020 11:39:49 -0500
changeset 1229 3ecd29a50d9d
parent 1228 22aa1f7914d3
child 1230 600d972bbd57
Metal Effect support!
CMakeLists.txt
mojoshader.h
mojoshader_effects.h
mojoshader_metal.c
--- a/CMakeLists.txt	Fri Jan 10 05:55:05 2020 +0100
+++ b/CMakeLists.txt	Sun Jan 12 11:39:49 2020 -0500
@@ -162,6 +162,7 @@
     mojoshader.c
     mojoshader_common.c
     mojoshader_opengl.c
+    mojoshader_metal.c
     profiles/mojoshader_profile_arb1.c
     profiles/mojoshader_profile_bytecode.c
     profiles/mojoshader_profile_d3d.c
--- a/mojoshader.h	Fri Jan 10 05:55:05 2020 +0100
+++ b/mojoshader.h	Sun Jan 12 11:39:49 2020 -0500
@@ -3252,6 +3252,56 @@
  */
 DECLSPEC void MOJOSHADER_glDestroyContext(MOJOSHADER_glContext *ctx);
 
+
+/* Metal interface... */
+
+typedef struct MOJOSHADER_mtlShader MOJOSHADER_mtlShader;
+
+/*
+ * Get the MTLFunction* from the given MOJOSHADER_mtlShader.
+ *
+ * This function calls [retain] on the MTLFunction* before returning!
+ *  Please call [release] on the result when you no longer need it.
+ */
+DECLSPEC void *MOJOSHADER_mtlGetFunctionHandle(MOJOSHADER_mtlShader *shader);
+
+/*
+ * Swaps uniform buffers and resets offsets to prepare for the next frame.
+ *
+ * Always call this after submitting the final command buffer for a frame!
+ */
+DECLSPEC void MOJOSHADER_mtlEndFrame();
+
+/*
+ * Return the location of a vertex attribute for the given shader.
+ *
+ * (usage) and (index) map to Direct3D vertex declaration values: COLOR1 would
+ *  be MOJOSHADER_USAGE_COLOR and 1.
+ *
+ * The return value is the index of the attribute to be used to create
+ *  a MTLVertexAttributeDescriptor, or -1 if the stream is not used.
+ */
+int MOJOSHADER_mtlGetVertexAttribLocation(MOJOSHADER_mtlShader *vert,
+                                          MOJOSHADER_usage usage, int index);
+
+/*
+ * Get any error state we might have picked up, such as failed shader
+ *  compilation.
+ *
+ * Returns a human-readable string. This string is for debugging purposes, and
+ *  not guaranteed to be localized, coherent, or user-friendly in any way.
+ *  It's for programmers!
+ *
+ * The latest error may remain between calls. New errors replace any existing
+ *  error. Don't check this string for a sign that an error happened, check
+ *  return codes instead and use this for explanation when debugging.
+ *
+ * Do not free the returned string: it's a pointer to a static internal
+ *  buffer. Do not keep the pointer around, either, as it's likely to become
+ *  invalid as soon as you call into MojoShader again.
+ */
+DECLSPEC const char *MOJOSHADER_mtlGetError(void);
+
 #ifdef __cplusplus
 }
 #endif
--- a/mojoshader_effects.h	Fri Jan 10 05:55:05 2020 +0100
+++ b/mojoshader_effects.h	Sun Jan 12 11:39:49 2020 -0500
@@ -800,6 +800,119 @@
  */
 DECLSPEC void MOJOSHADER_glEffectEnd(MOJOSHADER_glEffect *glEffect);
 
+
+/* Metal effect interface... */
+
+typedef struct MOJOSHADER_mtlEffect MOJOSHADER_mtlEffect;
+typedef struct MOJOSHADER_mtlShader MOJOSHADER_mtlShader;
+typedef struct MOJOSHADER_mtlShaderState MOJOSHADER_mtlShaderState;
+
+/* Fully compile/link the shaders found within the effect.
+ *
+ * The MOJOSHADER_mtlEffect* is solely for use within the Metal-specific calls.
+ *  In all other cases you will be using the MOJOSHADER_effect* instead.
+ *
+ * In a typical use case, you will be calling this immediately after obtaining
+ *  the MOJOSHADER_effect*.
+ *
+ * (effect) is a MOJOSHADER_effect* obtained from MOJOSHADER_parseEffect().
+ * (mtlDevice) is a MTLDevice* obtained from a Metal device creation call,
+ *  such as MTLCreateSystemDefaultDevice().
+ * (numBackingBuffers) is the number of backing uniform buffers that you
+ *  want to create for each shader. If you are using double-buffering,
+ *  this should be 2; for triple buffering, this should be 3, etc.
+ *
+ * This function returns a MOJOSHADER_mtlEffect*, containing Metal-specific
+ *  data for an accompanying MOJOSHADER_effect*.
+ */
+DECLSPEC MOJOSHADER_mtlEffect *MOJOSHADER_mtlCompileEffect(MOJOSHADER_effect *effect,
+                                                           void *mtlDevice,
+                                                           int numBackingBuffers);
+
+/* Delete the shaders that were allocated for an effect.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_mtlCompileEffect().
+ */
+DECLSPEC void MOJOSHADER_mtlDeleteEffect(MOJOSHADER_mtlEffect *mtlEffect);
+
+/* Prepare the effect for rendering with the currently applied technique.
+ *
+ * This function maps to ID3DXEffect::Begin.
+ *
+ * In addition to the expected Begin parameters, we also include a parameter
+ *  to pass in a MOJOSHADER_effectRenderState. Rather than change the render
+ *  state within MojoShader itself we will simply provide what the effect wants
+ *  and allow you to use this information with your own renderer.
+ *  MOJOSHADER_glEffectBeginPass will update with the render state desired by
+ *  the current effect pass.
+ *
+ * Note that we only provide the ability to preserve the shader state, but NOT
+ * the ability to preserve the render/sampler states. You are expected to
+ * track your own Metal state and restore these states as needed for your
+ * application.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_mtlCompileEffect().
+ * (numPasses) will be filled with the number of passes that this technique
+ *  will need to fully render.
+ * (saveShaderState) is a boolean value informing the effect whether or not to
+ *  restore the shader bindings after calling MOJOSHADER_mtlEffectEnd.
+ * (renderState) will be filled by the effect to inform you of the render state
+ *  changes introduced by the technique and its passes.
+ */
+DECLSPEC void MOJOSHADER_mtlEffectBegin(MOJOSHADER_mtlEffect *mtlEffect,
+                                        unsigned int *numPasses,
+                                        int saveShaderState,
+                                        MOJOSHADER_effectStateChanges *stateChanges);
+
+/* Begin an effect pass from the currently applied technique.
+ *
+ * This function maps to ID3DXEffect::BeginPass.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_mtlCompileEffect().
+ * (pass) is the index of the effect pass as found in the current technique.
+ * (state) is a pointer to the current shader state object.
+ *
+ * The MOJOSHADER_mtlShaderState pointed to by (shState) must be created
+ * before calling this function!
+ */
+DECLSPEC void MOJOSHADER_mtlEffectBeginPass(MOJOSHADER_mtlEffect *mtlEffect,
+                                            unsigned int pass,
+                                            MOJOSHADER_mtlShaderState *shState);
+
+/* Push render state changes that occurred within an actively rendering pass.
+ *
+ * This function maps to ID3DXEffect::CommitChanges.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_mtlCompileEffect().
+ * (state) is a pointer to the current shader state object.
+ */
+DECLSPEC void MOJOSHADER_mtlEffectCommitChanges(MOJOSHADER_mtlEffect *mtlEffect,
+                                                MOJOSHADER_mtlShaderState *shState);
+
+/* End an effect pass from the currently applied technique.
+ *
+ * This function maps to ID3DXEffect::EndPass.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_mtlCompileEffect().
+ */
+DECLSPEC void MOJOSHADER_mtlEffectEndPass(MOJOSHADER_mtlEffect *mtlEffect);
+
+/* Complete rendering the effect technique, and restore the render state.
+ *
+ * This function maps to ID3DXEffect::End.
+ *
+ * (mtlEffect) is a MOJOSHADER_mtlEffect* obtained from
+ *  MOJOSHADER_glCompileEffect().
+ * (state) is a pointer to the current shader state object.
+ */
+DECLSPEC void MOJOSHADER_mtlEffectEnd(MOJOSHADER_mtlEffect *mtlEffect,
+                                      MOJOSHADER_mtlShaderState *shState);
+
 #endif /* MOJOSHADER_EFFECT_SUPPORT */
 
 #endif /* MOJOSHADER_EFFECTS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mojoshader_metal.c	Sun Jan 12 11:39:49 2020 -0500
@@ -0,0 +1,1002 @@
+/**
+ * MojoShader; generate shader programs from bytecode of compiled
+ *  Direct3D shaders.
+ *
+ * Please see the file LICENSE.txt in the source's root directory.
+ *
+ *  This file written by Ryan C. Gordon.
+ */
+
+#if (defined(__APPLE__) && defined(__MACH__))
+#define PLATFORM_APPLE 1
+#include "TargetConditionals.h"
+#define OBJC_OLD_DISPATCH_PROTOTYPES 1
+#include <objc/message.h>
+#define objc_msgSend_STR ((void* (*)(void*, void*, const char*))objc_msgSend)
+#define objc_msgSend_PTR ((void* (*)(void*, void*, void*))objc_msgSend)
+#define objc_msgSend_INT_PTR ((void* (*)(void*, void*, int, void*))objc_msgSend)
+#define objc_msgSend_PTR_PTR_PTR ((void* (*)(void*, void*, void*, void*, void*))objc_msgSend)
+#endif /* (defined(__APPLE__) && defined(__MACH__)) */
+
+#define __MOJOSHADER_INTERNAL__ 1
+#include "mojoshader_internal.h"
+
+typedef struct MOJOSHADER_mtlUniformBuffer MOJOSHADER_mtlUniformBuffer;
+typedef struct MOJOSHADER_mtlShader
+{
+    const MOJOSHADER_parseData *parseData;
+    MOJOSHADER_mtlUniformBuffer *ubo;
+    void *library; // MTLLibrary*
+    int numInternalBuffers;
+} MOJOSHADER_mtlShader;
+
+// Error state...
+static char error_buffer[1024] = { '\0' };
+
+static void set_error(const char *str)
+{
+    snprintf(error_buffer, sizeof (error_buffer), "%s", str);
+} // set_error
+
+static inline void out_of_memory(void)
+{
+    set_error("out of memory");
+} // out_of_memory
+
+// profile-specific implementations...
+
+#if SUPPORT_PROFILE_METAL && PLATFORM_APPLE
+#ifdef MOJOSHADER_EFFECT_SUPPORT
+
+/* Structs */
+
+typedef struct MOJOSHADER_mtlEffect
+{
+    MOJOSHADER_effect *effect;
+    unsigned int num_shaders;
+    MOJOSHADER_mtlShader *shaders;
+    unsigned int *shader_indices;
+    unsigned int num_preshaders;
+    unsigned int *preshader_indices;
+    MOJOSHADER_mtlShader *current_vert;
+    MOJOSHADER_mtlShader *current_frag;
+    MOJOSHADER_effectShader *current_vert_raw;
+    MOJOSHADER_effectShader *current_frag_raw;
+    MOJOSHADER_mtlShader *prev_vert;
+    MOJOSHADER_mtlShader *prev_frag;
+    void *library; // MTLLibrary*
+} MOJOSHADER_mtlEffect;
+
+typedef struct MOJOSHADER_mtlUniformBuffer
+{
+    void *device; // MTLDevice*
+    int bufferSize;
+    int numInternalBuffers;
+    void **internalBuffers; // MTLBuffer*
+    int internalBufferSize;
+    int internalOffset;
+    int currentFrame;
+    int alreadyWritten;
+} MOJOSHADER_mtlUniformBuffer;
+
+typedef struct MOJOSHADER_mtlShaderState
+{
+    MOJOSHADER_mtlShader *vertexShader;
+    MOJOSHADER_mtlShader *fragmentShader;
+    void *vertexUniformBuffer; // MTLBuffer*
+    void *fragmentUniformBuffer; // MTLBuffer*
+    int vertexUniformOffset;
+    int fragmentUniformOffset;
+} MOJOSHADER_mtlShaderState;
+
+/* Objective-C selector references */
+
+static void *classNSString = NULL;
+static void *selAlloc = NULL;
+static void *selInitWithUTF8String = NULL;
+static void *selUTF8String = NULL;
+static void *selLength = NULL;
+static void *selContents = NULL;
+static void *selNewBufferWithLength = NULL;
+static void *selRelease = NULL;
+static void *selNewLibraryWithSource = NULL;
+static void *selLocalizedDescription = NULL;
+static void *selNewFunctionWithName = NULL;
+static void *selRetain = NULL;
+
+/* Helper functions */
+
+static void initSelectors(void)
+{
+    classNSString = (void*) objc_getClass("NSString");
+    selAlloc = sel_registerName("alloc");
+    selInitWithUTF8String = sel_registerName("initWithUTF8String:");
+    selUTF8String = sel_registerName("UTF8String");
+    selLength = sel_registerName("length");
+    selContents = sel_registerName("contents");
+    selNewBufferWithLength = sel_registerName("newBufferWithLength:options:");
+    selRelease = sel_registerName("release");
+    selNewLibraryWithSource = sel_registerName("newLibraryWithSource:options:error:");
+    selLocalizedDescription = sel_registerName("localizedDescription");
+    selNewFunctionWithName = sel_registerName("newFunctionWithName:");
+    selRetain = sel_registerName("retain");
+} // initSelectors
+
+static void *cstr_to_nsstr(const char *str)
+{
+    return objc_msgSend_STR(
+        objc_msgSend(classNSString, selAlloc),
+        selInitWithUTF8String,
+        str
+    );
+} // cstr_to_nsstr
+
+static const char *nsstr_to_cstr(void *str)
+{
+    return (char *) objc_msgSend(str, selUTF8String);
+} // nssstr_to_cstr
+
+/* Linked list */
+
+typedef struct LLNODE {
+    MOJOSHADER_mtlUniformBuffer *data;
+    struct LLNODE *next;
+} LLNODE;
+
+static LLNODE *LL_append_node(LLNODE **baseNode,
+                              MOJOSHADER_malloc m,
+                              void *d)
+{
+    LLNODE *prev = NULL;
+    LLNODE *node = *baseNode;
+
+    /* Append a node to the linked list. */
+    while (node != NULL)
+    {
+        prev = node;
+        node = node->next;
+    } // while
+    node = m(sizeof(LLNODE), d);
+    node->next = NULL;
+
+    /* Connect the old to the new. */
+    if (prev != NULL)
+        prev->next = node;
+
+    /* Special case for the first node. */
+    if (*baseNode == NULL)
+        *baseNode = node;
+
+    return node;
+} // LL_append_node
+
+static void LL_remove_node(LLNODE **baseNode,
+                           MOJOSHADER_mtlUniformBuffer *data,
+                           MOJOSHADER_free f,
+                           void *d)
+{
+    LLNODE *prev = NULL;
+    LLNODE *node = *baseNode;
+
+    /* Search for node with matching data pointer. */
+    while (node != NULL && node->data != data)
+    {
+        prev = node;
+        node = node->next;
+    } // while
+
+    if (node == NULL)
+    {
+        /* This should never happen. */
+        assert(0);
+    } // if
+
+    /* Clear data pointer. The data must be freed separately. */
+    node->data = NULL;
+
+    /* Connect the old to the new. */
+    if (prev != NULL)
+        prev->next = node->next;
+
+    /* Special cases where the first node is removed. */
+    if (prev == NULL)
+        *baseNode = (node->next != NULL) ? node->next : NULL;
+
+    /* Free the node! */
+    f(node, d);
+} // LL_remove_node
+
+/* Internal register utilities */
+
+// Max entries for each register file type...
+#define MAX_REG_FILE_F 8192
+#define MAX_REG_FILE_I 2047
+#define MAX_REG_FILE_B 2047
+
+// The constant register files...
+// !!! FIXME: Man, it kills me how much memory this takes...
+// !!! FIXME:  ... make this dynamically allocated on demand.
+float vs_reg_file_f[MAX_REG_FILE_F * 4];
+int vs_reg_file_i[MAX_REG_FILE_I * 4];
+uint8 vs_reg_file_b[MAX_REG_FILE_B];
+float ps_reg_file_f[MAX_REG_FILE_F * 4];
+int ps_reg_file_i[MAX_REG_FILE_I * 4];
+uint8 ps_reg_file_b[MAX_REG_FILE_B];
+
+static inline void copy_parameter_data(MOJOSHADER_effectParam *params,
+                                       unsigned int *param_loc,
+                                       MOJOSHADER_symbol *symbols,
+                                       unsigned int symbol_count,
+                                       float *regf, int *regi, uint8 *regb)
+{
+    int i, j, r, c;
+
+    i = 0;
+    for (i = 0; i < symbol_count; i++)
+    {
+        const MOJOSHADER_symbol *sym = &symbols[i];
+        const MOJOSHADER_effectValue *param = &params[param_loc[i]].value;
+
+        // float/int registers are vec4, so they have 4 elements each
+        const uint32 start = sym->register_index << 2;
+
+        if (param->type.parameter_type == MOJOSHADER_SYMTYPE_FLOAT)
+            memcpy(regf + start, param->valuesF, sym->register_count << 4);
+        else if (sym->register_set == MOJOSHADER_SYMREGSET_FLOAT4)
+        {
+            // Structs are a whole different world...
+            if (param->type.parameter_class == MOJOSHADER_SYMCLASS_STRUCT)
+                memcpy(regf + start, param->valuesF, sym->register_count << 4);
+            else
+            {
+                // Sometimes int/bool parameters get thrown into float registers...
+                j = 0;
+                do
+                {
+                    c = 0;
+                    do
+                    {
+                        regf[start + (j << 2) + c] = (float) param->valuesI[(j << 2) + c];
+                    } while (++c < param->type.columns);
+                } while (++j < sym->register_count);
+            } // else
+        } // else if
+        else if (sym->register_set == MOJOSHADER_SYMREGSET_INT4)
+            memcpy(regi + start, param->valuesI, sym->register_count << 4);
+        else if (sym->register_set == MOJOSHADER_SYMREGSET_BOOL)
+        {
+            j = 0;
+            r = 0;
+            do
+            {
+                c = 0;
+                do
+                {
+                    // regb is not a vec4, enjoy that 'start' bitshift! -flibit
+                    regb[(start >> 2) + r + c] = param->valuesI[(j << 2) + c];
+                    c++;
+                } while (c < param->type.columns && ((r + c) < sym->register_count));
+                r += c;
+                j++;
+            } while (r < sym->register_count);
+        } // else if
+    } // for
+} // copy_parameter_data
+
+/* Uniform buffer utilities */
+
+static inline int next_highest_alignment(int n)
+{
+    #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_SIMULATOR
+    int align = 16;
+    #else
+    int align = 256;
+    #endif
+
+    return align * ((n + align - 1) / align);
+} // next_highest_alignment
+
+static int UBO_buffer_length(void *buffer)
+{
+    return (int) objc_msgSend(buffer, selLength);
+} // UBO_buffer_length
+
+static void *UBO_buffer_contents(void *buffer)
+{
+    return (void *) objc_msgSend(buffer, selContents);
+} // UBO_buffer_contents
+
+static void *UBO_create_backing_buffer(MOJOSHADER_mtlUniformBuffer *ubo, int f)
+{
+    void *oldBuffer = ubo->internalBuffers[f];
+    void *newBuffer = objc_msgSend_INT_PTR(
+        ubo->device,
+        selNewBufferWithLength,
+        ubo->internalBufferSize,
+        NULL
+    );
+    if (oldBuffer != NULL)
+    {
+        // Copy over data from old buffer
+        memcpy(
+            UBO_buffer_contents(newBuffer),
+            UBO_buffer_contents(oldBuffer),
+            UBO_buffer_length(oldBuffer)
+        );
+
+        // Free the old buffer
+        objc_msgSend(oldBuffer, selRelease);
+    } //if
+
+    return newBuffer;
+} // UBO_create_backing_buffer
+
+static void UBO_predraw(MOJOSHADER_mtlUniformBuffer *ubo)
+{
+    if (!ubo->alreadyWritten)
+    {
+        ubo->alreadyWritten = 1;
+        return;
+    } // if
+
+    ubo->internalOffset += ubo->bufferSize;
+
+    int buflen = UBO_buffer_length(ubo->internalBuffers[ubo->currentFrame]);
+    if (ubo->internalOffset >= buflen)
+    {
+        // Double capacity when we're out of room
+        if (ubo->internalOffset >= ubo->internalBufferSize)
+            ubo->internalBufferSize *= 2;
+
+        ubo->internalBuffers[ubo->currentFrame] =
+            UBO_create_backing_buffer(ubo, ubo->currentFrame);
+    } //if
+} // UBO_predraw
+
+static void UBO_end_frame(MOJOSHADER_mtlUniformBuffer *ubo)
+{
+    ubo->internalOffset = 0;
+    ubo->currentFrame = (ubo->currentFrame + 1) % ubo->numInternalBuffers;
+    ubo->alreadyWritten = 0;
+} // UBO_end_frame
+
+LLNODE *ubos = NULL; /* global linked list of all active UBOs */
+
+static MOJOSHADER_mtlUniformBuffer *create_ubo(MOJOSHADER_mtlShader *shader,
+                                               void *mtlDevice,
+                                               MOJOSHADER_malloc m,
+                                               void *d)
+{
+    int uniformCount = shader->parseData->uniform_count;
+    if (uniformCount == 0)
+        return NULL;
+
+    // Calculate how big we need to make the buffer
+    int buflen = 0;
+    for (int i = 0; i < uniformCount; i += 1)
+    {
+        int arrayCount = shader->parseData->uniforms[i].array_count;
+        int uniformSize = 16;
+        if (shader->parseData->uniforms[i].type == MOJOSHADER_UNIFORM_BOOL)
+            uniformSize = 1;
+        buflen += (arrayCount ? arrayCount : 1) * uniformSize;
+    } // for
+
+    // Make the UBO
+    MOJOSHADER_mtlUniformBuffer *ubo = (MOJOSHADER_mtlUniformBuffer *) m(sizeof(MOJOSHADER_mtlUniformBuffer), d);
+    ubo->device = mtlDevice;
+    ubo->alreadyWritten = 0;
+    ubo->bufferSize = next_highest_alignment(buflen);
+    ubo->currentFrame = 0;
+    ubo->numInternalBuffers = shader->numInternalBuffers;
+    ubo->internalBufferSize = ubo->bufferSize * 16; // pre-allocate some extra room!
+    ubo->internalBuffers = m(ubo->numInternalBuffers * sizeof(void*), d);
+    ubo->internalOffset = 0;
+    for (int i = 0; i < ubo->numInternalBuffers; i++)
+    {
+        ubo->internalBuffers[i] = NULL;
+        ubo->internalBuffers[i] = UBO_create_backing_buffer(ubo, i);
+    } // for
+
+    /* Add the UBO to the global list so it can be updated. */
+    LLNODE *node = LL_append_node(&ubos, m, d);
+    node->data = ubo;
+
+    return ubo;
+} // create_ubo
+
+static void dealloc_ubo(MOJOSHADER_mtlShader *shader,
+                        MOJOSHADER_free f,
+                        void* d)
+{
+    if (shader->ubo == NULL)
+        return;
+
+    LL_remove_node(&ubos, shader->ubo, f, d);
+    for (int i = 0; i < shader->ubo->numInternalBuffers; i++)
+    {
+        objc_msgSend(shader->ubo->internalBuffers[i], selRelease);
+        shader->ubo->internalBuffers[i] = NULL;
+    } // for
+
+    f(shader->ubo->internalBuffers, d);
+    f(shader->ubo, d);
+} // dealloc_ubo
+
+static void *get_uniform_buffer(MOJOSHADER_mtlShader *shader)
+{
+    if (shader == NULL || shader->ubo == NULL)
+        return NULL;
+
+    return shader->ubo->internalBuffers[shader->ubo->currentFrame];
+} // get_uniform_buffer
+
+static int get_uniform_offset(MOJOSHADER_mtlShader *shader)
+{
+    if (shader == NULL || shader->ubo == NULL)
+        return 0;
+
+    return shader->ubo->internalOffset;
+} // get_uniform_offset
+
+static void update_uniform_buffer(MOJOSHADER_mtlShader *shader)
+{
+    if (shader == NULL || shader->ubo == NULL)
+        return;
+
+    float *regF; int *regI; uint8 *regB;
+    if (shader->parseData->shader_type == MOJOSHADER_TYPE_VERTEX)
+    {
+        regF = vs_reg_file_f;
+        regI = vs_reg_file_i;
+        regB = vs_reg_file_b;
+    } // if
+    else
+    {
+        regF = ps_reg_file_f;
+        regI = ps_reg_file_i;
+        regB = ps_reg_file_b;
+    } // else
+
+    UBO_predraw(shader->ubo);
+    void *buf = shader->ubo->internalBuffers[shader->ubo->currentFrame];
+    void *contents = UBO_buffer_contents(buf) + shader->ubo->internalOffset;
+
+    int offset = 0;
+    for (int i = 0; i < shader->parseData->uniform_count; i++)
+    {
+        int idx = shader->parseData->uniforms[i].index;
+        int arrayCount = shader->parseData->uniforms[i].array_count;
+        int size = arrayCount ? arrayCount : 1;
+
+        switch (shader->parseData->uniforms[i].type)
+        {
+            case MOJOSHADER_UNIFORM_FLOAT:
+                memcpy(
+                    contents + (offset * 16),
+                    &regF[4 * idx],
+                    size * 16
+                );
+                break;
+
+            case MOJOSHADER_UNIFORM_INT:
+                // !!! FIXME: Need a test case
+                memcpy(
+                    contents + (offset * 16),
+                    &regI[4 * idx],
+                    size * 16
+                );
+                break;
+
+            case MOJOSHADER_UNIFORM_BOOL:
+                // !!! FIXME: Need a test case
+                memcpy(
+                    contents + offset,
+                    &regB[idx],
+                    size
+                );
+                break;
+
+            default:
+                assert(0); // This should never happen.
+                break;
+        } // switch
+
+        offset += size;
+    } // for
+} // update_uniform_buffer
+
+/* Public API */
+
+MOJOSHADER_mtlEffect *MOJOSHADER_mtlCompileEffect(MOJOSHADER_effect *effect,
+                                                  void *mtlDevice,
+                                                  int numBackingBuffers)
+{
+    int i;
+    MOJOSHADER_malloc m = effect->malloc;
+    MOJOSHADER_free f = effect->free;
+    void *d = effect->malloc_data;
+    int current_shader = 0;
+    int current_preshader = 0;
+    int src_len = 0;
+
+    // Make sure the Objective-C selectors have been initialized...
+    if (selAlloc == NULL)
+        initSelectors();
+
+    MOJOSHADER_mtlEffect *retval = (MOJOSHADER_mtlEffect *) m(sizeof (MOJOSHADER_mtlEffect), d);
+    if (retval == NULL)
+    {
+        out_of_memory();
+        return NULL;
+    } // if
+    memset(retval, '\0', sizeof (MOJOSHADER_mtlEffect));
+
+    // Count the number of shaders before allocating
+    for (i = 0; i < effect->object_count; i++)
+    {
+        MOJOSHADER_effectObject *object = &effect->objects[i];
+        if (object->type == MOJOSHADER_SYMTYPE_PIXELSHADER
+         || object->type == MOJOSHADER_SYMTYPE_VERTEXSHADER)
+        {
+            if (object->shader.is_preshader)
+                retval->num_preshaders++;
+            else
+            {
+                retval->num_shaders++;
+                src_len += object->shader.shader->output_len;
+            } // else
+        } // if
+    } // for
+
+    // Alloc shader source buffer
+    char *shader_source = (char *) m(src_len + 1, d);
+    memset(shader_source, '\0', src_len + 1);
+    int src_pos = 0;
+
+    // Copy all the source text into the buffer
+    for (i = 0; i < effect->object_count; i++)
+    {
+        MOJOSHADER_effectObject *object = &effect->objects[i];
+        if (object->type == MOJOSHADER_SYMTYPE_PIXELSHADER
+         || object->type == MOJOSHADER_SYMTYPE_VERTEXSHADER)
+        {
+            if (!object->shader.is_preshader)
+            {
+                int output_len = object->shader.shader->output_len;
+                memcpy(&shader_source[src_pos], object->shader.shader->output, output_len);
+                src_pos += output_len;
+            } // if
+        } // if
+    } // for
+
+    // Handle texcoord0 -> point_coord conversion
+    if (strstr(shader_source, "[[point_size]]"))
+    {
+        // !!! FIXME: This assumes all texcoord0 attributes in the effect are
+        // !!! FIXME:  actually point coords! It ain't necessarily so! -caleb
+        const char *repl = "[[  point_coord  ]]";
+        char *ptr;
+        while ((ptr = strstr(shader_source, "[[user(texcoord0)]]")))
+        {
+            memcpy(ptr, repl, strlen(repl));
+
+            // float4 -> float2
+            int spaces = 0;
+            while (spaces < 2)
+                if (*(ptr--) == ' ')
+                    spaces++;
+            memcpy(ptr, "2", sizeof(char));
+        } // while
+    } // if
+
+    // Alloc shader information
+    retval->shaders = (MOJOSHADER_mtlShader *) m(retval->num_shaders * sizeof (MOJOSHADER_mtlShader), d);
+    if (retval->shaders == NULL)
+    {
+        f(retval, d);
+        out_of_memory();
+        return NULL;
+    } // if
+    memset(retval->shaders, '\0', retval->num_shaders * sizeof (MOJOSHADER_mtlShader));
+    retval->shader_indices = (unsigned int *) m(retval->num_shaders * sizeof (unsigned int), d);
+    if (retval->shader_indices == NULL)
+    {
+        f(retval->shaders, d);
+        f(retval, d);
+        out_of_memory();
+        return NULL;
+    } // if
+    memset(retval->shader_indices, '\0', retval->num_shaders * sizeof (unsigned int));
+
+    // Alloc preshader information
+    if (retval->num_preshaders > 0)
+    {
+        retval->preshader_indices = (unsigned int *) m(retval->num_preshaders * sizeof (unsigned int), d);
+        if (retval->preshader_indices == NULL)
+        {
+            f(retval->shaders, d);
+            f(retval->shader_indices, d);
+            f(retval, d);
+            out_of_memory();
+            return NULL;
+        } // if
+        memset(retval->preshader_indices, '\0', retval->num_preshaders * sizeof (unsigned int));
+    } // if
+
+    // Compile the source into a library
+    void *compileError = NULL;
+    void *shader_source_ns = cstr_to_nsstr(shader_source);
+    void *library = objc_msgSend_PTR_PTR_PTR(
+        mtlDevice,
+        selNewLibraryWithSource,
+        shader_source_ns,
+        NULL,
+        &compileError
+    );
+    retval->library = library;
+    f(shader_source, d);
+    objc_msgSend(shader_source_ns, selRelease);
+
+    if (library == NULL)
+    {
+        // Set the error
+        void *error_nsstr = objc_msgSend(compileError, selLocalizedDescription);
+        set_error(nsstr_to_cstr(error_nsstr));
+
+        goto compile_shader_fail;
+    } // if
+
+    // Run through the shaders again, tracking the object indices
+    for (i = 0; i < effect->object_count; i++)
+    {
+        MOJOSHADER_effectObject *object = &effect->objects[i];
+        if (object->type == MOJOSHADER_SYMTYPE_PIXELSHADER
+         || object->type == MOJOSHADER_SYMTYPE_VERTEXSHADER)
+        {
+            if (object->shader.is_preshader)
+            {
+                retval->preshader_indices[current_preshader++] = i;
+                continue;
+            } // if
+
+            MOJOSHADER_mtlShader *curshader = &retval->shaders[current_shader];
+            curshader->parseData = object->shader.shader;
+            curshader->numInternalBuffers = numBackingBuffers;
+            curshader->ubo = create_ubo(curshader, mtlDevice, m, d);
+            curshader->library = library;
+
+            retval->shader_indices[current_shader] = i;
+
+            current_shader++;
+        } // if
+    } // for
+
+    retval->effect = effect;
+    return retval;
+
+compile_shader_fail:
+    f(retval->shader_indices, d);
+    f(retval->shaders, d);
+    f(retval, d);
+    return NULL;
+} // MOJOSHADER_mtlCompileEffect
+
+void MOJOSHADER_mtlDeleteEffect(MOJOSHADER_mtlEffect *mtlEffect)
+{
+    MOJOSHADER_free f = mtlEffect->effect->free;
+    void *d = mtlEffect->effect->malloc_data;
+
+    int i;
+    for (i = 0; i < mtlEffect->num_shaders; i++)
+    {
+        /* Release the uniform buffers */
+        dealloc_ubo(&mtlEffect->shaders[i], f, d);
+    } // for
+
+    /* Release the library */
+    objc_msgSend(mtlEffect->library, selRelease);
+
+    f(mtlEffect->shader_indices, d);
+    f(mtlEffect->preshader_indices, d);
+    f(mtlEffect, d);
+} // MOJOSHADER_mtlDeleteEffect
+
+
+void MOJOSHADER_mtlEffectBegin(MOJOSHADER_mtlEffect *mtlEffect,
+                               unsigned int *numPasses,
+                               int saveShaderState,
+                               MOJOSHADER_effectStateChanges *stateChanges)
+{
+    *numPasses = mtlEffect->effect->current_technique->pass_count;
+    mtlEffect->effect->restore_shader_state = saveShaderState;
+    mtlEffect->effect->state_changes = stateChanges;
+
+    if (mtlEffect->effect->restore_shader_state)
+    {
+        mtlEffect->prev_vert = mtlEffect->current_vert;
+        mtlEffect->prev_frag = mtlEffect->current_frag;
+    } // if
+} // MOJOSHADER_mtlEffectBegin
+
+// Predeclare
+void MOJOSHADER_mtlEffectCommitChanges(MOJOSHADER_mtlEffect *mtlEffect,
+                                       MOJOSHADER_mtlShaderState *shState);
+
+void MOJOSHADER_mtlEffectBeginPass(MOJOSHADER_mtlEffect *mtlEffect,
+                                   unsigned int pass,
+                                   MOJOSHADER_mtlShaderState *shState)
+{
+    int i, j;
+    MOJOSHADER_effectPass *curPass;
+    MOJOSHADER_effectState *state;
+    MOJOSHADER_effectShader *rawVert = mtlEffect->current_vert_raw;
+    MOJOSHADER_effectShader *rawFrag = mtlEffect->current_frag_raw;
+    int has_preshader = 0;
+
+    assert(shState != NULL);
+    assert(mtlEffect->effect->current_pass == -1);
+    mtlEffect->effect->current_pass = pass;
+    curPass = &mtlEffect->effect->current_technique->passes[pass];
+
+    // !!! FIXME: I bet this could be stored at parse/compile time. -flibit
+    for (i = 0; i < curPass->state_count; i++)
+    {
+        state = &curPass->states[i];
+        #define ASSIGN_SHADER(stype, raw, mtls) \
+            (state->type == stype) \
+            { \
+                j = 0; \
+                do \
+                { \
+                    if (*state->value.valuesI == mtlEffect->shader_indices[j]) \
+                    { \
+                        raw = &mtlEffect->effect->objects[*state->value.valuesI].shader; \
+                        mtlEffect->mtls = &mtlEffect->shaders[j]; \
+                        break; \
+                    } \
+                    else if (mtlEffect->num_preshaders > 0 \
+                          && *state->value.valuesI == mtlEffect->preshader_indices[j]) \
+                    { \
+                        raw = &mtlEffect->effect->objects[*state->value.valuesI].shader; \
+                        has_preshader = 1; \
+                        break; \
+                    } \
+                } while (++j < mtlEffect->num_shaders); \
+            }
+        if ASSIGN_SHADER(MOJOSHADER_RS_VERTEXSHADER, rawVert, current_vert)
+        else if ASSIGN_SHADER(MOJOSHADER_RS_PIXELSHADER, rawFrag, current_frag)
+        #undef ASSIGN_SHADER
+    } // for
+
+    mtlEffect->effect->state_changes->render_state_changes = curPass->states;
+    mtlEffect->effect->state_changes->render_state_change_count = curPass->state_count;
+
+    mtlEffect->current_vert_raw = rawVert;
+    mtlEffect->current_frag_raw = rawFrag;
+
+    /* If this effect pass has an array of shaders, we get to wait until
+     * CommitChanges to actually bind the final shaders.
+     * -flibit
+     */
+    if (!has_preshader)
+    {
+        if (mtlEffect->current_vert != NULL)
+        {
+            MOJOSHADER_mtlShader *vert = mtlEffect->current_vert;
+            shState->vertexShader = vert;
+            shState->vertexUniformBuffer = get_uniform_buffer(vert);
+            shState->vertexUniformOffset = get_uniform_offset(vert);
+        } // if
+
+        if (mtlEffect->current_frag != NULL)
+        {
+            MOJOSHADER_mtlShader *frag = mtlEffect->current_frag;
+            shState->fragmentShader = frag;
+            shState->fragmentUniformBuffer = get_uniform_buffer(frag);
+            shState->fragmentUniformOffset = get_uniform_offset(frag);
+        } // if
+
+        if (mtlEffect->current_vert_raw != NULL)
+        {
+            mtlEffect->effect->state_changes->vertex_sampler_state_changes = rawVert->samplers;
+            mtlEffect->effect->state_changes->vertex_sampler_state_change_count = rawVert->sampler_count;
+        } // if
+        if (mtlEffect->current_frag_raw != NULL)
+        {
+            mtlEffect->effect->state_changes->sampler_state_changes = rawFrag->samplers;
+            mtlEffect->effect->state_changes->sampler_state_change_count = rawFrag->sampler_count;
+        } // if
+    } // if
+
+    MOJOSHADER_mtlEffectCommitChanges(mtlEffect, shState);
+} // MOJOSHADER_mtlEffectBeginPass
+
+void MOJOSHADER_mtlEffectCommitChanges(MOJOSHADER_mtlEffect *mtlEffect,
+                                       MOJOSHADER_mtlShaderState *shState)
+{
+    MOJOSHADER_effectShader *rawVert = mtlEffect->current_vert_raw;
+    MOJOSHADER_effectShader *rawFrag = mtlEffect->current_frag_raw;
+
+    /* Used for shader selection from preshaders */
+    int i, j;
+    MOJOSHADER_effectValue *param;
+    float selector;
+    int shader_object;
+    int selector_ran = 0;
+
+    /* For effect passes with arrays of shaders, we have to run a preshader
+     * that determines which shader to use, based on a parameter's value.
+     * -flibit
+     */
+    // !!! FIXME: We're just running the preshaders every time. Blech. -flibit
+    #define SELECT_SHADER_FROM_PRESHADER(raw, mtls) \
+        if (raw != NULL && raw->is_preshader) \
+        { \
+            i = 0; \
+            do \
+            { \
+                param = &mtlEffect->effect->params[raw->preshader_params[i]].value; \
+                for (j = 0; j < (param->value_count >> 2); j++) \
+                    memcpy(raw->preshader->registers + raw->preshader->symbols[i].register_index + j, \
+                           param->valuesI + (j << 2), \
+                           param->type.columns << 2); \
+            } while (++i < raw->preshader->symbol_count); \
+            MOJOSHADER_runPreshader(raw->preshader, &selector); \
+            shader_object = mtlEffect->effect->params[raw->params[0]].value.valuesI[(int) selector]; \
+            raw = &mtlEffect->effect->objects[shader_object].shader; \
+            i = 0; \
+            do \
+            { \
+                if (shader_object == mtlEffect->shader_indices[i]) \
+                { \
+                    mtls = &mtlEffect->shaders[i]; \
+                    break; \
+                } \
+            } while (++i < mtlEffect->num_shaders); \
+            selector_ran = 1; \
+        }
+    SELECT_SHADER_FROM_PRESHADER(rawVert, mtlEffect->current_vert)
+    SELECT_SHADER_FROM_PRESHADER(rawFrag, mtlEffect->current_frag)
+    #undef SELECT_SHADER_FROM_PRESHADER
+    if (selector_ran)
+    {
+        if (mtlEffect->current_vert != NULL)
+            shState->vertexShader = mtlEffect->current_vert;
+
+        if (mtlEffect->current_frag != NULL)
+            shState->fragmentShader = mtlEffect->current_frag;
+
+        if (mtlEffect->current_vert_raw != NULL)
+        {
+            mtlEffect->effect->state_changes->vertex_sampler_state_changes = rawVert->samplers;
+            mtlEffect->effect->state_changes->vertex_sampler_state_change_count = rawVert->sampler_count;
+        } // if
+        if (mtlEffect->current_frag_raw != NULL)
+        {
+            mtlEffect->effect->state_changes->sampler_state_changes = rawFrag->samplers;
+            mtlEffect->effect->state_changes->sampler_state_change_count = rawFrag->sampler_count;
+        } // if
+    } // if
+
+    /* This is where parameters are copied into the constant buffers.
+     * If you're looking for where things slow down immensely, look at
+     * the copy_parameter_data() and MOJOSHADER_runPreshader() functions.
+     * -flibit
+     */
+    // !!! FIXME: We're just copying everything every time. Blech. -flibit
+    // !!! FIXME: We're just running the preshaders every time. Blech. -flibit
+    // !!! FIXME: Will the preshader ever want int/bool registers? -flibit
+    #define COPY_PARAMETER_DATA(raw, stage) \
+        if (raw != NULL) \
+        { \
+            copy_parameter_data(mtlEffect->effect->params, raw->params, \
+                                raw->shader->symbols, \
+                                raw->shader->symbol_count, \
+                                stage##_reg_file_f, \
+                                stage##_reg_file_i, \
+                                stage##_reg_file_b); \
+            if (raw->shader->preshader) \
+            { \
+                copy_parameter_data(mtlEffect->effect->params, raw->preshader_params, \
+                                    raw->shader->preshader->symbols, \
+                                    raw->shader->preshader->symbol_count, \
+                                    raw->shader->preshader->registers, \
+                                    NULL, \
+                                    NULL); \
+                MOJOSHADER_runPreshader(raw->shader->preshader, stage##_reg_file_f); \
+            } \
+        }
+    COPY_PARAMETER_DATA(rawVert, vs)
+    COPY_PARAMETER_DATA(rawFrag, ps)
+    #undef COPY_PARAMETER_DATA
+
+    update_uniform_buffer(shState->vertexShader);
+    shState->vertexUniformBuffer = get_uniform_buffer(shState->vertexShader);
+    shState->vertexUniformOffset = get_uniform_offset(shState->vertexShader);
+
+    update_uniform_buffer(shState->fragmentShader);
+    shState->fragmentUniformBuffer = get_uniform_buffer(shState->fragmentShader);
+    shState->fragmentUniformOffset = get_uniform_offset(shState->fragmentShader);
+} // MOJOSHADER_mtlEffectCommitChanges
+
+
+void MOJOSHADER_mtlEffectEndPass(MOJOSHADER_mtlEffect *mtlEffect)
+{
+    assert(mtlEffect->effect->current_pass != -1);
+    mtlEffect->effect->current_pass = -1;
+} // MOJOSHADER_mtlEffectEndPass
+
+
+void MOJOSHADER_mtlEffectEnd(MOJOSHADER_mtlEffect *mtlEffect,
+                             MOJOSHADER_mtlShaderState *shState)
+{
+    if (mtlEffect->effect->restore_shader_state)
+    {
+        mtlEffect->effect->restore_shader_state = 0;
+        shState->vertexShader = mtlEffect->prev_vert;
+        shState->fragmentShader = mtlEffect->prev_frag;
+        shState->vertexUniformBuffer = get_uniform_buffer(mtlEffect->prev_vert);
+        shState->fragmentUniformBuffer = get_uniform_buffer(mtlEffect->prev_frag);
+        shState->vertexUniformOffset = get_uniform_offset(mtlEffect->prev_vert);
+        shState->fragmentUniformOffset = get_uniform_offset(mtlEffect->prev_frag);
+    } // if
+
+    mtlEffect->effect->state_changes = NULL;
+} // MOJOSHADER_mtlEffectEnd
+
+void *MOJOSHADER_mtlGetFunctionHandle(MOJOSHADER_mtlShader *shader)
+{
+    if (shader == NULL)
+        return NULL;
+
+    void *fnname = cstr_to_nsstr(shader->parseData->mainfn);
+    void *ret = objc_msgSend_PTR(
+        shader->library,
+        selNewFunctionWithName,
+        fnname
+    );
+    objc_msgSend(fnname, selRelease);
+    objc_msgSend(ret, selRetain);
+
+    return ret;
+} // MOJOSHADER_mtlGetFunctionHandle
+
+void MOJOSHADER_mtlEndFrame()
+{
+    LLNODE *node = ubos;
+    while (node != NULL)
+    {
+        UBO_end_frame((MOJOSHADER_mtlUniformBuffer *) node->data);
+        node = node->next;
+    } // while
+} // MOJOSHADER_mtlEndFrame
+
+int MOJOSHADER_mtlGetVertexAttribLocation(MOJOSHADER_mtlShader *vert,
+                                          MOJOSHADER_usage usage, int index)
+{
+    if (vert == NULL)
+        return -1;
+
+    for (int i = 0; i < vert->parseData->attribute_count; i++)
+    {
+        if (vert->parseData->attributes[i].usage == usage
+            && vert->parseData->attributes[i].index == index)
+        {
+            return i;
+        } // if
+    } // for
+
+    // failure, couldn't find requested attribute
+    return -1;
+} // MOJOSHADER_mtlGetVertexAttribLocation
+
+const char *MOJOSHADER_mtlGetError(void)
+{
+    return error_buffer;
+} // MOJOSHADER_mtlGetError
+
+#endif /* MOJOSHADER_EFFECT_SUPPORT */
+#endif /* SUPPORT_PROFILE_METAL && PLATFORM_APPLE */
+
+// end of mojoshader_metal.c ...