/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2018      - Stuart Carnie
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <Foundation/Foundation.h>
#include <Metal/Metal.h>
#include <MetalKit/MetalKit.h>
#include <QuartzCore/QuartzCore.h>

#include <stdio.h>
#include <stdint.h>
#include <math.h>
#include <memory.h>
#include <string.h>

#include <simd/simd.h>

#include <compat/strl.h>
#include <gfx/scaler/scaler.h>
#include <gfx/video_frame.h>
#include <formats/image.h>
#include <retro_inline.h>
#include <retro_miscellaneous.h>
#include <retro_math.h>
#include <libretro.h>

#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif

#ifdef HAVE_MENU
#include "../../menu/menu_driver.h"
#endif
#ifdef HAVE_GFX_WIDGETS
#include "../gfx_widgets.h"
#endif

#include "../font_driver.h"

#include "../common/metal_common.h"

#include "../../driver.h"
#include "../../configuration.h"

#include "../../retroarch.h"
#ifdef HAVE_REWIND
#include "../../state_manager.h"
#endif
#include "../../verbosity.h"

#include "../video_coord_array.h"

#include "../../ui/drivers/cocoa/apple_platform.h"
#include "../../ui/drivers/cocoa/cocoa_common.h"

#define STRUCT_ASSIGN(x, y) \
{ \
   NSObject * __y = y; \
   if (x != nil) { \
      NSObject * __foo = (__bridge_transfer NSObject *)(__bridge void *)(x); \
      __foo = nil; \
      x = (__bridge __typeof__(x))nil; \
   } \
   if (__y != nil) \
      x = (__bridge __typeof__(x))(__bridge_retained void *)((NSObject *)__y); \
   }

@implementation MetalView

#if !defined(HAVE_COCOATOUCH)
- (void)keyDown:(NSEvent*)theEvent { }
#endif

/* Stop the annoying sound when pressing a key. */
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)isFlipped { return YES; }
@end

#pragma mark - private categories

@interface FrameView()

@property (nonatomic, readwrite) video_viewport_t *viewport;

- (instancetype)initWithDescriptor:(ViewDescriptor *)td context:(Context *)context;
- (void)drawWithContext:(Context *)ctx;
- (void)drawWithEncoder:(id<MTLRenderCommandEncoder>)rce;

@end

@interface MetalMenu()
@property (nonatomic, readonly) TexturedView *view;
- (instancetype)initWithContext:(Context *)context;
@end

@interface Overlay()
- (instancetype)initWithContext:(Context *)context;
- (void)drawWithEncoder:(id<MTLRenderCommandEncoder>)rce;
@end

@implementation MetalDriver
{
   FrameView *_frameView;
   MetalMenu *_menu;
   Overlay *_overlay;

   video_info_t _video;

   id<MTLDevice> _device;
   id<MTLLibrary> _library;
   Context *_context;

   CAMetalLayer *_layer;

   /* Render target layer state */
   id<MTLRenderPipelineState> _t_pipelineState;
   id<MTLRenderPipelineState> _t_pipelineStateNoAlpha;

   id<MTLSamplerState> _samplerStateLinear;
   id<MTLSamplerState> _samplerStateNearest;

   /* other state */
   Uniforms _viewportMVP;
}

- (instancetype)initWithVideo:(const video_info_t *)video
                        input:(input_driver_t **)input
                    inputData:(void **)inputData
{
   if (self = [super init])
   {
      _device                       = MTLCreateSystemDefaultDevice();
      MetalView *view               = (MetalView *)apple_platform.renderView;
      view.device                   = _device;
      view.delegate                 = self;
      _layer                        = (CAMetalLayer *)view.layer;

      if (![self _initMetal])
         return nil;

      _video                        = *video;
      _viewport                     = (video_viewport_t *)calloc(1, sizeof(video_viewport_t));
      _viewportMVP.projectionMatrix = matrix_proj_ortho(0, 1, 0, 1);

      _keepAspect                   = _video.force_aspect;

      gfx_ctx_mode_t mode = {
         .width = _video.width,
         .height = _video.height,
         .fullscreen = _video.fullscreen,
      };

      if (mode.width == 0 || mode.height == 0)
      {
         /* 0 indicates full screen, so we'll use the view's dimensions, 
          * which should already be full screen
          * If this turns out to be the wrong assumption, we can use NSScreen 
          * to query the dimensions */
         CGSize size = view.frame.size;
         mode.width  = (unsigned int)size.width;
         mode.height = (unsigned int)size.height;
      }

      [apple_platform setVideoMode:mode];

#ifdef HAVE_COCOATOUCH
      [self mtkView:view drawableSizeWillChange:CGSizeMake(mode.width, mode.height)];
#endif

      *input         = NULL;
      *inputData     = NULL;
      /* graphics display driver */
      _display       = [[MenuDisplay alloc] initWithContext:_context];
      /* menu view */
      _menu          = [[MetalMenu alloc] initWithContext:_context];

      /* Framebuffer view */
      {
         ViewDescriptor *vd  = [ViewDescriptor new];
         vd.format           = _video.rgb32 ? RPixelFormatBGRX8Unorm : RPixelFormatB5G6R5Unorm;
         vd.size             = CGSizeMake(video->width, video->height);
         vd.filter           = _video.smooth ? RTextureFilterLinear : RTextureFilterNearest;
         _frameView          = [[FrameView alloc] initWithDescriptor:vd context:_context];
         _frameView.viewport = _viewport;
         [_frameView setFilteringIndex:0 smooth:video->smooth];
      }

      /* Overlay view */
      _overlay = [[Overlay alloc] initWithContext:_context];

      font_driver_init_osd((__bridge void *)self,
            video,
            false,
            video->is_threaded,
            FONT_DRIVER_RENDER_METAL_API);
   }
   return self;
}

- (void)dealloc
{
   if (_viewport)
   {
      free(_viewport);
      _viewport = nil;
   }
   font_driver_free_osd();
}

- (bool)_initMetal
{
   _library = [_device newDefaultLibrary];
   _context = [[Context alloc] initWithDevice:_device
                                        layer:_layer
                                      library:_library];

   {
      NSError *err;
      MTLRenderPipelineDescriptor *psd;
      MTLRenderPipelineColorAttachmentDescriptor *ca;
      MTLVertexDescriptor *vd        = [MTLVertexDescriptor new];
      vd.attributes[0].offset        = 0;
      vd.attributes[0].format        = MTLVertexFormatFloat3;
      vd.attributes[1].offset        = offsetof(Vertex, texCoord);
      vd.attributes[1].format        = MTLVertexFormatFloat2;
      vd.layouts[0].stride           = sizeof(Vertex);

      psd                            = [MTLRenderPipelineDescriptor new];
      psd.label                      = @"Pipeline+Alpha";

      ca                             = psd.colorAttachments[0];
      ca.pixelFormat                 = _layer.pixelFormat;
      ca.blendingEnabled             = YES;
      ca.sourceAlphaBlendFactor      = MTLBlendFactorSourceAlpha;
      ca.sourceRGBBlendFactor        = MTLBlendFactorSourceAlpha;
      ca.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
      ca.destinationRGBBlendFactor   = MTLBlendFactorOneMinusSourceAlpha;

      psd.sampleCount                = 1;
      psd.vertexDescriptor           = vd;
      psd.vertexFunction             = [_library newFunctionWithName:@"basic_vertex_proj_tex"];
      psd.fragmentFunction           = [_library newFunctionWithName:@"basic_fragment_proj_tex"];


      _t_pipelineState = [_device newRenderPipelineStateWithDescriptor:psd error:&err];
      if (err != nil)
      {
         RARCH_ERR("[Metal]: error creating pipeline state %s\n", err.localizedDescription.UTF8String);
         return NO;
      }

      psd.label               = @"Pipeline+No Alpha";
      ca.blendingEnabled      = NO;
      _t_pipelineStateNoAlpha = [_device newRenderPipelineStateWithDescriptor:psd error:&err];
      if (err != nil)
      {
         RARCH_ERR("[Metal]: error creating pipeline state (no alpha) %s\n", err.localizedDescription.UTF8String);
         return NO;
      }
   }

   {
      MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];
      _samplerStateNearest     = [_device newSamplerStateWithDescriptor:sd];

      sd.minFilter             = MTLSamplerMinMagFilterLinear;
      sd.magFilter             = MTLSamplerMinMagFilterLinear;
      _samplerStateLinear      = [_device newSamplerStateWithDescriptor:sd];
   }

   return YES;
}

- (void)setViewportWidth:(unsigned)width height:(unsigned)height forceFull:(BOOL)forceFull allowRotate:(BOOL)allowRotate
{
   _viewport->full_width   = width;
   _viewport->full_height  = height;
   video_driver_set_size(_viewport->full_width, _viewport->full_height);
   _layer.drawableSize     = CGSizeMake(width, height);
   video_driver_update_viewport(_viewport, forceFull, _keepAspect);
   _context.viewport       = _viewport; /* Update matrix */
   _viewportMVP.outputSize = simd_make_float2(_viewport->full_width, _viewport->full_height);
}

#pragma mark - video

- (void)setVideo:(const video_info_t *)video { }

- (bool)renderFrame:(const void *)frame
               data:(void*)data
              width:(unsigned)width
             height:(unsigned)height
         frameCount:(uint64_t)frameCount
              pitch:(unsigned)pitch
                msg:(const char *)msg
               info:(video_frame_info_t *)video_info
{
   @autoreleasepool
   {
      bool statistics_show = video_info->statistics_show;

      [self _beginFrame];

      _frameView.frameCount = frameCount;
      if (frame && width && height)
      {
         _frameView.size = CGSizeMake(width, height);
         [_frameView updateFrame:frame pitch:pitch];
      }

      [self _drawCore];
      [self _drawMenu:video_info];

#ifdef HAVE_OVERLAY
       id<MTLRenderCommandEncoder> rce = _context.rce;

      if (_overlay.enabled)
      {
         [_context resetRenderViewport:_overlay.fullscreen ? kFullscreenViewport : kVideoViewport];
         [rce setRenderPipelineState:[_context getStockShader:VIDEO_SHADER_STOCK_BLEND blend:YES]];
         [rce setVertexBytes:_context.uniforms length:sizeof(*_context.uniforms) atIndex:BufferIndexUniforms];
         [rce setFragmentSamplerState:_samplerStateLinear atIndex:SamplerIndexDraw];
         [_overlay drawWithEncoder:rce];
      }
#endif

      if (statistics_show)
      {
         struct font_params *osd_params = (struct font_params *)&video_info->osd_stat_params;

         if (osd_params)
            font_driver_render_msg(data, video_info->stat_text, osd_params, NULL);
      }

#ifdef HAVE_GFX_WIDGETS
      if (video_info->widgets_active)
         gfx_widgets_frame(video_info);
#endif

      if (msg && *msg)
         [self _renderMessage:msg data:data];
      [self _endFrame];
   }

   return YES;
}

- (void)_renderMessage:(const char *)msg
                  data:(void*)data
{
   settings_t *settings     = config_get_ptr();
   bool msg_bgcolor_enable  = settings->bools.video_msg_bgcolor_enable;

   if (msg_bgcolor_enable)
   {
      float r, g, b, a;
      int msg_width         =
         font_driver_get_message_width(NULL, msg, strlen(msg), 1.0f);
      float font_size       = settings->floats.video_font_size;
      unsigned bgcolor_red 
                            = settings->uints.video_msg_bgcolor_red;
      unsigned bgcolor_green
                            = settings->uints.video_msg_bgcolor_green;
      unsigned bgcolor_blue 
                            = settings->uints.video_msg_bgcolor_blue;
      float bgcolor_opacity = settings->floats.video_msg_bgcolor_opacity;
      float x               = settings->floats.video_msg_pos_x;
      float y               = 1.0f - settings->floats.video_msg_pos_y;
      float width           = msg_width / (float)_viewport->full_width;
      float height          = font_size / (float)_viewport->full_height;

      float x2              = 0.005f; /* extend background around text */
      float y2              = 0.005f;

      y                    -= height;

      x                    -= x2;
      y                    -= y2;
      width                += x2;
      height               += y2;

      r                     = bgcolor_red / 255.0f;
      g                     = bgcolor_green / 255.0f;
      b                     = bgcolor_blue / 255.0f;
      a                     = bgcolor_opacity;

      [_context resetRenderViewport:kFullscreenViewport];
      [_context drawQuadX:x y:y w:width h:height r:r g:g b:b a:a];
   }

   font_driver_render_msg(data, msg, NULL, NULL);
}

- (void)_beginFrame
{
   video_viewport_t vp = *_viewport;
   video_driver_update_viewport(_viewport, NO, _keepAspect);

   if (memcmp(&vp, _viewport, sizeof(vp)) != 0)
      _context.viewport = _viewport;

   [_context begin];
}

- (void)_drawCore
{
   id<MTLRenderCommandEncoder> rce = _context.rce;

   /* draw back buffer */
   [_frameView drawWithContext:_context];

   if ((_frameView.drawState & ViewDrawStateEncoder) != 0)
   {
      [rce setVertexBytes:_context.uniforms length:sizeof(*_context.uniforms) atIndex:BufferIndexUniforms];
      [rce setRenderPipelineState:_t_pipelineStateNoAlpha];
      if (_frameView.filter == RTextureFilterNearest)
         [rce setFragmentSamplerState:_samplerStateNearest atIndex:SamplerIndexDraw];
      else
         [rce setFragmentSamplerState:_samplerStateLinear atIndex:SamplerIndexDraw];
      [_frameView drawWithEncoder:rce];
   }
}

- (void)_drawMenu:(video_frame_info_t *)video_info
{
   bool menu_is_alive = video_info->menu_is_alive;

   if (!_menu.enabled)
      return;

   id<MTLRenderCommandEncoder> rce = _context.rce;

   if (_menu.hasFrame)
   {
      [_menu.view drawWithContext:_context];
      [rce setVertexBytes:_context.uniforms length:sizeof(*_context.uniforms) atIndex:BufferIndexUniforms];
      [rce setRenderPipelineState:_t_pipelineState];
      if (_menu.view.filter == RTextureFilterNearest)
         [rce setFragmentSamplerState:_samplerStateNearest atIndex:SamplerIndexDraw];
      else
         [rce setFragmentSamplerState:_samplerStateLinear atIndex:SamplerIndexDraw];
      [_menu.view drawWithEncoder:rce];
   }
#if defined(HAVE_MENU)
   else
   {
      [_context resetRenderViewport:kFullscreenViewport];
      menu_driver_frame(menu_is_alive, video_info);
   }
#endif
}

- (void)_endFrame { [_context end]; }
/* TODO/FIXME (sgc): resize*/
- (void)setNeedsResize { }
- (void)setRotation:(unsigned)rotation { [_context setRotation:rotation]; }
- (Uniforms *)viewportMVP { return &_viewportMVP; }

#pragma mark - MTKViewDelegate

- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
{
#ifdef HAVE_COCOATOUCH
    CGFloat scale = [[UIScreen mainScreen] scale];
    [self setViewportWidth:(unsigned int)view.bounds.size.width*scale height:(unsigned int)view.bounds.size.height*scale forceFull:NO allowRotate:YES];
#else
   [self setViewportWidth:(unsigned int)size.width height:(unsigned int)size.height forceFull:NO allowRotate:YES];
#endif
}

- (void)drawInMTKView:(MTKView *)view { }
@end

@implementation MetalMenu
{
   Context *_context;
   TexturedView *_view;
   bool _enabled;
}

- (instancetype)initWithContext:(Context *)context
{
   if (self = [super init])
      _context = context;
   return self;
}

- (bool)hasFrame { return _view != nil; }

- (void)setEnabled:(bool)enabled
{
   if (_enabled == enabled)
      return;
   _enabled      = enabled;
   _view.visible = enabled;
}

- (bool)enabled { return _enabled; }

- (void)updateWidth:(int)width
             height:(int)height
             format:(RPixelFormat)format
             filter:(RTextureFilter)filter
{
   CGSize size = CGSizeMake(width, height);

   if (_view)
   {
      if (!(CGSizeEqualToSize(_view.size, size) &&
            _view.format == format &&
            _view.filter == filter))
         _view = nil;
   }

   if (!_view)
   {
      ViewDescriptor *vd = [ViewDescriptor new];
      vd.format          = format;
      vd.filter          = filter;
      vd.size            = size;
      _view              = [[TexturedView alloc] initWithDescriptor:vd context:_context];
      _view.visible      = _enabled;
   }
}

- (void)updateFrame:(void const *)source
{
   [_view updateFrame:source pitch:RPixelFormatToBPP(_view.format) * (NSUInteger)_view.size.width];
}

@end

#pragma mark - FrameView

#define MTLALIGN(x) __attribute__((aligned(x)))

typedef struct
{
   float x;
   float y;
   float z;
   float w;
} float4_t;

typedef struct texture
{
   __unsafe_unretained id<MTLTexture> view;
   float4_t size_data;
} texture_t;

typedef struct MTLALIGN(16)
{
   matrix_float4x4 mvp;

   struct
   {
      texture_t texture[GFX_MAX_FRAME_HISTORY + 1];
      MTLViewport viewport;
      float4_t output_size;
   } frame;

   struct
   {
      __unsafe_unretained id<MTLBuffer> buffers[SLANG_CBUFFER_MAX];
      texture_t rt;
      texture_t feedback;
      uint32_t frame_count;
      int32_t frame_direction;
      pass_semantics_t semantics;
      MTLViewport viewport;
      __unsafe_unretained id<MTLRenderPipelineState> _state;
   } pass[GFX_MAX_SHADERS];

   texture_t luts[GFX_MAX_TEXTURES];

} engine_t;

@implementation FrameView
{
   Context *_context;
   id<MTLTexture> _texture; /* final render texture */
   Vertex _v[4];
   VertexSlang _vertex[4];
   CGSize _size; /* size of view in pixels */
   CGRect _frame;
   NSUInteger _bpp;

   id<MTLTexture> _src; /* source texture */
   bool _srcDirty;

   id<MTLSamplerState> _samplers[RARCH_FILTER_MAX][RARCH_WRAP_MAX];
   struct video_shader *_shader;

   engine_t _engine;

   bool resize_render_targets;
   bool init_history;
   video_viewport_t *_viewport;
}

- (instancetype)initWithDescriptor:(ViewDescriptor *)d context:(Context *)c
{
   self = [super init];
   if (self)
   {
      _context              = c;
      _format               = d.format;
      _bpp                  = RPixelFormatToBPP(_format);
      _filter               = d.filter;
      if (_format == RPixelFormatBGRA8Unorm || _format == RPixelFormatBGRX8Unorm)
         _drawState         = ViewDrawStateEncoder;
      else
         _drawState         = ViewDrawStateAll;
      _visible              = YES;
      _engine.mvp           = matrix_proj_ortho(0, 1, 0, 1);
      [self _initSamplers];

      self.size             = d.size;
      self.frame            = CGRectMake(0, 0, 1, 1);
      resize_render_targets = YES;

      /* Initialize slang vertex buffer */
      VertexSlang v[4]      = {
         {simd_make_float4(0, 1, 0, 1), simd_make_float2(0, 1)},
         {simd_make_float4(1, 1, 0, 1), simd_make_float2(1, 1)},
         {simd_make_float4(0, 0, 0, 1), simd_make_float2(0, 0)},
         {simd_make_float4(1, 0, 0, 1), simd_make_float2(1, 0)},
      };
      memcpy(_vertex, v, sizeof(_vertex));
   }
   return self;
}

- (void)_initSamplers
{
   int i;
   MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];

   /* Initialize samplers */
   for (i = 0; i < RARCH_WRAP_MAX; i++)
   {
      switch (i)
      {
         case RARCH_WRAP_BORDER:
#if defined(HAVE_COCOATOUCH)
            sd.sAddressMode = MTLSamplerAddressModeClampToZero;
#else
            sd.sAddressMode = MTLSamplerAddressModeClampToBorderColor;
#endif
            break;

         case RARCH_WRAP_EDGE:
            sd.sAddressMode = MTLSamplerAddressModeClampToEdge;
            break;

         case RARCH_WRAP_REPEAT:
            sd.sAddressMode = MTLSamplerAddressModeRepeat;
            break;

         case RARCH_WRAP_MIRRORED_REPEAT:
            sd.sAddressMode = MTLSamplerAddressModeMirrorRepeat;
            break;

         default:
            continue;
      }
      sd.tAddressMode        = sd.sAddressMode;
      sd.rAddressMode        = sd.sAddressMode;
      sd.minFilter           = MTLSamplerMinMagFilterLinear;
      sd.magFilter           = MTLSamplerMinMagFilterLinear;

      id<MTLSamplerState> ss = [_context.device newSamplerStateWithDescriptor:sd];
      _samplers[RARCH_FILTER_LINEAR][i] = ss;

      sd.minFilter           = MTLSamplerMinMagFilterNearest;
      sd.magFilter           = MTLSamplerMinMagFilterNearest;

      ss                     = [_context.device newSamplerStateWithDescriptor:sd];
      _samplers[RARCH_FILTER_NEAREST][i] = ss;
   }
}

- (void)setFilteringIndex:(int)index smooth:(bool)smooth
{
   int i;
   for (i = 0; i < RARCH_WRAP_MAX; i++)
   {
      if (smooth)
         _samplers[RARCH_FILTER_UNSPEC][i] = _samplers[RARCH_FILTER_LINEAR][i];
      else
         _samplers[RARCH_FILTER_UNSPEC][i] = _samplers[RARCH_FILTER_NEAREST][i];
   }
}

- (void)setSize:(CGSize)size
{
   if (CGSizeEqualToSize(_size, size))
      return;

   _size                 = size;
   resize_render_targets = YES;

   if (   _format != RPixelFormatBGRA8Unorm
       && _format != RPixelFormatBGRX8Unorm)
   {
      MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatR16Uint
                                 width:(NSUInteger)size.width
                                 height:(NSUInteger)size.height
                                 mipmapped:NO];
      _src = [_context.device newTextureWithDescriptor:td];
   }
}

- (CGSize)size { return _size; }

- (void)setFrame:(CGRect)frame
{
   if (CGRectEqualToRect(_frame, frame))
      return;

   /* update vertices */
   CGPoint o   = frame.origin;
   CGSize  s   = frame.size;

   CGFloat l   = o.x;
   CGFloat t   = o.y;
   CGFloat r   = o.x + s.width;
   CGFloat b   = o.y + s.height;

   Vertex v[4] = {
      {simd_make_float3(l, b, 0), simd_make_float2(0, 1)},
      {simd_make_float3(r, b, 0), simd_make_float2(1, 1)},
      {simd_make_float3(l, t, 0), simd_make_float2(0, 0)},
      {simd_make_float3(r, t, 0), simd_make_float2(1, 0)},
   };
   
   _frame      = frame;
   memcpy(_v, v, sizeof(_v));
}

- (CGRect)frame { return _frame; }

- (void)_convertFormat
{
   if (   _format == RPixelFormatBGRA8Unorm
       || _format == RPixelFormatBGRX8Unorm)
      return;

   if (!_srcDirty)
      return;

   [_context convertFormat:_format from:_src to:_texture];
   _srcDirty = NO;
}

- (void)_updateHistory
{
   if (_shader)
   {
      if (_shader->history_size)
      {
         if (init_history)
            [self _initHistory];
         else
         {
            int k;
            /* TODO/FIXME: what about frame-duping ?
             * maybe clone d3d10_texture_t with AddRef */
            texture_t tmp = _engine.frame.texture[_shader->history_size];
            for (k = _shader->history_size; k > 0; k--)
               _engine.frame.texture[k] = _engine.frame.texture[k - 1];
            _engine.frame.texture[0] = tmp;
         }
      }
   }

   /* Either no history, or we moved a texture of a different size in the front slot */
   if (   _engine.frame.texture[0].size_data.x != _size.width
       || _engine.frame.texture[0].size_data.y != _size.height)
   {
      MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
               width:(NSUInteger)_size.width
               height:(NSUInteger)_size.height
               mipmapped:false];
      td.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite;
      [self _initTexture:&_engine.frame.texture[0] withDescriptor:td];
   }
}

- (bool)readViewport:(uint8_t *)buffer isIdle:(bool)isIdle
{
   bool res;
   bool enabled = _context.captureEnabled;
   if (!enabled)
      _context.captureEnabled = YES;

   video_driver_cached_frame();

   res = [_context readBackBuffer:buffer];

   if (!enabled)
      _context.captureEnabled = NO;

   return res;
}

- (void)updateFrame:(void const *)src pitch:(NSUInteger)pitch
{
   if (_shader && (_engine.frame.output_size.x != _viewport->width
               ||  _engine.frame.output_size.y != _viewport->height))
      resize_render_targets       = YES;

   _engine.frame.viewport.originX = _viewport->x;
   _engine.frame.viewport.originY = _viewport->y;
   _engine.frame.viewport.width   = _viewport->width;
   _engine.frame.viewport.height  = _viewport->height;
   _engine.frame.viewport.znear   = 0.0f;
   _engine.frame.viewport.zfar    = 1.0f;
   _engine.frame.output_size.x    = _viewport->width;
   _engine.frame.output_size.y    = _viewport->height;
   _engine.frame.output_size.z    = 1.0f / _viewport->width;
   _engine.frame.output_size.w    = 1.0f / _viewport->height;

   if (resize_render_targets)
      [self _updateRenderTargets];

   [self _updateHistory];

   if (   _format == RPixelFormatBGRA8Unorm
       || _format == RPixelFormatBGRX8Unorm)
   {
      id<MTLTexture> tex = _engine.frame.texture[0].view;
      [tex replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)_size.width, (NSUInteger)_size.height)
             mipmapLevel:0 withBytes:src
             bytesPerRow:pitch];
   }
   else
   {
      [_src replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)_size.width, (NSUInteger)_size.height)
              mipmapLevel:0 withBytes:src
              bytesPerRow:(NSUInteger)(pitch)];
      _srcDirty = YES;
   }
}

- (void)_initTexture:(texture_t *)t withDescriptor:(MTLTextureDescriptor *)td
{
   STRUCT_ASSIGN(t->view, [_context.device newTextureWithDescriptor:td]);
   t->size_data.x = td.width;
   t->size_data.y = td.height;
   t->size_data.z = 1.0f / td.width;
   t->size_data.w = 1.0f / td.height;
}

- (void)_initHistory
{
   int i;
   MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
            width:(NSUInteger)_size.width
            height:(NSUInteger)_size.height
            mipmapped:false];
   td.usage = MTLTextureUsageShaderRead
            | MTLTextureUsageShaderWrite
            | MTLTextureUsageRenderTarget;

   for (i = 0; i < _shader->history_size + 1; i++)
      [self _initTexture:&_engine.frame.texture[i] withDescriptor:td];
   init_history = NO;
}

- (void)drawWithEncoder:(id<MTLRenderCommandEncoder>)rce
{
   if (_texture)
   {
      [rce setViewport:_engine.frame.viewport];
      [rce setVertexBytes:&_v length:sizeof(_v) atIndex:BufferIndexPositions];
      [rce setFragmentTexture:_texture atIndex:TextureIndexColor];
      [rce drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
   }
}

- (void)drawWithContext:(Context *)ctx
{
   int i;
   _texture = _engine.frame.texture[0].view;
   [self _convertFormat];

   if (!_shader || _shader->passes == 0)
      return;

   for (i = 0; i < _shader->passes; i++)
   {
      if (_shader->pass[i].feedback)
      {
         texture_t tmp            = _engine.pass[i].feedback;
         _engine.pass[i].feedback = _engine.pass[i].rt;
         _engine.pass[i].rt       = tmp;
      }
   }

   id<MTLCommandBuffer> cb = ctx.blitCommandBuffer;

   MTLRenderPassDescriptor *rpd        = [MTLRenderPassDescriptor new];
   rpd.colorAttachments[0].loadAction  = MTLLoadActionDontCare;
   rpd.colorAttachments[0].storeAction = MTLStoreActionStore;

   for (i = 0; i < _shader->passes; i++)
   {
      int j;
      __unsafe_unretained id<MTLTexture> textures[SLANG_NUM_BINDINGS] = {NULL};
      id<MTLSamplerState> samplers[SLANG_NUM_BINDINGS] = {NULL};
      id<MTLRenderCommandEncoder> rce                  = nil;
      BOOL backBuffer                                  = (_engine.pass[i].rt.view == nil);

      if (backBuffer)
         rce = _context.rce;
      else
      {
         rpd.colorAttachments[0].texture = _engine.pass[i].rt.view;
         rce = [cb renderCommandEncoderWithDescriptor:rpd];
      }

      [rce setRenderPipelineState:_engine.pass[i]._state];

      NSURL *shaderPath = [NSURL fileURLWithPath:_engine.pass[i]._state.label];
      rce.label = shaderPath.lastPathComponent.stringByDeletingPathExtension;

      _engine.pass[i].frame_count = (uint32_t)_frameCount;
      if (_shader->pass[i].frame_count_mod)
         _engine.pass[i].frame_count %= _shader->pass[i].frame_count_mod;

#ifdef HAVE_REWIND
      if (state_manager_frame_is_reversed())
         _engine.pass[i].frame_direction = -1;
      else
#else
         _engine.pass[i].frame_direction = 1;
#endif

      for (j = 0; j < SLANG_CBUFFER_MAX; j++)
      {
         id<MTLBuffer> buffer      = _engine.pass[i].buffers[j];
         cbuffer_sem_t *buffer_sem = &_engine.pass[i].semantics.cbuffers[j];

         if (buffer_sem->stage_mask && buffer_sem->uniforms)
         {
            void             *data = buffer.contents;
            uniform_sem_t *uniform = buffer_sem->uniforms;

            while (uniform->size)
            {
               if (uniform->data)
                  memcpy((uint8_t *)data + uniform->offset, uniform->data, uniform->size);
               uniform++;
            }

            if (buffer_sem->stage_mask & SLANG_STAGE_VERTEX_MASK)
               [rce setVertexBuffer:buffer offset:0 atIndex:buffer_sem->binding];

            if (buffer_sem->stage_mask & SLANG_STAGE_FRAGMENT_MASK)
               [rce setFragmentBuffer:buffer offset:0 atIndex:buffer_sem->binding];
#if !defined(HAVE_COCOATOUCH)
            [buffer didModifyRange:NSMakeRange(0, buffer.length)];
#endif
         }
      }

      texture_sem_t *texture_sem = _engine.pass[i].semantics.textures;
      while (texture_sem->stage_mask)
      {
         int binding        = texture_sem->binding;
         id<MTLTexture> tex = (__bridge id<MTLTexture>)*(void **)texture_sem->texture_data;
         textures[binding]  = tex;
         samplers[binding]  = _samplers[texture_sem->filter][texture_sem->wrap];
         texture_sem++;
      }

      if (backBuffer)
         [rce setViewport:_engine.frame.viewport];
      else
         [rce setViewport:_engine.pass[i].viewport];

      [rce setFragmentTextures:textures withRange:NSMakeRange(0, SLANG_NUM_BINDINGS)];
      [rce setFragmentSamplerStates:samplers withRange:NSMakeRange(0, SLANG_NUM_BINDINGS)];
      [rce setVertexBytes:_vertex length:sizeof(_vertex) atIndex:4];
      [rce drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];

      if (!backBuffer)
         [rce endEncoding];

      _texture = _engine.pass[i].rt.view;
   }

   if (_texture)
      _drawState = ViewDrawStateAll;
   else
      _drawState = ViewDrawStateContext;
}

- (void)_updateRenderTargets
{
   int i;
   NSUInteger width, height;
   if (!_shader || !resize_render_targets)
      return;

   /* Release existing targets */
   for (i = 0; i < _shader->passes; i++)
   {
      STRUCT_ASSIGN(_engine.pass[i].rt.view, nil);
      STRUCT_ASSIGN(_engine.pass[i].feedback.view, nil);
      memset(&_engine.pass[i].rt, 0, sizeof(_engine.pass[i].rt));
      memset(&_engine.pass[i].feedback, 0, sizeof(_engine.pass[i].feedback));
   }

   width  = (NSUInteger)_size.width;
   height = (NSUInteger)_size.height;

   for (i = 0; i < _shader->passes; i++)
   {
      struct video_shader_pass *shader_pass = &_shader->pass[i];

      if (shader_pass->fbo.flags & FBO_SCALE_FLAG_VALID)
      {
         switch (shader_pass->fbo.type_x)
         {
            case RARCH_SCALE_INPUT:
               width *= shader_pass->fbo.scale_x;
               break;

            case RARCH_SCALE_VIEWPORT:
               width = (NSUInteger)(_viewport->width * shader_pass->fbo.scale_x);
               break;

            case RARCH_SCALE_ABSOLUTE:
               width = shader_pass->fbo.abs_x;
               break;

            default:
               break;
         }

         if (!width)
            width = _viewport->width;

         switch (shader_pass->fbo.type_y)
         {
            case RARCH_SCALE_INPUT:
               height *= shader_pass->fbo.scale_y;
               break;

            case RARCH_SCALE_VIEWPORT:
               height = (NSUInteger)(_viewport->height * shader_pass->fbo.scale_y);
               break;

            case RARCH_SCALE_ABSOLUTE:
               height = shader_pass->fbo.abs_y;
               break;

            default:
               break;
         }

         if (!height)
            height = _viewport->height;
      }
      else if (i == (_shader->passes - 1))
      {
         width  = _viewport->width;
         height = _viewport->height;
      }

      /* Updating framebuffer size */

      MTLPixelFormat fmt = SelectOptimalPixelFormat(glslang_format_to_metal(_engine.pass[i].semantics.format));
      if (   (i      != (_shader->passes - 1))
          || (width  != _viewport->width) 
          || (height != _viewport->height)
          || fmt != MTLPixelFormatBGRA8Unorm)
      {
         _engine.pass[i].viewport.width  = width;
         _engine.pass[i].viewport.height = height;
         _engine.pass[i].viewport.znear  = 0.0;
         _engine.pass[i].viewport.zfar   = 1.0;

         MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:fmt
            width:width height:height mipmapped:false];
         td.storageMode = MTLStorageModePrivate;
         td.usage       = MTLTextureUsageShaderRead
                        | MTLTextureUsageRenderTarget;

         [self _initTexture:&_engine.pass[i].rt withDescriptor:td];

         if (shader_pass->feedback)
            [self _initTexture:&_engine.pass[i].feedback withDescriptor:td];
      }
      else
      {
         _engine.pass[i].rt.size_data.x = width;
         _engine.pass[i].rt.size_data.y = height;
         _engine.pass[i].rt.size_data.z = 1.0f / width;
         _engine.pass[i].rt.size_data.w = 1.0f / height;
      }
   }

   resize_render_targets = NO;
}

- (void)_freeVideoShader:(struct video_shader *)shader
{
   int i;
   if (!shader)
      return;

   for (i = 0; i < GFX_MAX_SHADERS; i++)
   {
      int j;
      STRUCT_ASSIGN(_engine.pass[i].rt.view, nil);
      STRUCT_ASSIGN(_engine.pass[i].feedback.view, nil);
      memset(&_engine.pass[i].rt, 0, sizeof(_engine.pass[i].rt));
      memset(&_engine.pass[i].feedback, 0, sizeof(_engine.pass[i].feedback));

      STRUCT_ASSIGN(_engine.pass[i]._state, nil);

      for (j = 0; j < SLANG_CBUFFER_MAX; j++)
      {
         STRUCT_ASSIGN(_engine.pass[i].buffers[j], nil);
      }
   }

   for (i = 0; i < GFX_MAX_TEXTURES; i++)
   {
      STRUCT_ASSIGN(_engine.luts[i].view, nil);
   }

   free(shader);
}

- (BOOL)setShaderFromPath:(NSString *)path
{
   [self _freeVideoShader:_shader];
   _shader                      = nil;

   struct video_shader *shader  = (struct video_shader *)calloc(1, sizeof(*shader));
   settings_t        *settings  = config_get_ptr();
   const char *dir_video_shader = settings->paths.directory_video_shader;
   NSString *shadersPath = [NSString stringWithFormat:@"%s/", dir_video_shader];

   @try
   {
      int i;
      texture_t *source = NULL;
      if (!video_shader_load_preset_into_shader(path.UTF8String, shader))
         return NO;

      source            = &_engine.frame.texture[0];

      for (i = 0; i < shader->passes; source = &_engine.pass[i++].rt)
      {
         matrix_float4x4 *mvp = (i == shader->passes-1) 
            ? &_context.uniforms->projectionMatrix 
            : &_engine.mvp;

         /* clang-format off */
         semantics_map_t semantics_map = {
            {
               /* Original */
               {&_engine.frame.texture[0].view, 0,
                &_engine.frame.texture[0].size_data, 0},

               /* Source */
               {&source->view, 0, &source->size_data, 0},

               /* OriginalHistory */
               {&_engine.frame.texture[0].view,      sizeof(*_engine.frame.texture),
                &_engine.frame.texture[0].size_data, sizeof(*_engine.frame.texture)},

               /* PassOutput */
               {&_engine.pass[0].rt.view,      sizeof(*_engine.pass),
                &_engine.pass[0].rt.size_data, sizeof(*_engine.pass)},

               /* PassFeedback */
               {&_engine.pass[0].feedback.view,      sizeof(*_engine.pass),
                &_engine.pass[0].feedback.size_data, sizeof(*_engine.pass)},

               /* User */
               {&_engine.luts[0].view,      sizeof(*_engine.luts),
                &_engine.luts[0].size_data, sizeof(*_engine.luts)},
            },
            {
               mvp,                              /* MVP */
               &_engine.pass[i].rt.size_data,    /* OutputSize */
               &_engine.frame.output_size,       /* FinalViewportSize */
               &_engine.pass[i].frame_count,     /* FrameCount */
               &_engine.pass[i].frame_direction, /* FrameDirection */
            }
         };
         /* clang-format on */

         if (!slang_process(shader, i, RARCH_SHADER_METAL, 20000, &semantics_map, &_engine.pass[i].semantics))
            return NO;

#ifdef DEBUG
         bool save_msl    = true;
#else
         bool save_msl    = false;
#endif
         NSString *vs_src = [NSString stringWithUTF8String:shader->pass[i].source.string.vertex];
         NSString *fs_src = [NSString stringWithUTF8String:shader->pass[i].source.string.fragment];

         /* Vertex descriptor */
         @try
         {
            NSError *err;
            MTLVertexDescriptor *vd      = [MTLVertexDescriptor new];
            vd.attributes[0].offset      = offsetof(VertexSlang, position);
            vd.attributes[0].format      = MTLVertexFormatFloat4;
            vd.attributes[0].bufferIndex = 4;
            vd.attributes[1].offset      = offsetof(VertexSlang, texCoord);
            vd.attributes[1].format      = MTLVertexFormatFloat2;
            vd.attributes[1].bufferIndex = 4;
            vd.layouts[4].stride         = sizeof(VertexSlang);
            vd.layouts[4].stepFunction   = MTLVertexStepFunctionPerVertex;

            MTLRenderPipelineDescriptor *psd = [MTLRenderPipelineDescriptor new];

            psd.label = [[NSString stringWithUTF8String:shader->pass[i].source.path]
                          stringByReplacingOccurrencesOfString:shadersPath withString:@""];

            MTLRenderPipelineColorAttachmentDescriptor *ca = psd.colorAttachments[0];

            ca.pixelFormat = SelectOptimalPixelFormat(glslang_format_to_metal(_engine.pass[i].semantics.format));

            /* TODO/FIXME (sgc): confirm we never need blending for render passes */
            ca.blendingEnabled             = NO;
            ca.sourceAlphaBlendFactor      = MTLBlendFactorSourceAlpha;
            ca.sourceRGBBlendFactor        = MTLBlendFactorSourceAlpha;
            ca.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
            ca.destinationRGBBlendFactor   = MTLBlendFactorOneMinusSourceAlpha;

            psd.sampleCount                = 1;
            psd.vertexDescriptor           = vd;

            id<MTLLibrary>             lib = [_context.device newLibraryWithSource:vs_src options:nil error:&err];
            if (err != nil)
            {
               if (lib == nil)
               {
                  save_msl = true;
                  RARCH_ERR("[Metal]: unable to compile vertex shader: %s\n", err.localizedDescription.UTF8String);
                  return NO;
               }
#if DEBUG
               RARCH_WARN("[Metal]: warnings compiling vertex shader: %s\n", err.localizedDescription.UTF8String);
#endif
            }

            psd.vertexFunction = [lib newFunctionWithName:@"main0"];

            lib = [_context.device newLibraryWithSource:fs_src options:nil error:&err];
            if (err != nil)
            {
               if (lib == nil)
               {
                  save_msl = true;
                  RARCH_ERR("[Metal]: unable to compile fragment shader: %s\n", err.localizedDescription.UTF8String);
                  return NO;
               }
#if DEBUG
               RARCH_WARN("[Metal]: warnings compiling fragment shader: %s\n", err.localizedDescription.UTF8String);
#endif
            }
            psd.fragmentFunction = [lib newFunctionWithName:@"main0"];

            STRUCT_ASSIGN(_engine.pass[i]._state,
                          [_context.device newRenderPipelineStateWithDescriptor:psd error:&err]);
            if (err != nil)
            {
               save_msl = true;
               RARCH_ERR("[Metal]: error creating pipeline state for pass %d: %s\n", i,
                         err.localizedDescription.UTF8String);
               return NO;
            }

            for (unsigned j = 0; j < SLANG_CBUFFER_MAX; j++)
            {
               unsigned int size = _engine.pass[i].semantics.cbuffers[j].size;
               if (size == 0)
                  continue;

                id<MTLBuffer> buf = [_context.device newBufferWithLength:size options:PLATFORM_METAL_RESOURCE_STORAGE_MODE];
               STRUCT_ASSIGN(_engine.pass[i].buffers[j], buf);
            }
         } @finally
         {
            if (save_msl)
            {
               NSError *err = nil;
               NSString *basePath = [[NSString stringWithUTF8String:shader->pass[i].source.path] stringByDeletingPathExtension];

               /* Saving Metal shader files... */

               [vs_src writeToFile:[basePath stringByAppendingPathExtension:@"vs.metal"]
                        atomically:NO
                          encoding:NSStringEncodingConversionAllowLossy
                             error:&err];
               if (err != nil)
               {
                  RARCH_ERR("[Metal]: unable to save vertex shader source: %s\n", err.localizedDescription.UTF8String);
               }

               err = nil;
               [fs_src writeToFile:[basePath stringByAppendingPathExtension:@"fs.metal"]
                        atomically:NO
                          encoding:NSStringEncodingConversionAllowLossy
                             error:&err];
               if (err != nil)
               {
                  RARCH_ERR("[Metal]: unable to save fragment shader source: %s\n",
                            err.localizedDescription.UTF8String);
               }
            }

            free(shader->pass[i].source.string.vertex);
            free(shader->pass[i].source.string.fragment);

            shader->pass[i].source.string.vertex   = NULL;
            shader->pass[i].source.string.fragment = NULL;
         }
      }

      for (i = 0; i < shader->luts; i++)
      {
         struct texture_image image;
         image.pixels               = NULL;
         image.width                = 0;
         image.height               = 0;
         image.supports_rgba        = true;

         if (!image_texture_load(&image, shader->lut[i].path))
            return NO;

         MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
             width:image.width height:image.height
             mipmapped:shader->lut[i].mipmap];
         td.usage                 = MTLTextureUsageShaderRead;
         [self _initTexture:&_engine.luts[i] withDescriptor:td];

         [_engine.luts[i].view replaceRegion:MTLRegionMake2D(0, 0, image.width, image.height)
                                 mipmapLevel:0 withBytes:image.pixels
                                 bytesPerRow:4 * image.width];

         /* TODO/FIXME (sgc): generate mip maps */
         image_texture_free(&image);
      }
      _shader = shader;
      shader = nil;
   }
   @finally
   {
      if (shader)
         [self _freeVideoShader:shader];
   }

   resize_render_targets = YES;
   init_history          = YES;

   return YES;
}

@end

@implementation Overlay
{
   Context *_context;
   NSMutableArray<id<MTLTexture>> *_images;
   id<MTLBuffer> _vert;
   bool _vertDirty;
}

- (instancetype)initWithContext:(Context *)context
{
   if (self = [super init])
      _context = context;
   return self;
}

- (bool)loadImages:(const struct texture_image *)images count:(NSUInteger)count
{
   int i;
   [self _freeImages];

   _images = [NSMutableArray arrayWithCapacity:count];

   NSUInteger needed = sizeof(SpriteVertex) * count * 4;
   if (!_vert || _vert.length < needed)
      _vert = [_context.device newBufferWithLength:needed options:PLATFORM_METAL_RESOURCE_STORAGE_MODE];

   for (i = 0; i < count; i++)
   {
      _images[i] = [_context newTexture:images[i] mipmapped:NO];
      [self updateVertexX:0 y:0 w:1 h:1 index:i];
      [self updateTextureCoordsX:0 y:0 w:1 h:1 index:i];
      [self _updateColorRed:1.0 green:1.0 blue:1.0 alpha:1.0 index:i];
   }

   _vertDirty = YES;

   return YES;
}

- (void)drawWithEncoder:(id<MTLRenderCommandEncoder>)rce
{
   int i;
   NSUInteger count;
#if !defined(HAVE_COCOATOUCH)
   if (_vertDirty)
   {
      [_vert didModifyRange:NSMakeRange(0, _vert.length)];
      _vertDirty = NO;
   }
#endif

   count = _images.count;
   for (i = 0; i < count; ++i)
   {
      NSUInteger offset = sizeof(SpriteVertex) * 4 * i;
      [rce setVertexBuffer:_vert offset:offset atIndex:BufferIndexPositions];
      [rce setFragmentTexture:_images[i] atIndex:TextureIndexColor];
      [rce drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
   }
}

- (SpriteVertex *)_getForIndex:(NSUInteger)index
{
   SpriteVertex *pv = (SpriteVertex *)_vert.contents;
   return &pv[index * 4];
}

- (void)_updateColorRed:(float)r green:(float)g blue:(float)b alpha:(float)a index:(NSUInteger)index
{
   simd_float4 color = simd_make_float4(r, g, b, a);
   SpriteVertex *pv  = [self _getForIndex:index];
   pv[0].color       = color;
   pv[1].color       = color;
   pv[2].color       = color;
   pv[3].color       = color;
   _vertDirty        = YES;
}

- (void)updateAlpha:(float)alpha index:(NSUInteger)index
{
   [self _updateColorRed:1.0 green:1.0 blue:1.0 alpha:alpha index:index];
}

- (void)updateVertexX:(float)x y:(float)y w:(float)w h:(float)h index:(NSUInteger)index
{
   SpriteVertex *pv = [self _getForIndex:index];
   pv[0].position   = simd_make_float2(x, y);
   pv[1].position   = simd_make_float2(x + w, y);
   pv[2].position   = simd_make_float2(x, y + h);
   pv[3].position   = simd_make_float2(x + w, y + h);
   _vertDirty       = YES;
}

- (void)updateTextureCoordsX:(float)x y:(float)y w:(float)w h:(float)h index:(NSUInteger)index
{
   SpriteVertex *pv = [self _getForIndex:index];
   pv[0].texCoord   = simd_make_float2(x, y);
   pv[1].texCoord   = simd_make_float2(x + w, y);
   pv[2].texCoord   = simd_make_float2(x, y + h);
   pv[3].texCoord   = simd_make_float2(x + w, y + h);
   _vertDirty       = YES;
}

- (void)_freeImages { _images = nil; }

@end

MTLPixelFormat glslang_format_to_metal(glslang_format fmt)
{
#undef FMT2
#define FMT2(x, y) case SLANG_FORMAT_##x: return MTLPixelFormat##y

   switch (fmt)
   {
      FMT2(R8_UNORM, R8Unorm);
      FMT2(R8_SINT, R8Sint);
      FMT2(R8_UINT, R8Uint);
      FMT2(R8G8_UNORM, RG8Unorm);
      FMT2(R8G8_SINT, RG8Sint);
      FMT2(R8G8_UINT, RG8Uint);
      FMT2(R8G8B8A8_UNORM, RGBA8Unorm);
      FMT2(R8G8B8A8_SINT, RGBA8Sint);
      FMT2(R8G8B8A8_UINT, RGBA8Uint);
      FMT2(R8G8B8A8_SRGB, RGBA8Unorm_sRGB);

      FMT2(A2B10G10R10_UNORM_PACK32, RGB10A2Unorm);
      FMT2(A2B10G10R10_UINT_PACK32, RGB10A2Uint);

      FMT2(R16_UINT, R16Uint);
      FMT2(R16_SINT, R16Sint);
      FMT2(R16_SFLOAT, R16Float);
      FMT2(R16G16_UINT, RG16Uint);
      FMT2(R16G16_SINT, RG16Sint);
      FMT2(R16G16_SFLOAT, RG16Float);
      FMT2(R16G16B16A16_UINT, RGBA16Uint);
      FMT2(R16G16B16A16_SINT, RGBA16Sint);
      FMT2(R16G16B16A16_SFLOAT, RGBA16Float);

      FMT2(R32_UINT, R32Uint);
      FMT2(R32_SINT, R32Sint);
      FMT2(R32_SFLOAT, R32Float);
      FMT2(R32G32_UINT, RG32Uint);
      FMT2(R32G32_SINT, RG32Sint);
      FMT2(R32G32_SFLOAT, RG32Float);
      FMT2(R32G32B32A32_UINT, RGBA32Uint);
      FMT2(R32G32B32A32_SINT, RGBA32Sint);
      FMT2(R32G32B32A32_SFLOAT, RGBA32Float);

      case SLANG_FORMAT_UNKNOWN:
      default:
         break;
   }
#undef FMT2
   return MTLPixelFormatInvalid;
}

MTLPixelFormat SelectOptimalPixelFormat(MTLPixelFormat fmt)
{
   switch (fmt)
   {
      case MTLPixelFormatRGBA8Unorm:
         return MTLPixelFormatBGRA8Unorm;

      case MTLPixelFormatRGBA8Unorm_sRGB:
         return MTLPixelFormatBGRA8Unorm_sRGB;

      default:
         break;
   }

   return fmt;
}

static uint32_t metal_get_flags(void *data);

#pragma mark Graphics Context for Metal

/* The graphics context for the Metal driver is just a stubbed out version
 * It supports getting metrics such as DPI which is needed for iOS/tvOS */
#if defined(HAVE_COCOATOUCH)
static bool metal_gfx_ctx_get_metrics(
      void *data, enum display_metric_types type,
      float *value)
{
    CGRect  screen_rect          = [[UIScreen mainScreen] bounds];
    CGFloat scale                = [[UIScreen mainScreen] scale];
    float   physical_width       = screen_rect.size.width  * scale;
    float   physical_height      = screen_rect.size.height * scale;
    float   dpi                  = 160                     * scale;
    CGFloat max_size             = fmaxf(physical_width, physical_height);
    NSInteger idiom_type         = UI_USER_INTERFACE_IDIOM();

    switch (idiom_type)
    {
       case UIUserInterfaceIdiomPad:
          dpi = 132 * scale;
          break;
       case UIUserInterfaceIdiomPhone:
            if (max_size >= 2208.0)
                /* Larger iPhones: iPhone Plus, X, XR, XS, XS Max, 
                 * 11, 12, 13, 14, etc */
                dpi = 81 * scale;
            else
                dpi = 163 * scale;
          break;
       case UIUserInterfaceIdiomTV:
       case UIUserInterfaceIdiomCarPlay:
       case -1:
          /* TODO/FIXME */
          break;
    }

    switch (type)
    {
        case DISPLAY_METRIC_MM_WIDTH:
            *value = physical_width;
            break;
        case DISPLAY_METRIC_MM_HEIGHT:
            *value = physical_height;
            break;
        case DISPLAY_METRIC_DPI:
            *value = dpi;
            break;
        case DISPLAY_METRIC_NONE:
        default:
            *value = 0;
            return false;
    }
    return true;
}
#endif

/* Temporary workaround for metal not being able to poll flags during init */
static gfx_ctx_driver_t metal_fake_context = {
       NULL,
       NULL,
       NULL,
       NULL,
       NULL,
       NULL,
       NULL,
       NULL, /* get_refresh_rate */
       NULL, /* get_video_output_size */
       NULL, /* get_video_output_prev */
       NULL, /* get_video_output_next */
#ifdef HAVE_COCOATOUCH
       metal_gfx_ctx_get_metrics,
#else
       NULL,
#endif
       NULL, /* translate_aspect */
       NULL, /* update_title */
       NULL,
       NULL, /* set_resize */
       NULL,
       NULL,
       false,
       NULL,
       NULL,
       NULL,
       NULL, /* image_buffer_init */
       NULL, /* image_buffer_write */
       NULL, /* show_mouse */
       "metal",
       NULL,
       NULL,
       NULL,
       NULL, /* get_context_data */
       NULL  /* make_current */
};

static bool metal_set_shader(void *data,
      enum rarch_shader_type type, const char *path);

static void *metal_init(
      const video_info_t *video,
      input_driver_t **input,
      void **input_data)
{
   const char *shader_path;
   MetalDriver *md = nil;

   [apple_platform setViewType:APPLE_VIEW_TYPE_METAL];

   md = [[MetalDriver alloc] initWithVideo:video input:input inputData:input_data];
   if (md == nil)
      return NULL;

   metal_fake_context.get_flags = metal_get_flags;
   video_context_driver_set(&metal_fake_context);

   shader_path = video_shader_get_current_shader_preset();
   metal_set_shader((__bridge void *)md,
         video_shader_parse_type(shader_path), shader_path);

   return (__bridge_retained void *)md;
}

static bool metal_frame(void *data, const void *frame,
      unsigned frame_width, unsigned frame_height,
      uint64_t frame_count,
      unsigned pitch, const char *msg,
      video_frame_info_t *video_info)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   return [md renderFrame:frame
                     data:data
                    width:frame_width
                   height:frame_height
               frameCount:frame_count
                    pitch:pitch
                      msg:msg
                     info:video_info];
}

static void metal_set_nonblock_state(void *data, bool non_block,
      bool adaptive_vsync_enabled, unsigned swap_interval)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   md.context.displaySyncEnabled = !non_block;
}

static bool metal_alive(void *data) { return true; }
static bool metal_has_windowed(void *data) { return true; }
static bool metal_focus(void *data) { return apple_platform.hasFocus; }

static bool metal_suppress_screensaver(void *data, bool disable)
{
   return [apple_platform setDisableDisplaySleep:disable];
}

static bool metal_set_shader(void *data,
                             enum rarch_shader_type type, const char *path)
{
#if defined(HAVE_SLANG) && defined(HAVE_SPIRV_CROSS)
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
   {
      if (type != RARCH_SHADER_SLANG)
      {
         if (!string_is_empty(path) && type != RARCH_SHADER_SLANG)
            RARCH_WARN("[Metal] Only Slang shaders are supported. Falling back to stock.\n");
         path = NULL;
      }

      /* TODO/FIXME - actually return to stock shader */
      if (string_is_empty(path))
         return true;

      if ([md.frameView setShaderFromPath:[NSString stringWithUTF8String:path]])
         return true;
   }
#endif
   return false;
}

static void metal_free(void *data)
{
   MetalDriver *md = (__bridge_transfer MetalDriver *)data;
   md = nil;
}

static void metal_set_viewport(void *data, unsigned viewport_width,
                               unsigned viewport_height, bool force_full, bool allow_rotate)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      [md setViewportWidth:viewport_width height:viewport_height forceFull:force_full allowRotate:allow_rotate];
}

static void metal_set_rotation(void *data, unsigned rotation)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      [md setRotation:rotation];
}

static void metal_viewport_info(void *data, struct video_viewport *vp)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   *vp = *md.viewport;
}

static bool metal_read_viewport(void *data, uint8_t *buffer, bool is_idle)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   return [md.frameView readViewport:buffer isIdle:is_idle];
}

static uintptr_t metal_load_texture(void *video_data, void *data,
      bool threaded, enum texture_filter_type filter_type)
{
   MetalDriver           *md  = (__bridge MetalDriver *)video_data;
   struct texture_image *img  = (struct texture_image *)data;
   if (!img)
      return 0;

   struct texture_image image = *img;
   Texture *t = [md.context newTexture:image filter:filter_type];
   return (uintptr_t)(__bridge_retained void *)(t);
}

static void metal_unload_texture(void *data, 
      bool threaded, uintptr_t handle)
{
   if (!handle)
      return;
   Texture *t = (__bridge_transfer Texture *)(void *)handle;
   t = nil;
}

/* TODO/FIXME - implement */
static void metal_set_video_mode(void *data,
                                 unsigned width, unsigned height,
                                 bool fullscreen)
{
   RARCH_LOG("[Metal]: set_video_mode res=%dx%d fullscreen=%s\n",
             width, height,
             fullscreen ? "YES" : "NO");
}

/* TODO/FIXME - implement */
static float metal_get_refresh_rate(void *data) { return 0.0f; }

static void metal_set_filtering(void *data, unsigned index, bool smooth, bool ctx_scaling)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   [md.frameView setFilteringIndex:index smooth:smooth];
}

static void metal_set_aspect_ratio(void *data, unsigned aspect_ratio_idx)
{
   MetalDriver *md = (__bridge MetalDriver *)data;

   md.keepAspect = YES;
   [md setNeedsResize];
}

static void metal_apply_state_changes(void *data)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   [md setNeedsResize];
}

static void metal_set_texture_frame(void *data, const void *frame,
      bool rgb32, unsigned width, unsigned height,
      float alpha)
{
   MetalDriver *md         = (__bridge MetalDriver *)data;
   settings_t *settings    = config_get_ptr();
   bool menu_linear_filter = settings->bools.menu_linear_filter;

   [md.menu updateWidth:width
                 height:height
                 format:rgb32 ? RPixelFormatBGRA8Unorm : RPixelFormatBGRA4Unorm
                 filter:menu_linear_filter ? RTextureFilterLinear : RTextureFilterNearest];
   [md.menu updateFrame:frame];
   md.menu.alpha = alpha;
}

static void metal_set_texture_enable(void *data, bool state, bool full_screen)
{
   MetalDriver *md    = (__bridge MetalDriver *)data;
   if (!md)
      return;

   md.menu.enabled    = state;
#if 0
   md.menu.fullScreen = full_screen;
#endif
}

static void metal_show_mouse(void *data, bool state)
{
   [apple_platform setCursorVisible:state];
}

static struct video_shader *metal_get_current_shader(void *data)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (!md)
      return NULL;

   return md.frameView.shader;
}

static uint32_t metal_get_flags(void *data)
{
   uint32_t flags = 0;

   BIT32_SET(flags, GFX_CTX_FLAGS_CUSTOMIZABLE_SWAPCHAIN_IMAGES);
   BIT32_SET(flags, GFX_CTX_FLAGS_BLACK_FRAME_INSERTION);
   BIT32_SET(flags, GFX_CTX_FLAGS_MENU_FRAME_FILTERING);
   BIT32_SET(flags, GFX_CTX_FLAGS_SCREENSHOTS_SUPPORTED);

#if defined(HAVE_SLANG) && defined(HAVE_SPIRV_CROSS)
   BIT32_SET(flags, GFX_CTX_FLAGS_SHADERS_SLANG);
#endif

   return flags;
}

static const video_poke_interface_t metal_poke_interface = {
   metal_get_flags,
   metal_load_texture,
   metal_unload_texture,
   metal_set_video_mode,
   metal_get_refresh_rate,
   metal_set_filtering,
   NULL,                      /* get_video_output_size */
   NULL,                      /* get_video_output_prev */
   NULL,                      /* get_video_output_next */
   NULL,                      /* get_current_framebuffer */
   NULL,                      /* get_proc_address */
   metal_set_aspect_ratio,
   metal_apply_state_changes,
   metal_set_texture_frame,
   metal_set_texture_enable,
   font_driver_render_msg,
   metal_show_mouse,
   NULL,                      /* grab_mouse_toggle */
   metal_get_current_shader,
   NULL,                      /* get_current_software_framebuffer */
   NULL,                      /* get_hw_render_interface */
   NULL,                      /* set_hdr_max_nits */
   NULL,                      /* set_hdr_paper_white_nits */
   NULL,                      /* set_hdr_contrast */
   NULL                       /* set_hdr_expand_gamut */
};

static void metal_get_poke_interface(void *data,
      const video_poke_interface_t **iface)
{
   *iface = &metal_poke_interface;
}

#ifdef HAVE_OVERLAY

static void metal_overlay_enable(void *data, bool state)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      md.overlay.enabled = state;
}

static bool metal_overlay_load(void *data,
      const void *images, unsigned num_images)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (!md)
      return NO;

   return [md.overlay loadImages:(const struct texture_image *)images count:num_images];
}

static void metal_overlay_tex_geom(void *data, unsigned index,
      float x, float y, float w, float h)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      [md.overlay updateTextureCoordsX:x y:y w:w h:h index:index];
}

static void metal_overlay_vertex_geom(void *data, unsigned index,
      float x, float y, float w, float h)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      [md.overlay updateVertexX:x y:y w:w h:h index:index];
}

static void metal_overlay_full_screen(void *data, bool enable)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      md.overlay.fullscreen = enable;
}

static void metal_overlay_set_alpha(void *data, unsigned index, float mod)
{
   MetalDriver *md = (__bridge MetalDriver *)data;
   if (md)
      [md.overlay updateAlpha:mod index:index];
}

static const video_overlay_interface_t metal_overlay_interface = {
   metal_overlay_enable,
   metal_overlay_load,
   metal_overlay_tex_geom,
   metal_overlay_vertex_geom,
   metal_overlay_full_screen,
   metal_overlay_set_alpha,
};

static void metal_get_overlay_interface(void *data,
      const video_overlay_interface_t **iface)
{
   *iface = &metal_overlay_interface;
}

#endif

#ifdef HAVE_GFX_WIDGETS
static bool metal_gfx_widgets_enabled(void *data) { return true; }
#endif

video_driver_t video_metal = {
   metal_init,
   metal_frame,
   metal_set_nonblock_state,
   metal_alive,
   metal_focus,
   metal_suppress_screensaver,
   metal_has_windowed,
   metal_set_shader,
   metal_free,
   "metal",
   metal_set_viewport,
   metal_set_rotation,
   metal_viewport_info,
   metal_read_viewport,
   NULL, /* read_frame_raw */
#ifdef HAVE_OVERLAY
   metal_get_overlay_interface,
#endif
   metal_get_poke_interface,
   NULL, /* metal_wrap_type_to_enum */
#ifdef HAVE_GFX_WIDGETS
   metal_gfx_widgets_enabled
#endif
};
