LukasBanana / LLGL

Low Level Graphics Library (LLGL) is a thin abstraction layer for the modern graphics APIs OpenGL, Direct3D, Vulkan, and Metal
BSD 3-Clause "New" or "Revised" License
2.03k stars 135 forks source link

Weird VSync behavior with Metal #117

Closed st0rmbtw closed 2 months ago

st0rmbtw commented 2 months ago

Metal ignores VSync interval set by glfwSwapInterval and SwapChain::SetVsyncInterval functions. FPS is still capped to the monitor refresh rate, even though I disabled VSync by calling glfwSwapInterval(0) and SwapChain::SetVsyncInterval(0).

FPS is capped when the window is not fullscreen though, but when it is FPS is uncapped even if VSync interval is set to a value > 0.

With OpenGL FPS is uncapped in windowed mode when VSync is disabled and always uncapped in fullscreen even if VSync is enabled.

main.cpp

#include <stdio.h>
#include <LLGL/LLGL.h>
#include <memory>
#include "custom_surface.hpp"

#if defined(_WIN32)
#define BACKEND "DirectX11"
#elif defined(__MACH__)
#define BACKEND "Metal"
#else
#define BACKEND "OpenGL"
#endif

void update() {
}

void render(LLGL::CommandBuffer* cmdBuffer, LLGL::SwapChain *swapChain) {
    cmdBuffer->Begin();
    cmdBuffer->BeginRenderPass(*swapChain);
    cmdBuffer->SetViewport(swapChain->GetResolution());
    cmdBuffer->Clear(LLGL::ClearFlags::Color, LLGL::ClearValue(7.0f, 0.3f, 0.3f, 1.0f));

    cmdBuffer->EndRenderPass();
    cmdBuffer->End();
    swapChain->Present();
}

inline const char* glfwGetError(void) {
    const char* description;
    glfwGetError(&description);
    return description;
}

int main(void) {
    LLGL::Log::RegisterCallbackStd();

    if (!glfwInit()) {
        printf("Couldn't initialize GLFW: %s\n", glfwGetError());
        return -1;
    }

    glfwWindowHint(GLFW_FOCUSED, 1);

    GLFWwindow *window = glfwCreateWindow(1280, 720, "AAA", nullptr, nullptr);
    if (window == nullptr) {
        printf("Couldn't create a window: %s\n", glfwGetError());
        return -1;
    }

    glfwMakeContextCurrent(window);
    glfwSwapInterval(0);

    auto surface = std::make_shared<CustomSurface>(window, LLGL::Extent2D(1280, 720));

    LLGL::Report report;
    LLGL::RenderSystemPtr renderer = LLGL::RenderSystem::Load("Metal", &report);
    if (!renderer) {
        LLGL::Log::Errorf("%s", report.GetText());
        return -1;
    }

    LLGL::SwapChainDescriptor swapChainDesc;
    swapChainDesc.resolution = {1280, 720};

    auto swapChain = renderer->CreateSwapChain(swapChainDesc, surface);
    swapChain->SetVsyncInterval(0);

    auto cmdBuffer = renderer->CreateCommandBuffer(LLGL::CommandBufferFlags::ImmediateSubmit);

    double prev_tick = glfwGetTime();

    while (surface->ProcessEvents()) {
        double current_tick = glfwGetTime();
        const double delta_time = (current_tick - prev_tick);
        prev_tick = current_tick;

        printf("%f\n", 1.0 / delta_time);

        update();
        render(cmdBuffer, swapChain);
    }

    return 0;
}

custom_surface.hpp

#if defined(_WIN32)
    #define GLFW_EXPOSE_NATIVE_WIN32
#elif defined(__MACH__)
    #define GLFW_EXPOSE_NATIVE_COCOA
#else
    #if defined(WAYLAND)
        #define GLFW_EXPOSE_NATIVE_WAYLAND
    #elif defined(X11)
        #define GLFW_EXPOSE_NATIVE_X11
  #endif
#endif

#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include <LLGL/LLGL.h>
#include <LLGL/Platform/NativeHandle.h>

class CustomSurface : public LLGL::Surface {
public:
    CustomSurface(GLFWwindow *window, const LLGL::Extent2D& size);
    ~CustomSurface();

    bool GetNativeHandle(void* nativeHandle, std::size_t nativeHandleSize) override;
    LLGL::Extent2D GetContentSize() const override;
    bool AdaptForVideoMode(LLGL::Extent2D* resolution, bool* fullscreen) override;
    void ResetPixelFormat() override;
    LLGL::Display* FindResidentDisplay() const override;

    bool ProcessEvents();
private:
    LLGL::Extent2D m_size;
    GLFWwindow* m_wnd = nullptr;

};

custom_surface.cpp

#include "custom_surface.hpp"

CustomSurface::CustomSurface(GLFWwindow *window, const LLGL::Extent2D& size) :
    m_size(size),
    m_wnd(window) {}

CustomSurface::~CustomSurface() {
    glfwDestroyWindow(m_wnd);
}

void CustomSurface::ResetPixelFormat() {
    // glfwDestroyWindow(m_wnd);
    // m_wnd = CreateGLFWWindow();
    printf("RESET PIXEL FORMAT CALLED\n");
}

bool CustomSurface::GetNativeHandle(void* nativeHandle, std::size_t nativeHandleSize) {
    auto handle = reinterpret_cast<LLGL::NativeHandle*>(nativeHandle);
#if defined(_WIN32)
    handle->window = glfwGetWin32Window(m_wnd);
#elif defined(__MACH__)
    handle->responder = glfwGetCocoaWindow(m_wnd);
#else
    // TODO
    #if defined(WAYLAND)
    #elif defined(X11)
  #endif
#endif
    return true;
}

LLGL::Extent2D CustomSurface::GetContentSize() const {
    return m_size;
}

bool CustomSurface::AdaptForVideoMode(LLGL::Extent2D *resolution, bool *fullscreen) {
    m_size = *resolution;
    glfwSetWindowSize(m_wnd, m_size.width, m_size.height);
    return true;
}

LLGL::Display* CustomSurface::FindResidentDisplay() const {
    return LLGL::Display::GetPrimary();
}

bool CustomSurface::ProcessEvents() {
    glfwPollEvents();
    return !glfwWindowShouldClose(m_wnd);
}

macOS version: Ventura 13.6.7

LukasBanana commented 2 months ago

GLFW only provides rendering capacilities like Vsync for OpenGL, not for Metal, to the best of my knowledge.

Moreover, using swap-chain modifying functions like Vsync from GLFW to control the swap-chain of LLGL is undefined behavior and should be avoided, even with the OpenGL backend. LLGL has its own swap-interval function SwapChain::SetVsyncInterval.

Having said that, I am not aware of a sync interval functionality in Metal other than the property of a preferred frame rate that can usually only be lower from 60 FPS downwards: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/FrameRate.html For the Metal backend, SetVsyncInterval(0) will only set this property to its default value of 60. If you can point me to documentation how to disable sync interval for MTKView, I'm happy to integrate it into LLGL.

st0rmbtw commented 2 months ago

I'm not sure if it's the right thing or not but there is this property: https://developer.apple.com/documentation/quartzcore/cametallayer/2887087-displaysyncenabled

LukasBanana commented 2 months ago

Awesome, that works! I haven't interacted with the CAMetalLayer of the MTKView a lot so far, but that seems to do the trick.