RetroArch/gfx/drivers/metal.m
Ophidon 72c901a90e
Squashed commit of the following: (#16142)
commit 793d41c303206b43932ddcefd44a45836def55eb
Author: Ophidon <jrogers2@gmail.com>
Date:   Fri Jan 19 23:12:31 2024 -0500

    Build Fix 2

    Move declarations of iterators.

commit c0e959b3d3cd773a66a17cfe034f08eaa53d525a
Author: Ophidon <jrogers2@gmail.com>
Date:   Fri Jan 19 22:57:01 2024 -0500

    Build Fix

    Help string was 14 characters too long for c89.

commit fc5506c7906bf82d6f88b7b0d7e4764d58d90622
Author: Ophidon <jrogers2@gmail.com>
Date:   Fri Jan 19 22:40:45 2024 -0500

    BFI Updates

    Significant BFI updates.

    - Adds BFI to dx10/11/12 in general.

    - Updates existing BFI menu option descriptions to be somewhat more clear in how to use correctly.

    - Adds Variable Strobe length via new 'Dark Frames' bfi sub-choice. Only valid at 180hz and above, as it must work with whole frames.

    - Algorithm to auto select 'decent' Dark Frames choice, for any given selected BFI refresh rate. Will also avoid defaults that can cause Image Retention at any Hz higher than 120. (Impossible to avoid at 120 if you have an affected screen... get an OLED :D ) .

    - Some sanity checking on selecting BFI or the other synchronizations options like Swap Interval > 1, that don't play well with BFI.
2024-01-19 23:11:31 -08:00

2646 lines
78 KiB
Objective-C

/* 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 <encodings/utf.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 "../video_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 "../../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); \
}
/*
* DISPLAY DRIVER
*/
static const float *gfx_display_metal_get_default_vertices(void)
{
return [MenuDisplay defaultVertices];
}
static const float *gfx_display_metal_get_default_tex_coords(void)
{
return [MenuDisplay defaultTexCoords];
}
static void *gfx_display_metal_get_default_mvp(void *data)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (!md)
return NULL;
return (void *)&md.viewportMVP->projectionMatrix;
}
static void gfx_display_metal_blend_begin(void *data)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (md)
md.display.blend = YES;
}
static void gfx_display_metal_blend_end(void *data)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (md)
md.display.blend = NO;
}
static void gfx_display_metal_draw(gfx_display_ctx_draw_t *draw,
void *data,
unsigned video_width,
unsigned video_height)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (md && draw)
[md.display draw:draw];
}
static void gfx_display_metal_draw_pipeline(
gfx_display_ctx_draw_t *draw,
gfx_display_t *p_disp,
void *data,
unsigned video_width,
unsigned video_height)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (md && draw)
[md.display drawPipeline:draw];
}
static void gfx_display_metal_scissor_begin(
void *data,
unsigned video_width,
unsigned video_height,
int x, int y, unsigned width, unsigned height)
{
MTLScissorRect r;
MetalDriver *md = (__bridge MetalDriver *)data;
if (!md)
return;
r.x = (NSUInteger)x;
r.y = (NSUInteger)y;
r.width = width;
r.height = height;
[md.display setScissorRect:r];
}
static void gfx_display_metal_scissor_end(void *data,
unsigned video_width,
unsigned video_height)
{
MetalDriver *md = (__bridge MetalDriver *)data;
if (md)
[md.display clearScissorRect];
}
gfx_display_ctx_driver_t gfx_display_ctx_metal = {
gfx_display_metal_draw,
gfx_display_metal_draw_pipeline,
gfx_display_metal_blend_begin,
gfx_display_metal_blend_end,
gfx_display_metal_get_default_mvp,
gfx_display_metal_get_default_vertices,
gfx_display_metal_get_default_tex_coords,
FONT_DRIVER_RENDER_METAL_API,
GFX_VIDEO_DRIVER_METAL,
"metal",
false,
gfx_display_metal_scissor_begin,
gfx_display_metal_scissor_end
};
/*
* FONT DRIVER
*/
@interface MetalRaster : NSObject
{
__weak MetalDriver *_driver;
const font_renderer_driver_t *_font_driver;
void *_font_data;
struct font_atlas *_atlas;
NSUInteger _stride;
id<MTLBuffer> _buffer;
id<MTLTexture> _texture;
id<MTLRenderPipelineState> _state;
id<MTLSamplerState> _sampler;
Context *_context;
Uniforms _uniforms;
id<MTLBuffer> _vert;
unsigned _capacity;
unsigned _offset;
unsigned _vertices;
}
@property (readonly) struct font_atlas *atlas;
- (void)deinit;
- (instancetype)initWithDriver:(MetalDriver *)driver fontPath:(const char *)font_path fontSize:(unsigned)font_size;
- (int)getWidthForMessage:(const char *)msg length:(NSUInteger)length scale:(float)scale;
- (const struct font_glyph *)getGlyph:(uint32_t)code;
@end
@implementation MetalRaster
- (void)deinit
{
if (_font_driver && _font_data)
_font_driver->free(_font_data);
}
- (instancetype)initWithDriver:(MetalDriver *)driver fontPath:(const char *)font_path fontSize:(unsigned)font_size
{
if (self = [super init])
{
if (driver == nil)
return nil;
_driver = driver;
_context = driver.context;
if (!font_renderer_create_default(
&_font_driver,
&_font_data, font_path, font_size))
return nil;
_uniforms.projectionMatrix = matrix_proj_ortho(0, 1, 0, 1);
_atlas = _font_driver->get_atlas(_font_data);
_stride = MTL_ALIGN_BUFFER(_atlas->width);
if (_stride == _atlas->width)
{
_buffer = [_context.device newBufferWithBytes:_atlas->buffer
length:(NSUInteger)(_stride * _atlas->height)
options:PLATFORM_METAL_RESOURCE_STORAGE_MODE];
// Even though newBufferWithBytes will copy the initial contents
// from our atlas, it doesn't seem to invalidate the buffer when
// doing so, causing corrupted text rendering if we hit this code
// path. To work around it we manually invalidate the buffer.
#if !defined(HAVE_COCOATOUCH)
[_buffer didModifyRange:NSMakeRange(0, _buffer.length)];
#endif
}
else
{
int i;
_buffer = [_context.device newBufferWithLength:(NSUInteger)(_stride * _atlas->height)
options:PLATFORM_METAL_RESOURCE_STORAGE_MODE];
void *dst = _buffer.contents;
void *src = _atlas->buffer;
for (i = 0; i < _atlas->height; i++)
{
memcpy(dst, src, _atlas->width);
dst += _stride;
src += _atlas->width;
}
#if !defined(HAVE_COCOATOUCH)
[_buffer didModifyRange:NSMakeRange(0, _buffer.length)];
#endif
}
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm
width:_atlas->width
height:_atlas->height
mipmapped:NO];
_texture = [_buffer newTextureWithDescriptor:td offset:0 bytesPerRow:_stride];
_capacity = 12000;
_vert = [_context.device newBufferWithLength:sizeof(SpriteVertex) *
_capacity options:PLATFORM_METAL_RESOURCE_STORAGE_MODE];
if (![self _initializeState])
return nil;
}
return self;
}
- (bool)_initializeState
{
{
NSError *err;
MTLVertexDescriptor *vd = [MTLVertexDescriptor new];
vd.attributes[0].offset = 0;
vd.attributes[0].format = MTLVertexFormatFloat2;
vd.attributes[1].offset = offsetof(SpriteVertex, texCoord);
vd.attributes[1].format = MTLVertexFormatFloat2;
vd.attributes[2].offset = offsetof(SpriteVertex, color);
vd.attributes[2].format = MTLVertexFormatFloat4;
vd.layouts[0].stride = sizeof(SpriteVertex);
vd.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
MTLRenderPipelineDescriptor *psd = [MTLRenderPipelineDescriptor new];
psd.label = @"font pipeline";
MTLRenderPipelineColorAttachmentDescriptor *ca = psd.colorAttachments[0];
ca.pixelFormat = MTLPixelFormatBGRA8Unorm;
ca.blendingEnabled = YES;
ca.sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha;
ca.sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
ca.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
ca.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
psd.sampleCount = 1;
psd.vertexDescriptor = vd;
psd.vertexFunction = [_context.library newFunctionWithName:@"sprite_vertex"];
psd.fragmentFunction = [_context.library newFunctionWithName:@"sprite_fragment_a8"];
_state = [_context.device newRenderPipelineStateWithDescriptor:psd error:&err];
if (err != nil)
return NO;
}
{
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];
sd.minFilter = MTLSamplerMinMagFilterLinear;
sd.magFilter = MTLSamplerMinMagFilterLinear;
_sampler = [_context.device newSamplerStateWithDescriptor:sd];
}
return YES;
}
- (void)updateGlyph:(const struct font_glyph *)glyph
{
if (_atlas->dirty)
{
unsigned row;
for (row = glyph->atlas_offset_y; row < (glyph->atlas_offset_y + glyph->height); row++)
{
uint8_t *src = _atlas->buffer + row * _atlas->width + glyph->atlas_offset_x;
uint8_t *dst = (uint8_t *)_buffer.contents + row * _stride + glyph->atlas_offset_x;
memcpy(dst, src, glyph->width);
}
#if !defined(HAVE_COCOATOUCH)
NSUInteger offset = glyph->atlas_offset_y;
NSUInteger len = glyph->height * _stride;
[_buffer didModifyRange:NSMakeRange(offset, len)];
#endif
_atlas->dirty = false;
}
}
- (int)getWidthForMessage:(const char *)msg length:(NSUInteger)length scale:(float)scale
{
NSUInteger i;
int delta_x = 0;
const struct font_glyph* glyph_q = _font_driver->get_glyph(_font_data, '?');
for (i = 0; i < length; i++)
{
const struct font_glyph *glyph;
/* Do something smarter here ... */
if (!(glyph = _font_driver->get_glyph(_font_data, (uint8_t)msg[i])))
if (!(glyph = glyph_q))
continue;
[self updateGlyph:glyph];
delta_x += glyph->advance_x;
}
return (int)(delta_x * scale);
}
- (const struct font_glyph *)getGlyph:(uint32_t)code
{
const struct font_glyph *glyph = _font_driver->get_glyph((void *)_font_driver, code);
if (glyph)
[self updateGlyph:glyph];
return glyph;
}
static INLINE void write_quad6(SpriteVertex *pv,
float x, float y, float width, float height,
float tex_x, float tex_y, float tex_width, float tex_height,
const vector_float4 *color)
{
int i;
static const float strip[2 * 6] = {
0.0f, 0.0f,
0.0f, 1.0f,
1.0f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f,
};
for (i = 0; i < 6; i++)
{
pv[i].position = simd_make_float2(
x + strip[2 * i + 0] * width,
y + strip[2 * i + 1] * height);
pv[i].texCoord = simd_make_float2(
tex_x + strip[2 * i + 0] * tex_width,
tex_y + strip[2 * i + 1] * tex_height);
pv[i].color = *color;
}
}
- (void)_renderLine:(const char *)msg
length:(NSUInteger)length
scale:(float)scale
color:(vector_float4)color
posX:(float)posX
posY:(float)posY
aligned:(unsigned)aligned
{
const struct font_glyph* glyph_q;
const char *msg_end = msg + length;
int x = (int)roundf(posX * _driver.viewport->full_width);
int y = (int)roundf((1.0f - posY) * _driver.viewport->full_height);
int delta_x = 0;
int delta_y = 0;
float inv_tex_size_x = 1.0f / _texture.width;
float inv_tex_size_y = 1.0f / _texture.height;
float inv_win_width = 1.0f / _driver.viewport->full_width;
float inv_win_height = 1.0f / _driver.viewport->full_height;
switch (aligned)
{
case TEXT_ALIGN_RIGHT:
x -= [self getWidthForMessage:msg length:length scale:scale];
break;
case TEXT_ALIGN_CENTER:
x -= [self getWidthForMessage:msg length:length scale:scale] / 2;
break;
default:
break;
}
SpriteVertex *v = (SpriteVertex *)_vert.contents;
v += _offset + _vertices;
glyph_q = _font_driver->get_glyph(_font_data, '?');
while (msg < msg_end)
{
int off_x, off_y, tex_x, tex_y, width, height;
const struct font_glyph *glyph;
unsigned code = utf8_walk(&msg);
/* Do something smarter here .. */
if (!(glyph = _font_driver->get_glyph(_font_data, code)))
if (!(glyph = glyph_q))
continue;
[self updateGlyph:glyph];
off_x = glyph->draw_offset_x;
off_y = glyph->draw_offset_y;
tex_x = glyph->atlas_offset_x;
tex_y = glyph->atlas_offset_y;
width = glyph->width;
height = glyph->height;
write_quad6(v,
(x + (off_x + delta_x) * scale) * inv_win_width,
(y + (off_y + delta_y) * scale) * inv_win_height,
width * scale * inv_win_width,
height * scale * inv_win_height,
tex_x * inv_tex_size_x,
tex_y * inv_tex_size_y,
width * inv_tex_size_x,
height * inv_tex_size_y,
&color);
_vertices += 6;
v += 6;
delta_x += glyph->advance_x;
delta_y += glyph->advance_y;
}
}
- (void)_flush
{
NSUInteger start = _offset * sizeof(SpriteVertex);
#if !defined(HAVE_COCOATOUCH)
[_vert didModifyRange:NSMakeRange(start, sizeof(SpriteVertex) * _vertices)];
#endif
id<MTLRenderCommandEncoder> rce = _context.rce;
[rce pushDebugGroup:@"render fonts"];
[_context resetRenderViewport:kFullscreenViewport];
[rce setRenderPipelineState:_state];
[rce setVertexBytes:&_uniforms length:sizeof(Uniforms) atIndex:BufferIndexUniforms];
[rce setVertexBuffer:_vert offset:start atIndex:BufferIndexPositions];
[rce setFragmentTexture:_texture atIndex:TextureIndexColor];
[rce setFragmentSamplerState:_sampler atIndex:SamplerIndexDraw];
[rce drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_vertices];
[rce popDebugGroup];
_offset += _vertices;
_vertices = 0;
}
- (void)renderMessage:(const char *)msg
height:(unsigned)height
scale:(float)scale
color:(vector_float4)color
posX:(float)posX
posY:(float)posY
aligned:(unsigned)aligned
{
int lines = 0;
float line_height;
struct font_line_metrics *line_metrics = NULL;
_font_driver->get_line_metrics(_font_data, &line_metrics);
line_height = line_metrics->height * scale / height;
for (;;)
{
const char *delim = strchr(msg, '\n');
size_t msg_len = delim ? (unsigned)(delim - msg) : strlen(msg);
/* Draw the line */
[self _renderLine:msg
length:msg_len
scale:scale
color:color
posX:posX
posY:posY - (float)lines * line_height
aligned:aligned];
if (!delim)
break;
msg += msg_len + 1;
lines++;
}
}
- (void)renderMessage:(const char *)msg
width:(unsigned)width
height:(unsigned)height
params:(const struct font_params *)params
{
float x, y, scale, drop_mod, drop_alpha;
int drop_x, drop_y;
enum text_alignment text_align;
vector_float4 color;
if (!msg || !*msg)
return;
if (params)
{
x = params->x;
y = params->y;
scale = params->scale;
text_align = params->text_align;
drop_x = params->drop_x;
drop_y = params->drop_y;
drop_mod = params->drop_mod;
drop_alpha = params->drop_alpha;
color = simd_make_float4(
FONT_COLOR_GET_RED(params->color) / 255.0f,
FONT_COLOR_GET_GREEN(params->color) / 255.0f,
FONT_COLOR_GET_BLUE(params->color) / 255.0f,
FONT_COLOR_GET_ALPHA(params->color) / 255.0f);
}
else
{
settings_t *settings = config_get_ptr();
float video_msg_pos_x = settings->floats.video_msg_pos_x;
float video_msg_pos_y = settings->floats.video_msg_pos_y;
float video_msg_color_r = settings->floats.video_msg_color_r;
float video_msg_color_g = settings->floats.video_msg_color_g;
float video_msg_color_b = settings->floats.video_msg_color_b;
x = video_msg_pos_x;
y = video_msg_pos_y;
scale = 1.0f;
text_align = TEXT_ALIGN_LEFT;
color = simd_make_float4(
video_msg_color_r,
video_msg_color_g,
video_msg_color_b,
1.0f);
drop_x = -2;
drop_y = -2;
drop_mod = 0.3f;
drop_alpha = 1.0f;
}
@autoreleasepool
{
size_t max_glyphs = strlen(msg);
if (drop_x || drop_y)
max_glyphs *= 2;
if (max_glyphs * 6 + _offset > _capacity)
_offset = 0;
if (drop_x || drop_y)
{
vector_float4 color_dark;
color_dark.x = color.x * drop_mod;
color_dark.y = color.y * drop_mod;
color_dark.z = color.z * drop_mod;
color_dark.w = color.w * drop_alpha;
[self renderMessage:msg
height:height
scale:scale
color:color_dark
posX:x + scale * drop_x / width
posY:y + scale * drop_y / height
aligned:text_align];
}
[self renderMessage:msg
height:height
scale:scale
color:color
posX:x
posY:y
aligned:text_align];
[self _flush];
}
}
@end
static void *metal_raster_font_init(void *data,
const char *font_path, float font_size,
bool is_threaded)
{
MetalRaster *r = [[MetalRaster alloc] initWithDriver:(__bridge MetalDriver *)data fontPath:font_path fontSize:(unsigned)font_size];
if (!r)
return NULL;
return (__bridge_retained void *)r;
}
static void metal_raster_font_free(void *data, bool is_threaded)
{
MetalRaster *r = (__bridge_transfer MetalRaster *)data;
[r deinit];
r = nil;
}
static int metal_raster_font_get_message_width(void *data, const char *msg,
size_t msg_len, float scale)
{
MetalRaster *r = (__bridge MetalRaster *)data;
return [r getWidthForMessage:msg length:(unsigned)msg_len scale:scale];
}
static void metal_raster_font_render_msg(
void *userdata,
void *data, const char *msg,
const struct font_params *params)
{
MetalRaster *r = (__bridge MetalRaster *)data;
MetalDriver *d = (__bridge MetalDriver *)userdata;
video_viewport_t *vp = [d viewport];
unsigned width = vp->full_width;
unsigned height = vp->full_height;
[r renderMessage:msg width:width height:height params:params];
}
static const struct font_glyph *metal_raster_font_get_glyph(
void *data, uint32_t code)
{
MetalRaster *r = (__bridge MetalRaster *)data;
return [r getGlyph:code];
}
font_renderer_t metal_raster_font = {
metal_raster_font_init,
metal_raster_font_free,
metal_raster_font_render_msg,
"metal",
metal_raster_font_get_glyph,
NULL, /* bind_block */
NULL, /* flush_block */
metal_raster_font_get_message_width,
NULL /* get_line_metrics */
};
/*
* VIDEO DRIVER
*/
@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_st_flags & MENU_ST_FLAG_ALIVE) ? true : false;
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;
uint32_t rotation;
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
_engine.pass[i].rotation = retroarch_get_rotation();
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 */
&_engine.pass[i].rotation, /* Rotation */
}
};
/* 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_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_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_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_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, /* wrap_type_to_enum */
#ifdef HAVE_GFX_WIDGETS
metal_widgets_enabled
#endif
};