Add Metal support to Qt frontend and clean up renderer creation code (#795)

* Qt: Initial support for Metal renderer

* Clean up graphics context code

* Nits

* More nits

* Qt: Move screen-related stuff to own folder

* Qt: Make screen widget polymorphic

* Qt: Re-add Metal

* Add factory for screen widget

* Qt: Support compilation without Metal

* Qt: Fix build without Metal

* Oops

* oops
This commit is contained in:
wheremyfoodat
2025-07-26 23:13:08 +03:00
committed by GitHub
parent 8b0b1939cf
commit 0446bcdaa1
23 changed files with 331 additions and 146 deletions

View File

@@ -191,7 +191,7 @@ void RendererGL::initGraphicsContextInternal() {
// The OpenGL renderer doesn't need to do anything with the GL context (For Qt frontend) or the SDL window (For SDL frontend)
// So we just call initGraphicsContextInternal for both
void RendererGL::initGraphicsContext([[maybe_unused]] SDL_Window* window) { initGraphicsContextInternal(); }
void RendererGL::initGraphicsContext([[maybe_unused]] void* context) { initGraphicsContextInternal(); }
// Set up the OpenGL blending context to match the emulated PICA
void RendererGL::setupBlending() {

View File

@@ -10,8 +10,8 @@
#include "PICA/gpu.hpp"
#include "PICA/pica_hash.hpp"
#include "screen_layout.hpp"
#include "SDL_metal.h"
#include "screen_layout.hpp"
using namespace PICA;
@@ -57,9 +57,7 @@ void RendererMTL::reset() {
colorRenderTargetCache.reset();
}
void RendererMTL::setMTKLayer(void* layer) {
metalLayer = (CA::MetalLayer*)layer;
}
void RendererMTL::setMTKLayer(void* layer) { metalLayer = (CA::MetalLayer*)layer; }
void RendererMTL::display() {
CA::MetalDrawable* drawable = metalLayer->nextDrawable();
@@ -151,13 +149,13 @@ void RendererMTL::display() {
drawable->release();
}
void RendererMTL::initGraphicsContext(SDL_Window* window) {
// On iOS, the SwiftUI side handles the MetalLayer
#ifdef PANDA3DS_IOS
void RendererMTL::initGraphicsContext(void* window) {
// On Qt and IOS, the frontend handles the MetalLayer
#if defined(PANDA3DS_FRONTEND_QT) || defined(PANDA3DS_IOS)
device = MTL::CreateSystemDefaultDevice();
#else
// TODO: what should be the type of the view?
void* view = SDL_Metal_CreateView(window);
void* view = SDL_Metal_CreateView((SDL_Window*)window);
metalLayer = (CA::MetalLayer*)SDL_Metal_GetLayer(view);
device = MTL::CreateSystemDefaultDevice();
metalLayer->setDevice(device);

View File

@@ -6,7 +6,7 @@ RendererNull::~RendererNull() {}
void RendererNull::reset() {}
void RendererNull::display() {}
void RendererNull::initGraphicsContext(SDL_Window* window) {}
void RendererNull::initGraphicsContext(void* context) {}
void RendererNull::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {}
void RendererNull::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {}
void RendererNull::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {}

View File

@@ -7,7 +7,7 @@ RendererSw::~RendererSw() {}
void RendererSw::reset() { printf("RendererSW: Unimplemented reset call\n"); }
void RendererSw::display() { printf("RendererSW: Unimplemented display call\n"); }
void RendererSw::initGraphicsContext(SDL_Window* window) { printf("RendererSW: Unimplemented initGraphicsContext call\n"); }
void RendererSw::initGraphicsContext(void* context) { printf("RendererSW: Unimplemented initGraphicsContext call\n"); }
void RendererSw::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) { printf("RendererSW: Unimplemented clearBuffer call\n"); }
void RendererSw::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {

View File

@@ -173,7 +173,8 @@ std::tuple<vk::UniquePipeline, vk::UniquePipelineLayout> createGraphicsPipeline(
vk::PipelineDynamicStateCreateInfo dynamicState = {};
static vk::DynamicState dynamicStates[] = {// The viewport and scissor of the framebuffer will be dynamic at
// run-time
vk::DynamicState::eViewport, vk::DynamicState::eScissor};
vk::DynamicState::eViewport, vk::DynamicState::eScissor
};
dynamicState.dynamicStateCount = std::size(dynamicStates);
dynamicState.pDynamicStates = dynamicStates;
@@ -469,7 +470,8 @@ vk::RenderPass RendererVK::getRenderPass(vk::Format colorFormat, std::optional<v
vk::SubpassDependency(
0, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eAllGraphics, vk::PipelineStageFlagBits::eAllGraphics,
vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eColorAttachmentWrite, vk::DependencyFlagBits::eByRegion
)};
)
};
renderPassInfo.setDependencies(subpassDependencies);
@@ -892,8 +894,8 @@ using VulkanDynamicLoader = vk::detail::DynamicLoader;
using VulkanDynamicLoader = vk::DynamicLoader;
#endif
void RendererVK::initGraphicsContext(SDL_Window* window) {
targetWindow = window;
void RendererVK::initGraphicsContext(void* windowPointer) {
targetWindow = (SDL_Window*)windowPointer;
// Resolve all instance function pointers
static VulkanDynamicLoader dl;
VULKAN_HPP_DEFAULT_DISPATCHER.init(dl.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr"));
@@ -978,8 +980,8 @@ void RendererVK::initGraphicsContext(SDL_Window* window) {
}
// Create surface
if (window) {
if (VkSurfaceKHR newSurface; SDL_Vulkan_CreateSurface(window, instance.get(), &newSurface)) {
if (targetWindow) {
if (VkSurfaceKHR newSurface; SDL_Vulkan_CreateSurface(targetWindow, instance.get(), &newSurface)) {
swapchainSurface = newSurface;
} else {
Helpers::warn("Error creating Vulkan surface");
@@ -1127,7 +1129,7 @@ void RendererVK::initGraphicsContext(SDL_Window* window) {
vk::Extent2D swapchainExtent;
{
int windowWidth, windowHeight;
SDL_Vulkan_GetDrawableSize(window, &windowWidth, &windowHeight);
SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight);
swapchainExtent.width = windowWidth;
swapchainExtent.height = windowHeight;
}
@@ -1275,7 +1277,8 @@ void RendererVK::initGraphicsContext(SDL_Window* window) {
static vk::DescriptorSetLayoutBinding displayShaderLayout[] = {
{// Just a singular texture slot
0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment
},
};
if (auto createResult = Vulkan::DescriptorUpdateBatch::create(device.get()); createResult.has_value()) {
@@ -1407,7 +1410,8 @@ void RendererVK::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 co
static vk::ImageSubresourceRange depthStencilRanges[2] = {
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1),
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1)};
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1)
};
// Clear RenderTarget
getCurrentCommandBuffer().clearDepthStencilImage(

View File

@@ -10,6 +10,7 @@
#include "cheats.hpp"
#include "input_mappings.hpp"
#include "panda_qt/dsp_debugger.hpp"
#include "panda_qt/screen/screen.hpp"
#include "sdl_sensors.hpp"
#include "services/dsp.hpp"
#include "version.hpp"
@@ -25,8 +26,19 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
resize(800, 240 * 4);
show();
const RendererType rendererType = emu->getConfig().rendererType;
usingGL = (rendererType == RendererType::OpenGL || rendererType == RendererType::Software || rendererType == RendererType::Null);
usingVk = (rendererType == RendererType::Vulkan);
usingMtl = (rendererType == RendererType::Metal);
ScreenWidget::API api = ScreenWidget::API::OpenGL;
if (usingVk)
api = ScreenWidget::API::Vulkan;
else if (usingMtl)
api = ScreenWidget::API::Metal;
// We pass a callback to the screen widget that will be triggered every time we resize the screen
screen = new ScreenWidget([this](u32 width, u32 height) { handleScreenResize(width, height); }, this);
screen = ScreenWidget::getWidget(api, [this](u32 width, u32 height) { handleScreenResize(width, height); }, this);
setCentralWidget(screen);
appRunning = true;
@@ -149,28 +161,29 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
// The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work
emuThread = std::thread([this]() {
const RendererType rendererType = emu->getConfig().rendererType;
usingGL = (rendererType == RendererType::OpenGL || rendererType == RendererType::Software || rendererType == RendererType::Null);
usingVk = (rendererType == RendererType::Vulkan);
usingMtl = (rendererType == RendererType::Metal);
switch (screen->api) {
case ScreenWidget::API::OpenGL: {
// Make GL context current for this thread, enable VSync
GL::Context* glContext = screen->getGLContext();
glContext->MakeCurrent();
glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0);
if (usingGL) {
// Make GL context current for this thread, enable VSync
GL::Context* glContext = screen->getGLContext();
glContext->MakeCurrent();
glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0);
if (glContext->IsGLES()) {
emu->getRenderer()->setupGLES();
}
if (glContext->IsGLES()) {
emu->getRenderer()->setupGLES();
emu->initGraphicsContext(glContext);
break;
}
emu->initGraphicsContext(glContext);
} else if (usingVk) {
Helpers::panic("Vulkan on Qt is currently WIP, try the SDL frontend instead!");
} else if (usingMtl) {
Helpers::panic("Metal on Qt currently doesn't work, try the SDL frontend instead!");
} else {
Helpers::panic("Unsupported graphics backend for Qt frontend!");
case ScreenWidget::API::Metal: {
emu->initGraphicsContext(nullptr);
emu->getRenderer()->setMTKLayer(screen->getMTKLayer());
break;
}
case ScreenWidget::API::Vulkan: Helpers::panic("Vulkan on Qt is currently WIP, try the SDL frontend instead!"); break;
default: Helpers::panic("Unsupported graphics backend for Qt frontend!"); break;
}
// We have to initialize controllers on the same thread they'll be polled in
@@ -213,6 +226,8 @@ void MainWindow::emuThreadMainLoop() {
void MainWindow::swapEmuBuffer() {
if (usingGL) {
screen->getGLContext()->SwapBuffers();
} else if (usingMtl) {
// The renderer itself calls presentDrawable to swap buffers on Metal
} else {
Helpers::panic("[Qt] Don't know how to swap buffers for the current rendering backend :(");
}
@@ -290,6 +305,7 @@ MainWindow::~MainWindow() {
delete aboutWindow;
delete configWindow;
delete cheatsEditor;
delete screen;
delete luaEditor;
}

View File

@@ -0,0 +1,71 @@
#import <AppKit/AppKit.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.hpp>
#import <QWindow>
#import <QuartzCore/QuartzCore.hpp>
#import "panda_qt/screen/screen_mtl.hpp"
id<MTLDevice> metalDevice = nil;
bool ScreenWidgetMTL::createMetalContext() {
NSView* nativeView = (NSView*)this->winId();
// Retain the layer so that we can manually memory manage it.
CAMetalLayer* metalLayer = [[CAMetalLayer layer] retain];
if (!metalLayer) {
return false;
}
metalDevice = MTLCreateSystemDefaultDevice();
if (!metalDevice) {
NSLog(@"Failed to create metal device");
return false;
}
metalLayer.device = metalDevice;
metalLayer.framebufferOnly = NO;
metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
CGFloat scale = [nativeView window].backingScaleFactor;
CGSize pointSize = nativeView.bounds.size;
metalLayer.contentsScale = scale;
metalLayer.drawableSize = CGSizeMake(pointSize.width * scale, pointSize.height * scale);
[nativeView setLayer:metalLayer];
[nativeView setWantsLayer:YES];
CA::MetalLayer* cppLayer = (CA::MetalLayer*)metalLayer;
mtkLayer = static_cast<void*>(cppLayer);
return true;
}
void ScreenWidgetMTL::resizeMetalView() {
NSView* view = (NSView*)this->windowHandle()->winId();
CAMetalLayer* metalLayer = (CAMetalLayer*)[view layer];
if (metalLayer) {
metalLayer.drawableSize = CGSizeMake(surfaceWidth, surfaceHeight);
}
}
ScreenWidgetMTL::~ScreenWidgetMTL() {
if (mtkLayer) {
CAMetalLayer* metalLayer = (__bridge CAMetalLayer*)static_cast<CA::MetalLayer*>(mtkLayer);
NSView* view = (NSView*)this->winId();
[view setLayer:nil];
[view setWantsLayer:NO];
// Release Metal device and layer
metalLayer.device = nil;
[metalLayer release];
[metalDevice release];
mtkLayer = nullptr;
}
}

View File

@@ -1,10 +1,12 @@
#ifdef PANDA3DS_ENABLE_OPENGL
#include "opengl.hpp"
#endif
// opengl.hpp must be included at the very top. This comment exists to make clang-format not reorder it :p
#include <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <algorithm>
#include <array>
#include <cmath>
#include <optional>
@@ -12,16 +14,16 @@
#include <qpa/qplatformnativeinterface.h>
#endif
#include "panda_qt/screen.hpp"
#include "panda_qt/screen/screen.hpp"
#include "panda_qt/screen/screen_gl.hpp"
#include "panda_qt/screen/screen_mtl.hpp"
// OpenGL screen widget, based on https://github.com/stenzek/duckstation/blob/master/src/duckstation-qt/displaywidget.cpp
// Screen widget, based on https://github.com/stenzek/duckstation/blob/master/src/duckstation-qt/displaywidget.cpp
// and https://github.com/melonDS-emu/melonDS/blob/master/src/frontend/qt_sdl/main.cpp
#ifdef PANDA3DS_ENABLE_OPENGL
ScreenWidget::ScreenWidget(ResizeCallback resizeCallback, QWidget* parent) : QWidget(parent), resizeCallback(resizeCallback) {
ScreenWidget::ScreenWidget(API api, ResizeCallback resizeCallback, QWidget* parent) : api(api), QWidget(parent), resizeCallback(resizeCallback) {
// Create a native window for use with our graphics API of choice
resize(800, 240 * 4);
setAutoFillBackground(false);
setAttribute(Qt::WA_NativeWindow, true);
setAttribute(Qt::WA_NoSystemBackground, true);
@@ -29,11 +31,8 @@ ScreenWidget::ScreenWidget(ResizeCallback resizeCallback, QWidget* parent) : QWi
setAttribute(Qt::WA_KeyCompression, false);
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
show();
if (!createGLContext()) {
Helpers::panic("Failed to create GL context for display");
}
// The graphics context, as well as resizing and showing the widget, is handled by the screen backend
}
void ScreenWidget::resizeEvent(QResizeEvent* event) {
@@ -48,18 +47,7 @@ void ScreenWidget::resizeEvent(QResizeEvent* event) {
}
reloadScreenCoordinates();
// This will call take care of calling resizeSurface from the emulator thread
resizeCallback(surfaceWidth, surfaceHeight);
}
// Note: This will run on the emulator thread, we don't want any Qt calls happening there.
void ScreenWidget::resizeSurface(u32 width, u32 height) {
if (previousWidth != width || previousHeight != height) {
if (glContext) {
glContext->ResizeSurface(width, height);
}
}
resizeDisplay();
}
void ScreenWidget::reloadScreenCoordinates() {
@@ -73,30 +61,6 @@ void ScreenWidget::reloadScreenLayout(ScreenLayout::Layout newLayout, float newT
reloadScreenCoordinates();
}
bool ScreenWidget::createGLContext() {
// List of GL context versions we will try. Anything 4.1+ is good for desktop OpenGL, and 3.1+ for OpenGL ES
static constexpr std::array<GL::Context::Version, 8> versionsToTry = {
GL::Context::Version{GL::Context::Profile::Core, 4, 6}, GL::Context::Version{GL::Context::Profile::Core, 4, 5},
GL::Context::Version{GL::Context::Profile::Core, 4, 4}, GL::Context::Version{GL::Context::Profile::Core, 4, 3},
GL::Context::Version{GL::Context::Profile::Core, 4, 2}, GL::Context::Version{GL::Context::Profile::Core, 4, 1},
GL::Context::Version{GL::Context::Profile::ES, 3, 2}, GL::Context::Version{GL::Context::Profile::ES, 3, 1},
};
std::optional<WindowInfo> windowInfo = getWindowInfo();
if (windowInfo.has_value()) {
this->windowInfo = *windowInfo;
glContext = GL::Context::Create(*getWindowInfo(), versionsToTry);
if (glContext == nullptr) {
return false;
}
glContext->DoneCurrent();
}
return glContext != nullptr;
}
qreal ScreenWidget::devicePixelRatioFromScreen() const {
const QScreen* screenForRatio = windowHandle()->screen();
if (!screenForRatio) {
@@ -156,3 +120,15 @@ std::optional<WindowInfo> ScreenWidget::getWindowInfo() {
return wi;
}
#endif
ScreenWidget* ScreenWidget::getWidget(API api, ResizeCallback resizeCallback, QWidget* parent) {
if (api == API::OpenGL) {
return new ScreenWidgetGL(api, resizeCallback, parent);
} else if (api == API::Metal) {
return new ScreenWidgetMTL(api, resizeCallback, parent);
} else if (api == API::Vulkan) {
Helpers::panic("Vulkan is not yet supported on Panda3DS-Qt. Try SDL instead");
} else {
Helpers::panic("ScreenWidget::getWidget: Unimplemented graphics API");
}
}

View File

@@ -0,0 +1,64 @@
#include "panda_qt/screen/screen_gl.hpp"
#include <array>
#ifdef PANDA3DS_ENABLE_OPENGL
ScreenWidgetGL::ScreenWidgetGL(API api, ResizeCallback resizeCallback, QWidget* parent) : ScreenWidget(api, resizeCallback, parent) {
// On Wayland + OpenGL, we have to show the window before we can create a graphics context.
resize(800, 240 * 4);
show();
if (!createContext()) {
Helpers::panic("Failed to create GL context for display");
}
}
bool ScreenWidgetGL::createContext() {
// List of GL context versions we will try. Anything 4.1+ is good for desktop OpenGL, and 3.1+ for OpenGL ES
static constexpr std::array<GL::Context::Version, 8> versionsToTry = {
GL::Context::Version{GL::Context::Profile::Core, 4, 6}, GL::Context::Version{GL::Context::Profile::Core, 4, 5},
GL::Context::Version{GL::Context::Profile::Core, 4, 4}, GL::Context::Version{GL::Context::Profile::Core, 4, 3},
GL::Context::Version{GL::Context::Profile::Core, 4, 2}, GL::Context::Version{GL::Context::Profile::Core, 4, 1},
GL::Context::Version{GL::Context::Profile::ES, 3, 2}, GL::Context::Version{GL::Context::Profile::ES, 3, 1},
};
std::optional<WindowInfo> windowInfo = getWindowInfo();
if (windowInfo.has_value()) {
this->windowInfo = *windowInfo;
glContext = GL::Context::Create(*getWindowInfo(), versionsToTry);
if (glContext == nullptr) {
return false;
}
glContext->DoneCurrent();
}
return glContext != nullptr;
}
void ScreenWidgetGL::resizeDisplay() {
// This will call take care of calling resizeSurface from the emulator thread, as the GL renderer must resize from the emu thread
resizeCallback(surfaceWidth, surfaceHeight);
}
// Note: This will run on the emulator thread, we don't want any Qt calls happening there.
void ScreenWidgetGL::resizeSurface(u32 width, u32 height) {
if (previousWidth != width || previousHeight != height) {
if (glContext) {
glContext->ResizeSurface(width, height);
}
}
}
GL::Context* ScreenWidgetGL::getGLContext() { return glContext.get(); }
#else
ScreenWidgetGL::ScreenWidgetGL(API api, ResizeCallback resizeCallback, QWidget* parent) : ScreenWidget(api, resizeCallback, parent) {
Helpers::panic("OpenGL renderer not supported. Make sure you've compiled with OpenGL support and that you're on a compatible platform");
}
GL::Context* ScreenWidgetGL::getGLContext() { nullptr; }
bool ScreenWidgetGL::createContext() { return false; }
void ScreenWidgetGL::resizeDisplay() {}
void ScreenWidgetGL::resizeSurface(u32 width, u32 height) {}
#endif

View File

@@ -0,0 +1,33 @@
#include "panda_qt/screen/screen_mtl.hpp"
#ifdef PANDA3DS_ENABLE_METAL
ScreenWidgetMTL::ScreenWidgetMTL(API api, ResizeCallback resizeCallback, QWidget* parent) : ScreenWidget(api, resizeCallback, parent) {
if (!createContext()) {
Helpers::panic("Failed to create Metal context for display");
}
resize(800, 240 * 4);
show();
}
void ScreenWidgetMTL::resizeDisplay() {
resizeMetalView();
resizeCallback(surfaceWidth, surfaceHeight);
}
bool ScreenWidgetMTL::createContext() { return createMetalContext(); }
void* ScreenWidgetMTL::getMTKLayer() { return mtkLayer; }
#else
ScreenWidgetMTL::ScreenWidgetMTL(API api, ResizeCallback resizeCallback, QWidget* parent) : ScreenWidget(api, resizeCallback, parent) {
Helpers::panic("Metal renderer not supported. Make sure you've compiled with Metal support and that you're on a compatible platform");
}
ScreenWidgetMTL::~ScreenWidgetMTL() {}
bool ScreenWidgetMTL::createContext() { return false; }
bool ScreenWidgetMTL::createMetalContext() { return false; }
void* ScreenWidgetMTL::getMTKLayer() { return nullptr; }
void ScreenWidgetMTL::resizeDisplay() {}
void ScreenWidgetMTL::resizeMetalView() {}
#endif