Files
ARMSX2/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h
T
Jeen 14fc29c0cd iOS: gate macOS-only APIs behind TARGET_OS_IPHONE and add frontend link stubs
Gate all macOS-only system APIs behind TARGET_OS_IPHONE checks across the shared core: ApplicationServices, IOKit, mouse APIs, AppKit, MetalFX, CDVD Darwin sources, USB, discord-rpc, cubeb CoreAudio HAL, and BSD networking headers in DEV9. Port TARGET_OS_IPHONE guards for DEV9 AdapterUtils with iOS fallbacks for sockaddr_dl, rt_msghdr, and sysctl. Add ARMSX2_ROOT to the include path for the frontend's common include style. Drop the global _M_ARM64 define that triggered fast_float MSVC intrin.h inclusion since __aarch64__ covers the core paths. Gate MTLFeatureSet_macOS_GPUFamily1_v1 behind !TARGET_OS_IPHONE. Fix missing unistd.h and gate pthread_jit_write_protect_np which is unavailable on iOS. Add stubs for DarwinMisc JIT diagnostics, Achievements stats and info APIs, Discord_Register, Host capture callbacks, and the host hotkeys map so the frontend links cleanly. Merge upstream master.
2026-07-12 16:38:59 +02:00

527 lines
19 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "GS/Renderers/Common/GSDevice.h"
#ifndef __OBJC__
#error "This header is for use with Objective-C++ only.
#endif
#ifdef __APPLE__
#include "common/HashCombine.h"
#include "common/MRCHelpers.h"
#include "common/ReadbackSpinManager.h"
#include "GS/GS.h"
#include "GSMTLDeviceInfo.h"
#include "GSMTLSharedHeader.h"
#include <TargetConditionals.h>
#if !TARGET_OS_IPHONE
#include <AppKit/AppKit.h>
#include <MetalFX/MetalFX.h>
#endif
#include <Metal/Metal.h>
#include <QuartzCore/QuartzCore.h>
#include <atomic>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <utility>
struct PipelineSelectorExtrasMTL
{
union
{
struct
{
GSTexture::Format rt : 4;
u8 writemask : 4;
GSDevice::BlendFactor src_factor : 4;
GSDevice::BlendFactor dst_factor : 4;
GSDevice::BlendFactor src_factor_alpha : 4;
GSDevice::BlendFactor dst_factor_alpha : 4;
GSDevice::BlendOp blend_op : 2;
bool blend_enable : 1;
bool has_depth : 1;
bool has_stencil : 1;
bool has_rt1 : 1;
};
u32 fullkey;
};
PipelineSelectorExtrasMTL(): fullkey(0) {}
PipelineSelectorExtrasMTL(GSHWDrawConfig::BlendState blend, GSTexture* rt, GSHWDrawConfig::ColorMaskSelector cms, bool has_depth, bool has_stencil, bool has_rt1)
: fullkey(0)
{
this->rt = rt ? rt->GetFormat() : GSTexture::Format::Invalid;
MTLColorWriteMask mask = MTLColorWriteMaskNone;
if (cms.wr) mask |= MTLColorWriteMaskRed;
if (cms.wg) mask |= MTLColorWriteMaskGreen;
if (cms.wb) mask |= MTLColorWriteMaskBlue;
if (cms.wa) mask |= MTLColorWriteMaskAlpha;
this->writemask = mask;
this->src_factor = static_cast<GSDevice::BlendFactor>(blend.src_factor);
this->dst_factor = static_cast<GSDevice::BlendFactor>(blend.dst_factor);
this->blend_op = static_cast<GSDevice::BlendOp>(blend.op);
this->src_factor_alpha = static_cast<GSDevice::BlendFactor>(blend.src_factor_alpha);
this->dst_factor_alpha = static_cast<GSDevice::BlendFactor>(blend.dst_factor_alpha);
this->blend_enable = blend.enable;
this->has_depth = has_depth;
this->has_stencil = has_stencil;
this->has_rt1 = has_rt1;
}
};
struct PipelineSelectorMTL
{
GSHWDrawConfig::PSSelector ps;
PipelineSelectorExtrasMTL extras;
GSHWDrawConfig::VSSelector vs;
u8 pad[7];
PipelineSelectorMTL()
{
memset(this, 0, sizeof(*this));
}
PipelineSelectorMTL(GSHWDrawConfig::VSSelector vs, GSHWDrawConfig::PSSelector ps, PipelineSelectorExtrasMTL extras)
{
memset(this, 0, sizeof(*this));
this->vs = vs;
this->ps = ps;
this->extras = extras;
}
PipelineSelectorMTL(const PipelineSelectorMTL& other)
{
memcpy(this, &other, sizeof(other));
}
PipelineSelectorMTL& operator=(const PipelineSelectorMTL& other)
{
memcpy(this, &other, sizeof(other));
return *this;
}
bool operator==(const PipelineSelectorMTL& other) const
{
return BitEqual(*this, other);
}
};
static_assert(sizeof(PipelineSelectorMTL) == 28);
template <>
struct std::hash<PipelineSelectorMTL>
{
size_t operator()(const PipelineSelectorMTL& sel) const
{
size_t h = 0;
size_t pieces[(sizeof(PipelineSelectorMTL) + sizeof(size_t) - 1) / sizeof(size_t)] = {};
memcpy(pieces, &sel, sizeof(PipelineSelectorMTL));
for (auto& piece : pieces)
HashCombine(h, piece);
return h;
}
};
class GSScopedDebugGroupMTL
{
id<MTLCommandBuffer> m_buffer;
public:
GSScopedDebugGroupMTL(id<MTLCommandBuffer> buffer, NSString* name): m_buffer(buffer)
{
[m_buffer pushDebugGroup:name];
}
~GSScopedDebugGroupMTL()
{
[m_buffer popDebugGroup];
}
};
struct ImDrawData;
class GSTextureMTL;
class GSDeviceMTL final : public GSDevice
{
public:
using DepthStencilSelector = GSHWDrawConfig::DepthStencilSelector;
using SamplerSelector = GSHWDrawConfig::SamplerSelector;
enum class UsePresentDrawable : u8
{
Never = 0,
Always = 1,
IfVsync = 2,
};
enum class LoadAction
{
DontCare,
DontCareIfFull,
Load,
};
class UsageTracker
{
struct UsageEntry
{
u64 drawno;
size_t pos;
};
std::vector<UsageEntry> m_usage;
size_t m_size = 0;
size_t m_pos = 0;
public:
size_t Size() { return m_size; }
size_t Pos() { return m_pos; }
bool PrepareForAllocation(u64 last_draw, size_t amt);
size_t Allocate(u64 current_draw, size_t amt);
void Reset(size_t new_size);
};
struct Map
{
id<MTLBuffer> gpu_buffer;
size_t gpu_offset;
void* cpu_buffer;
};
struct UploadBuffer
{
UsageTracker usage;
MRCOwned<id<MTLBuffer>> mtlbuffer;
void* buffer = nullptr;
};
struct BufferPair
{
UsageTracker usage;
MRCOwned<id<MTLBuffer>> cpubuffer;
MRCOwned<id<MTLBuffer>> gpubuffer;
void* buffer = nullptr;
size_t last_upload = 0;
};
struct ConvertShaderVertex
{
simd_float2 pos;
simd_float2 texpos;
};
struct VSSelector
{
union
{
struct
{
bool iip : 1;
bool fst : 1;
bool point_size : 1;
GSShader::VSExpand expand : 3;
};
u8 key;
};
VSSelector(): key(0) {}
VSSelector(u8 key): key(key) {}
};
using PSSelector = GSHWDrawConfig::PSSelector;
// MARK: Permanent resources
std::shared_ptr<std::pair<std::mutex, GSDeviceMTL*>> m_backref;
GSMTLDevice m_dev;
MRCOwned<id<MTLCommandQueue>> m_queue;
MRCOwned<id<MTLFence>> m_draw_sync_fence;
MRCOwned<MTLFunctionConstantValues*> m_fn_constants;
MRCOwned<MTLVertexDescriptor*> m_hw_vertex;
MTLResourceOptions m_resource_options_shared_wc;
// Previously in MetalHostDisplay.
#if !TARGET_OS_IPHONE
MRCOwned<NSView*> m_view;
#endif
MRCOwned<CAMetalLayer*> m_layer;
MRCOwned<id<CAMetalDrawable>> m_current_drawable;
MRCOwned<MTLRenderPassDescriptor*> m_pass_desc;
u32 m_capture_start_frame;
UsePresentDrawable m_use_present_drawable;
bool m_gpu_timing_enabled = false;
double m_accumulated_gpu_time = 0;
double m_last_gpu_time_end = 0;
std::mutex m_mtx;
// Draw IDs are used to make sure we're not clobbering things
u64 m_current_draw = 1;
std::atomic<u64> m_last_finished_draw{0};
// Spinning
ReadbackSpinManager m_spin_manager;
u32 m_encoders_in_current_cmdbuf;
u32 m_spin_timer = 0;
MRCOwned<id<MTLComputePipelineState>> m_spin_pipeline;
MRCOwned<id<MTLBuffer>> m_spin_buffer;
MRCOwned<id<MTLFence>> m_spin_fence;
// Functions and Pipeline States
MRCOwned<id<MTLComputePipelineState>> m_cas_pipeline[2];
// MetalFX spatial upscaler. Creating the scaler is expensive, so it's cached and
// only rebuilt when the input/output size or format changes (the cache key below).
#if !TARGET_OS_IPHONE
API_AVAILABLE(macos(13.0)) MRCOwned<id<MTLFXSpatialScaler>> m_mfx_spatial;
#endif
int m_mfx_in_w = 0, m_mfx_in_h = 0, m_mfx_out_w = 0, m_mfx_out_h = 0;
MTLPixelFormat m_mfx_in_fmt = MTLPixelFormatInvalid, m_mfx_out_fmt = MTLPixelFormatInvalid;
std::vector<MRCOwned<id<MTLRenderPipelineState>>> m_convert_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_present_pipeline[static_cast<int>(PresentShader::Count)];
MRCOwned<id<MTLRenderPipelineState>> m_merge_pipeline[4];
MRCOwned<id<MTLRenderPipelineState>> m_interlace_pipeline[NUM_INTERLACE_SHADERS];
MRCOwned<id<MTLRenderPipelineState>> m_datm_pipeline[4];
MRCOwned<id<MTLRenderPipelineState>> m_clut_pipeline[2];
MRCOwned<id<MTLRenderPipelineState>> m_stencil_clear_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_primid_init_pipeline[2][4];
MRCOwned<id<MTLRenderPipelineState>> m_colclip_init_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_colclip_clear_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_colclip_resolve_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_fxaa_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_shadeboost_pipeline;
MRCOwned<id<MTLRenderPipelineState>> m_imgui_pipeline;
id<MTLRenderPipelineState> GetConvertPipeline(ShaderConvertSelector shader) const
{
return m_convert_pipeline[shader.Index()];
}
id<MTLRenderPipelineState> GetConvertPipeline(ShaderConvert shader) const
{
return m_convert_pipeline[ShaderConvertSelector(shader).Index()];
}
MRCOwned<id<MTLFunction>> m_hw_vs[6 << 3];
std::unordered_map<PSSelector, MRCOwned<id<MTLFunction>>> m_hw_ps;
std::unordered_map<PipelineSelectorMTL, MRCOwned<id<MTLRenderPipelineState>>> m_hw_pipeline;
MRCOwned<MTLRenderPassDescriptor*> m_render_pass_desc[16];
MRCOwned<MTLRenderPassDescriptor*> m_full_rov_render_pass_desc;
MRCOwned<id<MTLSamplerState>> m_sampler_hw[1 << 8];
MRCOwned<id<MTLDepthStencilState>> m_dss_stencil_zero;
MRCOwned<id<MTLDepthStencilState>> m_dss_stencil_write;
MRCOwned<id<MTLDepthStencilState>> m_dss_hw[1 << 5];
MRCOwned<id<MTLBuffer>> m_expand_index_buffer;
UploadBuffer m_texture_upload_buf;
BufferPair m_vertex_upload_buf;
// MARK: Ephemeral resources
MRCOwned<id<MTLCommandBuffer>> m_current_render_cmdbuf;
struct MainRenderEncoder
{
MRCOwned<id<MTLRenderCommandEncoder>> encoder;
GSTexture* color_target = nullptr;
GSTexture* depth_target = nullptr;
GSTexture* stencil_target = nullptr;
GSTexture* tex[GSMTLTextureIndexCount] = {};
id<MTLBuffer> vertex_buffer = nullptr;
id<MTLBuffer> vs_index_buffer = nullptr;
void* name = nullptr;
struct Has
{
bool cb_vs : 1;
bool cb_ps : 1;
bool scissor : 1;
bool blend_color : 1;
bool pipeline_sel : 1;
bool sampler : 1;
bool rt1_depth : 1;
} has = {};
DepthStencilSelector depth_sel = DepthStencilSelector::NoDepth();
// Clear line (Things below here are tracked by `has` and don't need to be cleared to reset)
SamplerSelector sampler_sel;
u8 blend_color;
struct { u32 w, h; } full_rov_size;
GSVector4i scissor;
PipelineSelectorMTL pipeline_sel;
GSHWDrawConfig::VSConstantBuffer cb_vs;
GSHWDrawConfig::PSConstantBuffer cb_ps;
MainRenderEncoder(const MainRenderEncoder&) = delete;
MainRenderEncoder() = default;
bool is_full_rov() const { return encoder && !color_target && !depth_target && !stencil_target; }
} m_current_render;
MRCOwned<id<MTLCommandBuffer>> m_texture_upload_cmdbuf;
MRCOwned<id<MTLBlitCommandEncoder>> m_texture_upload_encoder;
MRCOwned<id<MTLBlitCommandEncoder>> m_late_texture_upload_encoder;
MRCOwned<id<MTLCommandBuffer>> m_vertex_upload_cmdbuf;
MRCOwned<id<MTLBlitCommandEncoder>> m_vertex_upload_encoder;
id<MTLTexture> m_ds_as_rt_texture = nil;
GSTexture* m_ds_as_rt_gstexture = nullptr;
MRCOwned<id<MTLTexture>> m_rov_dummy_texture;
struct { u32 w, h; } m_rov_dummy_texture_size = {};
struct DebugEntry
{
enum Op { Push, Insert, Pop } op;
MRCOwned<NSString*> str;
DebugEntry(Op op, MRCOwned<NSString*> str): op(op), str(std::move(str)) {}
};
std::vector<DebugEntry> m_debug_entries;
u32 m_debug_group_level = 0;
GSDeviceMTL();
~GSDeviceMTL() override;
/// Allocate space in the given buffer
Map Allocate(UploadBuffer& buffer, size_t amt);
/// Allocate space in the given buffer for use with the given render command encoder
Map Allocate(BufferPair& buffer, size_t amt);
/// Enqueue upload of any outstanding data
void Sync(BufferPair& buffer);
/// Get the texture upload encoder, creating a new one if it doesn't exist
id<MTLBlitCommandEncoder> GetTextureUploadEncoder();
/// Get the late texture upload encoder, creating a new one if it doesn't exist
id<MTLBlitCommandEncoder> GetLateTextureUploadEncoder();
/// Get the vertex upload encoder, creating a new one if it doesn't exist
id<MTLBlitCommandEncoder> GetVertexUploadEncoder();
/// Get the render command buffer, creating a new one if it doesn't exist
id<MTLCommandBuffer> GetRenderCmdBuf();
/// Get the render command buffer, will not create a new one if it doesn't exist.
id<MTLCommandBuffer> GetRenderCmdBufWithoutCreate();
/// Get the spin fence if spinning is enabled.
id<MTLFence> GetSpinFence();
/// Get the texture to use as RT1 depth for the given depth texture
id<MTLTexture> GetRT1DepthTexture(GSTextureMTL* depth);
/// Called by command buffers when they finish
void DrawCommandBufferFinished(u64 draw, id<MTLCommandBuffer> buffer);
/// Flush pending operations from all encoders to the GPU
void FlushEncoders();
/// Flush pending operations and spins the GPU for a download.
void FlushEncodersForReadback();
/// End current render pass without flushing
void EndRenderPass();
/// Prepare to begin a new render pass
void PrepareBeginRenderPass();
/// Begin a new render pass (may reuse existing)
void BeginRenderPass(NSString* name, GSTexture* color, MTLLoadAction color_load, GSTexture* depth, MTLLoadAction depth_load, GSTexture* stencil = nullptr, MTLLoadAction stencil_load = MTLLoadActionDontCare, bool rt1 = false);
/// Begin a new full-ROV render pass (may reuse existing)
void BeginFullROV(NSString* name, uint32_t width, uint32_t height);
/// Call at the end of each frame
void FrameCompleted();
GSTexture* CreateSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format) override;
void DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, GSVector4* dRect, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c, const Filter filter) override;
void DoInterlace(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, ShaderInterlace shader, Filter filter, const InterlaceConstantBuffer& cb) override;
void DoFXAA(GSTexture* sTex, GSTexture* dTex) override;
void DoShadeBoost(GSTexture* sTex, GSTexture* dTex, const float params[4]) override;
bool DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array<u32, NUM_CAS_CONSTANTS>& constants) override;
/// (Re)builds m_mfx_spatial when the src/dst size or format changes. Returns false on failure.
API_AVAILABLE(macos(13.0)) bool EnsureMetalFXSpatial(GSTexture* sTex, GSTexture* dTex);
bool DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) override;
MRCOwned<id<MTLFunction>> LoadShader(NSString* name);
MRCOwned<id<MTLRenderPipelineState>> MakePipeline(MTLRenderPipelineDescriptor* desc, id<MTLFunction> vertex, id<MTLFunction> fragment, NSString* name);
MRCOwned<id<MTLComputePipelineState>> MakeComputePipeline(id<MTLFunction> compute, NSString* name);
bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override;
void AttachSurfaceOnMainThread();
void DetachSurfaceOnMainThread();
RenderAPI GetRenderAPI() const override;
bool HasSurface() const override;
void DestroySurface() override;
bool UpdateWindow() override;
bool SupportsExclusiveFullscreen() const override;
std::string GetDriverInfo() const override;
void ResizeWindow(u32 new_window_width, u32 new_window_height, float new_window_scale) override;
void UpdateTexture(id<MTLTexture> texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride);
PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override;
void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;
void AccumulateCommandBufferTime(id<MTLCommandBuffer> buffer);
bool SetGPUPipelineStatisticsEnabled(bool enabled) override { return false; }
GPUPipelineStatistics GetAndResetAccumulatedGPUPipelineStatistics() override { return {}; }
std::unique_ptr<GSDownloadTexture> CreateDownloadTexture(u32 width, u32 height, GSTexture::Format format) override;
void ClearSamplerCache() override;
void CopyRect(GSTexture* sTex, GSTexture* dTex, const GSVector4i& r, u32 destX, u32 destY) override;
void BeginStretchRect(NSString* name, GSTexture* dTex, MTLLoadAction action);
void DoStretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, id<MTLRenderPipelineState> pipeline, std::optional<Filter> filter, LoadAction load_action, const void* frag_uniform, size_t frag_uniform_len);
void DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const GSVector2& ds);
/// Copy from a position in sTex to the same position in the currently active render encoder using the given fs pipeline and rect
void RenderCopy(GSTexture* sTex, id<MTLRenderPipelineState> pipeline, const GSVector4i& rect);
void PresentRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect, PresentShader shader, float shaderTime, Filter filter) override;
void DrawMultiStretchRects(const MultiStretchRect* rects, u32 num_rects, GSTexture* dTex, ShaderConvertSelector shader) override;
void UpdateCLUTTexture(GSTexture* sTex, float sScale, u32 offsetX, u32 offsetY, GSTexture* dTex, u32 dOffset, u32 dSize) override;
void ConvertToIndexedTexture(GSTexture* sTex, float sScale, u32 offsetX, u32 offsetY, u32 SBW, u32 SPSM, GSTexture* dTex, u32 DBW, u32 DPSM) override;
void FilteredDownsampleTexture(GSTexture* sTex, GSTexture* dTex, u32 downsample_factor, const GSVector2i& clamp_min, const GSVector4& dRect) override;
void BeginDSAsRT(GSTexture* ds, const GSVector4i& drawarea) override;
void FlushClears(GSTexture* tex);
// MARK: Main Render Encoder operations
void MRESetHWPipelineState(GSHWDrawConfig::VSSelector vs, GSHWDrawConfig::PSSelector ps, GSHWDrawConfig::BlendState blend, GSHWDrawConfig::ColorMaskSelector cms);
void MRESetDSS(DepthStencilSelector sel);
void MRESetDSS(id<MTLDepthStencilState> dss);
void MRESetSampler(SamplerSelector sel);
void MRESetTexture(GSTexture* tex, int pos);
void MRESetTexture(id<MTLTexture> tex, int pos);
void MRESetVertices(id<MTLBuffer> buffer, size_t offset);
void MRESetVSIndices(id<MTLBuffer> buffer, size_t offset);
void MRESetScissor(const GSVector4i& scissor);
void MREClearScissor();
void MRESetCB(const GSHWDrawConfig::VSConstantBuffer& cb_vs);
void MRESetCB(const GSHWDrawConfig::PSConstantBuffer& cb_ps);
void MRESetBlendColor(u8 blend_color);
void MRESetPipeline(id<MTLRenderPipelineState> pipe);
void MREInitHWDraw(GSHWDrawConfig& config, const Map& verts);
// MARK: Render HW
void SetupDestinationAlpha(GSTexture* rt, GSTexture* ds, const GSVector4i& r, SetDATM datm);
void PrepareROVTexture(GSTexture** ptex);
void RenderHW(GSHWDrawConfig& config) override;
void SendHWDraw(GSHWDrawConfig& config, id<MTLRenderCommandEncoder> enc, id<MTLBuffer> buffer, size_t off,
bool one_barrier, bool full_barrier);
// MARK: Debug
void PushDebugGroup(const char* fmt, ...) override;
void PopDebugGroup() override;
void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override;
void ProcessDebugEntry(id<MTLCommandEncoder> enc, const DebugEntry& entry);
void FlushDebugEntries(id<MTLCommandEncoder> enc);
void EndDebugGroup(id<MTLCommandEncoder> enc);
// MARK: ImGui
void RenderImGui(ImDrawData* data);
u32 FrameNo() const { return m_frame; }
protected:
using GSDevice::DoStretchRect; // Suppress overloaded virtual function warning
virtual void DoStretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
ShaderConvertSelector shader, Filter filter) override;
};
static constexpr bool IsCommandBufferCompleted(MTLCommandBufferStatus status)
{
switch (status)
{
case MTLCommandBufferStatusNotEnqueued:
case MTLCommandBufferStatusEnqueued:
case MTLCommandBufferStatusCommitted:
case MTLCommandBufferStatusScheduled:
return false;
case MTLCommandBufferStatusCompleted:
case MTLCommandBufferStatusError:
return true;
}
}
#endif // __APPLE__