From 8b91c2223dbc27e6c3117bf5dce2c884d094a017 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Wed, 14 Oct 2026 22:58:55 +0200 Subject: [PATCH 1/6] GPU Display: Use fragment shader to downsample >2 FSAA, since blit has poor quality --- GPU/GPUTracking/display/CMakeLists.txt | 2 + GPU/GPUTracking/display/GPUDisplay.cxx | 8 ++ .../display/backend/GPUDisplayBackend.h | 1 + .../backend/GPUDisplayBackendOpenGL.cxx | 51 +++++++++++- .../display/backend/GPUDisplayBackendOpenGL.h | 4 + .../backend/GPUDisplayBackendVulkan.cxx | 77 ++++++++++++++++--- .../display/backend/GPUDisplayBackendVulkan.h | 3 +- .../display/shaders/fragmentDownsample.frag | 47 +++++++++++ .../display/shaders/vertexFullscreen.vert | 26 +++++++ 9 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 GPU/GPUTracking/display/shaders/fragmentDownsample.frag create mode 100644 GPU/GPUTracking/display/shaders/vertexFullscreen.vert diff --git a/GPU/GPUTracking/display/CMakeLists.txt b/GPU/GPUTracking/display/CMakeLists.txt index 73f4c7da99d6a..ce24228f2d134 100644 --- a/GPU/GPUTracking/display/CMakeLists.txt +++ b/GPU/GPUTracking/display/CMakeLists.txt @@ -260,6 +260,8 @@ if(GPUCA_EVENT_DISPLAY_VULKAN OR GPUCA_EVENT_DISPLAY_OPENGL) shaders/vertexSmoothPoint.vert shaders/vertexTexture.vert shaders/vertexPassthrough.vert + shaders/vertexFullscreen.vert + shaders/fragmentDownsample.frag shaders/fragment.frag shaders/fragmentSmoothPoint.frag shaders/vertexSmoothLine.vert diff --git a/GPU/GPUTracking/display/GPUDisplay.cxx b/GPU/GPUTracking/display/GPUDisplay.cxx index 2c0e6b447c4ed..fc34af50ba478 100644 --- a/GPU/GPUTracking/display/GPUDisplay.cxx +++ b/GPU/GPUTracking/display/GPUDisplay.cxx @@ -629,6 +629,14 @@ void GPUDisplay::DrawGLScene_drawCommands() void GPUDisplay::DrawGLScene_internal(float animateTime, bool renderToMixBuffer) // negative time = no mixing { + if (mRequestScreenshot || mAnimateScreenshot) { + const uint32_t factor = std::max(1, mCfgR.drawQualityDownsampleFSAA) * std::max(1, mCfgR.screenshotScaleFactor); + if (std::max(mBackend->mScreenWidth, mBackend->mScreenHeight) * factor > mBackend->mMaxRenderSize) { + GPUError("Screenshot not taken: requested render size %u x %u (window x FSAA x screenshot scale) exceeds the maximum size %u x %u", mBackend->mScreenWidth * factor, mBackend->mScreenHeight * factor, mBackend->mMaxRenderSize, mBackend->mMaxRenderSize); + mRequestScreenshot = false; + mAnimateScreenshot = 0; + } + } bool doScreenshot = (mRequestScreenshot || mAnimateScreenshot) && animateTime < 0; updateOptions(); diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h index 9342b8a642f33..4087ab8323d7d 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h @@ -126,6 +126,7 @@ class GPUDisplayBackend uint32_t mRenderWidth = 0; uint32_t mRenderHeight = 0; + uint32_t mMaxRenderSize = UINT32_MAX; // Maximum width / height of render targets, set by the backend uint32_t mScreenWidth = 0; uint32_t mScreenHeight = 0; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx index 1af8f564d4298..d1a7aace2b470 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx @@ -310,6 +310,11 @@ int32_t GPUDisplayBackendOpenGL::InitBackendA() GPUError("Unsupported OpenGL runtime %d.%d < %d.%d", glVersion[0], glVersion[1], GPUDisplayFrontend::GL_MIN_VERSION_MAJOR, GPUDisplayFrontend::GL_MIN_VERSION_MINOR); return (1); } + for (GLenum limit : {GL_MAX_TEXTURE_SIZE, GL_MAX_RENDERBUFFER_SIZE, GL_MAX_FRAMEBUFFER_WIDTH, GL_MAX_FRAMEBUFFER_HEIGHT}) { + GLint value = 0; + glGetIntegerv(limit, &value); + mMaxRenderSize = std::min(mMaxRenderSize, value); + } mVBOId.resize(GPUTPCGeometry::NSECTORS); CHKERR(glCreateBuffers(mVBOId.size(), mVBOId.data())); CHKERR(glBindBuffer(GL_ARRAY_BUFFER, mVBOId[0])); @@ -325,11 +330,13 @@ int32_t GPUDisplayBackendOpenGL::InitBackendA() CHKERR(mVertexShaderTexture = loadShader(GL_VERTEX_SHADER, _binary_opengl_vertexTexture_vert_spv_start, _binary_opengl_vertexTexture_vert_spv_len)); CHKERR(mFragmentShaderTexture = loadShader(GL_FRAGMENT_SHADER, _binary_opengl_fragmentTexture_frag_spv_start, _binary_opengl_fragmentTexture_frag_spv_len)); CHKERR(mFragmentShaderText = loadShader(GL_FRAGMENT_SHADER, _binary_opengl_fragmentText_frag_spv_start, _binary_opengl_fragmentText_frag_spv_len)); + CHKERR(mVertexShaderFullscreen = loadShader(GL_VERTEX_SHADER, _binary_opengl_vertexFullscreen_vert_spv_start, _binary_opengl_vertexFullscreen_vert_spv_len)); + CHKERR(mFragmentShaderDownsample = loadShader(GL_FRAGMENT_SHADER, _binary_opengl_fragmentDownsample_frag_spv_start, _binary_opengl_fragmentDownsample_frag_spv_len)); CHKERR(mVertexShaderSmoothPoint = loadShader(GL_VERTEX_SHADER, _binary_opengl_vertexSmoothPoint_vert_spv_start, _binary_opengl_vertexSmoothPoint_vert_spv_len)); CHKERR(mFragmentShaderSmoothPoint = loadShader(GL_FRAGMENT_SHADER, _binary_opengl_fragmentSmoothPoint_frag_spv_start, _binary_opengl_fragmentSmoothPoint_frag_spv_len)); CHKERR(mVertexShaderSmoothLine = loadShader(GL_VERTEX_SHADER, _binary_opengl_vertexSmoothLine_vert_spv_start, _binary_opengl_vertexSmoothLine_vert_spv_len)); CHKERR(mFragmentShaderSmoothLine = loadShader(GL_FRAGMENT_SHADER, _binary_opengl_fragmentSmoothLine_frag_spv_start, _binary_opengl_fragmentSmoothLine_frag_spv_len)); - if (checkShaderStatus(mVertexShader) || checkShaderStatus(mFragmentShader) || checkShaderStatus(mVertexShaderSmoothPoint) || checkShaderStatus(mFragmentShaderSmoothPoint) || checkShaderStatus(mVertexShaderSmoothLine) || checkShaderStatus(mFragmentShaderSmoothLine) || checkShaderStatus(mVertexShaderTexture) || checkShaderStatus(mFragmentShaderTexture) || checkShaderStatus(mFragmentShaderText)) { + if (checkShaderStatus(mVertexShader) || checkShaderStatus(mFragmentShader) || checkShaderStatus(mVertexShaderSmoothPoint) || checkShaderStatus(mFragmentShaderSmoothPoint) || checkShaderStatus(mVertexShaderSmoothLine) || checkShaderStatus(mFragmentShaderSmoothLine) || checkShaderStatus(mVertexShaderTexture) || checkShaderStatus(mFragmentShaderTexture) || checkShaderStatus(mFragmentShaderText) || checkShaderStatus(mVertexShaderFullscreen) || checkShaderStatus(mFragmentShaderDownsample)) { return 1; } CHKERR(mShaderProgram = glCreateProgram()); @@ -367,6 +374,13 @@ int32_t GPUDisplayBackendOpenGL::InitBackendA() if (checkProgramStatus(mShaderProgramTexture)) { return 1; } + CHKERR(mShaderProgramDownsample = glCreateProgram()); + CHKERR(glAttachShader(mShaderProgramDownsample, mVertexShaderFullscreen)); + CHKERR(glAttachShader(mShaderProgramDownsample, mFragmentShaderDownsample)); + CHKERR(glLinkProgram(mShaderProgramDownsample)); + if (checkProgramStatus(mShaderProgramDownsample)) { + return 1; + } CHKERR(glGenVertexArrays(1, &mVertexArray)); CHKERR(glGenVertexArrays(1, &VAO_texture)); @@ -399,6 +413,9 @@ void GPUDisplayBackendOpenGL::ExitBackendA() CHKERR(glDeleteProgram(mShaderProgramText)); CHKERR(glDeleteShader(mFragmentShaderText)); CHKERR(glDeleteProgram(mShaderProgramTexture)); + CHKERR(glDeleteProgram(mShaderProgramDownsample)); + CHKERR(glDeleteShader(mVertexShaderFullscreen)); + CHKERR(glDeleteShader(mFragmentShaderDownsample)); CHKERR(glDeleteShader(mVertexShaderTexture)); CHKERR(glDeleteShader(mFragmentShaderTexture)); @@ -548,8 +565,10 @@ void GPUDisplayBackendOpenGL::finishDraw(bool doScreenshot, bool toMixBuffer, fl setFrameBuffer(mDownsampleFactor != 1 ? mOffscreenBuffer.fb_id : 0); mixImages(includeMixImage); } - if (mDownsampleFactor != 1 && !toMixBuffer) { + if (mDownsampleFactor == 2 && !toMixBuffer) { // The linear blit is only exact for factor 2 CHKERR(glBlitNamedFramebuffer(mOffscreenBuffer.fb_id, 0, 0, 0, mRenderWidth, mRenderHeight, 0, 0, mScreenWidth, mScreenHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR)); + } else if (mDownsampleFactor != 1 && !toMixBuffer) { + drawDownsample(mOffscreenBuffer.fbCol_id, 0, mScreenWidth, mScreenHeight, mDownsampleFactor); } if (doScreenshot && !toMixBuffer) { @@ -565,7 +584,12 @@ void GPUDisplayBackendOpenGL::readImageToPixels() GLfb tmpBuffer; if (mDisplay->cfgR().drawQualityDownsampleFSAA && mDisplay->cfgR().screenshotScaleFactor != 1) { createFB(tmpBuffer, false, true, false, width, height); - CHKERR(glBlitNamedFramebuffer(mOffscreenBuffer.fb_id, tmpBuffer.fb_id, 0, 0, mRenderWidth, mRenderHeight, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR)); + const int32_t factor = mRenderWidth / width; + if (factor > 2) { + drawDownsample(mOffscreenBuffer.fbCol_id, tmpBuffer.fb_id, width, height, factor); + } else { + CHKERR(glBlitNamedFramebuffer(mOffscreenBuffer.fb_id, tmpBuffer.fb_id, 0, 0, mRenderWidth, mRenderHeight, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR)); + } } setFrameBuffer(tmpBuffer.created ? tmpBuffer.fb_id : (scaleFactor != 1 ? mOffscreenBuffer.fb_id : 0)); CHKERR(glPixelStorei(GL_PACK_ALIGNMENT, 1)); @@ -611,6 +635,25 @@ void GPUDisplayBackendOpenGL::finishText() } } +void GPUDisplayBackendOpenGL::drawDownsample(uint32_t srcTexture, uint32_t dstFramebuffer, uint32_t width, uint32_t height, int32_t factor) +{ + setFrameBuffer(dstFramebuffer); + CHKERR(glViewport(0, 0, width, height)); + CHKERR(glDisable(GL_DEPTH_TEST)); + CHKERR(glDisable(GL_BLEND)); + CHKERR(glUseProgram(mShaderProgramDownsample)); + CHKERR(glUniform1i(UNIFORM_LOCATION_COLOR, factor)); + CHKERR(glActiveTexture(GL_TEXTURE0)); + CHKERR(glBindTexture(GL_TEXTURE_2D, srcTexture)); + CHKERR(glBindVertexArray(VAO_texture)); // Any VAO, the full-screen triangle has no vertex input + CHKERR(glDrawArrays(GL_TRIANGLES, 0, 3)); + CHKERR(glBindVertexArray(0)); + CHKERR(glBindTexture(GL_TEXTURE_2D, 0)); + CHKERR(glUseProgram(0)); + CHKERR(glEnable(GL_BLEND)); + setDepthBuffer(); +} + void GPUDisplayBackendOpenGL::mixImages(float mixSlaveImage) { hmm_mat4 proj = HMM_Orthographic(0.f, mRenderWidth, 0.f, mRenderHeight, -1.f, 1.f); @@ -821,7 +864,7 @@ void GPUDisplayBackendOpenGL::updateRenderer(bool withScreenshot) createFB(mOffscreenBufferMSAA, false, true, true, mRenderWidth, mRenderHeight); } if (mDownsampleFactor != 1) { - createFB(mOffscreenBuffer, false, true, false, mRenderWidth, mRenderHeight); + createFB(mOffscreenBuffer, mDownsampleFactor != 2, true, false, mRenderWidth, mRenderHeight); // Texture for shader downsampling (factor != 2), screenshot downsampling uses the shader only if this factor is != 2 as well } createFB(mMixBuffer, true, true, false, mRenderWidth, mRenderHeight); setQuality(); diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h index ee54c1097ad35..a41c8df6b7715 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h @@ -65,6 +65,7 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend void prepareText() override; void finishText() override; void mixImages(float mixSlaveImage); + void drawDownsample(uint32_t srcTexture, uint32_t dstFramebuffer, uint32_t width, uint32_t height, int32_t factor); // Exact box filter (fragmentDownsample.frag) for FSAA factors other than 2 void pointSizeFactor(float factor) override; void lineWidthFactor(float factor) override; size_t needMultiVBO() override { return 0x100000000ll; } @@ -95,6 +96,8 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend uint32_t mVertexShaderPassthrough; uint32_t mFragmentShaderTexture; uint32_t mFragmentShaderText; + uint32_t mVertexShaderFullscreen; + uint32_t mFragmentShaderDownsample; uint32_t mGeometryShader; uint32_t mShaderProgram; uint32_t mShaderProgramSmoothPoint; // Points with fragmentSmoothPoint.frag, for smoothPoints in the core profile (no GL_POINT_SMOOTH) @@ -103,6 +106,7 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend bool mUseSmoothLineShader = false; uint32_t mShaderProgramText; uint32_t mShaderProgramTexture; + uint32_t mShaderProgramDownsample; uint32_t mShaderProgramField; uint32_t mVertexArray; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx index 92b3341917a2e..673780098222e 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx @@ -540,6 +540,7 @@ void GPUDisplayBackendVulkan::createDevice() GPUInfo("Using physical Vulkan device %s", &deviceProperties.deviceName[0]); mMaxMSAAsupported = getMaxUsableSampleCount(deviceProperties); mStrictLines = deviceProperties.limits.strictLines; + mMaxRenderSize = std::min({deviceProperties.limits.maxImageDimension2D, deviceProperties.limits.maxFramebufferWidth, deviceProperties.limits.maxFramebufferHeight}); mZSupported = (bool)(depth32FormatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); mStencilSupported = (bool)(depth64FormatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); bool mailboxSupported = std::find(mSwapChainDetails.presentModes.begin(), mSwapChainDetails.presentModes.end(), vk::PresentModeKHR::eMailbox) != mSwapChainDetails.presentModes.end(); @@ -853,7 +854,13 @@ void GPUDisplayBackendVulkan::createOffscreenBuffers(bool forScreenshot, bool fo mMixImages[i - mImageCount].view = createImageViewI(mDevice, mMixImages[i - mImageCount].image, mSurfaceFormat.format, vk::ImageAspectFlagBits::eColor, 1); } if (mDownsampleFSAA) { - vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment | (i >= mImageCount ? vk::ImageUsageFlagBits::eSampled : vk::ImageUsageFlagBits::eTransferSrc); + vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment; + if (i >= mImageCount || mDownsampleFactor != 2) { + usage |= vk::ImageUsageFlagBits::eSampled; // Mixing, shader downsampling + } + if (i < mImageCount && (mDownsampleFactor == 2 || forScreenshot)) { + usage |= vk::ImageUsageFlagBits::eTransferSrc; // Blit downsampling, scaled screenshots (blit or copy for screenshot factor <= 2) + } createImageI(mDevice, mPhysicalDevice, mDownsampleImages[i].image, mDownsampleImages[i].memory, mRenderWidth, mRenderHeight, mSurfaceFormat.format, usage, vk::MemoryPropertyFlagBits::eDeviceLocal, vk::ImageTiling::eOptimal); mDownsampleImages[i].view = createImageViewI(mDevice, mDownsampleImages[i].image, mSurfaceFormat.format, vk::ImageAspectFlagBits::eColor, 1); mRenderTargetView[i] = &mDownsampleImages[i].view; @@ -1027,10 +1034,23 @@ void GPUDisplayBackendVulkan::createPipeline() vk::PipelineRasterizationLineStateCreateInfo smoothLineState{}; smoothLineState.lineRasterizationMode = vk::LineRasterizationMode::eRectangularSmooth; // Coverage goes to alpha, blending is enabled - mPipelines.resize(8); // 0 points, 1 lines, 2 line strip (drawType index), 3 text, 4 texture, 5-7 smooth (shader-based) points, lines, line strip + mPipelines.resize(9); // 0 points, 1 lines, 2 line strip (drawType index), 3 text, 4 texture, 5-7 smooth (shader-based) points, lines, line strip, 8 FSAA downsampling static constexpr vk::PrimitiveTopology types[3] = {vk::PrimitiveTopology::ePointList, vk::PrimitiveTopology::eLineList, vk::PrimitiveTopology::eLineStrip}; for (uint32_t i = 0; i < mPipelines.size(); i++) { - if (i == 4) { // Texture rendering + if (i == 8) { // FSAA downsampling, full-screen triangle without vertex input (last pipeline, the changed states are not reset) + rasterizer.pNext = nullptr; + rasterizer.cullMode = vk::CullModeFlagBits::eNone; + vertexInputInfo.vertexBindingDescriptionCount = vertexInputInfo.vertexAttributeDescriptionCount = 0; + inputAssembly.topology = vk::PrimitiveTopology::eTriangleList; + vertShaderStageInfo.pNext = &mShaders.at("vertexFullscreen"); + fragShaderStageInfo.pNext = &mShaders.at("fragmentDownsample"); + dynamicState.dynamicStateCount = 2; + pipelineInfo.layout = mPipelineLayoutTexture; + renderingInfo.depthAttachmentFormat = vk::Format::eUndefined; + pipelineInfo.pDepthStencilState = nullptr; + colorBlendAttachment.blendEnable = false; + multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1; + } else if (i == 4) { // Texture rendering rasterizer.pNext = nullptr; bindingDescription.stride = 4 * sizeof(float); attributeDescriptions.format = vk::Format::eR32G32B32A32Sfloat; @@ -1148,6 +1168,8 @@ void GPUDisplayBackendVulkan::loadShaders() LOAD_SHADER(vertexTexture, vert); LOAD_SHADER(fragmentTexture, frag); LOAD_SHADER(fragmentText, frag); + LOAD_SHADER(vertexFullscreen, vert); + LOAD_SHADER(fragmentDownsample, frag); } // ---------------------------- VULKAN BUFFERS ---------------------------- @@ -1451,9 +1473,9 @@ void GPUDisplayBackendVulkan::finishFrame(bool doScreenshot, bool toMixBuffer, f queueSubmit(nullptr, 1, &submitInfo, VkFence(VK_NULL_HANDLE)); mDevice.waitIdle(); if (mDisplay->cfgR().screenshotScaleFactor != 1) { - readImageToPixels(mDownsampleImages[mCurrentImageIndex].image, vk::ImageLayout::eTransferSrcOptimal, mScreenshotPixels); + readImageToPixels(mDownsampleImages[mCurrentImageIndex].image, mDownsampleImages[mCurrentImageIndex].view, mDownsampleFactor == 2 ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eShaderReadOnlyOptimal, mScreenshotPixels); } else { - readImageToPixels(mSwapChainImages[mCurrentImageIndex], vk::ImageLayout::eColorAttachmentOptimal, mScreenshotPixels); + readImageToPixels(mSwapChainImages[mCurrentImageIndex], mSwapChainImageViews[mCurrentImageIndex], vk::ImageLayout::eColorAttachmentOptimal, mScreenshotPixels); } mFrameCommandBuffers.clear(); submitInfo.waitSemaphoreInfoCount = 0; @@ -1492,6 +1514,13 @@ void GPUDisplayBackendVulkan::downsampleToFramebuffer(vk::CommandBuffer& command beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; commandBuffer.begin(beginInfo); + if (mDownsampleFactor != 2) { // Exact box filter by shader, the linear blit below is only exact for factor 2 + cmdImageMemoryBarrier(commandBuffer, mSwapChainImages[mCurrentImageIndex], vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eColorAttachmentWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal); // Src stage chains with the acquire semaphore wait + cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eColorAttachmentWrite, vk::PipelineStageFlagBits2::eFragmentShader, vk::AccessFlagBits2::eShaderSampledRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + cmdDownsample(commandBuffer, mDownsampleImages[mCurrentImageIndex].view, mSwapChainImageViews[mCurrentImageIndex], mScreenWidth, mScreenHeight, mDownsampleFactor); + commandBuffer.end(); + return; + } cmdImageMemoryBarrier(commandBuffer, mSwapChainImages[mCurrentImageIndex], vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); // Src stage chains with the acquire semaphore wait cmdImageMemoryBarrier(commandBuffer, mDownsampleImages[mCurrentImageIndex].image, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eColorAttachmentWrite, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal); @@ -1517,6 +1546,18 @@ void GPUDisplayBackendVulkan::downsampleToFramebuffer(vk::CommandBuffer& command commandBuffer.end(); } +void GPUDisplayBackendVulkan::cmdDownsample(vk::CommandBuffer commandBuffer, vk::ImageView src, vk::ImageView dst, uint32_t width, uint32_t height, int32_t factor) +{ + vk::RenderingAttachmentInfo colorAttachment{dst, vk::ImageLayout::eColorAttachmentOptimal}; + colorAttachment.loadOp = vk::AttachmentLoadOp::eDontCare; // Fully overwritten + cmdBeginRendering(commandBuffer, width, height, colorAttachment); + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mPipelines[8]); + cmdPushDescriptors(commandBuffer, mPipelineLayoutTexture, mUniformBuffersMat[2][mCurrentImageIndex], src); // The UBO is not used by the shader + commandBuffer.pushConstants(mPipelineLayoutTexture, vk::ShaderStageFlagBits::eFragment, 0, sizeof(factor), &factor); + commandBuffer.draw(3, 1, 0, 0); + commandBuffer.endRendering(); +} + void GPUDisplayBackendVulkan::prepareText() { if (mUpdateUBOtextMatrix & (1u << mCurrentImageIndex)) { @@ -1773,7 +1814,7 @@ void GPUDisplayBackendVulkan::OpenGLPrint(const char* s, float x, float y, float } } -void GPUDisplayBackendVulkan::readImageToPixels(vk::Image image, vk::ImageLayout layout, std::vector& pixels) +void GPUDisplayBackendVulkan::readImageToPixels(vk::Image image, vk::ImageView view, vk::ImageLayout layout, std::vector& pixels) { const vk::Format format = mSurfaceFormat.format; if (format != vk::Format::eB8G8R8A8Unorm && format != vk::Format::eB8G8R8A8Srgb && format != vk::Format::eR8G8B8A8Unorm && format != vk::Format::eR8G8B8A8Srgb) { @@ -1787,11 +1828,24 @@ void GPUDisplayBackendVulkan::readImageToPixels(vk::Image image, vk::ImageLayout pixels.resize(width * height * bytesPerPixel); vk::Image dstImage, src; + vk::ImageView dstView; vk::DeviceMemory dstImageMemory; + const bool scaled = mDisplay->cfgR().screenshotScaleFactor != 1; + const int32_t factor = mRenderWidth / width; // FSAA factor between the rendered image and the screenshot + const bool shaderDownsample = scaled && factor != 1 && factor != 2; // Exact box filter, the linear blit is only exact for factor 2 + const vk::ImageLayout srcLayout = shaderDownsample ? vk::ImageLayout::eShaderReadOnlyOptimal : vk::ImageLayout::eTransferSrcOptimal; + const vk::PipelineStageFlags2 srcStage = shaderDownsample ? vk::PipelineStageFlagBits2::eFragmentShader : vk::PipelineStageFlagBits2::eTransfer; VulkanBuffer readBuffer = createBuffer(width * height * bytesPerPixel, nullptr, vk::BufferUsageFlagBits::eTransferDst, 0); vk::CommandBuffer cmdBuffer = getSingleTimeCommandBuffer(); - cmdImageMemoryBarrier(cmdBuffer, image, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eTransferWrite, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead, layout, vk::ImageLayout::eTransferSrcOptimal); - if (mDisplay->cfgR().screenshotScaleFactor != 1) { + cmdImageMemoryBarrier(cmdBuffer, image, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eTransfer | vk::PipelineStageFlagBits2::eFragmentShader, vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eTransferWrite, srcStage, shaderDownsample ? vk::AccessFlagBits2::eShaderSampledRead : vk::AccessFlagBits2::eTransferRead, layout, srcLayout); + if (shaderDownsample) { + createImageI(mDevice, mPhysicalDevice, dstImage, dstImageMemory, width, height, mSurfaceFormat.format, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eDeviceLocal, vk::ImageTiling::eOptimal); + dstView = createImageViewI(mDevice, dstImage, mSurfaceFormat.format); + cmdImageMemoryBarrier(cmdBuffer, dstImage, vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eColorAttachmentWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal); + cmdDownsample(cmdBuffer, view, dstView, width, height, factor); + cmdImageMemoryBarrier(cmdBuffer, dstImage, vk::PipelineStageFlagBits2::eColorAttachmentOutput, vk::AccessFlagBits2::eColorAttachmentWrite, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal); + src = dstImage; + } else if (scaled) { createImageI(mDevice, mPhysicalDevice, dstImage, dstImageMemory, width, height, mSurfaceFormat.format, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal, vk::ImageTiling::eOptimal); cmdImageMemoryBarrier(cmdBuffer, dstImage, vk::PipelineStageFlagBits2::eNone, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); vk::Offset3D blitSizeSrc = {(int32_t)mRenderWidth, (int32_t)mRenderHeight, 1}; @@ -1818,7 +1872,7 @@ void GPUDisplayBackendVulkan::readImageToPixels(vk::Image image, vk::ImageLayout vk::BufferMemoryBarrier2 hostBarrier{vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite, vk::PipelineStageFlagBits2::eHost, vk::AccessFlagBits2::eHostRead, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, readBuffer.buffer, 0, VK_WHOLE_SIZE}; cmdBuffer.pipelineBarrier2(vk::DependencyInfo{{}, 0, nullptr, 1, &hostBarrier, 0, nullptr}); - cmdImageMemoryBarrier(cmdBuffer, image, vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone, vk::ImageLayout::eTransferSrcOptimal, layout); // Back to the layout the following passes expect + cmdImageMemoryBarrier(cmdBuffer, image, srcStage, vk::AccessFlagBits2::eNone, vk::PipelineStageFlagBits2::eColorAttachmentOutput | vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eNone, srcLayout, layout); // Back to the layout the following passes expect submitSingleTimeCommandBuffer(cmdBuffer); const bool swapRB = format == vk::Format::eR8G8B8A8Unorm || format == vk::Format::eR8G8B8A8Srgb; // Raw copy in swapchain format, the BMP needs BGRA @@ -1833,7 +1887,10 @@ void GPUDisplayBackendVulkan::readImageToPixels(vk::Image image, vk::ImageLayout } } clearBuffer(readBuffer); - if (mDisplay->cfgR().screenshotScaleFactor != 1) { + if (scaled) { + if (dstView) { + mDevice.destroyImageView(dstView, nullptr); + } mDevice.freeMemory(dstImageMemory, nullptr); mDevice.destroyImage(dstImage, nullptr); } diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h index 29d90d4e1e496..c67cbb121a398 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h @@ -106,8 +106,9 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend template uint64_t queueSubmit(uint64_t* timelineSignalValue, Args&&... args); // Submit under mQueueMutex; if timelineSignalValue (the entry in pSignalSemaphoreValues) is set, write the next timeline value there first. Returns the current timeline value void submitSingleTimeCommandBuffer(vk::CommandBuffer commandBuffer); - void readImageToPixels(vk::Image image, vk::ImageLayout layout, std::vector& pixels); + void readImageToPixels(vk::Image image, vk::ImageView view, vk::ImageLayout layout, std::vector& pixels); void downsampleToFramebuffer(vk::CommandBuffer& commandBuffer); + void cmdDownsample(vk::CommandBuffer commandBuffer, vk::ImageView src, vk::ImageView dst, uint32_t width, uint32_t height, int32_t factor); // Exact box filter (fragmentDownsample.frag) for factors other than 2, src in eShaderReadOnlyOptimal, dst in eColorAttachmentOptimal void updateSwapChainDetails(const vk::PhysicalDevice& device); void createDevice(); diff --git a/GPU/GPUTracking/display/shaders/fragmentDownsample.frag b/GPU/GPUTracking/display/shaders/fragmentDownsample.frag new file mode 100644 index 0000000000000..ac8264def5101 --- /dev/null +++ b/GPU/GPUTracking/display/shaders/fragmentDownsample.frag @@ -0,0 +1,47 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \file fragmentDownsample.frag +/// \author David Rohr + +#version 460 core +layout (location = 0) out vec4 outColor; +#ifdef VULKAN +layout (binding = 2) uniform sampler2D tex; +layout (push_constant) uniform pushFactor { int factor; } pf; +#define FACTOR pf.factor +#else +layout (binding = 0) uniform sampler2D tex; +layout (location = 4) uniform int factor; +#define FACTOR factor +#endif + +void main() // Exact box filter for FSAA downsampling by an integer factor: average of the factor x factor source pixels of this pixel (a linear blit only uses 2 x 2) +{ + const ivec2 base = ivec2(gl_FragCoord.xy) * FACTOR; + vec3 sum = vec3(0.f); + if ((FACTOR & 1) == 0) { // Bilinear sample at the corner of 4 source pixels returns their average + const vec2 invSize = 1.f / vec2(textureSize(tex, 0)); + for (int y = 1; y < FACTOR; y += 2) { + for (int x = 1; x < FACTOR; x += 2) { + sum += texture(tex, vec2(base + ivec2(x, y)) * invSize).rgb; + } + } + sum *= 4.f; + } else { + for (int y = 0; y < FACTOR; y++) { + for (int x = 0; x < FACTOR; x++) { + sum += texelFetch(tex, base + ivec2(x, y), 0).rgb; + } + } + } + outColor = vec4(sum / float(FACTOR * FACTOR), 1.f); +} diff --git a/GPU/GPUTracking/display/shaders/vertexFullscreen.vert b/GPU/GPUTracking/display/shaders/vertexFullscreen.vert new file mode 100644 index 0000000000000..767e6d5b78225 --- /dev/null +++ b/GPU/GPUTracking/display/shaders/vertexFullscreen.vert @@ -0,0 +1,26 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \file vertexFullscreen.vert +/// \author David Rohr + +#version 460 core +#ifdef VULKAN +#define VERTEX_ID gl_VertexIndex +#else +#define VERTEX_ID gl_VertexID +#endif + +void main() // Full-screen triangle from the vertex index (0..2), no vertex buffer +{ + vec2 p = vec2((VERTEX_ID << 1) & 2, VERTEX_ID & 2); + gl_Position = vec4(p * 2.f - 1.f, 0.f, 1.f); +} From 47ffea13c954ac85681c5e2e97d9ca33f36dd0e1 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Wed, 14 Oct 2026 23:12:11 +0200 Subject: [PATCH 2/6] GPU Vulkan Display: Improve present mode selection --- .../backend/GPUDisplayBackendVulkan.cxx | 51 ++++++++++++++----- .../display/backend/GPUDisplayBackendVulkan.h | 3 ++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx index 673780098222e..23540dcaf1014 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx @@ -96,19 +96,22 @@ static vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availablePresentModes, vk::PresentModeKHR desiredMode = vk::PresentModeKHR::eMailbox) +vk::PresentModeKHR GPUDisplayBackendVulkan::chooseSwapPresentMode() { - for (const auto& availablePresentMode : availablePresentModes) { - if (availablePresentMode == desiredMode) { - return availablePresentMode; + // Lowest latency first. VSync: no tearing, MAILBOX, then FIFO_LATEST_READY (shows the newest ready image at each vblank). No VSync: IMMEDIATE, else the same (often no IMMEDIATE on Wayland). FIFO is always supported. + const bool vsync = mDisplay->cfgR().drawQualityVSync; + const auto& available = mSwapChainDetails.presentModes; + for (vk::PresentModeKHR mode : {vk::PresentModeKHR::eImmediate, vk::PresentModeKHR::eMailbox, vk::PresentModeKHR::eFifoLatestReady, vk::PresentModeKHR::eFifo}) { + if ((mode == vk::PresentModeKHR::eImmediate && vsync) || (mode == vk::PresentModeKHR::eFifoLatestReady && !mFifoLatestReadySupported) || std::find(available.begin(), available.end(), mode) == available.end()) { + continue; } + if (mode != (vsync ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eImmediate) && !mPresentModeErrorShown) { + mPresentModeErrorShown = true; + GPUError("VULKAN ERROR: Desired present mode %s not available, using %s", vsync ? "MAILBOX" : "IMMEDIATE", vk::to_string(mode).c_str()); + } + return mode; } - static bool errorShown = false; - if (!errorShown) { - errorShown = true; - GPUError("VULKAN ERROR: Desired present mode not available, using FIFO mode"); - } - return vk::PresentModeKHR::eFifo; + return vk::PresentModeKHR::eFifo; // Unreachable, FIFO is always supported } vk::Extent2D GPUDisplayBackendVulkan::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) @@ -532,6 +535,19 @@ void GPUDisplayBackendVulkan::createDevice() } mSmoothLinesSupported = getFeatures(mPhysicalDevice, reqDeviceFeatures).get().smoothLines; reqDeviceFeatures.get().smoothLines = mSmoothLinesSupported; + std::vector deviceExtensions = reqDeviceExtensions; + vk::PhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{}; + const std::vector availableDeviceExtensions = mPhysicalDevice.enumerateDeviceExtensionProperties(nullptr); + for (const char* ext : {VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME, VK_EXT_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME}) { // Optional, same feature struct for both + if (std::any_of(availableDeviceExtensions.begin(), availableDeviceExtensions.end(), [ext](const auto& e) { return strcmp(ext, e.extensionName) == 0; })) { + mFifoLatestReadySupported = mPhysicalDevice.getFeatures2().get().presentModeFifoLatestReady; + if (mFifoLatestReadySupported) { + deviceExtensions.emplace_back(ext); + fifoLatestReadyFeatures.presentModeFifoLatestReady = VK_TRUE; + } + break; + } + } updateSwapChainDetails(mPhysicalDevice); vk::PhysicalDeviceProperties deviceProperties = mPhysicalDevice.getProperties(); @@ -545,7 +561,7 @@ void GPUDisplayBackendVulkan::createDevice() mStencilSupported = (bool)(depth64FormatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); bool mailboxSupported = std::find(mSwapChainDetails.presentModes.begin(), mSwapChainDetails.presentModes.end(), vk::PresentModeKHR::eMailbox) != mSwapChainDetails.presentModes.end(); if (mDisplay->GetProcessingSettings().debugLevel >= 2) { - GPUInfo("Max MSAA: %d, 32 bit Z buffer %d, 32 bit Z buffer + stencil buffer %d, Mailbox present mode %d, smooth lines %d\n", (int32_t)mMaxMSAAsupported, (int32_t)mZSupported, (int32_t)mStencilSupported, (int32_t)mailboxSupported, (int32_t)mSmoothLinesSupported); + GPUInfo("Max MSAA: %d, 32 bit Z buffer %d, 32 bit Z buffer + stencil buffer %d, Mailbox present mode %d, FIFO latest ready present mode %d, smooth lines %d\n", (int32_t)mMaxMSAAsupported, (int32_t)mZSupported, (int32_t)mStencilSupported, (int32_t)mailboxSupported, (int32_t)mFifoLatestReadySupported, (int32_t)mSmoothLinesSupported); } vk::DeviceQueueCreateInfo queueCreateInfo{}; @@ -557,9 +573,13 @@ void GPUDisplayBackendVulkan::createDevice() deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; deviceCreateInfo.queueCreateInfoCount = 1; deviceCreateInfo.pNext = &reqDeviceFeatures.get(); + if (mFifoLatestReadySupported) { + fifoLatestReadyFeatures.pNext = &reqDeviceFeatures.get(); + deviceCreateInfo.pNext = &fifoLatestReadyFeatures; + } deviceCreateInfo.pEnabledFeatures = nullptr; - deviceCreateInfo.enabledExtensionCount = static_cast(reqDeviceExtensions.size()); - deviceCreateInfo.ppEnabledExtensionNames = reqDeviceExtensions.data(); + deviceCreateInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); mDevice = mPhysicalDevice.createDevice(deviceCreateInfo, nullptr); VULKAN_HPP_DEFAULT_DISPATCHER.init(mDevice); mGraphicsQueue = mDevice.getQueue(mGraphicsFamily, 0); @@ -720,7 +740,10 @@ void GPUDisplayBackendVulkan::createSwapChain(vk::SwapchainKHR oldSwapChain) { updateSwapChainDetails(mPhysicalDevice); mSurfaceFormat = chooseSwapSurfaceFormat(mSwapChainDetails.formats); - mPresentMode = chooseSwapPresentMode(mSwapChainDetails.presentModes, mDisplay->cfgR().drawQualityVSync ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eImmediate); + mPresentMode = chooseSwapPresentMode(); + if (mDisplay->GetProcessingSettings().debugLevel >= 2) { + GPUInfo("Vulkan present mode %s (VSync %d)", vk::to_string(mPresentMode).c_str(), (int32_t)mDisplay->cfgR().drawQualityVSync); + } vk::Extent2D extent = chooseSwapExtent(mSwapChainDetails.capabilities); uint32_t imageCount = std::max(mSwapChainDetails.capabilities.minImageCount + 1, mDisplay->cfg().vulkan.nFramesInFlight + 1); diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h index c67cbb121a398..d46b9f86b38f2 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h @@ -90,6 +90,7 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend using FeatureChain = vk::StructureChain; double checkDevice(vk::PhysicalDevice device, const std::vector& reqDeviceExtensions, const FeatureChain& reqDeviceFeatures, bool silent = false); vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); + vk::PresentModeKHR chooseSwapPresentMode(); void transitionImageLayout(vk::CommandBuffer commandBuffer, vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout); VulkanBuffer createBuffer(size_t size, const void* srcData = nullptr, vk::BufferUsageFlags type = vk::BufferUsageFlagBits::eVertexBuffer, int32_t deviceMemory = 1); void writeToBuffer(VulkanBuffer& buffer, size_t size, const void* srcData); @@ -212,6 +213,8 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend bool mZActive = false; bool mSmoothLinesSupported = false; // Optional Vulkan 1.4 feature smoothLines, enabled if available bool mSmoothLines = false; + bool mPresentModeErrorShown = false; + bool mFifoLatestReadySupported = false; // Optional present mode FIFO_LATEST_READY (VK_KHR / VK_EXT_present_mode_fifo_latest_ready), enabled if available bool mStrictLines = false; bool mZSupported = false; bool mStencilSupported = false; From 3b207596903b2fc3265c7048d0f8ad69f289d0b6 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 15 Oct 2026 00:05:32 +0200 Subject: [PATCH 3/6] GPU Display: Implement FPS limiting to VSync rate --- GPU/GPUTracking/Definitions/GPUSettingsList.h | 4 +- .../display/backend/GPUDisplayBackend.cxx | 9 ++ .../display/backend/GPUDisplayBackend.h | 5 + .../display/backend/GPUDisplayBackendOpenGL.h | 1 + .../backend/GPUDisplayBackendVulkan.cxx | 101 +++++++++++++++--- .../display/backend/GPUDisplayBackendVulkan.h | 19 ++-- .../display/frontend/GPUDisplayFrontend.cxx | 2 +- .../display/frontend/GPUDisplayFrontend.h | 3 +- .../frontend/GPUDisplayFrontendGlfw.cxx | 7 ++ .../display/frontend/GPUDisplayFrontendGlfw.h | 1 + .../frontend/GPUDisplayFrontendWayland.cxx | 13 ++- .../frontend/GPUDisplayFrontendWayland.h | 2 + .../frontend/GPUDisplayFrontendX11.cxx | 4 +- .../display/frontend/GPUDisplayKeys.cxx | 23 +++- 14 files changed, 162 insertions(+), 32 deletions(-) diff --git a/GPU/GPUTracking/Definitions/GPUSettingsList.h b/GPU/GPUTracking/Definitions/GPUSettingsList.h index f2a7927f9c1d2..15742a8d0638f 100644 --- a/GPU/GPUTracking/Definitions/GPUSettingsList.h +++ b/GPU/GPUTracking/Definitions/GPUSettingsList.h @@ -504,8 +504,8 @@ AddOption(rendererCompatSmoothing, bool, false, "", 0, "Use compat smoothing (if AddOption(drawQualityMSAA, int32_t, 0, "", 0, "MultiSample Anti Aliasing") AddOption(drawQualityDownsampleFSAA, int32_t, 0, "", 0, "Downsampling Anti Aliasing") AddOption(drawQualityVSync, bool, true, "", 0, "Enable Vertical Sync") -AddOption(maxFPSRate, bool, false, "", 0, "Do not limit FPS but run at maximum possible rate") -AddOption(fpsRate, int32_t, 100, "", 0, "Target frame rate (frames per second) if not running at maximum rate (0 = no limit)") +AddOption(fpsLimit, int32_t, 1, "", 0, "FPS limit: 0 = none (maximum rate), 1 = display refresh rate (no limit without VSync), 2 = fpsRate") +AddOption(fpsRate, int32_t, 100, "", 0, "Target frame rate (frames per second) for fpsLimit 2, or if the backend cannot limit to the refresh rate (0 = no limit)") AddOption(useGLIndirectDraw, bool, true, "", 0, "Use OpenGL indirect draws to reduce number of draw calls") AddOption(screenshotScaleFactor, int32_t, 1, "", 0, "Resolution scale factor when taking screenshots") AddOption(fov, int32_t, 45, "", 0, "Display FOV") diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx index dd39cb071efc8..8695cacffbc21 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx @@ -171,6 +171,15 @@ void GPUDisplayBackend::fillIndirectCmdBuffer() } } +int32_t GPUDisplayBackend::fpsLimitMode() const +{ + const auto& cfg = mDisplay->cfgR(); + if (cfg.fpsLimit == FPS_LIMIT_VSYNC) { + return !cfg.drawQualityVSync ? FPS_LIMIT_NONE : (canLimitToVSync() ? FPS_LIMIT_VSYNC : FPS_LIMIT_RATE); + } + return cfg.fpsLimit; +} + float GPUDisplayBackend::getDownsampleFactor(bool screenshot) { float factor = 1.0f; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h index 4087ab8323d7d..24ec53e5b4048 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h @@ -77,6 +77,11 @@ class GPUDisplayBackend virtual void ActivateColor(std::array& color) = 0; virtual void setQuality() {}; virtual void SetVSync(bool enable) {}; + enum fpsLimitModes : int32_t { FPS_LIMIT_NONE = 0, + FPS_LIMIT_VSYNC = 1, + FPS_LIMIT_RATE = 2 }; // Values of GPUSettingsDisplayRenderer::fpsLimit + virtual bool canLimitToVSync() const { return false; } // Rendering is throttled to the refresh rate with VSync (blocking buffer swap / present wait) + int32_t fpsLimitMode() const; // Effective mode: VSync limit without VSync = none, VSync limit not supported by the backend = fpsRate virtual bool backendNeedRedraw() { return true; } virtual void setDepthBuffer() = 0; virtual int32_t InitBackendA() = 0; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h index a41c8df6b7715..89671e746c652 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h @@ -35,6 +35,7 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend ~GPUDisplayBackendOpenGL() override = default; int32_t ExtInit() override; bool CoreProfile() override; + bool canLimitToVSync() const override { return true; } // Buffer swap blocks with VSync uint32_t DepthBits() override; protected: diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx index 23540dcaf1014..15b2831879328 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx @@ -99,6 +99,10 @@ static vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vectorcfgR().drawQualityVSync; const auto& available = mSwapChainDetails.presentModes; for (vk::PresentModeKHR mode : {vk::PresentModeKHR::eImmediate, vk::PresentModeKHR::eMailbox, vk::PresentModeKHR::eFifoLatestReady, vk::PresentModeKHR::eFifo}) { @@ -313,6 +317,25 @@ static void clearFeatures(vk::StructureChain& chain) // Zero all feature f ((memset(reinterpret_cast(&chain.template get()) + sizeof(VkBaseOutStructure), 0, sizeof(T) - sizeof(VkBaseOutStructure))), ...); } +template // Enable the first available of the extensions (aliases) if the device supports the feature, chaining the feature struct in front of chain +static bool enableOptionalFeature(vk::PhysicalDevice device, const std::vector& available, std::initializer_list extensions, vk::Bool32 T::* feature, T& featureStruct, std::vector& enabledExtensions, void*& chain) +{ + for (const char* ext : extensions) { + if (std::none_of(available.begin(), available.end(), [ext](const auto& e) { return strcmp(ext, e.extensionName) == 0; })) { + continue; + } + if (!(device.getFeatures2().template get().*feature)) { + return false; + } + enabledExtensions.emplace_back(ext); + featureStruct.*feature = VK_TRUE; + featureStruct.pNext = chain; + chain = &featureStruct; + return true; + } + return false; +} + template static vk::StructureChain getFeatures(vk::PhysicalDevice device, const vk::StructureChain&) { @@ -536,18 +559,10 @@ void GPUDisplayBackendVulkan::createDevice() mSmoothLinesSupported = getFeatures(mPhysicalDevice, reqDeviceFeatures).get().smoothLines; reqDeviceFeatures.get().smoothLines = mSmoothLinesSupported; std::vector deviceExtensions = reqDeviceExtensions; - vk::PhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{}; + void* featureChain = &reqDeviceFeatures.get(); const std::vector availableDeviceExtensions = mPhysicalDevice.enumerateDeviceExtensionProperties(nullptr); - for (const char* ext : {VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME, VK_EXT_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME}) { // Optional, same feature struct for both - if (std::any_of(availableDeviceExtensions.begin(), availableDeviceExtensions.end(), [ext](const auto& e) { return strcmp(ext, e.extensionName) == 0; })) { - mFifoLatestReadySupported = mPhysicalDevice.getFeatures2().get().presentModeFifoLatestReady; - if (mFifoLatestReadySupported) { - deviceExtensions.emplace_back(ext); - fifoLatestReadyFeatures.presentModeFifoLatestReady = VK_TRUE; - } - break; - } - } + vk::PhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{}; + mFifoLatestReadySupported = enableOptionalFeature(mPhysicalDevice, availableDeviceExtensions, {VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME, VK_EXT_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME}, &vk::PhysicalDevicePresentModeFifoLatestReadyFeaturesKHR::presentModeFifoLatestReady, fifoLatestReadyFeatures, deviceExtensions, featureChain); updateSwapChainDetails(mPhysicalDevice); vk::PhysicalDeviceProperties deviceProperties = mPhysicalDevice.getProperties(); @@ -572,11 +587,7 @@ void GPUDisplayBackendVulkan::createDevice() vk::DeviceCreateInfo deviceCreateInfo{}; deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; deviceCreateInfo.queueCreateInfoCount = 1; - deviceCreateInfo.pNext = &reqDeviceFeatures.get(); - if (mFifoLatestReadySupported) { - fifoLatestReadyFeatures.pNext = &reqDeviceFeatures.get(); - deviceCreateInfo.pNext = &fifoLatestReadyFeatures; - } + deviceCreateInfo.pNext = featureChain; deviceCreateInfo.pEnabledFeatures = nullptr; deviceCreateInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); @@ -648,6 +659,19 @@ void GPUDisplayBackendVulkan::createSemaphores() mTimelineValue = 0; mFrameTimelineValue.assign(mFramesInFlight, 0); mImageTimelineValue.assign(mImageCount, 0); + for (uint32_t i = 0; i < mImageCount; i++) { + mPresentFences.emplace_back(mDevice.createFence(vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}, nullptr)); + } +} + +void GPUDisplayBackendVulkan::waitPresentFences() +{ + if (mPresentFences.size()) { // Timeout in case a failed present did not signal its fence + vk::Result retVal = mDevice.waitForFences(mPresentFences.size(), mPresentFences.data(), true, 1'000'000'000); + if (retVal != vk::Result::eTimeout) { + CHKERR(retVal); + } + } } void GPUDisplayBackendVulkan::clearSemaphores() @@ -655,6 +679,7 @@ void GPUDisplayBackendVulkan::clearSemaphores() clearVector(mImageAvailableSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); }); clearVector(mPresentSemaphore, [&](auto& x) { mDevice.destroySemaphore(x, nullptr); }); mDevice.destroySemaphore(mTimelineSemaphore, nullptr); + clearVector(mPresentFences, [&](auto& x) { mDevice.destroyFence(x, nullptr); }); } // ---------------------------- VULKAN UNIFORM LAYOUTS AND BUFFERS ---------------------------- @@ -770,8 +795,23 @@ void GPUDisplayBackendVulkan::createSwapChain(vk::SwapchainKHR oldSwapChain) swapCreateInfo.presentMode = mPresentMode; swapCreateInfo.clipped = true; swapCreateInfo.oldSwapchain = oldSwapChain; + vk::SwapchainPresentScalingCreateInfoKHR scalingInfo{}; + { // Let the compositor scale the last image while resizing, until the new swapchain is presented + const vk::SurfacePresentModeKHR surfacePresentMode{mPresentMode}; + const auto caps = mPhysicalDevice.getSurfaceCapabilities2KHR(vk::PhysicalDeviceSurfaceInfo2KHR{mSurface, &surfacePresentMode}).get(); + auto gravity = [](vk::PresentGravityFlagsKHR f) { return (f & vk::PresentGravityFlagBitsKHR::eCentered) ? vk::PresentGravityFlagBitsKHR::eCentered : (f & vk::PresentGravityFlagBitsKHR::eMin) ? vk::PresentGravityFlagBitsKHR::eMin + : vk::PresentGravityFlagsKHR(f & vk::PresentGravityFlagBitsKHR::eMax); }; + scalingInfo.scalingBehavior = (caps.supportedPresentScaling & vk::PresentScalingFlagBitsKHR::eAspectRatioStretch) ? vk::PresentScalingFlagBitsKHR::eAspectRatioStretch : vk::PresentScalingFlagsKHR(caps.supportedPresentScaling & vk::PresentScalingFlagBitsKHR::eStretch); + scalingInfo.presentGravityX = gravity(caps.supportedPresentGravityX); + scalingInfo.presentGravityY = gravity(caps.supportedPresentGravityY); + if (scalingInfo.scalingBehavior && scalingInfo.presentGravityX && scalingInfo.presentGravityY) { + swapCreateInfo.pNext = &scalingInfo; + } + } mSwapChain = mDevice.createSwapchainKHR(swapCreateInfo, nullptr); + mLastPresentId = 0; // Present IDs are per swapchain if (oldSwapChain) { + waitPresentFences(); // Presentation engine done with the old swapchain mDevice.destroySwapchainKHR(oldSwapChain, nullptr); } @@ -809,6 +849,7 @@ void GPUDisplayBackendVulkan::clearSwapChain(bool keepSwapChain) { clearVector(mSwapChainImageViews, [&](auto& x) { mDevice.destroyImageView(x, nullptr); }); if (!keepSwapChain) { + waitPresentFences(); mDevice.destroySwapchainKHR(mSwapChain, nullptr); } } @@ -1424,11 +1465,22 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& if (includeMixImage == 0.f) { mCurrentFrame = (mCurrentFrame + 1) % mFramesInFlight; waitTimeline(mFrameTimelineValue[mCurrentFrame]); + if (mLastPresentId > 1 && fpsLimitMode() == FPS_LIMIT_VSYNC) { // Refresh rate limit (FIFO): at most one frame queued (wait for the second-last present), keeps the rate if a frame misses the deadline, timeout in case the window is hidden + const vk::Result retVal = (vk::Result)VULKAN_HPP_DEFAULT_DISPATCHER.vkWaitForPresentKHR(static_cast(mDevice), static_cast(mSwapChain), mLastPresentId - 1, 100'000'000); + if (retVal == vk::Result::eErrorOutOfDateKHR || retVal == vk::Result::eSuboptimalKHR) { + mMustUpdateSwapChain = true; + } else if (retVal != vk::Result::eTimeout) { + CHKERR(retVal); + } + } auto getImage = [&]() { return mDevice.acquireNextImageKHR(mSwapChain, UINT64_MAX, mImageAvailableSemaphore[mCurrentFrame], VkFence(VK_NULL_HANDLE), &mCurrentImageIndex); }; vk::Result retVal = vk::Result::eSuccess; + if (mPresentModeForVSyncLimit != (fpsLimitMode() == FPS_LIMIT_VSYNC)) { // Present mode depends on the FPS limit mode + mMustUpdateSwapChain = true; + } bool mustUpdateSwapChain = mMustUpdateSwapChain || mDisplay->updateRenderPipeline() || (toMixBuffer && !mMixingSupported) || mDownsampleFactor != getDownsampleFactor(requestScreenshot) || mSmoothLines != useNativeSmoothLines(); if (!mustUpdateSwapChain) { retVal = getImage(); @@ -1442,7 +1494,11 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& } mMustUpdateSwapChain = retVal == vk::Result::eSuboptimalKHR; CHKERR(retVal); - waitTimeline(mImageTimelineValue[mCurrentImageIndex]); // Previous frame using this image's resources (command buffers, uniform buffer, ...) finished + waitTimeline(mImageTimelineValue[mCurrentImageIndex]); // Previous frame using this image's resources (command buffers, uniform buffer, ...) finished + if (mPresentFences.size()) { // Previous present of this image done, its present semaphore can be signaled again + CHKERR(mDevice.waitForFences(1, &mPresentFences[mCurrentImageIndex], true, 1'000'000'000)); // Timeout (positive result, not an error) in case a failed present did not signal it + CHKERR(mDevice.resetFences(1, &mPresentFences[mCurrentImageIndex])); + } mHasDrawnText = false; mFrameCommandBuffers.clear(); @@ -1521,6 +1577,17 @@ void GPUDisplayBackendVulkan::finishFrame(bool doScreenshot, bool toMixBuffer, f presentInfo.pSwapchains = &mSwapChain; presentInfo.pImageIndices = &mCurrentImageIndex; presentInfo.pResults = nullptr; + const uint64_t presentId = mLastPresentId + 1; + vk::PresentIdKHR presentIdInfo{1, &presentId}; + if (fpsLimitMode() == FPS_LIMIT_VSYNC) { // Present wait only for the refresh rate limit, the fpsRate limit (frontend timer) must not be capped at the refresh rate + presentInfo.pNext = &presentIdInfo; + mLastPresentId = presentId; + } + vk::SwapchainPresentFenceInfoKHR presentFenceInfo{}; + if (mPresentFences.size()) { + presentFenceInfo = vk::SwapchainPresentFenceInfoKHR{1, &mPresentFences[mCurrentImageIndex], presentInfo.pNext}; + presentInfo.pNext = &presentFenceInfo; + } vk::Result retVal = mGraphicsQueue.presentKHR(&presentInfo); if (retVal == vk::Result::eErrorOutOfDateKHR || retVal == vk::Result::eSuboptimalKHR) { mMustUpdateSwapChain = true; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h index d46b9f86b38f2..6557dfb0bb176 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h @@ -83,6 +83,8 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend void lineWidthFactor(float factor) override; bool useNativeSmoothLines() const; bool useSmoothLineShader() const; + bool canLimitToVSync() const override { return true; } // Present wait + void waitPresentFences(); void resizeScene(uint32_t width, uint32_t height) override; float getYFactor() const override { return -1.0f; } int32_t getMaxMSAA() const override { return mMaxMSAAsupported; } @@ -208,16 +210,21 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend vecpod mFontVertexBufferHost; bool mHasDrawnText = false; - vk::SampleCountFlagBits mMSAASampleCount = vk::SampleCountFlagBits::e16; + // Device capabilities, set in createDevice (optional features are enabled only if supported) uint32_t mMaxMSAAsupported = 0; + bool mZSupported = false; + bool mStencilSupported = false; + bool mStrictLines = false; // Wide lines are rectangles, not parallelograms + bool mSmoothLinesSupported = false; // Optional Vulkan 1.4 feature smoothLines + bool mFifoLatestReadySupported = false; // Optional present mode FIFO_LATEST_READY (VK_KHR / VK_EXT_present_mode_fifo_latest_ready) + + vk::SampleCountFlagBits mMSAASampleCount = vk::SampleCountFlagBits::e16; bool mZActive = false; - bool mSmoothLinesSupported = false; // Optional Vulkan 1.4 feature smoothLines, enabled if available bool mSmoothLines = false; bool mPresentModeErrorShown = false; - bool mFifoLatestReadySupported = false; // Optional present mode FIFO_LATEST_READY (VK_KHR / VK_EXT_present_mode_fifo_latest_ready), enabled if available - bool mStrictLines = false; - bool mZSupported = false; - bool mStencilSupported = false; + bool mPresentModeForVSyncLimit = false; // Swapchain created with FIFO for the refresh rate FPS limit + std::vector mPresentFences; // Per swapchain image, signaled when its last present is done (swapchain_maintenance1) + uint64_t mLastPresentId = 0; // Last present ID of the current swapchain, 0 = none bool mDownsampleFSAA = false; bool mMixingSupported = 0; diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx index ab6d8162c6d0c..d202b1a621afb 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx @@ -81,7 +81,7 @@ void GPUDisplayFrontend::waitForFPSLimit() mNeedUpdate = 0; const auto now = std::chrono::steady_clock::now(); const auto& cfg = mDisplay->cfgR(); - if (cfg.maxFPSRate || cfg.fpsRate <= 0) { + if (mBackend->fpsLimitMode() != GPUDisplayBackend::FPS_LIMIT_RATE || cfg.fpsRate <= 0) { mNextFrameTime = now; return; } diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h index 03f5a0524c817..6ce0840ef1723 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h @@ -61,6 +61,7 @@ class GPUDisplayFrontend : public GPUDisplayFrontendInterface GPUDisplayBackend* backend(); static GPUDisplayFrontend* getFrontend(const char* type); virtual void getSize(int32_t& width, int32_t& height) { width = height = 0; } + virtual float getRefreshRate() { return 0.f; } // Refresh rate of the display the window is on in Hz, 0 = unknown virtual float getDisplayScale() const { return 1.f; } // HiDPI scale factor: physical pixels per logical pixel, text positions and sizes are scaled by it virtual int32_t getVulkanSurface(void* instance, void* surface) { return 1; } virtual uint32_t getReqVulkanExtensions(const char**& p) { return 0; }; @@ -80,7 +81,7 @@ class GPUDisplayFrontend : public GPUDisplayFrontendInterface virtual int32_t FrontendMain() = 0; virtual void StopDisplayA(); static void* FrontendThreadWrapper(void*); - void waitForFPSLimit(); // Sleep until the next frame is due according to fpsRate (no sleep with maxFPSRate or fpsRate <= 0) + void waitForFPSLimit(); // Sleep until the next frame is due according to fpsRate (only in FPS_LIMIT_RATE mode, see GPUDisplayBackend::fpsLimitMode) static constexpr int32_t INIT_WIDTH = 1024, INIT_HEIGHT = 768; // Initial window size, before maximizing static constexpr int32_t MIN_WIDTH = 200, MIN_HEIGHT = 150; // Minimum window size diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx index 789c1c2f5f451..961058e974804 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx @@ -396,6 +396,13 @@ bool GPUDisplayFrontendGlfw::EnableSendKey() return true; } +float GPUDisplayFrontendGlfw::getRefreshRate() +{ + GLFWmonitor* monitor = glfwGetWindowMonitor(mWindow) ? glfwGetWindowMonitor(mWindow) : glfwGetPrimaryMonitor(); // Monitor of a windowed window is not known, use the primary one + const GLFWvidmode* mode = monitor ? glfwGetVideoMode(monitor) : nullptr; + return mode ? mode->refreshRate : 0.f; +} + void GPUDisplayFrontendGlfw::getSize(int32_t& width, int32_t& height) { glfwGetFramebufferSize(mWindow, &width, &height); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h index 31b9a8b5211d7..a5820bb001131 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h @@ -35,6 +35,7 @@ class GPUDisplayFrontendGlfw : public GPUDisplayFrontend bool EnableSendKey() override; void getSize(int32_t& width, int32_t& height) override; float getDisplayScale() const override { return mScale; } + float getRefreshRate() override; int32_t getVulkanSurface(void* instance, void* surface) override; uint32_t getReqVulkanExtensions(const char**& p) override; diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx index 987217f93f336..b2d1f054bcb06 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx @@ -309,7 +309,12 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() }; auto output_geometry = [](void* data, wl_output* output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, int32_t transform) {}; - auto output_mode = [](void* data, wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {}; + auto output_mode = [](void* data, wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + GPUDisplayFrontendWayland* me = (GPUDisplayFrontendWayland*)data; + if (auto* o = me->findOutput(output); o && (flags & WL_OUTPUT_MODE_CURRENT)) { + o->refresh = refresh; + } + }; auto output_done = [](void* data, wl_output* output) {}; auto output_scale = [](void* data, wl_output* output, int32_t factor) { GPUDisplayFrontendWayland* me = (GPUDisplayFrontendWayland*)data; @@ -702,6 +707,12 @@ void GPUDisplayFrontendWayland::ToggleMaximized(bool set) } } +float GPUDisplayFrontendWayland::getRefreshRate() +{ + auto* o = mSurfaceOutputs.size() ? findOutput(mSurfaceOutputs.back()) : nullptr; + return o ? o->refresh / 1000.f : 0.f; +} + GPUDisplayFrontendWayland::OutputInfo* GPUDisplayFrontendWayland::findOutput(wl_output* output) { auto it = std::find_if(mOutputs.begin(), mOutputs.end(), [output](const auto& o) { return o.output == output; }); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.h index 61c95907d500d..bd55aa53f4b55 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.h @@ -46,6 +46,7 @@ class GPUDisplayFrontendWayland : public GPUDisplayFrontend void OpenGLPrint(const char* s, float x, float y, float r, float g, float b, float a, bool fromBotton = true) override; void getSize(int32_t& width, int32_t& height) override; float getDisplayScale() const override { return mScale; } + float getRefreshRate() override; int32_t getVulkanSurface(void* instance, void* surface) override; uint32_t getReqVulkanExtensions(const char**& p) override; @@ -64,6 +65,7 @@ class GPUDisplayFrontendWayland : public GPUDisplayFrontend uint32_t version; int32_t scale = 1; std::string name; + int32_t refresh = 0; // mHz, current mode }; OutputInfo* findOutput(wl_output* output); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx index e832ee88bd751..20449c8f907d5 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx @@ -291,6 +291,8 @@ int32_t GPUDisplayFrontendX11::FrontendMain() } XMapWindow(mXDisplay, mWindow); + XEvent mapEvent; // Maximize only after the window manager placed the window (on the active monitor), so that it restores to this position when un-maximized + XPeekIfEvent(mXDisplay, &mapEvent, [](Display*, XEvent* ev, XPointer window) -> Bool { return ev->type == MapNotify && ev->xmap.window == *(Window*)window; }, (XPointer)&mWindow); applyInitialWindowState(); // Receive signal when window closed @@ -523,7 +525,7 @@ void GPUDisplayFrontendX11::ToggleMaximized(bool set) xev.xclient.window = mWindow; xev.xclient.message_type = XInternAtom(mXDisplay, "_NET_WM_STATE", False); xev.xclient.format = 32; - xev.xclient.data.l[0] = set ? 1 : 2; //_NET_WM_STATE_ADD + xev.xclient.data.l[0] = set ? 1 : 0; // _NET_WM_STATE_ADD / _NET_WM_STATE_REMOVE xev.xclient.data.l[1] = XInternAtom(mXDisplay, "_NET_WM_STATE_MAXIMIZED_HORZ", False); xev.xclient.data.l[2] = XInternAtom(mXDisplay, "_NET_WM_STATE_MAXIMIZED_VERT", False); XSendEvent(mXDisplay, DefaultRootWindow(mXDisplay), False, SubstructureNotifyMask, &xev); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx index fd94451741e85..b94ea0b2ccf06 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx @@ -43,7 +43,7 @@ const char* HelpText[] = { "[Z] Change screenshot resolution (scaling factor)", "[S] / [A] / [D] Enable or disable smoothing of points / smoothing of lines / depth buffer", "[W] / [U] / [V] Toggle anti-aliasing (MSAA at raster level / change downsampling FSAA factor) / toggle VSync", - "[F] / [_] / [R] Switch mFullScreen / Maximized window / FPS rate limiter", + "[F] / [_] / [R] Switch mFullScreen / Maximized window / FPS limit (none, refresh rate, fpsRate)", "[I] Enable / disable GL indirect draw", "[o] / [p] / [O] / [P] Save / restore current camera position / Animation path", "[h] Print Help", @@ -158,8 +158,25 @@ void GPUDisplay::HandleKey(uint8_t key) mCfgR.maximized ^= 1; SetInfo("Toggling Maximized window (%d)", (int32_t)mCfgR.maximized); } else if (key == 'R') { - mCfgR.maxFPSRate = !mCfgR.maxFPSRate; - SetInfo("FPS rate %s", mCfgR.maxFPSRate ? "not limited" : "limited"); + mCfgR.fpsLimit = (mCfgR.fpsLimit + 1) % 3; + if (mCfgR.fpsLimit == GPUDisplayBackend::FPS_LIMIT_RATE) { + SetInfo("FPS limit: %d FPS", mCfgR.fpsRate); + } else if (mCfgR.fpsLimit == GPUDisplayBackend::FPS_LIMIT_VSYNC) { + char rate[32] = "unknown rate"; + if (mFrontend->getRefreshRate() > 0.f) { + snprintf(rate, sizeof(rate), "%.1f Hz", mFrontend->getRefreshRate()); + } + const int32_t mode = mBackend->fpsLimitMode(); + if (mode == GPUDisplayBackend::FPS_LIMIT_NONE) { + SetInfo("FPS limit: refresh rate (%s) - VSync disabled, no limit", rate); + } else if (mode == GPUDisplayBackend::FPS_LIMIT_RATE) { + SetInfo("FPS limit: refresh rate (%s) - not supported by the backend, fallback to %d FPS", rate, mCfgR.fpsRate); + } else { + SetInfo("FPS limit: refresh rate (%s)", rate); + } + } else { + SetInfo("FPS limit: none"); + } } else if (key == 'H') { mPrintInfoText += 1; mPrintInfoText &= 3; From b6b52e70ead744107ef4a01e40471e816fdd2baa Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 15 Oct 2026 01:09:17 +0200 Subject: [PATCH 4/6] GPU Display: Add vsync support for GLUT frontend --- .../display/frontend/GPUDisplayFrontend.cxx | 5 +++++ .../display/frontend/GPUDisplayFrontend.h | 1 + .../display/frontend/GPUDisplayFrontendGlut.cxx | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx index d202b1a621afb..1a2b5e0932d40 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.cxx @@ -226,6 +226,11 @@ void GPUDisplayFrontend::applyInitialWindowState() } } +bool GPUDisplayFrontend::vsyncEnabled() const +{ + return mDisplay->cfgR().drawQualityVSync; +} + int32_t& GPUDisplayFrontend::drawTextFontSize() { return mDisplay->drawTextFontSize(); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h index 6ce0840ef1723..1b384b27e6c5a 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h @@ -155,6 +155,7 @@ class GPUDisplayFrontend : public GPUDisplayFrontendInterface int32_t InitDisplay(bool initFailure = false); // Callback to initialize the GL Display (to be called in StartDisplay) void ExitDisplay(); // Callback to clean up the GL Display int32_t& drawTextFontSize(); + bool vsyncEnabled() const; // VSync setting (drawQualityVSync) void setWindowState(bool fullScreen, bool maximized); // Sync the settings if the window manager changed the window state void focusLost(); // Release all keys and mouse buttons when the window loses focus, since the release events go to another window void applyInitialWindowState(); // Switch to fullscreen / maximize at startup according to the settings diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx index 6b75eb7493627..ad4d7da014d2d 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx @@ -21,6 +21,10 @@ #include #include #include +#ifdef GPUCA_BUILD_EVENT_DISPLAY_X11 +#include +#include +#endif #include using namespace o2::gpu; @@ -264,6 +268,7 @@ int32_t GPUDisplayFrontendGlut::FrontendMain() glutPassiveMotionFunc(mouseMoveFunc); glutMouseWheelFunc(mMouseWheelFunc); applyInitialWindowState(); + SetVSync(vsyncEnabled()); { std::lock_guard lock(mExitStateMutex); @@ -308,4 +313,12 @@ void GPUDisplayFrontendGlut::SwitchFullscreen(bool set) } void GPUDisplayFrontendGlut::ToggleMaximized(bool set) {} -void GPUDisplayFrontendGlut::SetVSync(bool enable) {} +void GPUDisplayFrontendGlut::SetVSync(bool enable) // GLUT has no swap interval API, set it via GLX directly (no-op if freeglut does not use GLX) +{ +#ifdef GPUCA_BUILD_EVENT_DISPLAY_X11 + auto swapInterval = (PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddressARB((const GLubyte*)"glXSwapIntervalEXT"); + if (swapInterval && glXGetCurrentDisplay()) { + swapInterval(glXGetCurrentDisplay(), glXGetCurrentDrawable(), enable); + } +#endif +} From 57be3455440d871902aa7da1a1c8e7c0747b40f0 Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 15 Oct 2026 01:41:54 +0200 Subject: [PATCH 5/6] GPU Display: Many minor fixes suggested by Claude --- GPU/GPUTracking/Definitions/GPUSettingsList.h | 2 +- GPU/GPUTracking/display/GPUDisplay.cxx | 4 +- .../display/backend/GPUDisplayBackend.cxx | 5 +- .../display/backend/GPUDisplayBackend.h | 2 +- .../display/backend/GPUDisplayBackendNone.h | 2 +- .../backend/GPUDisplayBackendOpenGL.cxx | 43 ++++++----- .../display/backend/GPUDisplayBackendOpenGL.h | 8 +- .../backend/GPUDisplayBackendVulkan.cxx | 74 ++++++++++++++----- .../display/backend/GPUDisplayBackendVulkan.h | 7 +- .../display/frontend/GPUDisplayFrontend.h | 1 + .../frontend/GPUDisplayFrontendGlfw.cxx | 29 +++++--- .../display/frontend/GPUDisplayFrontendGlfw.h | 1 + .../frontend/GPUDisplayFrontendGlut.cxx | 14 +++- .../display/frontend/GPUDisplayFrontendGlut.h | 1 + .../frontend/GPUDisplayFrontendWayland.cxx | 52 +++++++++---- .../frontend/GPUDisplayFrontendX11.cxx | 6 +- .../display/frontend/GPUDisplayFrontendX11.h | 1 + .../display/frontend/GPUDisplayKeys.cxx | 8 +- dependencies/FindO2GPUWayland.cmake | 3 +- 19 files changed, 177 insertions(+), 86 deletions(-) diff --git a/GPU/GPUTracking/Definitions/GPUSettingsList.h b/GPU/GPUTracking/Definitions/GPUSettingsList.h index 15742a8d0638f..8b4f6a8df1769 100644 --- a/GPU/GPUTracking/Definitions/GPUSettingsList.h +++ b/GPU/GPUTracking/Definitions/GPUSettingsList.h @@ -500,7 +500,7 @@ AddOption(camYUp, bool, false, "", 0, "Orient the camera such that the y-axis is AddOption(cameraMode, int32_t, 0, "", 0, "Camera mode") AddOption(fullScreen, bool, false, "", 0, "Full Screen") AddOption(maximized, bool, true, "", 0, "Maximized window") -AddOption(rendererCompatSmoothing, bool, false, "", 0, "Use compat smoothing (if available) of instad of shader for smooth lines / points") +AddOption(rendererCompatSmoothing, bool, false, "", 0, "Use compat smoothing (if available) instead of shader for smooth lines / points") AddOption(drawQualityMSAA, int32_t, 0, "", 0, "MultiSample Anti Aliasing") AddOption(drawQualityDownsampleFSAA, int32_t, 0, "", 0, "Downsampling Anti Aliasing") AddOption(drawQualityVSync, bool, true, "", 0, "Enable Vertical Sync") diff --git a/GPU/GPUTracking/display/GPUDisplay.cxx b/GPU/GPUTracking/display/GPUDisplay.cxx index fc34af50ba478..2db90ea9a5fed 100644 --- a/GPU/GPUTracking/display/GPUDisplay.cxx +++ b/GPU/GPUTracking/display/GPUDisplay.cxx @@ -674,7 +674,9 @@ void GPUDisplay::DrawGLScene_internal(float animateTime, bool renderToMixBuffer) nextViewMatrix = nextViewMatrix * *mModelMatrix; const float zFar = ((mParam->par.continuousTracking ? (mMaxClusterZ * GL_SCALE_FACTOR) : 8.f) + 50.f) * 2.f; const hmm_mat4 proj = HMM_Perspective(mCfgR.fov, (float)mBackend->mRenderWidth / (float)mBackend->mRenderHeight, 0.1f, zFar); - mBackend->prepareDraw(proj, nextViewMatrix, doScreenshot || mRequestScreenshot, renderToMixBuffer, mixSlaveImage); + if (!mBackend->prepareDraw(proj, nextViewMatrix, doScreenshot || mRequestScreenshot, renderToMixBuffer, mixSlaveImage)) { + return; // No image available (e.g. hidden window), retry with the next frame + } mBackend->pointSizeFactor(1); mBackend->lineWidthFactor(1); diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx index 8695cacffbc21..c1e39311b1768 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackend.cxx @@ -119,7 +119,8 @@ void GPUDisplayBackend::loadFont() return; } if (FT_New_Face(ft, fontName.c_str(), 0, &face)) { - GPUError("Error loading freetypoe font"); + GPUError("Error loading freetype font"); + FT_Done_FreeType(ft); return; } @@ -134,6 +135,8 @@ void GPUDisplayBackend::loadFont() for (uint32_t i = 0; i < 128; i++) { if (FT_Load_Char(face, i, FT_LOAD_RENDER)) { GPUError("Error loading freetype symbol"); + FT_Done_Face(face); + FT_Done_FreeType(ft); return; } const auto& glyph = face->glyph; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h index 24ec53e5b4048..38114662714af 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackend.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackend.h @@ -90,7 +90,7 @@ class GPUDisplayBackend void updateFontScale(float scale); // Rasterize the font again if the display (HiDPI) scale changed void ExitBackend(); virtual void loadDataToGPU(size_t totalVertizes) = 0; - virtual void prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot = false, bool toMixBuffer = false, float includeMixImage = 0.f) = 0; + virtual bool prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot = false, bool toMixBuffer = false, float includeMixImage = 0.f) = 0; // Returns false if the frame must be skipped (no swapchain image available) virtual void finishDraw(bool doScreenshot, bool toMixBuffer = false, float includeMixImage = 0.f) = 0; virtual void finishFrame(bool doScreenshot, bool toMixBuffer = false, float includeMixImage = 0.f) = 0; virtual void prepareText() = 0; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendNone.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendNone.h index 4af69692d79c1..6d4160c093206 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendNone.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendNone.h @@ -33,7 +33,7 @@ class GPUDisplayBackendNone : public GPUDisplayBackend int32_t InitBackendA() override; void ExitBackendA() override {} void loadDataToGPU(size_t totalVertizes) override {} - void prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override {} + bool prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override { return true; } void finishDraw(bool doScreenshot, bool toMixBuffer, float includeMixImage) override {} void finishFrame(bool doScreenshot, bool toMixBuffer, float includeMixImage) override {} void prepareText() override {} diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx index d1a7aace2b470..d5342e0ab5abb 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.cxx @@ -61,6 +61,11 @@ int32_t GPUDisplayBackendOpenGL::ExtInit() #endif } +bool GPUDisplayBackendOpenGL::canLimitToVSync() const +{ + return mDisplay->frontend()->canSetVSync(); +} + bool GPUDisplayBackendOpenGL::CoreProfile() { #ifdef GPUCA_DISPLAY_OPENGL_CORE @@ -471,11 +476,14 @@ void GPUDisplayBackendOpenGL::loadDataToGPU(size_t totalVertizes) } } -void GPUDisplayBackendOpenGL::prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) +bool GPUDisplayBackendOpenGL::prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) { if (mDisplay->updateRenderPipeline() || mDownsampleFactor != getDownsampleFactor(requestScreenshot)) { updateRenderer(requestScreenshot); } + if (toMixBuffer && !mMixBuffer.created) { // Only allocated when mixing is used, freed with the other offscreen buffers + createFB(mMixBuffer, true, true, false, mRenderWidth, mRenderHeight); + } if (mOffscreenBufferMSAA.created) { setFrameBuffer(mOffscreenBufferMSAA.fb_id); } else if (toMixBuffer) { @@ -543,6 +551,7 @@ void GPUDisplayBackendOpenGL::prepareDraw(const hmm_mat4& proj, const hmm_mat4& CHKERR(glNamedBufferSubData(mFieldModelViewBuffer, 0, sizeof(modelViewProj), &modelViewProj)); } } + return true; } void GPUDisplayBackendOpenGL::finishDraw(bool doScreenshot, bool toMixBuffer, float includeMixImage) @@ -557,7 +566,7 @@ void GPUDisplayBackendOpenGL::finishDraw(bool doScreenshot, bool toMixBuffer, fl CHKERR(glUseProgram(0)); } - if (mDisplay->cfgR().drawQualityMSAA) { + if (mOffscreenBufferMSAA.created) { uint32_t dstId = toMixBuffer ? mMixBuffer.fb_id : (mDownsampleFactor != 1 ? mOffscreenBuffer.fb_id : 0); CHKERR(glBlitNamedFramebuffer(mOffscreenBufferMSAA.fb_id, dstId, 0, 0, mRenderWidth, mRenderHeight, 0, 0, mRenderWidth, mRenderHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR)); } @@ -788,8 +797,8 @@ void GPUDisplayBackendOpenGL::OpenGLPrint(const char* s, float x, float y, float if (smoothFont()) { scale *= 0.25f; // Font size is 48 to have nice bitmap, scale to size 12 } - for (const char* c = s; *c; c++) { - if ((int32_t)*c > (int32_t)mFontSymbols.size()) { + for (const unsigned char* c = (const unsigned char*)s; *c; c++) { + if ((int32_t)*c >= (int32_t)mFontSymbols.size()) { GPUError("Trying to draw unsupported symbol: %d > %d\n", (int32_t)*c, (int32_t)mFontSymbols.size()); continue; } @@ -838,20 +847,6 @@ void GPUDisplayBackendOpenGL::resizeScene(uint32_t width, uint32_t height) mScreenHeight = height; updateRenderer(false); - - float vertices[6][4] = { - {0, (float)mRenderHeight, 0.0f, 1.0f}, - {0, 0, 0.0f, 0.0f}, - {(float)mRenderWidth, 0, 1.0f, 0.0f}, - {0, (float)mRenderHeight, 0.0f, 1.0f}, - {(float)mRenderWidth, 0, 1.0f, 0.0f}, - {(float)mRenderWidth, (float)mRenderHeight, 1.0f, 1.0f}}; - CHKERR(glBindBuffer(GL_ARRAY_BUFFER, VBO_texture)); - CHKERR(glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices)); - CHKERR(glBindBuffer(GL_ARRAY_BUFFER, 0)); - if (!mDisplay->useMultiVBO()) { - CHKERR(glBindBuffer(GL_ARRAY_BUFFER, mVBOId[0])); // If we don't use multiple buffers, we keep the default buffer bound - } } void GPUDisplayBackendOpenGL::updateRenderer(bool withScreenshot) @@ -866,6 +861,16 @@ void GPUDisplayBackendOpenGL::updateRenderer(bool withScreenshot) if (mDownsampleFactor != 1) { createFB(mOffscreenBuffer, mDownsampleFactor != 2, true, false, mRenderWidth, mRenderHeight); // Texture for shader downsampling (factor != 2), screenshot downsampling uses the shader only if this factor is != 2 as well } - createFB(mMixBuffer, true, true, false, mRenderWidth, mRenderHeight); setQuality(); + + float vertices[6][4] = {// Mix quad at render size + {0, (float)mRenderHeight, 0.0f, 1.0f}, + {0, 0, 0.0f, 0.0f}, + {(float)mRenderWidth, 0, 1.0f, 0.0f}, + {0, (float)mRenderHeight, 0.0f, 1.0f}, + {(float)mRenderWidth, 0, 1.0f, 0.0f}, + {(float)mRenderWidth, (float)mRenderHeight, 1.0f, 1.0f}}; + CHKERR(glBindBuffer(GL_ARRAY_BUFFER, VBO_texture)); + CHKERR(glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices)); + CHKERR(glBindBuffer(GL_ARRAY_BUFFER, mDisplay->useMultiVBO() ? 0 : mVBOId[0])); // If we don't use multiple buffers, we keep the default buffer bound (also called from prepareDraw before the vertex pointers are set) } diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h index 89671e746c652..f30a22846a1ac 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendOpenGL.h @@ -35,7 +35,7 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend ~GPUDisplayBackendOpenGL() override = default; int32_t ExtInit() override; bool CoreProfile() override; - bool canLimitToVSync() const override { return true; } // Buffer swap blocks with VSync + bool canLimitToVSync() const override; // Buffer swap blocks with VSync, if the frontend can set the swap interval uint32_t DepthBits() override; protected: @@ -57,7 +57,7 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend static int32_t checkProgramStatus(uint32_t program); void clearScreen(bool alphaOnly = false); void loadDataToGPU(size_t totalVertizes) override; - void prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override; + bool prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override; void resizeScene(uint32_t width, uint32_t height) override; void updateRenderer(bool withScreenshot); void ClearOffscreenBuffers(); @@ -101,9 +101,9 @@ class GPUDisplayBackendOpenGL : public GPUDisplayBackend uint32_t mFragmentShaderDownsample; uint32_t mGeometryShader; uint32_t mShaderProgram; - uint32_t mShaderProgramSmoothPoint; // Points with fragmentSmoothPoint.frag, for smoothPoints in the core profile (no GL_POINT_SMOOTH) + uint32_t mShaderProgramSmoothPoint; // Points with fragmentSmoothPoint.frag, for smoothPoints with the shader path (not rendererCompatSmoothing / GL_POINT_SMOOTH) bool mUseSmoothPointShader = false; - uint32_t mShaderProgramSmoothLine; // Lines with vertexSmoothLine.vert / fragmentSmoothLine.frag, for smoothLines in the core profile (no GL_LINE_SMOOTH) + uint32_t mShaderProgramSmoothLine; // Lines with vertexSmoothLine.vert / fragmentSmoothLine.frag, for smoothLines with the shader path (not rendererCompatSmoothing / GL_LINE_SMOOTH) bool mUseSmoothLineShader = false; uint32_t mShaderProgramText; uint32_t mShaderProgramTexture; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx index 15b2831879328..46ab5c3c48591 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx @@ -571,6 +571,8 @@ void GPUDisplayBackendVulkan::createDevice() GPUInfo("Using physical Vulkan device %s", &deviceProperties.deviceName[0]); mMaxMSAAsupported = getMaxUsableSampleCount(deviceProperties); mStrictLines = deviceProperties.limits.strictLines; + mMaxLineWidth = deviceProperties.limits.lineWidthRange[1]; + mMaxPointSize = deviceProperties.limits.pointSizeRange[1]; mMaxRenderSize = std::min({deviceProperties.limits.maxImageDimension2D, deviceProperties.limits.maxFramebufferWidth, deviceProperties.limits.maxFramebufferHeight}); mZSupported = (bool)(depth32FormatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); mStencilSupported = (bool)(depth64FormatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); @@ -1196,7 +1198,9 @@ void GPUDisplayBackendVulkan::startFillCommandBuffer(vk::CommandBuffer& commandB cmdBeginRendering(commandBuffer, mRenderWidth, mRenderHeight, colorAttachment, mZActive ? &depthAttachment : nullptr); vk::DeviceSize offsets[] = {0}; - commandBuffer.bindVertexBuffers(0, 1, &mVBO.buffer, offsets); + if (mVBO.size) { + commandBuffer.bindVertexBuffers(0, 1, &mVBO.buffer, offsets); + } cmdPushDescriptors(commandBuffer, mPipelineLayout, mUniformBuffersMat[0][imageIndex]); const float viewport[2] = {(float)mRenderWidth, (float)mRenderHeight}; commandBuffer.pushConstants(mPipelineLayout, vk::ShaderStageFlagBits::eVertex, 6 * sizeof(float), sizeof(viewport), viewport); @@ -1399,6 +1403,11 @@ void GPUDisplayBackendVulkan::ExitBackendA() } clearTextureSampler(); clearDevice(); + mCommandInfrastructureCreated = false; + mImageCount = 0; + mLastPresentId = 0; + mSwapChainSuboptimal = 0; + mFrameSkipped = false; } // ---------------------------- USER CODE ---------------------------- @@ -1419,10 +1428,14 @@ void GPUDisplayBackendVulkan::loadDataToGPU(size_t totalVertizes) { mDevice.waitIdle(); clearVertexBuffers(); - mVBO = createBuffer(totalVertizes * sizeof(mDisplay->vertexBuffer()[0][0]), mDisplay->vertexBuffer()[0].data(), vk::BufferUsageFlagBits::eVertexBuffer, 1); + if (totalVertizes) { // Zero-size buffers are invalid (empty event) + mVBO = createBuffer(totalVertizes * sizeof(mDisplay->vertexBuffer()[0][0]), mDisplay->vertexBuffer()[0].data(), vk::BufferUsageFlagBits::eVertexBuffer, 1); + } if (mDisplay->cfgR().useGLIndirectDraw) { fillIndirectCmdBuffer(); - mIndirectCommandBuffer = createBuffer(mCmdBuffer.size() * sizeof(mCmdBuffer[0]), mCmdBuffer.data(), vk::BufferUsageFlagBits::eIndirectBuffer, 1); + if (mCmdBuffer.size()) { + mIndirectCommandBuffer = createBuffer(mCmdBuffer.size() * sizeof(mCmdBuffer[0]), mCmdBuffer.data(), vk::BufferUsageFlagBits::eIndirectBuffer, 1); + } mCmdBuffer.clear(); } needRecordCommandBuffers(); @@ -1456,25 +1469,27 @@ uint32_t GPUDisplayBackendVulkan::drawVertices(const vboList& v, const drawType return count; } -void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) +bool GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) { if (mDisplay->updateDrawCommands() || toMixBuffer || includeMixImage > 0) { needRecordCommandBuffers(); } + if (includeMixImage > 0.f && mFrameSkipped) { + return false; // The render to the mix buffer of this frame did not get an image + } if (includeMixImage == 0.f) { mCurrentFrame = (mCurrentFrame + 1) % mFramesInFlight; waitTimeline(mFrameTimelineValue[mCurrentFrame]); if (mLastPresentId > 1 && fpsLimitMode() == FPS_LIMIT_VSYNC) { // Refresh rate limit (FIFO): at most one frame queued (wait for the second-last present), keeps the rate if a frame misses the deadline, timeout in case the window is hidden const vk::Result retVal = (vk::Result)VULKAN_HPP_DEFAULT_DISPATCHER.vkWaitForPresentKHR(static_cast(mDevice), static_cast(mSwapChain), mLastPresentId - 1, 100'000'000); - if (retVal == vk::Result::eErrorOutOfDateKHR || retVal == vk::Result::eSuboptimalKHR) { - mMustUpdateSwapChain = true; - } else if (retVal != vk::Result::eTimeout) { + handleSwapChainResult(retVal, 1); + if (retVal != vk::Result::eTimeout && retVal != vk::Result::eErrorOutOfDateKHR) { CHKERR(retVal); } } - auto getImage = [&]() { - return mDevice.acquireNextImageKHR(mSwapChain, UINT64_MAX, mImageAvailableSemaphore[mCurrentFrame], VkFence(VK_NULL_HANDLE), &mCurrentImageIndex); + auto getImage = [&]() { // Timeout so that a hidden window cannot block the thread (which may also dispatch the window system events) forever + return mDevice.acquireNextImageKHR(mSwapChain, 100'000'000, mImageAvailableSemaphore[mCurrentFrame], VkFence(VK_NULL_HANDLE), &mCurrentImageIndex); }; vk::Result retVal = vk::Result::eSuccess; @@ -1491,8 +1506,13 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& if (mustUpdateSwapChain) { recreateRendering(requestScreenshot, toMixBuffer); retVal = getImage(); + mMustUpdateSwapChain = false; + } + handleSwapChainResult(retVal, 2); + mFrameSkipped = retVal == vk::Result::eTimeout || retVal == vk::Result::eNotReady || retVal == vk::Result::eErrorOutOfDateKHR; + if (mFrameSkipped) { + return false; } - mMustUpdateSwapChain = retVal == vk::Result::eSuboptimalKHR; CHKERR(retVal); waitTimeline(mImageTimelineValue[mCurrentImageIndex]); // Previous frame using this image's resources (command buffers, uniform buffer, ...) finished if (mPresentFences.size()) { // Previous present of this image done, its present semaphore can be signaled again @@ -1519,6 +1539,20 @@ void GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& if (!mCommandBufferUpToDate[mCurrentImageIndex]) { startFillCommandBuffer(mCurrentCommandBuffer, mCurrentImageIndex, toMixBuffer); } + return true; +} + +void GPUDisplayBackendVulkan::handleSwapChainResult(vk::Result retVal, uint8_t suboptimalBit) +{ + if (retVal == vk::Result::eErrorOutOfDateKHR || (retVal == vk::Result::eSuboptimalKHR && !(mSwapChainSuboptimal & suboptimalBit))) { + mMustUpdateSwapChain = true; + if (retVal == vk::Result::eSuboptimalKHR) { + mSwapChainSuboptimal |= suboptimalBit; + } + } + if (retVal == vk::Result::eSuccess && !mMustUpdateSwapChain) { + mSwapChainSuboptimal &= ~suboptimalBit; + } } void GPUDisplayBackendVulkan::finishDraw(bool doScreenshot, bool toMixBuffer, float includeMixImage) @@ -1588,10 +1622,13 @@ void GPUDisplayBackendVulkan::finishFrame(bool doScreenshot, bool toMixBuffer, f presentFenceInfo = vk::SwapchainPresentFenceInfoKHR{1, &mPresentFences[mCurrentImageIndex], presentInfo.pNext}; presentInfo.pNext = &presentFenceInfo; } - vk::Result retVal = mGraphicsQueue.presentKHR(&presentInfo); - if (retVal == vk::Result::eErrorOutOfDateKHR || retVal == vk::Result::eSuboptimalKHR) { - mMustUpdateSwapChain = true; - } else { + vk::Result retVal; + { + std::lock_guard guard(mQueueMutex); + retVal = mGraphicsQueue.presentKHR(&presentInfo); + } + handleSwapChainResult(retVal, 4); + if (retVal != vk::Result::eErrorOutOfDateKHR) { CHKERR(retVal); } } @@ -1740,7 +1777,7 @@ void GPUDisplayBackendVulkan::pointSizeFactor(float factor) if (mCommandBufferUpToDate[mCurrentImageIndex]) { return; } - float size = mDisplay->cfgL().pointSize * mDownsampleFactor * factor + (mDisplay->cfgL().smoothPoints ? SMOOTH_POINT_EXTRA_SIZE : 0.f); + float size = std::min(mDisplay->cfgL().pointSize * mDownsampleFactor * factor + (mDisplay->cfgL().smoothPoints ? SMOOTH_POINT_EXTRA_SIZE : 0.f), mMaxPointSize); mCurrentCommandBuffer.pushConstants(mPipelineLayout, vk::ShaderStageFlagBits::eVertex, sizeof(std::array), sizeof(size), &size); } @@ -1761,11 +1798,12 @@ void GPUDisplayBackendVulkan::lineWidthFactor(float factor) } float width = mDisplay->cfgL().lineWidth * mDownsampleFactor * factor; if (useSmoothLineShader()) { - width += SMOOTH_LINE_EXTRA_WIDTH; + const float expand = mStrictLines ? 1.f : 1.4143f; // Non-strict wide lines may be parallelograms, widen so that the perpendicular width is covered + width = std::min(width + SMOOTH_LINE_EXTRA_WIDTH, mMaxLineWidth / expand); // The shader must see the width that is actually rasterized mCurrentCommandBuffer.pushConstants(mPipelineLayout, vk::ShaderStageFlagBits::eVertex, 5 * sizeof(float), sizeof(width), &width); - width *= mStrictLines ? 1.f : 1.4143f; // Non-strict wide lines may be parallelograms, widen so that the perpendicular width is covered + width *= expand; } - mCurrentCommandBuffer.setLineWidth(width); + mCurrentCommandBuffer.setLineWidth(std::min(width, mMaxLineWidth)); } void GPUDisplayBackendVulkan::needRecordCommandBuffers() diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h index 6557dfb0bb176..61c74756b3252 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h @@ -72,7 +72,7 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend int32_t InitBackendA() override; void ExitBackendA() override; void loadDataToGPU(size_t totalVertizes) override; - void prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override; + bool prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) override; void finishDraw(bool doScreenshot, bool toMixBuffer, float includeMixImage) override; void finishFrame(bool doScreenshot, bool toMixBuffer, float includeMixImage) override; void prepareText() override; @@ -93,7 +93,6 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend double checkDevice(vk::PhysicalDevice device, const std::vector& reqDeviceExtensions, const FeatureChain& reqDeviceFeatures, bool silent = false); vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities); vk::PresentModeKHR chooseSwapPresentMode(); - void transitionImageLayout(vk::CommandBuffer commandBuffer, vk::Image image, vk::Format format, vk::ImageLayout oldLayout, vk::ImageLayout newLayout); VulkanBuffer createBuffer(size_t size, const void* srcData = nullptr, vk::BufferUsageFlags type = vk::BufferUsageFlagBits::eVertexBuffer, int32_t deviceMemory = 1); void writeToBuffer(VulkanBuffer& buffer, size_t size, const void* srcData); void clearBuffer(VulkanBuffer& buffer); @@ -153,6 +152,9 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend vk::PresentModeKHR mPresentMode; vk::SwapchainKHR mSwapChain; bool mMustUpdateSwapChain = false; + uint8_t mSwapChainSuboptimal = 0; // Bitmasl if Last acquire / present / ... returned SUBOPTIMAL: recreate only on the first one + bool mFrameSkipped = false; // No image acquired in this frame (timeout) + void handleSwapChainResult(vk::Result retVal, uint8_t suboptimalBit); std::vector mSwapChainImages; std::vector mSwapChainImageViews; std::vector mRenderTargetView; @@ -214,6 +216,7 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend uint32_t mMaxMSAAsupported = 0; bool mZSupported = false; bool mStencilSupported = false; + float mMaxLineWidth = 1.f, mMaxPointSize = 1.f; bool mStrictLines = false; // Wide lines are rectangles, not parallelograms bool mSmoothLinesSupported = false; // Optional Vulkan 1.4 feature smoothLines bool mFifoLatestReadySupported = false; // Optional present mode FIFO_LATEST_READY (VK_KHR / VK_EXT_present_mode_fifo_latest_ready) diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h index 1b384b27e6c5a..bc38c1b29a57d 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontend.h @@ -56,6 +56,7 @@ class GPUDisplayFrontend : public GPUDisplayFrontendInterface virtual void SwitchFullscreen(bool set) = 0; // Toggle full-screen mode virtual void ToggleMaximized(bool set) = 0; // Maximize window virtual void SetVSync(bool enable) = 0; // Enable / disable vsync + virtual bool canSetVSync() { return false; } // OpenGL: swap interval can be set bool EnableSendKey() override; // Request external keys (e.g. from terminal) virtual void OpenGLPrint(const char* s, float x, float y, float r, float g, float b, float a, bool fromBotton = true) = 0; // Print text on the display (needs the backend to build the font) GPUDisplayBackend* backend(); diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx index 961058e974804..49566fa690340 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.cxx @@ -307,14 +307,7 @@ int32_t GPUDisplayFrontendGlfw::FrontendMain() glfwGetWindowSize(mWindow, &windowWidth, &windowHeight); mPixelRatio = windowWidth ? (double)fbWidth / windowWidth : 1.; // Updated in resize_callback, which must not be called before InitDisplay applyInitialWindowState(); - if (backend()->backendType() == GPUDisplayBackend::TYPE_OPENGL) { - glfwSwapInterval(1); - } - - { - std::lock_guard lock(mExitStateMutex); - mDisplayRunning = true; - } + SetVSync(vsyncEnabled()); if (mBackend->ExtInit()) { fprintf(stderr, "Error initializing GL extension wrapper\n"); @@ -326,11 +319,18 @@ int32_t GPUDisplayFrontendGlfw::FrontendMain() return (1); } + { + std::lock_guard lock(mExitStateMutex); + mDisplayRunning = true; // Only after successful initialization, StopDisplay waits for it to become false again + } + + int32_t retVal = 0; while (!glfwWindowShouldClose(mWindow)) { HandleSendKeys(); if (DrawGLScene()) { fprintf(stderr, "Error drawing GL scene\n"); - return (1); + retVal = 1; + break; // Cleanup below } if (backend()->backendType() == GPUDisplayBackend::TYPE_OPENGL) { glfwSwapBuffers(mWindow); @@ -354,7 +354,7 @@ int32_t GPUDisplayFrontendGlfw::FrontendMain() mDisplayRunning = false; } - return 0; + return retVal; } void GPUDisplayFrontendGlfw::StopDisplayA() @@ -389,7 +389,14 @@ void GPUDisplayFrontendGlfw::ToggleMaximized(bool set) } } -void GPUDisplayFrontendGlfw::SetVSync(bool enable) { glfwSwapInterval(enable); } +void GPUDisplayFrontendGlfw::SetVSync(bool enable) +{ + if (canSetVSync()) { // No GL context with Vulkan + glfwSwapInterval(enable); + } +} + +bool GPUDisplayFrontendGlfw::canSetVSync() { return backend()->backendType() == GPUDisplayBackend::TYPE_OPENGL; } bool GPUDisplayFrontendGlfw::EnableSendKey() { diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h index a5820bb001131..274b08ca33d99 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlfw.h @@ -36,6 +36,7 @@ class GPUDisplayFrontendGlfw : public GPUDisplayFrontend void getSize(int32_t& width, int32_t& height) override; float getDisplayScale() const override { return mScale; } float getRefreshRate() override; + bool canSetVSync() override; int32_t getVulkanSurface(void* instance, void* surface) override; uint32_t getReqVulkanExtensions(const char**& p) override; diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx index ad4d7da014d2d..445f1e40bb22b 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.cxx @@ -316,9 +316,17 @@ void GPUDisplayFrontendGlut::ToggleMaximized(bool set) {} void GPUDisplayFrontendGlut::SetVSync(bool enable) // GLUT has no swap interval API, set it via GLX directly (no-op if freeglut does not use GLX) { #ifdef GPUCA_BUILD_EVENT_DISPLAY_X11 - auto swapInterval = (PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddressARB((const GLubyte*)"glXSwapIntervalEXT"); - if (swapInterval && glXGetCurrentDisplay()) { - swapInterval(glXGetCurrentDisplay(), glXGetCurrentDrawable(), enable); + if (canSetVSync()) { + ((PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddressARB((const GLubyte*)"glXSwapIntervalEXT"))(glXGetCurrentDisplay(), glXGetCurrentDrawable(), enable); } #endif } + +bool GPUDisplayFrontendGlut::canSetVSync() +{ +#ifdef GPUCA_BUILD_EVENT_DISPLAY_X11 + return glXGetProcAddressARB((const GLubyte*)"glXSwapIntervalEXT") && glXGetCurrentDisplay(); +#else + return false; +#endif +} diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.h index d595fedcb16cf..9fe8e7b44d018 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendGlut.h @@ -29,6 +29,7 @@ class GPUDisplayFrontendGlut : public GPUDisplayFrontend void SwitchFullscreen(bool set) override; void ToggleMaximized(bool set) override; void SetVSync(bool enable) override; + bool canSetVSync() override; void OpenGLPrint(const char* s, float x, float y, float r, float g, float b, float a, bool fromBotton = true) override; private: diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx index b2d1f054bcb06..264a8e9dabece 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendWayland.cxx @@ -12,13 +12,11 @@ /// \file GPUDisplayFrontendWayland.cxx /// \author David Rohr -// Now the other headers #include "GPUDisplayFrontendWayland.h" #include "backend/GPUDisplayBackend.h" -#include "GPUDisplayGUIWrapper.h" +#include "GPUDisplayGUIWrapper.h" // Destructor of the GUI wrapper unique_ptr member #include "GPUDisplay.h" #include "GPULogging.h" -#include "GPUParam.h" #include #include #include @@ -32,7 +30,6 @@ #include #include -#include #include #include #include @@ -229,15 +226,26 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() #pragma GCC diagnostic pop auto keyboard_keymap = [](void* data, wl_keyboard* wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { GPUDisplayFrontendWayland* me = (GPUDisplayFrontendWayland*)data; + char* keymap_string = format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 ? (char*)mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0) : (char*)MAP_FAILED; // MAP_PRIVATE required since wl_seat version 7 + close(fd); + if (keymap_string == MAP_FAILED) { + GPUError("Cannot read Wayland keymap"); + return; + } + xkb_keymap* keymap = xkb_keymap_new_from_string(me->mXKBcontext, keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(keymap_string, size); + xkb_state* state = keymap ? xkb_state_new(keymap) : nullptr; + if (!state) { // Keep the previous keymap + GPUError("Cannot compile Wayland keymap"); + xkb_keymap_unref(keymap); + return; + } if (me->mXKBkeymap) { xkb_state_unref(me->mXKBstate); xkb_keymap_unref(me->mXKBkeymap); } - char* keymap_string = (char*)mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0); - me->mXKBkeymap = xkb_keymap_new_from_string(me->mXKBcontext, keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); - me->mXKBstate = xkb_state_new(me->mXKBkeymap); - munmap(keymap_string, size); - close(fd); + me->mXKBkeymap = keymap; + me->mXKBstate = state; }; auto keyboard_enter = [](void* data, wl_keyboard* wl_keyboard, uint32_t serial, wl_surface* surface, wl_array* keys) {}; auto keyboard_leave = [](void* data, wl_keyboard* wl_keyboard, uint32_t serial, wl_surface* surface) { @@ -270,7 +278,7 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() GPUDisplayFrontendWayland* me = (GPUDisplayFrontendWayland*)data; xkb_state_update_mask(me->mXKBstate, mods_depressed, mods_latched, mods_locked, 0, 0, group); }; - auto keyboard_repat = [](void* data, wl_keyboard* wl_keyboard, int32_t rate, int32_t delay) { + auto keyboard_repeat = [](void* data, wl_keyboard* wl_keyboard, int32_t rate, int32_t delay) { GPUDisplayFrontendWayland* me = (GPUDisplayFrontendWayland*)data; me->mRepeatRate = rate; me->mRepeatDelay = delay; @@ -278,7 +286,7 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() me->mRepeatKeyCode = -1; } }; - mKeyboardListener = wl_keyboard_listener{.keymap = keyboard_keymap, .enter = keyboard_enter, .leave = keyboard_leave, .key = keyboard_key, .modifiers = keyboard_modifiers, .repeat_info = keyboard_repat}; + mKeyboardListener = wl_keyboard_listener{.keymap = keyboard_keymap, .enter = keyboard_enter, .leave = keyboard_leave, .key = keyboard_key, .modifiers = keyboard_modifiers, .repeat_info = keyboard_repeat}; auto xdg_wm_base_ping = [](void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial) { xdg_wm_base_pong(xdg_wm_base, serial); @@ -545,20 +553,28 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() xdg_toplevel_set_title(mXdgToplevel, DISPLAY_WINDOW_NAME); xdg_toplevel_set_min_size(mXdgToplevel, MIN_WIDTH, MIN_HEIGHT); // Logical size, avoids degenerate swapchains / projections xdg_toplevel_set_app_id(mXdgToplevel, "o2-gpu-event-display"); // Used by the compositor for window grouping, window rules, and matching a .desktop file - applyInitialWindowState(); // Must be requested before the first commit if (mDecManager) { // Must be requested before the first commit. Without the protocol (e.g. GNOME), the window has no decorations mXdgDecoration = zxdg_decoration_manager_v1_get_toplevel_decoration(mDecManager, mXdgToplevel); zxdg_toplevel_decoration_v1_set_mode(mXdgDecoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); - } else if (mDisplay->GetProcessingSettings().debugLevel >= 0) { + } else if (mDisplay->GetProcessingSettings().debugLevel >= 2) { GPUInfo("Wayland compositor does not support server-side decorations"); } - wl_surface_commit(mSurface); // Initial commit without buffer, the compositor answers with the first configure + const bool initialFullScreen = mDisplay->cfgR().fullScreen, initialMaximized = mDisplay->cfgR().maximized; // Read before the configure, which syncs the settings to the (windowed) state + wl_surface_commit(mSurface); // Initial commit without buffer, the compositor answers with the first configure if (wl_display_roundtrip(mWayland) == -1) { reportError("initial roundtrip"); throw std::runtime_error("Wayland initialization failed"); } + if (initialFullScreen || initialMaximized) { // Only after the windowed initial configure, so that the compositor has a normal (un-maximized) placement, then wait for the new configure before creating the swapchain + setWindowState(initialFullScreen, initialMaximized); + applyInitialWindowState(); + if (wl_display_roundtrip(mWayland) == -1) { + reportError("initial window state roundtrip"); + throw std::runtime_error("Wayland initialization failed"); + } + } if (InitDisplay()) { return (1); @@ -580,7 +596,7 @@ int32_t GPUDisplayFrontendWayland::FrontendMain() } wl_display_flush(mWayland); pollfd pfd = {wl_display_get_fd(mWayland), POLLIN, 0}; - if (poll(&pfd, 1, 0) > 0) { + if (poll(&pfd, 1, mSuspended ? 100 : 0) > 0) { // Block while suspended (no rendering), otherwise the loop spins at full CPU if (wl_display_read_events(mWayland) == -1) { reportError("reading events"); break; @@ -781,9 +797,13 @@ void GPUDisplayFrontendWayland::handleKeyRepeat() return; } const auto now = std::chrono::steady_clock::now(); + const auto period = std::chrono::microseconds(1000000 / mRepeatRate); + if (now >= mRepeatNext + period) { // More than one repeat behind (long frame), no burst of repeats + mRepeatNext = now; + } while (now >= mRepeatNext && mRepeatKeyCode >= 0) { HandleKey(mRepeatSymbol); - mRepeatNext += std::chrono::microseconds(1000000 / mRepeatRate); + mRepeatNext += period; } } diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx index 20449c8f907d5..42de509f47ebf 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.cxx @@ -224,7 +224,9 @@ int32_t GPUDisplayFrontendX11::FrontendMain() const char* glxExt = glXQueryExtensionsString(mXDisplay, DefaultScreen(mXDisplay)); if (strstr(glxExt, "GLX_EXT_swap_control") == nullptr) { - GPUError("No vsync support!"); + if (backend()->backendType() == GPUDisplayBackend::TYPE_OPENGL) { + GPUError("GLX_EXT_swap_control not available, cannot change VSync"); + } vsync_supported = false; } else { vsync_supported = true; @@ -339,7 +341,7 @@ int32_t GPUDisplayFrontendX11::FrontendMain() GPUError("Cannot enable vsync"); return (-1); } - mGlXSwapIntervalEXT(mXDisplay, glXGetCurrentDrawable(), 1); + mGlXSwapIntervalEXT(mXDisplay, glXGetCurrentDrawable(), vsyncEnabled()); } if (InitDisplay()) { diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.h b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.h index bca7def502aaa..41032debeb9f5 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.h +++ b/GPU/GPUTracking/display/frontend/GPUDisplayFrontendX11.h @@ -52,6 +52,7 @@ class GPUDisplayFrontendX11 : public GPUDisplayFrontend PFNGLXSWAPINTERVALEXTPROC mGlXSwapIntervalEXT = nullptr; bool vsync_supported = false; + bool canSetVSync() override { return vsync_supported && mGlXSwapIntervalEXT; } }; } // namespace o2::gpu diff --git a/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx b/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx index b94ea0b2ccf06..97d2cd17e3faa 100644 --- a/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx +++ b/GPU/GPUTracking/display/frontend/GPUDisplayKeys.cxx @@ -24,7 +24,7 @@ const char* HelpText[] = { "[l] / [k] / [J] Draw single sector (next / previous sector), draw related sectors (same plane in phi)", "[;] / [:] Show splitting of TPC in sectors by extruding volume, [:] resets", "[#] Invert colors", - "[y] / [Y] / [X] / [M] Start Animation, Add / remove Animation point, Reset Points, Cycle animation camera mode (resets)", + "[y] / [Y] / ['] / [X] / [M] Start Animation, Add / remove Animation point, Reset Points, Cycle animation camera mode (resets)", "[>] / [<] Toggle config interpolation during Animation / change Animation interval (via movement)", "[g] Draw Grid", "[i] Project onto XY-plane", @@ -54,7 +54,7 @@ const char* HelpText[] = { "[e] / [f] Rotate left / right", "[+] / [-] Increase / decrease point size (Hold SHIFT for lines)", "[b] Change FOV (field of view)", - "['] Switch between core / compat renderer path (OpenGL profile, Vulkan smooth lines)", + "[m] Switch between core / compat renderer path (OpenGL profile, Vulkan smooth lines)", "[MOUSE 1] Look around", "[MOUSE 2] Strafe camera", "[MOUSE 1+2] Zoom / Rotate", @@ -65,7 +65,7 @@ const char* HelpText[] = { "[F1] / [F2] / [F3] / [F4] Enable / disable drawing of TPC / TRD / TOF / ITS", "[SHIFT] + [F1] to [F4] Enable / disable track detector filter", "[SHIFT] + [F12] Switch track detector filter between AND and OR mode" - // FREE: [m] [SPACE] [Q] + // FREE: [SPACE] [Q] // Test setting: ^ }; @@ -205,7 +205,7 @@ void GPUDisplay::HandleKey(uint8_t key) mCfgR.fov = 5; } SetInfo("Set FOV to %f", mCfgR.fov); - } else if (key == 39) { // character = "'" + } else if (key == 'm') { #ifdef GPUCA_DISPLAY_OPENGL_CORE if (backend()->backendType() == GPUDisplayBackend::TYPE_OPENGL) { SetInfo("OpenGL compat profile not available, using core profile", 1); diff --git a/dependencies/FindO2GPUWayland.cmake b/dependencies/FindO2GPUWayland.cmake index 7ada679c97ce1..fd5c36b6d98ea 100644 --- a/dependencies/FindO2GPUWayland.cmake +++ b/dependencies/FindO2GPUWayland.cmake @@ -47,8 +47,7 @@ if(${PKGNAME}_LIBRARY_SHARED AND ${PKGNAME}_XKBCOMMON_LIBRARY_SHARED AND ${PKGNA add_library(WAYLAND-all_libraries INTERFACE) target_link_libraries(WAYLAND-all_libraries INTERFACE ${${PKGNAME}_LIBRARY_SHARED} ${${PKGNAME}_XKBCOMMON_LIBRARY_SHARED}) set_target_properties(WAYLAND-all_libraries PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${${PKGNAME}_INCLUDE_DIR} - INTERFACE_INCLUDE_DIRECTORIES ${${PKGNAME}_XKBCOMMON_INCLUDE_DIR}) + INTERFACE_INCLUDE_DIRECTORIES "${${PKGNAME}_INCLUDE_DIR};${${PKGNAME}_XKBCOMMON_INCLUDE_DIR}") add_library(GPUWayland::wayland-client ALIAS WAYLAND-all_libraries) endif() From 46260777c4e0d4eb9510f33949ea362fa429200c Mon Sep 17 00:00:00 2001 From: David Rohr Date: Thu, 15 Oct 2026 01:46:26 +0200 Subject: [PATCH 6/6] GPU Vulkan Display: don't recreate command buffer for mixing images --- GPU/GPUTracking/display/GPUDisplay.h | 1 + .../display/backend/GPUDisplayBackendVulkan.cxx | 6 +++--- GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h | 6 +++--- GPU/GPUTracking/display/helpers/GPUDisplayAnimation.cxx | 8 +++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/GPU/GPUTracking/display/GPUDisplay.h b/GPU/GPUTracking/display/GPUDisplay.h index 541d722e8eaaf..cb8bbce8bfc4f 100644 --- a/GPU/GPUTracking/display/GPUDisplay.h +++ b/GPU/GPUTracking/display/GPUDisplay.h @@ -297,6 +297,7 @@ class GPUDisplay : public GPUDisplayInterface HighResTimer mAnimationTimer; int32_t mAnimationFrame = 0; int32_t mAnimationLastBase = 0; + int32_t mAnimationMixBase = -1; // Target keyframe of the current config blend (mixing), -1 = none int32_t mAnimateScreenshot = 0; int32_t mAnimationExport = 0; bool mAnimationChangeConfig = true; diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx index 46ab5c3c48591..b0d63bfda17cf 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.cxx @@ -623,7 +623,7 @@ void GPUDisplayBackendVulkan::createCommandBuffers() allocInfo.commandPool = mCommandPool; allocInfo.level = vk::CommandBufferLevel::ePrimary; allocInfo.commandBufferCount = mImageCount; - mCommandBufferUpToDate.resize(mImageCount, false); + mCommandBufferUpToDate.assign(mImageCount, false); mCommandBuffers = mDevice.allocateCommandBuffers(allocInfo); mCommandBuffersText = mDevice.allocateCommandBuffers(allocInfo); mCommandBuffersTexture = mDevice.allocateCommandBuffers(allocInfo); @@ -1471,7 +1471,7 @@ uint32_t GPUDisplayBackendVulkan::drawVertices(const vboList& v, const drawType bool GPUDisplayBackendVulkan::prepareDraw(const hmm_mat4& proj, const hmm_mat4& view, bool requestScreenshot, bool toMixBuffer, float includeMixImage) { - if (mDisplay->updateDrawCommands() || toMixBuffer || includeMixImage > 0) { + if (mDisplay->updateDrawCommands()) { // Main and mix command buffers stay cached while mixing, only the blend pass with the alpha value is recorded per frame needRecordCommandBuffers(); } @@ -1559,7 +1559,7 @@ void GPUDisplayBackendVulkan::finishDraw(bool doScreenshot, bool toMixBuffer, fl { if (!mCommandBufferUpToDate[mCurrentImageIndex]) { endFillCommandBuffer(mCurrentCommandBuffer); - if (!toMixBuffer && includeMixImage == 0.f) { + if (!toMixBuffer) { // The mix pass is recorded before the main pass of the same frame, both are valid only after the latter mCommandBufferUpToDate[mCurrentImageIndex] = true; } } diff --git a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h index 61c74756b3252..4a621edc096d0 100644 --- a/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h +++ b/GPU/GPUTracking/display/backend/GPUDisplayBackendVulkan.h @@ -152,8 +152,8 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend vk::PresentModeKHR mPresentMode; vk::SwapchainKHR mSwapChain; bool mMustUpdateSwapChain = false; - uint8_t mSwapChainSuboptimal = 0; // Bitmasl if Last acquire / present / ... returned SUBOPTIMAL: recreate only on the first one - bool mFrameSkipped = false; // No image acquired in this frame (timeout) + uint8_t mSwapChainSuboptimal = 0; // Bitmask if last acquire / present / ... returned SUBOPTIMAL: recreate only on the first one + bool mFrameSkipped = false; // No image acquired in this frame (timeout) void handleSwapChainResult(vk::Result retVal, uint8_t suboptimalBit); std::vector mSwapChainImages; std::vector mSwapChainImageViews; @@ -185,7 +185,7 @@ class GPUDisplayBackendVulkan : public GPUDisplayBackend std::vector mCommandBuffersMix; std::vector mCommandBuffersPresent; std::vector mFrameCommandBuffers; // All command buffers of the current frame, submitted as one batch, ordered by barriers - std::vector mCommandBufferUpToDate; + std::vector mCommandBufferUpToDate; // Per image: main and mix command buffers recorded and still valid (cached across frames), set after the main pass of the frame std::vector mImageAvailableSemaphore; std::vector mPresentSemaphore; vk::Semaphore mTimelineSemaphore; // Signaled with an increasing value by every queue submit, replaces fences diff --git a/GPU/GPUTracking/display/helpers/GPUDisplayAnimation.cxx b/GPU/GPUTracking/display/helpers/GPUDisplayAnimation.cxx index 1d35e2c3f714e..bb2bacfc08434 100644 --- a/GPU/GPUTracking/display/helpers/GPUDisplayAnimation.cxx +++ b/GPU/GPUTracking/display/helpers/GPUDisplayAnimation.cxx @@ -102,6 +102,7 @@ void GPUDisplay::startAnimation() mAnimationFrame = 0; mAnimate = 1; mAnimationLastBase = 0; + mAnimationMixBase = -1; } int32_t GPUDisplay::animateCamera(float& animateTime, float& mixSlaveImage, hmm_mat4& nextViewMatrix) @@ -141,7 +142,12 @@ int32_t GPUDisplay::animateCamera(float& animateTime, float& mixSlaveImage, hmm_ mAnimationLastBase = base - 1; } - if (base != mAnimationLastBase && mAnimateVectors[0][mAnimationLastBase] != mAnimateVectors[0][base] && memcmp(&mAnimateConfig[base], &mAnimateConfig[mAnimationLastBase], sizeof(mAnimateConfig[base]))) { + const bool mixing = base != mAnimationLastBase && mAnimateVectors[0][mAnimationLastBase] != mAnimateVectors[0][base] && memcmp(&mAnimateConfig[base], &mAnimateConfig[mAnimationLastBase], sizeof(mAnimateConfig[base])); + if ((mixing ? base : -1) != mAnimationMixBase) { // Blended configs changed: cached (Vulkan) command buffers must be re-recorded, otherwise they are kept while mixing + mAnimationMixBase = mixing ? base : -1; + mUpdateDrawCommands = true; + } + if (mixing) { mixSlaveImage = 1.f - (time - mAnimateVectors[0][mAnimationLastBase]) / (mAnimateVectors[0][base] - mAnimateVectors[0][mAnimationLastBase]); if (mixSlaveImage > 0) { mCfgL = mAnimateConfig[mAnimationLastBase];