mirror of https://github.com/PCSX2/pcsx2.git
468 lines
15 KiB
Plaintext
468 lines
15 KiB
Plaintext
/* PCSX2 - PS2 Emulator for PCs
|
|
* Copyright (C) 2002-2022 PCSX2 Dev Team
|
|
*
|
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* PCSX2 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 PCSX2.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "PrecompiledHeader.h"
|
|
#include "MetalHostDisplay.h"
|
|
#include "GS/Renderers/Metal/GSMetalCPPAccessible.h"
|
|
#include "GS/Renderers/Metal/GSDeviceMTL.h"
|
|
#include <imgui.h>
|
|
|
|
#ifdef __APPLE__
|
|
|
|
class MetalHostDisplayTexture final : public HostDisplayTexture
|
|
{
|
|
MRCOwned<id<MTLTexture>> m_tex;
|
|
u32 m_width, m_height;
|
|
public:
|
|
MetalHostDisplayTexture(MRCOwned<id<MTLTexture>> tex, u32 width, u32 height)
|
|
: m_tex(std::move(tex))
|
|
, m_width(width)
|
|
, m_height(height)
|
|
{
|
|
}
|
|
|
|
void* GetHandle() const override { return (__bridge void*)m_tex; };
|
|
u32 GetWidth() const override { return m_width; }
|
|
u32 GetHeight() const override { return m_height; }
|
|
};
|
|
|
|
HostDisplay* MakeMetalHostDisplay()
|
|
{
|
|
return new MetalHostDisplay();
|
|
}
|
|
|
|
MetalHostDisplay::MetalHostDisplay()
|
|
{
|
|
}
|
|
|
|
MetalHostDisplay::~MetalHostDisplay()
|
|
{
|
|
MetalHostDisplay::DestroySurface();
|
|
m_queue = nullptr;
|
|
m_dev.Reset();
|
|
}
|
|
|
|
HostDisplay::AdapterAndModeList GetMetalAdapterAndModeList()
|
|
{ @autoreleasepool {
|
|
HostDisplay::AdapterAndModeList list;
|
|
auto devs = MRCTransfer(MTLCopyAllDevices());
|
|
for (id<MTLDevice> dev in devs.Get())
|
|
list.adapter_names.push_back([[dev name] UTF8String]);
|
|
return list;
|
|
}}
|
|
|
|
template <typename Fn>
|
|
static void OnMainThread(Fn&& fn)
|
|
{
|
|
if ([NSThread isMainThread])
|
|
fn();
|
|
else
|
|
dispatch_sync(dispatch_get_main_queue(), fn);
|
|
}
|
|
|
|
RenderAPI MetalHostDisplay::GetRenderAPI() const
|
|
{
|
|
return RenderAPI::Metal;
|
|
}
|
|
|
|
void* MetalHostDisplay::GetDevice() const { return const_cast<void*>(static_cast<const void*>(&m_dev)); }
|
|
void* MetalHostDisplay::GetContext() const { return (__bridge void*)m_queue; }
|
|
void* MetalHostDisplay::GetSurface() const { return (__bridge void*)m_layer; }
|
|
bool MetalHostDisplay::HasDevice() const { return m_dev.IsOk(); }
|
|
bool MetalHostDisplay::HasSurface() const { return static_cast<bool>(m_layer);}
|
|
|
|
void MetalHostDisplay::AttachSurfaceOnMainThread()
|
|
{
|
|
ASSERT([NSThread isMainThread]);
|
|
m_layer = MRCRetain([CAMetalLayer layer]);
|
|
[m_layer setDrawableSize:CGSizeMake(m_window_info.surface_width, m_window_info.surface_height)];
|
|
[m_layer setDevice:m_dev.dev];
|
|
m_view = MRCRetain((__bridge NSView*)m_window_info.window_handle);
|
|
[m_view setWantsLayer:YES];
|
|
[m_view setLayer:m_layer];
|
|
}
|
|
|
|
void MetalHostDisplay::DetachSurfaceOnMainThread()
|
|
{
|
|
ASSERT([NSThread isMainThread]);
|
|
[m_view setLayer:nullptr];
|
|
[m_view setWantsLayer:NO];
|
|
m_view = nullptr;
|
|
m_layer = nullptr;
|
|
}
|
|
|
|
bool MetalHostDisplay::CreateDevice(const WindowInfo& wi, VsyncMode vsync)
|
|
{ @autoreleasepool {
|
|
m_window_info = wi;
|
|
pxAssertRel(!m_dev.dev, "Device already created!");
|
|
NSString* ns_adapter_name = [NSString stringWithUTF8String:EmuConfig.GS.Adapter.c_str()];
|
|
auto devs = MRCTransfer(MTLCopyAllDevices());
|
|
for (id<MTLDevice> dev in devs.Get())
|
|
{
|
|
if ([[dev name] isEqualToString:ns_adapter_name])
|
|
m_dev = GSMTLDevice(MRCRetain(dev));
|
|
}
|
|
if (!m_dev.dev)
|
|
{
|
|
if (!EmuConfig.GS.Adapter.empty())
|
|
Console.Warning("Metal: Couldn't find adapter %s, using default", EmuConfig.GS.Adapter.c_str());
|
|
m_dev = GSMTLDevice(MRCTransfer(MTLCreateSystemDefaultDevice()));
|
|
if (!m_dev.dev)
|
|
Host::ReportErrorAsync("No Metal Devices Available", "No Metal-supporting GPUs were found. PCSX2 requires a Metal GPU (available on all macs from 2012 onwards).");
|
|
}
|
|
m_queue = MRCTransfer([m_dev.dev newCommandQueue]);
|
|
|
|
m_pass_desc = MRCTransfer([MTLRenderPassDescriptor new]);
|
|
[m_pass_desc colorAttachments][0].loadAction = MTLLoadActionClear;
|
|
[m_pass_desc colorAttachments][0].clearColor = MTLClearColorMake(0, 0, 0, 0);
|
|
[m_pass_desc colorAttachments][0].storeAction = MTLStoreActionStore;
|
|
|
|
if (char* env = getenv("MTL_USE_PRESENT_DRAWABLE"))
|
|
m_use_present_drawable = static_cast<UsePresentDrawable>(atoi(env));
|
|
else if (@available(macOS 13.0, *))
|
|
m_use_present_drawable = UsePresentDrawable::Always;
|
|
else // Before Ventura, presentDrawable acts like vsync is on when windowed
|
|
m_use_present_drawable = UsePresentDrawable::IfVsync;
|
|
|
|
m_capture_start_frame = 0;
|
|
if (char* env = getenv("MTL_CAPTURE"))
|
|
{
|
|
m_capture_start_frame = atoi(env);
|
|
}
|
|
if (m_capture_start_frame)
|
|
{
|
|
Console.WriteLn("Metal will capture frame %u", m_capture_start_frame);
|
|
}
|
|
|
|
if (m_dev.IsOk() && m_queue)
|
|
{
|
|
OnMainThread([this]
|
|
{
|
|
AttachSurfaceOnMainThread();
|
|
});
|
|
SetVSync(vsync);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}}
|
|
|
|
bool MetalHostDisplay::SetupDevice()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool MetalHostDisplay::MakeCurrent() { return true; }
|
|
bool MetalHostDisplay::DoneCurrent() { return true; }
|
|
|
|
void MetalHostDisplay::DestroySurface()
|
|
{
|
|
if (!m_layer)
|
|
return;
|
|
OnMainThread([this]{ DetachSurfaceOnMainThread(); });
|
|
m_layer = nullptr;
|
|
}
|
|
|
|
bool MetalHostDisplay::ChangeWindow(const WindowInfo& wi)
|
|
{
|
|
OnMainThread([this, &wi]
|
|
{
|
|
DetachSurfaceOnMainThread();
|
|
m_window_info = wi;
|
|
AttachSurfaceOnMainThread();
|
|
});
|
|
return true;
|
|
}
|
|
|
|
bool MetalHostDisplay::SupportsFullscreen() const { return false; }
|
|
bool MetalHostDisplay::IsFullscreen() { return false; }
|
|
bool MetalHostDisplay::SetFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) { return false; }
|
|
|
|
HostDisplay::AdapterAndModeList MetalHostDisplay::GetAdapterAndModeList()
|
|
{
|
|
return GetMetalAdapterAndModeList();
|
|
}
|
|
|
|
std::string MetalHostDisplay::GetDriverInfo() const
|
|
{ @autoreleasepool {
|
|
std::string desc([[m_dev.dev description] UTF8String]);
|
|
desc += "\n Texture Swizzle: " + std::string(m_dev.features.texture_swizzle ? "Supported" : "Unsupported");
|
|
desc += "\n Unified Memory: " + std::string(m_dev.features.unified_memory ? "Supported" : "Unsupported");
|
|
desc += "\n Framebuffer Fetch: " + std::string(m_dev.features.framebuffer_fetch ? "Supported" : "Unsupported");
|
|
desc += "\n Primitive ID: " + std::string(m_dev.features.primid ? "Supported" : "Unsupported");
|
|
desc += "\n Shader Version: " + std::string(to_string(m_dev.features.shader_version));
|
|
desc += "\n Max Texture Size: " + std::to_string(m_dev.features.max_texsize);
|
|
return desc;
|
|
}}
|
|
|
|
void MetalHostDisplay::ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale)
|
|
{
|
|
m_window_info.surface_scale = new_window_scale;
|
|
if (m_window_info.surface_width == static_cast<u32>(new_window_width) && m_window_info.surface_height == static_cast<u32>(new_window_height))
|
|
return;
|
|
m_window_info.surface_width = new_window_width;
|
|
m_window_info.surface_height = new_window_height;
|
|
@autoreleasepool
|
|
{
|
|
[m_layer setDrawableSize:CGSizeMake(new_window_width, new_window_height)];
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<HostDisplayTexture> MetalHostDisplay::CreateTexture(u32 width, u32 height, const void* data, u32 data_stride, bool dynamic)
|
|
{ @autoreleasepool {
|
|
MTLTextureDescriptor* desc = [MTLTextureDescriptor
|
|
texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
|
width:width
|
|
height:height
|
|
mipmapped:false];
|
|
[desc setUsage:MTLTextureUsageShaderRead];
|
|
[desc setStorageMode:MTLStorageModePrivate];
|
|
MRCOwned<id<MTLTexture>> tex = MRCTransfer([m_dev.dev newTextureWithDescriptor:desc]);
|
|
if (!tex)
|
|
return nullptr; // Something broke yay
|
|
[tex setLabel:@"MetalHostDisplay Texture"];
|
|
if (data)
|
|
UpdateTexture(tex, 0, 0, width, height, data, data_stride);
|
|
return std::make_unique<MetalHostDisplayTexture>(std::move(tex), width, height);
|
|
}}
|
|
|
|
void MetalHostDisplay::UpdateTexture(id<MTLTexture> texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride)
|
|
{
|
|
id<MTLCommandBuffer> cmdbuf = [m_queue commandBuffer];
|
|
id<MTLBlitCommandEncoder> enc = [cmdbuf blitCommandEncoder];
|
|
size_t bytes = data_stride * height;
|
|
MRCOwned<id<MTLBuffer>> buf = MRCTransfer([m_dev.dev newBufferWithLength:bytes options:MTLResourceStorageModeShared | MTLResourceCPUCacheModeWriteCombined]);
|
|
memcpy([buf contents], data, bytes);
|
|
[enc copyFromBuffer:buf
|
|
sourceOffset:0
|
|
sourceBytesPerRow:data_stride
|
|
sourceBytesPerImage:bytes
|
|
sourceSize:MTLSizeMake(width, height, 1)
|
|
toTexture:texture
|
|
destinationSlice:0
|
|
destinationLevel:0
|
|
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
[enc endEncoding];
|
|
[cmdbuf commit];
|
|
}
|
|
|
|
void MetalHostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, u32 data_stride)
|
|
{ @autoreleasepool {
|
|
UpdateTexture((__bridge id<MTLTexture>)texture->GetHandle(), x, y, width, height, data, data_stride);
|
|
}}
|
|
|
|
static bool s_capture_next = false;
|
|
|
|
HostDisplay::PresentResult MetalHostDisplay::BeginPresent(bool frame_skip)
|
|
{ @autoreleasepool {
|
|
GSDeviceMTL* dev = static_cast<GSDeviceMTL*>(g_gs_device.get());
|
|
if (dev && m_capture_start_frame && dev->FrameNo() == m_capture_start_frame)
|
|
s_capture_next = true;
|
|
if (frame_skip || m_window_info.type == WindowInfo::Type::Surfaceless || !g_gs_device)
|
|
{
|
|
ImGui::EndFrame();
|
|
return PresentResult::FrameSkipped;
|
|
}
|
|
id<MTLCommandBuffer> buf = dev->GetRenderCmdBuf();
|
|
m_current_drawable = MRCRetain([m_layer nextDrawable]);
|
|
dev->EndRenderPass();
|
|
if (!m_current_drawable)
|
|
{
|
|
[buf pushDebugGroup:@"Present Skipped"];
|
|
[buf popDebugGroup];
|
|
dev->FlushEncoders();
|
|
ImGui::EndFrame();
|
|
return PresentResult::FrameSkipped;
|
|
}
|
|
[m_pass_desc colorAttachments][0].texture = [m_current_drawable texture];
|
|
id<MTLRenderCommandEncoder> enc = [buf renderCommandEncoderWithDescriptor:m_pass_desc];
|
|
[enc setLabel:@"Present"];
|
|
dev->m_current_render.encoder = MRCRetain(enc);
|
|
return PresentResult::OK;
|
|
}}
|
|
|
|
void MetalHostDisplay::EndPresent()
|
|
{ @autoreleasepool {
|
|
GSDeviceMTL* dev = static_cast<GSDeviceMTL*>(g_gs_device.get());
|
|
pxAssertDev(dev && dev->m_current_render.encoder && dev->m_current_render_cmdbuf, "BeginPresent cmdbuf was destroyed");
|
|
ImGui::Render();
|
|
dev->RenderImGui(ImGui::GetDrawData());
|
|
dev->EndRenderPass();
|
|
if (m_current_drawable)
|
|
{
|
|
const bool use_present_drawable = m_use_present_drawable == UsePresentDrawable::Always ||
|
|
(m_use_present_drawable == UsePresentDrawable::IfVsync && m_vsync_mode != VsyncMode::Off);
|
|
|
|
if (use_present_drawable)
|
|
[dev->m_current_render_cmdbuf presentDrawable:m_current_drawable];
|
|
else
|
|
[dev->m_current_render_cmdbuf addScheduledHandler:[drawable = std::move(m_current_drawable)](id<MTLCommandBuffer>){
|
|
[drawable present];
|
|
}];
|
|
}
|
|
dev->FlushEncoders();
|
|
dev->FrameCompleted();
|
|
m_current_drawable = nullptr;
|
|
if (m_capture_start_frame)
|
|
{
|
|
if (@available(macOS 10.15, iOS 13, *))
|
|
{
|
|
static NSString* const path = @"/tmp/PCSX2MTLCapture.gputrace";
|
|
static u32 frames;
|
|
if (frames)
|
|
{
|
|
--frames;
|
|
if (!frames)
|
|
{
|
|
[[MTLCaptureManager sharedCaptureManager] stopCapture];
|
|
Console.WriteLn("Metal Trace Capture to /tmp/PCSX2MTLCapture.gputrace finished");
|
|
[[NSWorkspace sharedWorkspace] selectFile:path
|
|
inFileViewerRootedAtPath:@"/tmp/"];
|
|
}
|
|
}
|
|
else if (s_capture_next)
|
|
{
|
|
s_capture_next = false;
|
|
MTLCaptureManager* mgr = [MTLCaptureManager sharedCaptureManager];
|
|
if ([mgr supportsDestination:MTLCaptureDestinationGPUTraceDocument])
|
|
{
|
|
MTLCaptureDescriptor* desc = [[MTLCaptureDescriptor new] autorelease];
|
|
[desc setCaptureObject:m_dev.dev];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:path])
|
|
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
|
|
[desc setOutputURL:[NSURL fileURLWithPath:path]];
|
|
[desc setDestination:MTLCaptureDestinationGPUTraceDocument];
|
|
NSError* err = nullptr;
|
|
[mgr startCaptureWithDescriptor:desc error:&err];
|
|
if (err)
|
|
{
|
|
Console.Error("Metal Trace Capture failed: %s", [[err localizedDescription] UTF8String]);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLn("Metal Trace Capture to /tmp/PCSX2MTLCapture.gputrace started");
|
|
frames = 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.Error("Metal Trace Capture Failed: MTLCaptureManager doesn't support GPU trace documents! (Did you forget to run with METAL_CAPTURE_ENABLED=1?)");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
|
|
void MetalHostDisplay::SetVSync(VsyncMode mode)
|
|
{
|
|
[m_layer setDisplaySyncEnabled:mode != VsyncMode::Off];
|
|
m_vsync_mode = mode;
|
|
}
|
|
|
|
bool MetalHostDisplay::CreateImGuiContext()
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.BackendRendererName = "pcsx2_imgui_metal";
|
|
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
|
return true;
|
|
}
|
|
|
|
void MetalHostDisplay::DestroyImGuiContext()
|
|
{
|
|
ImGui::GetIO().Fonts->SetTexID(nullptr);
|
|
}
|
|
|
|
bool MetalHostDisplay::UpdateImGuiFontTexture()
|
|
{ @autoreleasepool {
|
|
u8* data;
|
|
int width, height;
|
|
ImFontAtlas* fonts = ImGui::GetIO().Fonts;
|
|
fonts->GetTexDataAsAlpha8(&data, &width, &height);
|
|
MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatA8Unorm width:width height:height mipmapped:false];
|
|
[desc setUsage:MTLTextureUsageShaderRead];
|
|
[desc setStorageMode:MTLStorageModePrivate];
|
|
if (@available(macOS 10.15, *))
|
|
if (m_dev.features.texture_swizzle)
|
|
[desc setSwizzle:MTLTextureSwizzleChannelsMake(MTLTextureSwizzleOne, MTLTextureSwizzleOne, MTLTextureSwizzleOne, MTLTextureSwizzleAlpha)];
|
|
m_font_tex = MRCTransfer([m_dev.dev newTextureWithDescriptor:desc]);
|
|
[m_font_tex setLabel:@"ImGui Font"];
|
|
UpdateTexture(m_font_tex, 0, 0, width, height, data, width);
|
|
fonts->SetTexID((__bridge void*)m_font_tex);
|
|
return static_cast<bool>(m_font_tex);
|
|
}}
|
|
|
|
bool MetalHostDisplay::GetHostRefreshRate(float* refresh_rate)
|
|
{
|
|
OnMainThread([this, refresh_rate]
|
|
{
|
|
u32 did = [[[[[m_view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
|
|
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
|
|
{
|
|
*refresh_rate = CGDisplayModeGetRefreshRate(mode);
|
|
CGDisplayModeRelease(mode);
|
|
}
|
|
else
|
|
{
|
|
*refresh_rate = 0;
|
|
}
|
|
});
|
|
return *refresh_rate != 0;
|
|
}
|
|
|
|
bool MetalHostDisplay::SetGPUTimingEnabled(bool enabled)
|
|
{
|
|
if (enabled == m_gpu_timing_enabled)
|
|
return true;
|
|
if (@available(macOS 10.15, iOS 10.3, *))
|
|
{
|
|
std::lock_guard<std::mutex> l(m_mtx);
|
|
m_gpu_timing_enabled = enabled;
|
|
m_accumulated_gpu_time = 0;
|
|
m_last_gpu_time_end = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float MetalHostDisplay::GetAndResetAccumulatedGPUTime()
|
|
{
|
|
std::lock_guard<std::mutex> l(m_mtx);
|
|
float time = m_accumulated_gpu_time * 1000;
|
|
m_accumulated_gpu_time = 0;
|
|
return time;
|
|
}
|
|
|
|
void MetalHostDisplay::AccumulateCommandBufferTime(id<MTLCommandBuffer> buffer)
|
|
{
|
|
std::lock_guard<std::mutex> l(m_mtx);
|
|
if (!m_gpu_timing_enabled)
|
|
return;
|
|
// We do the check before enabling m_gpu_timing_enabled
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
|
// It's unlikely, but command buffers can overlap or run out of order
|
|
// This doesn't handle every case (fully out of order), but it should at least handle overlapping
|
|
double begin = std::max(m_last_gpu_time_end, [buffer GPUStartTime]);
|
|
double end = [buffer GPUEndTime];
|
|
if (end > begin)
|
|
{
|
|
m_accumulated_gpu_time += end - begin;
|
|
m_last_gpu_time_end = end;
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
#endif // __APPLE__
|