mirror of
https://github.com/libretro/RetroArch.git
synced 2024-11-28 02:30:35 +00:00
0c526b6498
Partial update to work with shaders directly More Edits More changes more shader fixes More Fixes Compiling, reference load still wrong Added Feedback & things are working Logging Fixes Log Fix More Fixes Added Feedback Logging Fixes for file pathing in Linux Fix GLCore and Crash in QT Saving Code Cleanup Removed Unused Function filepath.c Code Cleanup
1624 lines
47 KiB
Objective-C
1624 lines
47 KiB
Objective-C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2018-2019 - Stuart Carnie
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
*
|
|
* 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/>.
|
|
*/
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import <Metal/Metal.h>
|
|
#import <QuartzCore/QuartzCore.h>
|
|
|
|
#import <memory.h>
|
|
#import <stddef.h>
|
|
#include <simd/simd.h>
|
|
|
|
#import <gfx/video_frame.h>
|
|
|
|
#import "metal_common.h"
|
|
#include "metal/Context.h"
|
|
|
|
#include "../../ui/drivers/cocoa/cocoa_common.h"
|
|
|
|
#ifdef HAVE_REWIND
|
|
#include "../../state_manager.h"
|
|
#endif
|
|
#ifdef HAVE_MENU
|
|
#include "../../menu/menu_driver.h"
|
|
#endif
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
#include "../gfx_widgets.h"
|
|
#endif
|
|
|
|
#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];
|
|
|
|
*input = NULL;
|
|
*inputData = NULL;
|
|
|
|
// graphics display driver
|
|
_display = [[MenuDisplay alloc] initWithContext:_context];
|
|
|
|
// menu view
|
|
_menu = [[MetalMenu alloc] initWithContext:_context];
|
|
|
|
// frame buffer 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
|
|
{
|
|
RARCH_LOG("[MetalDriver]: destroyed\n");
|
|
if (_viewport)
|
|
{
|
|
free(_viewport);
|
|
_viewport = nil;
|
|
}
|
|
font_driver_free_osd();
|
|
}
|
|
|
|
- (bool)_initMetal
|
|
{
|
|
_library = [_device newDefaultLibrary];
|
|
_context = [[Context alloc] initWithDevice:_device
|
|
layer:_layer
|
|
library:_library];
|
|
|
|
{
|
|
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);
|
|
|
|
MTLRenderPipelineDescriptor *psd = [MTLRenderPipelineDescriptor new];
|
|
psd.label = @"Pipeline+Alpha";
|
|
|
|
MTLRenderPipelineColorAttachmentDescriptor *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"];
|
|
|
|
NSError *err;
|
|
_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
|
|
{
|
|
#if 0
|
|
RARCH_LOG("[Metal]: setViewportWidth size %dx%d\n", width, height);
|
|
#endif
|
|
|
|
_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);
|
|
|
|
// update matrix
|
|
_context.viewport = _viewport;
|
|
|
|
_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];
|
|
|
|
id<MTLRenderCommandEncoder> rce = _context.rce;
|
|
|
|
#ifdef HAVE_OVERLAY
|
|
if (_overlay.enabled)
|
|
{
|
|
[rce pushDebugGroup:@"overlay"];
|
|
[_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];
|
|
[rce popDebugGroup];
|
|
}
|
|
#endif
|
|
|
|
if (statistics_show)
|
|
{
|
|
struct font_params *osd_params = (struct font_params *)&video_info->osd_stat_params;
|
|
|
|
if (osd_params)
|
|
{
|
|
[rce pushDebugGroup:@"video stats"];
|
|
font_driver_render_msg(data, video_info->stat_text, osd_params, NULL);
|
|
[rce popDebugGroup];
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
[rce pushDebugGroup:@"display widgets"];
|
|
if (video_info->widgets_active)
|
|
gfx_widgets_frame(video_info);
|
|
[rce popDebugGroup];
|
|
#endif
|
|
|
|
if (msg && *msg)
|
|
{
|
|
[rce pushDebugGroup:@"message"];
|
|
[self _renderMessage:msg data:data];
|
|
[rce popDebugGroup];
|
|
}
|
|
|
|
[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, (unsigned)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 */
|
|
[rce pushDebugGroup:@"core frame"];
|
|
[_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];
|
|
}
|
|
[rce popDebugGroup];
|
|
}
|
|
|
|
- (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)
|
|
{
|
|
[rce pushDebugGroup:@"menu frame"];
|
|
[_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];
|
|
[rce popDebugGroup];
|
|
}
|
|
#if defined(HAVE_MENU)
|
|
else
|
|
{
|
|
[rce pushDebugGroup:@"menu"];
|
|
[_context resetRenderViewport:kFullscreenViewport];
|
|
menu_driver_frame(menu_is_alive, video_info);
|
|
[rce popDebugGroup];
|
|
}
|
|
#endif
|
|
}
|
|
|
|
- (void)_endFrame
|
|
{
|
|
[_context end];
|
|
}
|
|
|
|
- (void)setNeedsResize
|
|
{
|
|
// TODO(sgc): resize all drawables
|
|
}
|
|
|
|
- (void)setRotation:(unsigned)rotation
|
|
{
|
|
[_context setRotation:rotation];
|
|
}
|
|
|
|
- (Uniforms *)viewportMVP
|
|
{
|
|
return &_viewportMVP;
|
|
}
|
|
|
|
#pragma mark - MTKViewDelegate
|
|
|
|
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
|
|
{
|
|
NSLog(@"mtkView drawableSizeWillChange to: %f x %f",size.width,size.height);
|
|
#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; // src 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;
|
|
|
|
// init 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
|
|
{
|
|
MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new];
|
|
|
|
/* Initialize samplers */
|
|
for (unsigned 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
|
|
{
|
|
for (int 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;
|
|
}
|
|
|
|
_frame = frame;
|
|
|
|
// 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)},
|
|
};
|
|
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: 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
|
|
{
|
|
RARCH_LOG("[Metal]: readViewport is_idle = %s\n", isIdle ? "YES" : "NO");
|
|
|
|
bool enabled = _context.captureEnabled;
|
|
if (!enabled)
|
|
_context.captureEnabled = YES;
|
|
|
|
video_driver_cached_frame();
|
|
|
|
bool 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
|
|
{
|
|
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
|
|
width:(NSUInteger)_size.width
|
|
height:(NSUInteger)_size.height
|
|
mipmapped:false];
|
|
td.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite | MTLTextureUsageRenderTarget;
|
|
|
|
for (int 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
|
|
{
|
|
_texture = _engine.frame.texture[0].view;
|
|
[self _convertFormat];
|
|
|
|
if (!_shader || _shader->passes == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (unsigned 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;
|
|
[cb pushDebugGroup:@"shaders"];
|
|
|
|
MTLRenderPassDescriptor *rpd = [MTLRenderPassDescriptor new];
|
|
rpd.colorAttachments[0].loadAction = MTLLoadActionDontCare;
|
|
rpd.colorAttachments[0].storeAction = MTLStoreActionStore;
|
|
|
|
for (unsigned i = 0; i < _shader->passes; i++)
|
|
{
|
|
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 (unsigned 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
|
|
}
|
|
}
|
|
|
|
__unsafe_unretained id<MTLTexture> textures[SLANG_NUM_BINDINGS] = {NULL};
|
|
id<MTLSamplerState> samplers[SLANG_NUM_BINDINGS] = {NULL};
|
|
|
|
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 == nil)
|
|
_drawState = ViewDrawStateContext;
|
|
else
|
|
_drawState = ViewDrawStateAll;
|
|
|
|
[cb popDebugGroup];
|
|
}
|
|
|
|
- (void)_updateRenderTargets
|
|
{
|
|
if (!_shader || !resize_render_targets) return;
|
|
|
|
// release existing targets
|
|
for (int 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));
|
|
}
|
|
|
|
NSUInteger width = (NSUInteger)_size.width, height = (NSUInteger)_size.height;
|
|
|
|
for (unsigned i = 0; i < _shader->passes; i++)
|
|
{
|
|
struct video_shader_pass *shader_pass = &_shader->pass[i];
|
|
|
|
if (shader_pass->fbo.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;
|
|
}
|
|
|
|
RARCH_LOG("[Metal]: Updating framebuffer size %u x %u.\n", width, height);
|
|
|
|
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
|
|
{
|
|
if (!shader)
|
|
return;
|
|
|
|
for (int i = 0; i < GFX_MAX_SHADERS; 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));
|
|
|
|
STRUCT_ASSIGN(_engine.pass[i]._state, nil);
|
|
|
|
for (unsigned j = 0; j < SLANG_CBUFFER_MAX; j++)
|
|
{
|
|
STRUCT_ASSIGN(_engine.pass[i].buffers[j], nil);
|
|
}
|
|
}
|
|
|
|
for (int 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
|
|
{
|
|
unsigned 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(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];
|
|
|
|
RARCH_LOG("[Metal]: saving metal shader files to %s\n", basePath.UTF8String);
|
|
|
|
[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 = {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(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
|
|
{
|
|
[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 (NSUInteger 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
|
|
{
|
|
#if !defined(HAVE_COCOATOUCH)
|
|
if (_vertDirty)
|
|
{
|
|
[_vert didModifyRange:NSMakeRange(0, _vert.length)];
|
|
_vertDirty = NO;
|
|
}
|
|
#endif
|
|
|
|
NSUInteger count = _images.count;
|
|
for (NSUInteger 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:
|
|
return fmt;
|
|
}
|
|
}
|