mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Whether a backend can attach a single layer of a texture to a framebuffer was one Bool, so it could only give the most conservative answer any target needed. DirectVulkan therefore declined every layer of every target and direct_state_access.framebuffers_texture_layer_attachment failed with 542 messages across four targets. The three ways a GL layer maps onto Vulkan are independent capabilities, so the flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a VkImage array layer and needed nothing but the gate opened. A cube map array is one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a shape VkTextureManager simply did not have - it is declined softly when the depth is not a whole number of cubes or the level is not square, because that function's Bool return exists for unrepresentable shapes and asserting there would abort on ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is a z slice, which needs a 2D-array-compatible image and a per-slice clear, because vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D view over that one slice. VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability is per format+usage, so a single global probe answers a different question than the one the frontend goes on to ask. Losing it costs per-slice attachment for that format; failing creation would lose the texture. Three things found on the way that are not the headline: glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes the two entry points equivalent, so they now decline in the same places - leaving one ungated is what let an unrepresentable attachment reach the renderer. ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for six mip levels on an image whose legal maximum is three (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound it. lavapipe had been letting that through. A layered GL clear queues layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to 0/1, read as the whole mip level) and the old code passed it straight through. Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining case being the fp64 one it declines by design. Known and deliberately not fixed here, with a FIXME at the site: KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out by bisection so the next reader does not repeat them.
884 lines
50 KiB
C++
884 lines
50 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#include "BackendObject_DirectVulkan.h"
|
|
#include "MG_Backend/BackendObject.h"
|
|
#include "DirectVulkan.h"
|
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
|
#include "MG_State/GLState/TextureState/TextureState.h"
|
|
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
|
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
|
|
|
#include <Config.h>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
namespace {
|
|
Bool IsR11G11B10FFallbackEnabled() {
|
|
return MG_Config::Features.MagmaR11G11B10FFallback;
|
|
}
|
|
|
|
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
|
(void)dpy;
|
|
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
|
|
}
|
|
|
|
Bool IsFormatIndexValid(TextureInternalFormat format) {
|
|
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
|
|
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
|
|
}
|
|
|
|
Bool IsLayeredTarget(TextureTarget target) {
|
|
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
|
|
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
|
|
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
|
|
}
|
|
|
|
Bool IsMultisampleTarget(TextureTarget target) {
|
|
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
|
|
}
|
|
|
|
Bool IsTextureBufferTarget(TextureTarget target) {
|
|
return target == TextureTarget::TextureBuffer;
|
|
}
|
|
|
|
Bool IsIntegerInternalFormat(TextureInternalFormat format) {
|
|
const GLenum glFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
|
|
GLenum normalizedInternalFormat = glFormat;
|
|
GLenum imageFormat = GL_RGBA;
|
|
GLenum imageType = GL_UNSIGNED_BYTE;
|
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
|
|
&normalizedInternalFormat, &imageFormat, &imageType);
|
|
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
|
|
imageFormat == GL_RGBA_INTEGER;
|
|
}
|
|
|
|
FormatCapabilityFlags GetAttachmentCaps(TextureInternalFormat format) {
|
|
FormatCapabilityFlags caps = FormatCapability::FramebufferRenderable;
|
|
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(format);
|
|
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(format);
|
|
if (!isDepth && !isStencil) {
|
|
caps |= FormatCapability::ColorAttachment;
|
|
}
|
|
if (isDepth) {
|
|
caps |= FormatCapability::DepthAttachment;
|
|
}
|
|
if (isStencil) {
|
|
caps |= FormatCapability::StencilAttachment;
|
|
}
|
|
return caps;
|
|
}
|
|
|
|
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
|
|
VkFormatFeatureFlags features) {
|
|
FormatCapabilityFlags caps;
|
|
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
|
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
|
|
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
|
|
|
|
if (IsTextureBufferTarget(target)) {
|
|
if ((features & VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT) != 0) {
|
|
caps |= FormatCapability::Creatable;
|
|
caps |= FormatCapability::Sampled;
|
|
caps |= FormatCapability::TextureBuffer;
|
|
}
|
|
return caps;
|
|
}
|
|
|
|
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
|
|
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
|
|
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
|
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
|
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
|
|
|
|
if (sampled || renderable) {
|
|
caps |= FormatCapability::Creatable;
|
|
}
|
|
if (sampled) {
|
|
caps |= FormatCapability::Sampled;
|
|
if (linearFilter && !isInteger && !isStencil) {
|
|
caps |= FormatCapability::LinearFilter;
|
|
}
|
|
if (!isStencil && (features & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0 &&
|
|
(features & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0) {
|
|
caps |= FormatCapability::GenerateMipmap;
|
|
}
|
|
if (!isInteger && !isDepth && !isStencil) {
|
|
caps |= FormatCapability::TextureGather;
|
|
}
|
|
if (isDepth && !isStencil) {
|
|
caps |= FormatCapability::TextureShadow;
|
|
}
|
|
}
|
|
if (renderable) {
|
|
caps |= GetAttachmentCaps(logicalFormat);
|
|
if (IsLayeredTarget(target)) {
|
|
caps |= FormatCapability::FramebufferLayered;
|
|
}
|
|
}
|
|
if (IsMultisampleTarget(target)) {
|
|
caps |= FormatCapability::MultisampleTexture;
|
|
}
|
|
return caps;
|
|
}
|
|
|
|
Optional<TextureInternalFormat> ResolveVulkanFallbackLogicalFormat(TextureInternalFormat format) {
|
|
switch (format) {
|
|
case TextureInternalFormat::RGB:
|
|
case TextureInternalFormat::RGB8:
|
|
return TextureInternalFormat::RGBA8;
|
|
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
|
|
// encoding; a wider normalized fallback keeps at least the required precision.
|
|
case TextureInternalFormat::R3G3B2:
|
|
case TextureInternalFormat::RGB4:
|
|
case TextureInternalFormat::RGB5:
|
|
case TextureInternalFormat::RGBA2:
|
|
case TextureInternalFormat::RGBA4:
|
|
case TextureInternalFormat::RGB5A1:
|
|
return TextureInternalFormat::RGBA8;
|
|
case TextureInternalFormat::RGB10:
|
|
return TextureInternalFormat::RGB10A2;
|
|
case TextureInternalFormat::RGB12:
|
|
case TextureInternalFormat::RGBA12:
|
|
return TextureInternalFormat::RGBA16;
|
|
case TextureInternalFormat::SRGB8:
|
|
return TextureInternalFormat::SRGB8Alpha8;
|
|
case TextureInternalFormat::RGB8Snorm:
|
|
return TextureInternalFormat::RGBA8Snorm;
|
|
case TextureInternalFormat::RGB16:
|
|
return TextureInternalFormat::RGBA16;
|
|
case TextureInternalFormat::RGB16Snorm:
|
|
return TextureInternalFormat::RGBA16Snorm;
|
|
case TextureInternalFormat::RGB16F:
|
|
return TextureInternalFormat::RGBA16F;
|
|
case TextureInternalFormat::R11FG11FB10F:
|
|
if (IsR11G11B10FFallbackEnabled()) {
|
|
return TextureInternalFormat::RGBA16F;
|
|
}
|
|
return Nullopt;
|
|
case TextureInternalFormat::RGB32F:
|
|
return TextureInternalFormat::RGBA32F;
|
|
case TextureInternalFormat::RGB8I:
|
|
return TextureInternalFormat::RGBA8I;
|
|
case TextureInternalFormat::RGB8UI:
|
|
return TextureInternalFormat::RGBA8UI;
|
|
case TextureInternalFormat::RGB16I:
|
|
return TextureInternalFormat::RGBA16I;
|
|
case TextureInternalFormat::RGB16UI:
|
|
return TextureInternalFormat::RGBA16UI;
|
|
case TextureInternalFormat::RGB32I:
|
|
return TextureInternalFormat::RGBA32I;
|
|
case TextureInternalFormat::RGB32UI:
|
|
return TextureInternalFormat::RGBA32UI;
|
|
default:
|
|
return Nullopt;
|
|
}
|
|
}
|
|
|
|
Optional<VkFormat> ResolveVulkanFallbackFormat(TextureInternalFormat format) {
|
|
const Optional<TextureInternalFormat> fallbackLogicalFormat = ResolveVulkanFallbackLogicalFormat(format);
|
|
if (!fallbackLogicalFormat) {
|
|
return Nullopt;
|
|
}
|
|
return MG_Util::ConvertTextureInternalFormatToVkEnum(*fallbackLogicalFormat);
|
|
}
|
|
|
|
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
|
|
for (FormatCapability capability : kReportedFormatCapabilities) {
|
|
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
|
|
TextureInternalFormat fallbackFormat) {
|
|
MGLOG_D(
|
|
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
|
|
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
|
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
|
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
|
|
}
|
|
|
|
Vector<Int> BuildSampleCounts(Int maxSamples) {
|
|
Vector<Int> counts;
|
|
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
|
|
counts.push_back(samples);
|
|
}
|
|
counts.push_back(1);
|
|
return counts;
|
|
}
|
|
|
|
void PopulateFormatCapabilitiesImpl(VkPhysicalDevice physicalDevice,
|
|
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
|
|
const MG_External::VulkanCapabilities& capabilities,
|
|
FormatCapabilityCache& cache) {
|
|
cache.Clear();
|
|
if (physicalDevice == VK_NULL_HANDLE || getFormatProperties == nullptr) {
|
|
return;
|
|
}
|
|
|
|
for (SizeT formatIndex = 0; formatIndex < kFormatCapabilityFormatCount; ++formatIndex) {
|
|
const auto logicalFormat = static_cast<TextureInternalFormat>(formatIndex);
|
|
if (!IsFormatIndexValid(logicalFormat)) {
|
|
continue;
|
|
}
|
|
|
|
VkFormat nativeFormat = MG_Util::ConvertTextureInternalFormatToVkEnum(logicalFormat);
|
|
const Optional<TextureInternalFormat> fallbackLogicalFormat =
|
|
ResolveVulkanFallbackLogicalFormat(logicalFormat);
|
|
VkFormat fallbackFormat = ResolveVulkanFallbackFormat(logicalFormat).value_or(VK_FORMAT_UNDEFINED);
|
|
|
|
VkFormatProperties nativeProperties{};
|
|
if (nativeFormat != VK_FORMAT_UNDEFINED) {
|
|
getFormatProperties(physicalDevice, nativeFormat, &nativeProperties);
|
|
}
|
|
|
|
VkFormatProperties fallbackProperties{};
|
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
|
getFormatProperties(physicalDevice, fallbackFormat, &fallbackProperties);
|
|
}
|
|
|
|
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
|
const auto target = static_cast<TextureTarget>(targetIndex);
|
|
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
|
|
? nativeProperties.bufferFeatures
|
|
: nativeProperties.optimalTilingFeatures;
|
|
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
|
|
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
|
|
|
|
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
|
|
? fallbackProperties.bufferFeatures
|
|
: fallbackProperties.optimalTilingFeatures;
|
|
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
|
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
|
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
|
|
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(nativeCaps, fallbackCaps)) {
|
|
LogVulkanFormatCaveat(logicalFormat, targetIndex, *fallbackLogicalFormat);
|
|
}
|
|
}
|
|
|
|
if (HasFormatCapability(nativeCaps | fallbackCaps, FormatCapability::MultisampleTexture)) {
|
|
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
|
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
|
|
const Bool isInteger = IsIntegerInternalFormat(logicalFormat);
|
|
Int maxSamples = capabilities.MaxColorTextureSamples;
|
|
if (isDepth || isStencil) {
|
|
maxSamples = capabilities.MaxDepthTextureSamples;
|
|
} else if (isInteger) {
|
|
maxSamples = capabilities.MaxIntegerSamples;
|
|
}
|
|
cache.SampleCounts[targetIndex][formatIndex] = BuildSampleCounts(maxSamples);
|
|
}
|
|
}
|
|
|
|
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
|
FormatCapabilityFlags renderbufferCaps =
|
|
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D, nativeProperties.optimalTilingFeatures);
|
|
renderbufferCaps &= FormatCapability::Creatable;
|
|
if ((nativeProperties.optimalTilingFeatures &
|
|
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) != 0) {
|
|
renderbufferCaps |= GetAttachmentCaps(logicalFormat);
|
|
renderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
|
|
}
|
|
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
|
|
|
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
|
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
|
|
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
|
|
fallbackRenderbufferCaps &= FormatCapability::Creatable;
|
|
if ((fallbackProperties.optimalTilingFeatures &
|
|
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
|
|
0) {
|
|
fallbackRenderbufferCaps |= GetAttachmentCaps(logicalFormat);
|
|
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
|
|
}
|
|
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
|
|
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
|
|
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
|
|
}
|
|
}
|
|
|
|
const FormatCapabilityFlags rbCaps = cache.FullCaps[renderbufferTargetIndex][formatIndex] |
|
|
cache.CaveatCaps[renderbufferTargetIndex][formatIndex];
|
|
if (HasFormatCapability(rbCaps, FormatCapability::MultisampleRenderbuffer)) {
|
|
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
|
BuildSampleCounts(capabilities.MaxFramebufferSamples);
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
|
|
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
|
|
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
|
|
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
|
}
|
|
|
|
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
|
|
|
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
|
|
|
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
|
if (!m_windowHandle.Handle) {
|
|
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
|
|
return false;
|
|
}
|
|
|
|
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
|
|
|
// Any renderer instance this assignment replaces is destroyed here;
|
|
// fence/timer-query handles stamped with the old generation go stale.
|
|
BumpRendererGeneration();
|
|
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
|
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
|
pVulkanRenderer->Initialize();
|
|
return true;
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::InitPbufferSurface(EGLint width, EGLint height) {
|
|
VulkanRendererConfig config;
|
|
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
|
|
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
|
|
// Any renderer instance this assignment replaces is destroyed here;
|
|
// fence/timer-query handles stamped with the old generation go stale.
|
|
BumpRendererGeneration();
|
|
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
|
|
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
|
|
pVulkanRenderer->Initialize();
|
|
return true;
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::Initialize() {
|
|
m_initialized = true;
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::InitCapabilities() {
|
|
if (!m_initialized) {
|
|
MGLOG_E("Cannot initialize capabilities before backend is initialized");
|
|
return false;
|
|
}
|
|
if (!pVulkanRenderer) {
|
|
MGLOG_E("Cannot initialize capabilities: Vulkan renderer has not been created");
|
|
return false;
|
|
}
|
|
|
|
const auto& physicalDevice = pVulkanRenderer->GetPhysicalDevice();
|
|
if (!MG_Util::BackendLoader::QueryVulkanCapabilities(m_vulkanCaps, pVulkanRenderer->GetInstance(),
|
|
physicalDevice.handle)) {
|
|
MGLOG_W("DirectVulkan: failed to query extended Vulkan capabilities, using basic properties");
|
|
MG_Util::BackendLoader::FillInVulkanCapabilities(m_vulkanCaps, physicalDevice.properties);
|
|
}
|
|
UpdateDynamicBackendParameters();
|
|
UpdateAdvertisedExtensions();
|
|
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
|
MutableFormatCapabilities());
|
|
PrintFormatCapabilities(GetFormatCapabilities());
|
|
return true;
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
|
if (!m_initialized) {
|
|
MGLOG_E("DirectVulkan backend not initialized");
|
|
return false;
|
|
}
|
|
return BackendObject::InitializeEGLDisplay(dpy, major, minor);
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
if (!m_initialized) {
|
|
MGLOG_E("DirectVulkan backend not initialized");
|
|
return false;
|
|
}
|
|
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
|
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
|
|
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
|
return false;
|
|
}
|
|
|
|
return RegisterEGLWindowSurface(surface, handle);
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
if (!m_initialized) {
|
|
MGLOG_E("DirectVulkan backend not initialized");
|
|
return false;
|
|
}
|
|
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
|
|
return false;
|
|
}
|
|
if (pVulkanRenderer && m_eglSurface == surface) {
|
|
pVulkanRenderer->RequestSwapchainResize(width, height);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
if (!m_initialized) {
|
|
MGLOG_E("DirectVulkan backend not initialized");
|
|
return false;
|
|
}
|
|
return RegisterEGLPbufferSurface(surface, width, height);
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
|
}
|
|
|
|
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
if (!pVulkanRenderer) {
|
|
MGLOG_E("DirectVulkan renderer is not initialized");
|
|
return false;
|
|
}
|
|
return BackendObject::SwapEGLBuffers(dpy, draw);
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
BackendObject::ReleaseEGLSurface(surface);
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::ReleaseEGLResources() {
|
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
|
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
|
// treat them as signaled/available with zero results from here on.
|
|
BumpRendererGeneration();
|
|
pVulkanRenderer.reset();
|
|
// The reflection cache is file-scope, not renderer-owned; without this the
|
|
// deleted programs' reflection strings survive full context teardown.
|
|
ClearProgramResourceCaches();
|
|
BackendObject::ReleaseEGLResources();
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
|
|
(void)surface;
|
|
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
|
// treat them as signaled/available with zero results from here on.
|
|
BumpRendererGeneration();
|
|
pVulkanRenderer.reset();
|
|
// The reflection cache is file-scope, not renderer-owned; without this the
|
|
// deleted programs' reflection strings survive full context teardown.
|
|
ClearProgramResourceCaches();
|
|
}
|
|
|
|
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
|
|
return m_rendererInfo;
|
|
}
|
|
|
|
String BackendObject_DirectVulkan::GetBackendAPIVersionString() const {
|
|
if (!m_initialized) {
|
|
return "<uninitialized DirectVulkan backend>";
|
|
}
|
|
return FormatBackendAPIVersionString(m_vulkanCaps.DeviceName, m_vulkanCaps.VulkanAPIVersion.toString(),
|
|
m_vulkanCaps.DriverVersionString);
|
|
}
|
|
|
|
const RendererInfo& GetRendererIdentity() {
|
|
static const RendererInfo rendererInfo = {
|
|
.RendererName = "Magma",
|
|
.BackendName = "Direct (Vulkan)",
|
|
.ExtraVendor = Nullopt,
|
|
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
|
.TargetGLSLVersion = {4, 6, 0},
|
|
// Baseline advertisement (no shader subgroup, no timer queries); a
|
|
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
|
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
|
.IsCompatibilityProfile = false},
|
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
|
return rendererInfo;
|
|
}
|
|
|
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
|
Bool anisotropicFilteringSupported) {
|
|
Vector<GLExtension> extensions = {
|
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
|
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
|
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
|
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
|
E_GL_ARB_explicit_attrib_location,
|
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
|
// extension explicitly permits. It is also the only thing that
|
|
// exposes glProgramParameteri before GL 4.1.
|
|
E_GL_ARB_get_program_binary};
|
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
|
}
|
|
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
|
// only advertised when the device actually supports timestamp queries and the
|
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
|
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
|
|
extensions.push_back(E_GL_ARB_timer_query);
|
|
}
|
|
// Only advertised when the samplerAnisotropy device feature was granted: without it the
|
|
// sampler state is accepted but never applied, and an app trusting the string (LWJGL builds
|
|
// GLCapabilities from it) would think it enabled anisotropic filtering.
|
|
if (anisotropicFilteringSupported) {
|
|
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
|
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
|
}
|
|
return extensions;
|
|
}
|
|
|
|
String FormatBackendAPIVersionString(const String& deviceName, const String& vulkanApiVersionString,
|
|
const String& driverVersionString) {
|
|
// Format:
|
|
// <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version>
|
|
return deviceName + ", Vulkan " + vulkanApiVersionString + ", Driver " + driverVersionString;
|
|
}
|
|
|
|
BackendType BackendObject_DirectVulkan::GetBackendType() const {
|
|
return BackendType::DirectVulkan;
|
|
}
|
|
|
|
const GlobalBackendFunctionsTable& BackendObject_DirectVulkan::GetBackendFunctions() const {
|
|
static GlobalBackendFunctionsTable funcsTable;
|
|
static Bool funcsTableInitialized = false;
|
|
if (!funcsTableInitialized) {
|
|
funcsTable.Present = Present;
|
|
funcsTable.GL.DrawArrays = DrawArrays;
|
|
funcsTable.GL.DrawElements = DrawElements;
|
|
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
|
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
|
|
funcsTable.GL.MultiDrawElements = MultiDrawElements;
|
|
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
|
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
|
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
|
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
|
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
|
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
|
funcsTable.GL.DrawRangeElements = DrawRangeElements;
|
|
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
|
funcsTable.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
|
|
funcsTable.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
|
|
funcsTable.GL.DrawElementsInstanced = DrawElementsInstanced;
|
|
funcsTable.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
|
|
funcsTable.GL.DrawArraysInstanced = DrawArraysInstanced;
|
|
funcsTable.GL.DrawElementsIndirect = DrawElementsIndirect;
|
|
funcsTable.GL.DrawArraysIndirect = DrawArraysIndirect;
|
|
funcsTable.GL.Clear = Clear;
|
|
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
|
funcsTable.GL.ClearBufferfv = ClearBufferfv;
|
|
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
|
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
|
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
|
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
|
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
|
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
|
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
|
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
|
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
|
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
|
funcsTable.GL.CopyImageSubData = CopyImageSubData;
|
|
funcsTable.GL.GenerateMipmap = GenerateMipmap;
|
|
funcsTable.GL.ReadPixels = ReadPixels;
|
|
funcsTable.GL.GetTexImage = GetTexImage;
|
|
funcsTable.GL.GetTextureImage = GetTextureImage;
|
|
funcsTable.GL.DispatchCompute = DispatchCompute;
|
|
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
|
|
funcsTable.GL.MemoryBarrier = MemoryBarrier;
|
|
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
|
funcsTable.GL.BindImageTexture = BindImageTexture;
|
|
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
|
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
|
funcsTable.GL.GetProgramiv = GetProgramiv;
|
|
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
|
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
|
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
|
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
|
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
|
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
|
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
|
funcsTable.GL.FenceSync = FenceSync;
|
|
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
|
funcsTable.GL.WaitSync = WaitSync;
|
|
funcsTable.GL.DeleteSync = DeleteSync;
|
|
funcsTable.GL.GetSyncStatus = GetSyncStatus;
|
|
// Optional timer-query group: left null (the frontend then falls
|
|
// back) when disabled via MOBILEGL_DISABLE_TIMERQUERY. The hooks
|
|
// themselves additionally degrade to null handles when the device
|
|
// lacks timestamp support.
|
|
if (!MG_Config::Features.DisableTimerQuery) {
|
|
funcsTable.GL.IsTimerQuerySupported = IsTimerQuerySupported;
|
|
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
|
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
|
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
|
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
|
|
}
|
|
// Occlusion queries share the handle-based result/delete entries, which must
|
|
// exist even when timer queries are disabled.
|
|
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
|
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
|
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
|
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
|
funcsTableInitialized = true;
|
|
}
|
|
return funcsTable;
|
|
}
|
|
|
|
const DynamicBackendParameters& BackendObject_DirectVulkan::GetDynamicParameters() const {
|
|
return m_dynamicParameters;
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::ApplyVulkanCapabilitiesForTesting(
|
|
const MG_External::VulkanCapabilities& capabilities) {
|
|
m_vulkanCaps = capabilities;
|
|
UpdateDynamicBackendParameters();
|
|
UpdateAdvertisedExtensions();
|
|
MutableFormatCapabilities().Clear();
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::UpdateAdvertisedExtensions() {
|
|
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension
|
|
// string). InitCapabilities runs after InitWindowSurface has created
|
|
// and initialized the renderer, so the advertisement can be gated on
|
|
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
|
// run without a renderer; no timer query is advertised then. Rebuilding
|
|
// the whole list keeps re-runs idempotent.
|
|
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
|
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
|
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
|
}
|
|
|
|
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
|
const auto mapShaderStages = [](Uint32 vkStages) {
|
|
Uint32 glStages = 0;
|
|
if ((vkStages & VK_SHADER_STAGE_VERTEX_BIT) != 0) glStages |= GL_VERTEX_SHADER_BIT;
|
|
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0) glStages |= GL_TESS_CONTROL_SHADER_BIT;
|
|
if ((vkStages & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0) {
|
|
glStages |= GL_TESS_EVALUATION_SHADER_BIT;
|
|
}
|
|
if ((vkStages & VK_SHADER_STAGE_GEOMETRY_BIT) != 0) glStages |= GL_GEOMETRY_SHADER_BIT;
|
|
if ((vkStages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0) glStages |= GL_FRAGMENT_SHADER_BIT;
|
|
if ((vkStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0) glStages |= GL_COMPUTE_SHADER_BIT;
|
|
return glStages;
|
|
};
|
|
|
|
const auto mapSubgroupFeatures = [](Uint32 vkFeatures) {
|
|
Uint32 glFeatures = 0;
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_BASIC_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_VOTE_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_VOTE_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_CLUSTERED_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR;
|
|
}
|
|
if ((vkFeatures & VK_SUBGROUP_FEATURE_QUAD_BIT) != 0) {
|
|
glFeatures |= GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
|
}
|
|
return glFeatures;
|
|
};
|
|
|
|
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
|
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
|
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
|
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
|
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
|
// rather than a maximum the sampler manager will never apply.
|
|
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
|
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
|
: 1.0f;
|
|
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
|
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
|
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
|
m_dynamicParameters.PointSizeRangeMin = m_vulkanCaps.PointSizeRangeMin;
|
|
m_dynamicParameters.PointSizeRangeMax = m_vulkanCaps.PointSizeRangeMax;
|
|
m_dynamicParameters.PointSizeGranularity = m_vulkanCaps.PointSizeGranularity;
|
|
m_dynamicParameters.Max3DTextureSize = m_vulkanCaps.Max3DTextureSize;
|
|
m_dynamicParameters.MaxArrayTextureLayers = m_vulkanCaps.MaxArrayTextureLayers;
|
|
m_dynamicParameters.MaxCubeMapTextureSize = m_vulkanCaps.MaxCubeMapTextureSize;
|
|
m_dynamicParameters.MaxFramebufferWidth = m_vulkanCaps.MaxFramebufferWidth;
|
|
m_dynamicParameters.MaxFramebufferHeight = m_vulkanCaps.MaxFramebufferHeight;
|
|
m_dynamicParameters.MaxFramebufferLayers = m_vulkanCaps.MaxFramebufferLayers;
|
|
m_dynamicParameters.MaxRenderbufferSize = m_vulkanCaps.MaxRenderbufferSize;
|
|
m_dynamicParameters.MaxTextureSize = m_vulkanCaps.MaxTextureSize;
|
|
m_dynamicParameters.MaxColorTextureSamples = m_vulkanCaps.MaxColorTextureSamples;
|
|
m_dynamicParameters.MaxDepthTextureSamples = m_vulkanCaps.MaxDepthTextureSamples;
|
|
m_dynamicParameters.MaxFramebufferSamples = m_vulkanCaps.MaxFramebufferSamples;
|
|
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
|
|
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
|
|
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
|
|
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
|
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
|
|
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
|
|
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
|
|
// an array bound: Minecraft's Blaze3D GlStateManager.TEXTURES[] holds 128 entries and Iris
|
|
// iterates [0, GL_MAX_TEXTURE_IMAGE_UNITS) over it (CompositeRenderer.renderAll), so any value
|
|
// > 128 throws ArrayIndexOutOfBoundsException. Match desktop drivers (32) for the per-stage
|
|
// limits while keeping the combined limit at our texture-unit array capacity.
|
|
constexpr Int maxPerStageTextureUnits =
|
|
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
|
|
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
|
|
m_dynamicParameters.MaxVertexTextureImageUnits =
|
|
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
|
|
m_dynamicParameters.MaxComputeTextureImageUnits =
|
|
std::min(m_vulkanCaps.MaxComputeTextureImageUnits, maxPerStageTextureUnits);
|
|
m_dynamicParameters.MaxCombinedTextureImageUnits =
|
|
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
|
|
// Never advertise more attributes than the state layer can store: the current-value array and
|
|
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
|
m_dynamicParameters.MaxVertexAttribs = std::min(
|
|
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
|
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
|
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
|
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
|
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
|
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
|
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
|
const Int maxPerStageImageUniforms =
|
|
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
|
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
|
|
// optional device features. VulkanRenderer enables each feature whenever the physical
|
|
// device reports it, so these are the exact limits the logical device can compile and run.
|
|
m_dynamicParameters.MaxVertexImageUniforms =
|
|
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
|
m_dynamicParameters.MaxGeometryImageUniforms =
|
|
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
|
|
? maxPerStageImageUniforms
|
|
: 0;
|
|
m_dynamicParameters.MaxFragmentImageUniforms =
|
|
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
|
m_dynamicParameters.MaxComputeImageUniforms =
|
|
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
|
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
|
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
|
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
|
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
|
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
|
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
|
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
|
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
|
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
|
|
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
|
|
m_dynamicParameters.MinFragmentInterpolationOffset =
|
|
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
|
|
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
|
|
? m_vulkanCaps.MinFragmentInterpolationOffset
|
|
: -0.5f;
|
|
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
|
|
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
|
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
|
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
|
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
|
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
|
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
|
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
|
|
}
|
|
}
|
|
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
|
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
|
|
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
|
|
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
|
|
// layered targets are declared separately as their own machinery lands.
|
|
{
|
|
using DynParams = MG_Backend::DynamicBackendParameters;
|
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
|
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
|
|
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
|
|
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
|
|
// image can name. Optimistic: a format that refuses the flag is caught at image creation
|
|
// and declines the slice view there, which the clear path handles as a soft miss.
|
|
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
|
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
|
|
}
|
|
if (m_vulkanCaps.SupportsImageCubeArray) {
|
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
|
}
|
|
}
|
|
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
|
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
|
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
|
|
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
|
|
m_dynamicParameters.SubgroupSupportedFeatures =
|
|
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
|
} else {
|
|
m_dynamicParameters.SubgroupSize = 0;
|
|
m_dynamicParameters.SubgroupSupportedStages = 0;
|
|
m_dynamicParameters.SubgroupSupportedFeatures = 0;
|
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
|
}
|
|
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
|
|
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
|
|
m_vulkanCaps.MaxShaderStorageBlockSize, m_dynamicParameters.MaxShaderStorageBlockSize);
|
|
}
|
|
switch (m_vulkanCaps.VendorId) {
|
|
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
|
|
break;
|
|
case 0x13B5u: // ARM
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
|
|
break;
|
|
case 0x10DEu: // NVIDIA
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
|
|
break;
|
|
case 0x1002u: // AMD
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
|
|
break;
|
|
case 0x8086u: // Intel
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
|
|
break;
|
|
case 0x1010u: // Imagination
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
|
|
break;
|
|
case 0x10005u: // Mesa software (lavapipe)
|
|
case 0x1AE0u: // Google (SwiftShader)
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
|
|
break;
|
|
default:
|
|
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
|
|
break;
|
|
}
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|