Compare commits

...
2 Commits
41 changed files with 3196 additions and 140 deletions
+3
View File
@@ -272,6 +272,9 @@ namespace MobileGL {
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8; Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
@@ -947,6 +947,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_dynamicParameters; return m_dynamicParameters;
} }
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
const MG_External::GLESCapabilities& capabilities) {
m_GLESCapabilities = capabilities;
UpdateDynamicBackendParameters();
}
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
@@ -1003,9 +1009,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
const Int maxSupportedTextureUnits = const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits); m_dynamicParameters.MaxImageUnits =
m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms; std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms; m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
const auto clampStageImageUniforms = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
m_dynamicParameters.MaxCombinedImageUniforms});
};
m_dynamicParameters.MaxVertexImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxGeometryImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
m_dynamicParameters.MaxFragmentImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
m_dynamicParameters.MaxComputeImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESFunctionsTable& GetGLESFunctions() const; const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
const MG_External::EGLFunctionsTable& GetEGLFunctions() const; const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
private: private:
void UpdateDynamicBackendParameters(); void UpdateDynamicBackendParameters();
@@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, 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_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_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_direct_state_access, 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_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_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_vertex_attrib_binding, E_GL_ARB_shader_image_size};
@@ -746,9 +746,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits); m_dynamicParameters.MaxImageUnits =
m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms; std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms; 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 = const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
@@ -1197,6 +1197,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return TextureTarget::Unknown; return TextureTarget::Unknown;
} }
} }
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
switch (uniformType) {
case GL_IMAGE_1D:
case GL_IMAGE_2D:
case GL_IMAGE_3D:
case GL_IMAGE_2D_RECT:
case GL_IMAGE_CUBE:
case GL_IMAGE_BUFFER:
case GL_IMAGE_1D_ARRAY:
case GL_IMAGE_2D_ARRAY:
case GL_IMAGE_CUBE_MAP_ARRAY:
case GL_IMAGE_2D_MULTISAMPLE:
case GL_IMAGE_2D_MULTISAMPLE_ARRAY:
return true;
default:
return false;
}
}
} // namespace } // namespace
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
@@ -1218,6 +1237,105 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) {
switch (format) {
case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED;
case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT;
case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT;
case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT;
case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM;
case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM;
case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT;
case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT;
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
case SpvImageFormatR8: return VK_FORMAT_R8_UNORM;
case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM;
case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM;
case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM;
case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM;
case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM;
case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT;
case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT;
case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT;
case SpvImageFormatR32i: return VK_FORMAT_R32_SINT;
case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT;
case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT;
case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT;
case SpvImageFormatR16i: return VK_FORMAT_R16_SINT;
case SpvImageFormatR8i: return VK_FORMAT_R8_SINT;
case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT;
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT;
case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT;
case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT;
case SpvImageFormatR64i: return VK_FORMAT_R64_SINT;
case SpvImageFormatMax: return VK_FORMAT_UNDEFINED;
}
return VK_FORMAT_UNDEFINED;
}
SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) {
switch (glType) {
case GL_INT_SAMPLER_1D:
case GL_INT_SAMPLER_2D:
case GL_INT_SAMPLER_3D:
case GL_INT_SAMPLER_CUBE:
case GL_INT_SAMPLER_2D_RECT:
case GL_INT_SAMPLER_1D_ARRAY:
case GL_INT_SAMPLER_2D_ARRAY:
case GL_INT_SAMPLER_BUFFER:
case GL_INT_SAMPLER_2D_MULTISAMPLE:
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
return SamplerNumericDomain::SignedInteger;
case GL_UNSIGNED_INT_SAMPLER_1D:
case GL_UNSIGNED_INT_SAMPLER_2D:
case GL_UNSIGNED_INT_SAMPLER_3D:
case GL_UNSIGNED_INT_SAMPLER_CUBE:
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
return SamplerNumericDomain::UnsignedInteger;
case GL_SAMPLER_1D:
case GL_SAMPLER_2D:
case GL_SAMPLER_3D:
case GL_SAMPLER_CUBE:
case GL_SAMPLER_2D_RECT:
case GL_SAMPLER_1D_ARRAY:
case GL_SAMPLER_2D_ARRAY:
case GL_SAMPLER_BUFFER:
case GL_SAMPLER_2D_MULTISAMPLE:
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_SAMPLER_CUBE_MAP_ARRAY:
case GL_SAMPLER_1D_SHADOW:
case GL_SAMPLER_2D_SHADOW:
case GL_SAMPLER_CUBE_SHADOW:
case GL_SAMPLER_2D_RECT_SHADOW:
case GL_SAMPLER_1D_ARRAY_SHADOW:
case GL_SAMPLER_2D_ARRAY_SHADOW:
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
return SamplerNumericDomain::Float;
default:
return SamplerNumericDomain::Unknown;
}
}
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
CompileOptionFlags flags) const { CompileOptionFlags flags) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
@@ -1465,6 +1583,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.samplerNameByBinding.assign(m_maxBindings, String()); entry.samplerNameByBinding.assign(m_maxBindings, String());
entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1); entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D); entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown);
entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED);
entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false);
entry.storageBlockNameByBinding.assign(m_maxBindings, String()); entry.storageBlockNameByBinding.assign(m_maxBindings, String());
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1); entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
entry.globalUboBinding = -1; entry.globalUboBinding = -1;
@@ -1592,10 +1713,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const TextureTarget target = UniformTypeToTextureTarget(program.GetUniformType(static_cast<Uint>(location))); const GLenum uniformType = program.GetUniformType(static_cast<Uint>(location));
if (descriptorKind == DescriptorBindingKind::StorageImage) {
const VkFormat reflectedFormat =
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
reflectedFormat == VK_FORMAT_UNDEFINED ||
existingFormat == reflectedFormat,
"ProgramFactory::ReflectLayout: storage image binding %u ('%s') has "
"conflicting reflected formats (%d vs %d)",
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
static_cast<Int>(reflectedFormat));
if (existingFormat == VK_FORMAT_UNDEFINED) {
existingFormat = reflectedFormat;
}
if (m_unformattedFloatStorageImagesEnabled &&
existingFormat == VK_FORMAT_UNDEFINED &&
IsFloatStorageImageUniformType(uniformType)) {
entry.storageImageUsesBindingFormatByBinding[binding] = true;
} else if (reflectedFormat != VK_FORMAT_UNDEFINED) {
// A typed declaration in any stage wins for the entire binding. This is
// required when another stage reaches the same image through an atomic
// path and therefore could not be made formatless.
entry.storageImageUsesBindingFormatByBinding[binding] = false;
}
}
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
MOBILEGL_ASSERT(target != TextureTarget::Unknown, MOBILEGL_ASSERT(target != TextureTarget::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'", "ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
uniformName.c_str()); uniformName.c_str());
if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) {
const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType);
MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain "
"for '%s' (uniformType=0x%x)",
uniformName.c_str(), uniformType);
MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] ==
SamplerNumericDomain::Unknown ||
entry.samplerNumericDomainByBinding[binding] == numericDomain,
"ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting "
"numeric domains (%d vs %d)",
binding, uniformName.c_str(),
static_cast<Int>(entry.samplerNumericDomainByBinding[binding]),
static_cast<Int>(numericDomain));
entry.samplerNumericDomainByBinding[binding] = numericDomain;
}
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 || MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
entry.samplerUniformLocationByBinding[binding] == location, entry.samplerUniformLocationByBinding[binding] == location,
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)", "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
@@ -1721,6 +1886,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
} }
// When Vulkan can legally access storage images without a statically declared
// format, let GL's glBindImageTexture format select the runtime image view. This
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
// float image qualifier can disagree with the bound render-target format. Integer
// storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the
// exact descriptor format required by their shader operations.
if (m_unformattedFloatStorageImagesEnabled) {
Vector<Uint> unformattedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
moduleSpirvs[i], unformattedSpirv)) {
moduleSpirvs[i] = std::move(unformattedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
program.GetExternalIndex());
}
}
} }
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
@@ -14,8 +14,16 @@
#include "MG_State/GLState/TextureState/TextureEnum.h" #include "MG_State/GLState/TextureState/TextureEnum.h"
#include <Includes.h> #include <Includes.h>
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8 {
Unknown = 0,
Float,
SignedInteger,
UnsignedInteger,
};
class ProgramFactory { class ProgramFactory {
public: public:
enum class DescriptorBindingKind : Uint8 { enum class DescriptorBindingKind : Uint8 {
@@ -54,6 +62,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<String> samplerNameByBinding; Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding; Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding; Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding; Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding; Vector<Int> storageBlockIndexByBinding;
Int globalUboBinding = -1; Int globalUboBinding = -1;
@@ -82,6 +93,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
@@ -118,6 +133,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
@@ -167,9 +186,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false) Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config), : m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) { m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device; VkProgramObject::s_device = device;
} }
~ProgramFactory() = default; ~ProgramFactory() = default;
@@ -180,6 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
static VkShaderStageFlagBits ToVkStage(ShaderStage stage); static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
private: private:
struct ProgramLookupCache { struct ProgramLookupCache {
@@ -206,6 +229,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass // True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
// (which needs the DrawParameters capability / gl_BaseInstance builtin). // (which needs the DrawParameters capability / gl_BaseInstance builtin).
Bool m_shaderDrawParametersEnabled = false; Bool m_shaderDrawParametersEnabled = false;
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup; mutable ProgramLookupCache m_lastLookup;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
@@ -13,6 +13,7 @@
#include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObject2D.h"
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h" #include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
@@ -78,6 +79,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return uniformUnit >= 0 ? uniformUnit : 0; return uniformUnit >= 0 ? uniformUnit : 0;
} }
VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat) {
if (useBindingFormat) {
const TextureInternalFormat bindingInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat);
return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat);
}
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
}
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager, Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
ProgramFactory* programFactory, ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
@@ -276,6 +287,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding); static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
} }
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
// descriptor valid.
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
numericDomain == SamplerNumericDomain::UnsignedInteger;
const VkFormat sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
return false;
}
const VkImageView sampledImageView =
m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
static_cast<Int>(numericDomain));
return false;
}
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
// sampler object and texture (both by lifetime id + version) are unchanged from the // sampler object and texture (both by lifetime id + version) are unchanged from the
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler // last draw that resolved it: the resulting sampler key, and therefore the VkSampler
@@ -291,23 +332,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint64 textureLifetimeId = texture->GetLifetimeId(); const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) { memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
memo.forceNearestFiltering == forceNearestFiltering) {
resolvedSampler = memo.sampler; resolvedSampler = memo.sampler;
} else { } else {
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
memo.samplerLifetimeId = samplerLifetimeId; memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion; memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId; memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion; memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering;
memo.sampler = resolvedSampler; memo.sampler = resolvedSampler;
memo.valid = true; memo.valid = true;
} }
} else { } else {
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture); resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
} }
outImageInfo = { outImageInfo = {
.sampler = resolvedSampler, .sampler = resolvedSampler,
.imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView, .imageView = sampledImageView,
.imageLayout = resource->layout, .imageLayout = resource->layout,
}; };
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
@@ -572,9 +617,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level)); const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel); MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(),
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
binding);
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
const VkFormat viewFormat = ResolveStorageImageViewFormat(
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
if (viewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
useBindingFormat ? "true" : "false");
return false;
}
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
if (view == VK_NULL_HANDLE) { if (view == VK_NULL_HANDLE) {
view = resource->fullView; MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u "
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
static_cast<Int>(viewFormat),
useBindingFormat ? "true" : "false");
return false;
} }
outImageInfo.sampler = VK_NULL_HANDLE; outImageInfo.sampler = VK_NULL_HANDLE;
outImageInfo.imageView = view; outImageInfo.imageView = view;
@@ -632,6 +700,50 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool UniformManager::CollectStorageImageTextures(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
outTextures.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
"CollectStorageImageTextures: GL context is null");
const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
continue;
}
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
return false;
}
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u",
imageUnit, binding);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u",
imageUnit, binding);
return false;
}
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
outTextures.push_back(texture);
}
}
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const { UboBindResult& out) const {
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures); Vector<MG_State::GLState::ITextureObject*>& outTextures);
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
@@ -49,6 +52,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr); const SamplerBindingOverride* samplerBindingOverride = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
// format and never silently fall back to the backing image format.
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
private: private:
struct DescriptorPoolBucket { struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE; VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -165,9 +174,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampler sampler = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE;
Uint16 samplerVersion = 0; Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0; Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false; Bool valid = false;
}; };
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo; mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -66,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Uint32 unsupportedAttribMask = 0; Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -74,8 +75,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); const VkFormat sourceVkFormat =
if (vkFormat == VK_FORMAT_UNDEFINED) { ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat", "enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -83,6 +85,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None;
if (!SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
}
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
}
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) { if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is " MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
@@ -92,8 +121,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const Uint32 stride = const Uint32 sourceStride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize); attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
const VkVertexInputRate inputRate = const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -103,6 +152,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0); bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
bindingAttributeLocations.push_back(location); bindingAttributeLocations.push_back(location);
bindingUsesClientMemory.push_back(attr.Buffer == nullptr); bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate); builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0); builder.AddAttribute(location, binding, vkFormat, 0);
} }
@@ -117,6 +167,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.bindingBaseOffsets = std::move(bindingBaseOffsets); entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations); entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory); entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask; entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state; entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
@@ -282,4 +333,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT componentSize = GetComponentSize(type); const SizeT componentSize = GetComponentSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size); return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
} }
Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_USCALED:
case VK_FORMAT_R8_SSCALED:
case VK_FORMAT_R8G8_USCALED:
case VK_FORMAT_R8G8_SSCALED:
case VK_FORMAT_R8G8B8_USCALED:
case VK_FORMAT_R8G8B8_SSCALED:
case VK_FORMAT_R8G8B8A8_USCALED:
case VK_FORMAT_R8G8B8A8_SSCALED:
case VK_FORMAT_R16_USCALED:
case VK_FORMAT_R16_SSCALED:
case VK_FORMAT_R16G16_USCALED:
case VK_FORMAT_R16G16_SSCALED:
case VK_FORMAT_R16G16B16_USCALED:
case VK_FORMAT_R16G16B16_SSCALED:
case VK_FORMAT_R16G16B16A16_USCALED:
case VK_FORMAT_R16G16B16A16_SSCALED:
return true;
default:
return false;
}
}
VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) {
switch (componentCount) {
case 1: return VK_FORMAT_R32_SFLOAT;
case 2: return VK_FORMAT_R32G32_SFLOAT;
case 3: return VK_FORMAT_R32G32B32_SFLOAT;
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
}
Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const {
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
return false;
}
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
}
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,6 +19,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public: public:
using HashType = Uint64; using HashType = Uint64;
enum class VertexStreamConversion : Uint8 {
None = 0,
Repack,
ScaledIntegerToFloat32,
};
struct BackendVertexInputState { struct BackendVertexInputState {
HashType hash = 0; HashType hash = 0;
Vector<VkVertexInputBindingDescription> bindings; Vector<VkVertexInputBindingDescription> bindings;
@@ -27,6 +33,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are // Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
// absent from `attributes`, so without this mask the draw path cannot tell them apart from // absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value. // a genuinely disabled array and would silently feed the shader the current attribute value.
@@ -36,8 +43,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
}; };
explicit VertexInputStateFactory(const VulkanRendererConfig& config): VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
m_config(config) {} m_config(config), m_physicalDevice(physicalDevice) {}
~VertexInputStateFactory() = default; ~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete; VertexInputStateFactory(const VertexInputStateFactory&) = delete;
@@ -56,8 +63,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private: private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false); static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
const VulkanRendererConfig& m_config; const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache; UnorderedMap<HashType, BackendVertexInputState> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
@@ -157,6 +157,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
if (resolvedSize == 0) {
return true;
}
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
}
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const { BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range"); MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size; const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
@@ -44,6 +44,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* Map(); void* Map();
void Unmap(); void Unmap();
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0); Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
Bool Invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
VkBuffer GetHandle() const { return m_buffer; } VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; } VkDeviceSize GetSize() const { return m_size; }
@@ -65,8 +65,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const { Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
Bool forceNearestFiltering) const {
if (!m_samplerAnisotropySupported) return 1.0f; if (!m_samplerAnisotropySupported) return 1.0f;
if (forceNearestFiltering) return 1.0f;
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to // VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy]. // be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
if (sampler.GetMinFilter() != SamplerFilterMode::Linear || if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
@@ -90,10 +92,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const { const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
const auto minFilter = sampler.GetMinFilter(); const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter(); const auto magFilter = sampler.GetMagFilter();
@@ -115,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan // The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one // will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's. // VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler); const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy))); XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode(); const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
@@ -127,8 +132,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) { const MG_State::GLState::ITextureObject& texture,
const Uint64 key = BuildSamplerKey(sampler, texture); Bool forceNearestFiltering) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
auto it = m_samplers.find(key); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
return it->second.handle; return it->second.handle;
@@ -136,16 +142,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSamplerCreateInfo samplerInfo{}; VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter()); samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter()); samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode()); samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS()); samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias(); samplerInfo.mipLodBias = sampler.GetLodBias();
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two // Must use the same resolver as BuildSamplerKey - a divergence would either collide two
// different samplers or silently create duplicates. // different samplers or silently create duplicates.
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler); const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy; samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
@@ -34,7 +34,8 @@ public:
void Shutdown(); void Shutdown();
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture); const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false);
private: private:
struct SamplerCacheEntry { struct SamplerCacheEntry {
@@ -44,7 +45,8 @@ private:
}; };
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const; const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -57,7 +59,8 @@ private:
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw. // that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const; Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
Bool forceNearestFiltering) const;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr; const VulkanRendererConfig* m_config = nullptr;
@@ -8,6 +8,8 @@
#include "VkTextureManager.h" #include "VkTextureManager.h"
#include "ProgramFactory.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
@@ -17,6 +19,7 @@
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <vulkan/utility/vk_format_utils.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule // Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
@@ -63,6 +66,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
target == TextureUploadTarget::ProxyTexture2DMultisampleArray; target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
} }
static Bool IsMutableStorageImageFormat(VkFormat format) {
if (!vkuFormatIsColor(format) || vkuFormatIsCompressed(format)) {
return false;
}
// These are the uncompressed color compatibility classes covered by the core GLSL/SPIR-V
// storage-image formats. OpenGL mutable texture storage uses image-format compatibility by
// size, so a shader may legally reinterpret (for example) RGBA16_UNORM storage as rgba16f. Vulkan
// requires the image to be mutable and the view formats to share this exact compatibility
// class for the equivalent operation.
switch (vkuFormatCompatibilityClass(format)) {
case VKU_FORMAT_COMPATIBILITY_CLASS_8BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_16BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_32BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_64BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_128BIT:
return true;
default:
return false;
}
}
static Bool HasMatchingColorComponentLayout(VkFormat lhs, VkFormat rhs) {
const VKU_FORMAT_INFO lhsInfo = vkuGetFormatInfo(lhs);
const VKU_FORMAT_INFO rhsInfo = vkuGetFormatInfo(rhs);
if (lhsInfo.component_count == 0 || lhsInfo.component_count != rhsInfo.component_count ||
lhsInfo.texel_block_size != rhsInfo.texel_block_size ||
lhsInfo.texels_per_block != 1 || rhsInfo.texels_per_block != 1) {
return false;
}
for (Uint32 component = 0; component < lhsInfo.component_count; ++component) {
if (lhsInfo.components[component].type != rhsInfo.components[component].type ||
lhsInfo.components[component].size != rhsInfo.components[component].size) {
return false;
}
}
return true;
}
static Bool FormatMatchesSamplerNumericDomain(VkFormat format, SamplerNumericDomain numericDomain) {
switch (numericDomain) {
case SamplerNumericDomain::Float:
return vkuFormatIsSampledFloat(format);
case SamplerNumericDomain::SignedInteger:
return vkuFormatIsSINT(format);
case SamplerNumericDomain::UnsignedInteger:
return vkuFormatIsUINT(format);
case SamplerNumericDomain::Unknown:
return true;
}
return false;
}
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) { static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples) { switch (requestedSamples) {
case 1: case 1:
@@ -767,6 +823,180 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return perMipSampledView; return perMipSampledView;
} }
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
resource->sampledView == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
return resource->sampledView;
}
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
const TextureResource::SampledImageViewKey key{
.baseMipLevel = resource->sampledBaseMipLevel,
.levelCount = resource->sampledLevelCount,
.viewType = resource->viewType,
.format = format,
};
const auto existing = resource->alternateSampledViews.find(key);
if (existing != resource->alternateSampledViews.end()) {
return existing->second;
}
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
"for textureId=%d (available=0x%x)",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
return VK_NULL_HANDLE;
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageView view = CreateImageView(
resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType,
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
resource->alternateSampledViews.emplace(key, view);
MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format), resource->sampledBaseMipLevel,
resource->sampledBaseMipLevel + resource->sampledLevelCount);
return view;
}
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, VkFormat format,
Bool layered, Int32 layer) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
(resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED) {
format = resource->format;
}
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if (format != resource->format &&
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
Uint32 baseArrayLayer = 0;
Uint32 layerCount = resource->arrayLayers;
VkImageViewType viewType = resource->viewType;
if (!layered) {
switch (resource->viewType) {
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
viewType = VK_IMAGE_VIEW_TYPE_1D;
break;
case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
case VK_IMAGE_VIEW_TYPE_CUBE:
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
viewType = VK_IMAGE_VIEW_TYPE_2D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
__func__, texture.GetExternalIndex());
return VK_NULL_HANDLE;
default:
break;
}
if (viewType != resource->viewType) {
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
return VK_NULL_HANDLE;
}
baseArrayLayer = static_cast<Uint32>(layer);
layerCount = 1;
}
}
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType;
if (format == resource->format && isFullResourceView) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
const TextureResource::StorageImageViewKey key{
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
.format = format,
};
auto it = resource->storageImageViews.find(key);
if (it != resource->storageImageViews.end()) {
return it->second;
}
VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
if (format != resource->format &&
(format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) {
requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT;
}
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
"(available=0x%x)",
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
return VK_NULL_HANDLE;
}
const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType,
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
VK_IMAGE_USAGE_STORAGE_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
resource->storageImageViews.emplace(key, view);
MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return view;
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) { void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null"); MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture)); auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1085,6 +1315,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
if (supportsStorageImage && IsMutableStorageImageFormat(format)) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) && resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) && resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
@@ -1092,6 +1334,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.arrayLayers == shapeInfo.arrayLayers && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType && resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resource.mipLevels == backingMipLevels; resource.mipLevels == backingMipLevels;
if (compatible) { if (compatible) {
if (resource.perMipViews.size() != backingMipLevels) { if (resource.perMipViews.size() != backingMipLevels) {
@@ -1112,6 +1355,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.arrayLayers == shapeInfo.arrayLayers && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType && resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
resource.mipLevels < backingMipLevels && resource.mipLevels < backingMipLevels &&
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
@@ -1123,26 +1367,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DeferResourceRelease(Move(resource)); DeferResourceRelease(Move(resource));
} }
auto aspect = GetAspectMaskForFormat(format);
VkImageCreateInfo imageInfo{}; VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = shapeInfo.imageFlags; imageInfo.flags = imageCreateFlags;
imageInfo.imageType = shapeInfo.imageType; imageInfo.imageType = shapeInfo.imageType;
imageInfo.extent.width = static_cast<Uint32>(texelSize.x()); imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
imageInfo.extent.height = static_cast<Uint32>(texelSize.y()); imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
imageInfo.extent.depth = shapeInfo.depth; imageInfo.extent.depth = shapeInfo.depth;
imageInfo.mipLevels = backingMipLevels; imageInfo.mipLevels = backingMipLevels;
imageInfo.arrayLayers = shapeInfo.arrayLayers; imageInfo.arrayLayers = shapeInfo.arrayLayers;
imageInfo.format = format; imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
@@ -1153,15 +1389,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
} }
imageInfo.samples = resolvedSampleCount; imageInfo.samples = resolvedSampleCount;
if (isMultisampleTexture) { if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
VkImageFormatProperties imageFormatProperties{}; VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties); imageInfo.flags, &imageFormatProperties);
if (imageFormatResult != VK_SUCCESS || if (imageFormatResult != VK_SUCCESS ||
(imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) { (isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x", MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
__func__, texture.GetSamples(), texture.GetExternalIndex(), "format=%d usage=0x%x",
__func__, static_cast<Uint32>(imageInfo.flags), texture.GetSamples(),
texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage)); static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
return false; return false;
@@ -1188,6 +1426,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.aspect = aspect; resource.aspect = aspect;
resource.viewType = shapeInfo.viewType; resource.viewType = shapeInfo.viewType;
resource.sampleCount = resolvedSampleCount; resource.sampleCount = resolvedSampleCount;
resource.imageCreateFlags = imageCreateFlags;
resource.syncedTextureParamsVersion = 0; resource.syncedTextureParamsVersion = 0;
if (preservedResource) { if (preservedResource) {
@@ -1203,7 +1442,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) { void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE && if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
resource.sampledView == VK_NULL_HANDLE && resource.sampledView == VK_NULL_HANDLE &&
resource.perMipViews.empty() && resource.perMipSampledViews.empty()) { resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
resource.storageImageViews.empty()) {
return; return;
} }
@@ -1299,6 +1540,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampledView = VK_NULL_HANDLE; sampledView = VK_NULL_HANDLE;
} }
} }
for (const auto& [_, sampledView] : resource.alternateSampledViews) {
DeferViewRelease(sampledView);
}
resource.alternateSampledViews.clear();
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
@@ -1324,7 +1569,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer, Uint32 baseArrayLayer,
Uint32 layerCount, Uint32 layerCount,
const VkComponentMapping* components) const { const VkComponentMapping* components,
VkImageUsageFlags viewUsage) const {
VkImageViewCreateInfo viewInfo{}; VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image; viewInfo.image = image;
@@ -1340,6 +1586,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer; viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
viewInfo.subresourceRange.layerCount = layerCount; viewInfo.subresourceRange.layerCount = layerCount;
VkImageViewUsageCreateInfo usageInfo{};
if (viewUsage != 0) {
usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
usageInfo.usage = viewUsage;
viewInfo.pNext = &usageInfo;
}
VkImageView view = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)"); VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
return view; return view;
@@ -1665,4 +1918,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
return imageAspect; return imageAspect;
} }
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
SamplerNumericDomain numericDomain) {
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
return imageFormat;
}
if (!IsMutableStorageImageFormat(imageFormat)) {
return VK_FORMAT_UNDEFINED;
}
// Preserve component ordering and bit widths. This selects R32_UINT for an R32_SFLOAT
// texture sampled by a usampler rather than an arbitrary member (such as
// R8G8B8A8_UINT) of Vulkan's broad 32-bit compatibility class.
for (Int candidateValue = static_cast<Int>(VK_FORMAT_R4G4_UNORM_PACK8);
candidateValue <= static_cast<Int>(VK_FORMAT_ASTC_12x12_SRGB_BLOCK);
++candidateValue) {
const VkFormat candidate = static_cast<VkFormat>(candidateValue);
if (!IsMutableStorageImageFormat(candidate) ||
!FormatMatchesSamplerNumericDomain(candidate, numericDomain) ||
!HasMatchingColorComponentLayout(imageFormat, candidate) ||
!AreSampledImageViewFormatsCompatible(imageFormat, candidate)) {
continue;
}
// If an integer backing is intentionally bit-read through a float sampler, require
// a true floating-point view. Normalized/scaled views satisfy OpTypeFloat but apply
// an unrelated numeric conversion to those bits.
if (numericDomain == SamplerNumericDomain::Float && !vkuFormatIsSFLOAT(candidate)) {
continue;
}
return candidate;
}
return VK_FORMAT_UNDEFINED;
}
Bool VkTextureManager::AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
if (imageFormat == viewFormat) {
return true;
}
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
}
Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
if (imageFormat == viewFormat) {
return true;
}
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
}
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,6 +19,8 @@ class ITextureObject;
} }
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
class VkTextureManager { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -78,6 +80,61 @@ public:
} }
}; };
struct StorageImageViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const StorageImageViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKeyHash {
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKeyHash {
SizeT operator()(const StorageImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr; VmaAllocation allocation = nullptr;
VkImageView fullView = VK_NULL_HANDLE; VkImageView fullView = VK_NULL_HANDLE;
@@ -85,6 +142,8 @@ public:
Vector<VkImageView> perMipViews; Vector<VkImageView> perMipViews;
Vector<VkImageView> perMipSampledViews; Vector<VkImageView> perMipSampledViews;
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews; UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0}; VkExtent2D extent = {0, 0};
Uint32 depth = 1; Uint32 depth = 1;
@@ -96,6 +155,7 @@ public:
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
Uint16 syncedTextureParamsVersion = 0; Uint16 syncedTextureParamsVersion = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged. // lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
@@ -115,6 +175,8 @@ public:
std::swap(this->perMipViews, that.perMipViews); std::swap(this->perMipViews, that.perMipViews);
std::swap(this->perMipSampledViews, that.perMipSampledViews); std::swap(this->perMipSampledViews, that.perMipSampledViews);
std::swap(this->attachmentViews, that.attachmentViews); std::swap(this->attachmentViews, that.attachmentViews);
std::swap(this->alternateSampledViews, that.alternateSampledViews);
std::swap(this->storageImageViews, that.storageImageViews);
std::swap(this->layout, that.layout); std::swap(this->layout, that.layout);
std::swap(this->extent, that.extent); std::swap(this->extent, that.extent);
std::swap(this->depth, that.depth); std::swap(this->depth, that.depth);
@@ -126,6 +188,7 @@ public:
std::swap(this->aspect, that.aspect); std::swap(this->aspect, that.aspect);
std::swap(this->viewType, that.viewType); std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount); std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
@@ -153,6 +216,16 @@ public:
vkDestroyImageView(s_device, attachmentView, nullptr); vkDestroyImageView(s_device, attachmentView, nullptr);
} }
} }
for (const auto& [_, sampledView] : alternateSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
}
for (const auto& [_, storageImageView] : storageImageViews) {
if (storageImageView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, storageImageView, nullptr);
}
}
if (image != VK_NULL_HANDLE && allocation != nullptr) { if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator, image, allocation); vmaDestroyImage(s_allocator, image, allocation);
} }
@@ -161,6 +234,8 @@ public:
perMipViews.clear(); perMipViews.clear();
perMipSampledViews.clear(); perMipSampledViews.clear();
attachmentViews.clear(); attachmentViews.clear();
alternateSampledViews.clear();
storageImageViews.clear();
image = VK_NULL_HANDLE; image = VK_NULL_HANDLE;
allocation = nullptr; allocation = nullptr;
layout = VK_IMAGE_LAYOUT_UNDEFINED; layout = VK_IMAGE_LAYOUT_UNDEFINED;
@@ -174,6 +249,7 @@ public:
aspect = VK_IMAGE_ASPECT_NONE; aspect = VK_IMAGE_ASPECT_NONE;
viewType = VK_IMAGE_VIEW_TYPE_2D; viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT; sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
@@ -198,6 +274,9 @@ public:
Uint32 baseArrayLayer, Uint32 layerCount, Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType); VkImageViewType viewType);
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
VkFormat format, Bool layered, Int32 layer);
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout); void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture, MG_State::GLState::ITextureObject* texture,
@@ -207,6 +286,9 @@ public:
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
@@ -255,7 +337,8 @@ private:
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer, Uint32 baseArrayLayer,
Uint32 layerCount, Uint32 layerCount,
const VkComponentMapping* components = nullptr) const; const VkComponentMapping* components = nullptr,
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture, Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget, TextureUploadTarget uploadTarget,
TextureResource &outResource); TextureResource &outResource);
@@ -19,11 +19,13 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h" #include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h> #include <Config.h>
#include <algorithm> #include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <vulkan/utility/vk_format_utils.h>
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#if defined(__APPLE__) #if defined(__APPLE__)
@@ -576,6 +578,88 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
template <typename ComponentT>
static Float ConvertIntegerVertexComponentToFloat(ComponentT value, Bool normalized) {
if (!normalized) {
return static_cast<Float>(value);
}
if constexpr (std::is_signed_v<ComponentT>) {
const Float scaled = static_cast<Float>(value) /
static_cast<Float>(std::numeric_limits<ComponentT>::max());
return std::max<Float>(-1.0f, scaled);
} else {
return static_cast<Float>(value) /
static_cast<Float>(std::numeric_limits<ComponentT>::max());
}
}
template <typename ComponentT>
static Bool ConvertIntegerVertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
SizeT sourceStride,
SizeT elementCount,
Vector<Float>& outData) {
if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attribute.Size);
outData.resize(elementCount * componentCount);
for (SizeT element = 0; element < elementCount; ++element) {
const Uint8* sourceElement = sourceData + element * sourceStride;
Float* destinationElement = outData.data() + element * componentCount;
for (SizeT component = 0; component < componentCount; ++component) {
ComponentT value{};
Memcpy(&value, sourceElement + component * sizeof(ComponentT), sizeof(ComponentT));
destinationElement[component] =
ConvertIntegerVertexComponentToFloat(value, attribute.Normalized);
}
}
return true;
}
static Bool ConvertScaledIntegerVertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
SizeT sourceStride,
SizeT elementCount,
Vector<Float>& outData) {
switch (attribute.Type) {
case DataType::Int8:
return ConvertIntegerVertexStreamToFloat32<Int8>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Uint8:
return ConvertIntegerVertexStreamToFloat32<Uint8>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Int16:
return ConvertIntegerVertexStreamToFloat32<Int16>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Uint16:
return ConvertIntegerVertexStreamToFloat32<Uint16>(
attribute, sourceData, sourceStride, elementCount, outData);
default:
return false;
}
}
static Bool RepackVertexStream(const Uint8* sourceData,
SizeT sourceStride,
SizeT elementSize,
SizeT elementCount,
Vector<Uint8>& outData) {
if (sourceData == nullptr || sourceStride == 0 || elementSize == 0) {
return false;
}
outData.resize(elementCount * elementSize);
for (SizeT element = 0; element < elementCount; ++element) {
Memcpy(outData.data() + element * elementSize,
sourceData + element * sourceStride,
elementSize);
}
return true;
}
static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) {
switch (format) { switch (format) {
case TextureInternalFormat::R8I: case TextureInternalFormat::R8I:
@@ -1041,6 +1125,7 @@ void main() {
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr; VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE;
VkFormat format = VK_FORMAT_UNDEFINED;
IntVec2 extent = {0, 0}; IntVec2 extent = {0, 0};
Uint32 mipLevel = 0; Uint32 mipLevel = 0;
Uint32 mipLevelCount = 1; Uint32 mipLevelCount = 1;
@@ -1204,6 +1289,7 @@ void main() {
outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.image = swapchainObject.GetImage(swapchainImageIndex);
outBinding.trackedLayout = nullptr; outBinding.trackedLayout = nullptr;
outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
outBinding.format = swapchainObject.GetSurfaceFormat().format;
const auto extent = swapchainObject.GetExtent(); const auto extent = swapchainObject.GetExtent();
outBinding.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)}; outBinding.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
outBinding.mipLevel = 0; outBinding.mipLevel = 0;
@@ -1252,6 +1338,7 @@ void main() {
outBinding.image = resource->image; outBinding.image = resource->image;
outBinding.trackedLayout = &resource->layout; outBinding.trackedLayout = &resource->layout;
outBinding.aspectMask = resource->aspect; outBinding.aspectMask = resource->aspect;
outBinding.format = resource->format;
const auto attachmentExtent = attachment.GetSize(); const auto attachmentExtent = attachment.GetSize();
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0)); outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
@@ -1498,19 +1585,54 @@ void main() {
} }
} }
static Bool IsBgraVkFormat(VkFormat format) { static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) {
switch (format) { switch (sourceFormat) {
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
rgba[0] = static_cast<Float>(source[0]) / 255.0f;
rgba[1] = static_cast<Float>(source[1]) / 255.0f;
rgba[2] = static_cast<Float>(source[2]) / 255.0f;
rgba[3] = static_cast<Float>(source[3]) / 255.0f;
return true;
case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SNORM:
case VK_FORMAT_B8G8R8A8_SRGB: case VK_FORMAT_B8G8R8A8_SRGB:
case VK_FORMAT_B8G8R8A8_USCALED: rgba[0] = static_cast<Float>(source[2]) / 255.0f;
case VK_FORMAT_B8G8R8A8_SSCALED: rgba[1] = static_cast<Float>(source[1]) / 255.0f;
rgba[2] = static_cast<Float>(source[0]) / 255.0f;
rgba[3] = static_cast<Float>(source[3]) / 255.0f;
return true;
case VK_FORMAT_R16G16B16A16_UNORM:
for (SizeT component = 0; component < 4; ++component) {
Uint16 value = 0;
Memcpy(&value, source + component * sizeof(value), sizeof(value));
rgba[component] = static_cast<Float>(value) / 65535.0f;
}
return true;
case VK_FORMAT_R16G16B16A16_SFLOAT:
for (SizeT component = 0; component < 4; ++component) {
Uint16 value = 0;
Memcpy(&value, source + component * sizeof(value), sizeof(value));
rgba[component] = MG_Util::DecodeHalfBitsToFloat(value);
}
return true;
case VK_FORMAT_R32G32B32A32_SFLOAT:
Memcpy(rgba, source, sizeof(Float) * 4);
return true; return true;
default: default:
return false; return false;
} }
} }
static Uint8 EncodeReadbackUnorm8(Float value) {
if (!(value > 0.0f)) {
return 0;
}
if (value >= 1.0f) {
return 255;
}
return static_cast<Uint8>(value * 255.0f + 0.5f);
}
// Remap raw swapchain pixels (top-left origin, preTransform-rotated) into // Remap raw swapchain pixels (top-left origin, preTransform-rotated) into
// GL-oriented pixels (bottom-left origin) for the retrace snapshot path. // GL-oriented pixels (bottom-left origin) for the retrace snapshot path.
// Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip // Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip
@@ -1520,6 +1642,7 @@ void main() {
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels, static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
VkExtent2D rawExtent, VkExtent2D rawExtent,
VkSurfaceTransformFlagBitsKHR preTransform, VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize,
Uint8* outPixels) { Uint8* outPixels) {
if (IsQuarterTurnPreTransform(preTransform)) { if (IsQuarterTurnPreTransform(preTransform)) {
return false; return false;
@@ -1543,12 +1666,9 @@ void main() {
default: default:
break; break;
} }
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * 4; const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * texelSize;
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * 4; Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * texelSize;
dst[0] = src[0]; Memcpy(dst, src, texelSize);
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
} }
} }
return true; return true;
@@ -1572,12 +1692,11 @@ void main() {
} }
} }
static void StoreReadbackPixel(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Uint8* dst) { static void StoreReadbackPixel(const Float* rgba, GLenum dstFormat, Uint8* dst) {
const Uint8 r = srcIsBgra ? src[2] : src[0]; const Uint8 r = EncodeReadbackUnorm8(rgba[0]);
const Uint8 g = src[1]; const Uint8 g = EncodeReadbackUnorm8(rgba[1]);
const Uint8 b = srcIsBgra ? src[0] : src[2]; const Uint8 b = EncodeReadbackUnorm8(rgba[2]);
const Uint8 a = src[3]; const Uint8 a = EncodeReadbackUnorm8(rgba[3]);
switch (dstFormat) { switch (dstFormat) {
case GL_RGB: case GL_RGB:
dst[0] = r; dst[0] = r;
@@ -1606,13 +1725,11 @@ void main() {
} }
} }
static void StoreReadbackPixelFloat(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Float* dst) { static void StoreReadbackPixelFloat(const Float* rgba, GLenum dstFormat, Float* dst) {
const Float r = static_cast<Float>(srcIsBgra ? src[2] : src[0]) / 255.0f; const Float r = rgba[0];
const Float g = static_cast<Float>(src[1]) / 255.0f; const Float g = rgba[1];
const Float b = static_cast<Float>(srcIsBgra ? src[0] : src[2]) / 255.0f; const Float b = rgba[2];
const Float a = static_cast<Float>(src[3]) / 255.0f; const Float a = rgba[3];
// TODO: extend readback packing to integer/depth formats instead of only normalized color formats.
switch (dstFormat) { switch (dstFormat) {
case GL_RGB: case GL_RGB:
dst[0] = r; dst[0] = r;
@@ -1670,20 +1787,11 @@ void main() {
(static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes); (static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes);
Vector<Uint8> packed(packedSize, 0); Vector<Uint8> packed(packedSize, 0);
const Bool srcIsBgra = IsBgraVkFormat(srcFormat); if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type,
for (GLsizei row = 0; row < height; ++row) { dstRowStride, packed.data() + dstOffset)) {
const Uint8* srcRow = srcPixels + static_cast<SizeT>(row) * static_cast<SizeT>(width) * 4; MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
Uint8* dstRow = packed.data() + dstOffset + static_cast<SizeT>(row) * dstRowStride; static_cast<Int>(srcFormat));
for (GLsizei col = 0; col < width; ++col) { return false;
const auto* src = srcRow + static_cast<SizeT>(col) * 4;
auto* dst = dstRow + static_cast<SizeT>(col) * static_cast<SizeT>(dstChannels) *
dstComponentBytes;
if (type == GL_FLOAT) {
StoreReadbackPixelFloat(src, srcIsBgra, format, reinterpret_cast<Float*>(dst));
} else {
StoreReadbackPixel(src, srcIsBgra, format, dst);
}
}
} }
const auto& pixelPackBufferObject = const auto& pixelPackBufferObject =
@@ -1705,6 +1813,60 @@ void main() {
} }
} // namespace } // namespace
SizeT VulkanRenderer::GetReadbackTexelSize(VkFormat sourceFormat) {
const VKU_FORMAT_INFO formatInfo = vkuGetFormatInfo(sourceFormat);
if (formatInfo.texels_per_block != 1) {
return 0;
}
return formatInfo.texel_block_size;
}
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels) {
if (width <= 0 || height <= 0) {
return true;
}
if (sourcePixels == nullptr || destinationPixels == nullptr) {
return false;
}
const SizeT sourceTexelSize = GetReadbackTexelSize(sourceFormat);
const Int destinationChannels = GetReadbackChannelCount(destinationFormat);
if (sourceTexelSize == 0 || destinationChannels == 0 ||
(destinationType != GL_UNSIGNED_BYTE && destinationType != GL_FLOAT)) {
return false;
}
const SizeT destinationComponentSize = destinationType == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
const SizeT destinationPixelSize = static_cast<SizeT>(destinationChannels) * destinationComponentSize;
if (destinationRowStride < static_cast<SizeT>(width) * destinationPixelSize) {
return false;
}
for (GLsizei row = 0; row < height; ++row) {
const Uint8* sourceRow = sourcePixels +
static_cast<SizeT>(row) * static_cast<SizeT>(width) * sourceTexelSize;
Uint8* destinationRow = destinationPixels + static_cast<SizeT>(row) * destinationRowStride;
for (GLsizei column = 0; column < width; ++column) {
const Uint8* source = sourceRow + static_cast<SizeT>(column) * sourceTexelSize;
Uint8* destination = destinationRow + static_cast<SizeT>(column) * destinationPixelSize;
Float rgba[4]{};
if (!DecodeReadbackPixel(source, sourceFormat, rgba)) {
return false;
}
if (destinationType == GL_FLOAT) {
Float converted[4]{};
StoreReadbackPixelFloat(rgba, destinationFormat, converted);
Memcpy(destination, converted, destinationPixelSize);
} else {
StoreReadbackPixel(rgba, destinationFormat, destination);
}
}
}
return true;
}
VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType, VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
@@ -1885,7 +2047,8 @@ void main() {
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config); m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings, m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled); m_shaderDrawParametersFeatureEnabled,
m_unformattedFloatStorageImagesEnabled);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
m_samplerManager = MakeUnique<VkSamplerManager>(); m_samplerManager = MakeUnique<VkSamplerManager>();
@@ -1905,7 +2068,7 @@ void main() {
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight, m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get()); maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config); m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle);
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image. // Prime the first frame so Render() always targets an acquired swapchain image.
@@ -1920,6 +2083,7 @@ void main() {
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
MGLOG_D("VulkanRenderer initialized"); MGLOG_D("VulkanRenderer initialized");
} }
@@ -2038,7 +2202,8 @@ void main() {
Bool VulkanRenderer::UploadAndBindVertexBuffers( Bool VulkanRenderer::UploadAndBindVertexBuffers(
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams) { const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams,
Bool indexedDraw) {
// programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat // programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw. // the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
@@ -2064,6 +2229,39 @@ void main() {
return nullptr; return nullptr;
}; };
auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion,
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData, SizeT sourceStride,
SizeT elementSize, SizeT elementCount,
BufferSlice& outSlice) -> Bool {
const void* uploadData = nullptr;
VkDeviceSize uploadSize = 0;
switch (conversion) {
case VertexInputStateFactory::VertexStreamConversion::Repack:
if (!RepackVertexStream(sourceData, sourceStride, elementSize, elementCount,
m_vertexRepackScratch)) {
return false;
}
uploadData = m_vertexRepackScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexRepackScratch.size());
break;
case VertexInputStateFactory::VertexStreamConversion::ScaledIntegerToFloat32:
if (!ConvertScaledIntegerVertexStreamToFloat32(attribute, sourceData, sourceStride,
elementCount, m_vertexConversionScratch)) {
return false;
}
uploadData = m_vertexConversionScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
break;
case VertexInputStateFactory::VertexStreamConversion::None:
return false;
}
return uploadSize > 0 &&
m_bufferManager.UploadTransient(BufferKind::Vertex,
m_frameContext.GetCurrentFrameIndex(),
uploadData, uploadSize, 16, outSlice);
};
for (SizeT binding = 0; binding < bindingCount; ++binding) { for (SizeT binding = 0; binding < bindingCount; ++binding) {
if (binding >= vertexInputState.bindings.size()) { if (binding >= vertexInputState.bindings.size()) {
break; break;
@@ -2073,28 +2271,53 @@ void main() {
: static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); : static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() && const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() &&
vertexInputState.bindingUsesClientMemory[binding]; vertexInputState.bindingUsesClientMemory[binding];
const auto conversion = binding < vertexInputState.bindingConversions.size()
? vertexInputState.bindingConversions[binding]
: VertexInputStateFactory::VertexStreamConversion::None;
if (usesClientMemory) { if (usesClientMemory) {
const Uint32 location = bindingLocation; const Uint32 location = bindingLocation;
MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"UploadAndBindVertexStreams failed to resolve client attribute location"); "UploadAndBindVertexStreams failed to resolve client attribute location");
const auto& attr = vao.GetAttribute(location); const auto& attr = vao.GetAttribute(location);
const SizeT componentSize = VertexInputStateFactory::GetComponentSize(attr.Type); const SizeT elementSize =
const SizeT elementSize = componentSize * static_cast<SizeT>(attr.Size); VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
const SizeT stride = attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize; const SizeT stride = attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
const auto* clientData = reinterpret_cast<const Uint8*>(attr.Offset); const auto* clientData = reinterpret_cast<const Uint8*>(attr.Offset);
if (!clientData || componentSize == 0 || elementSize == 0 || stride == 0) { if (!clientData || elementSize == 0 || stride == 0) {
MGLOG_E("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location); MGLOG_E("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location);
return false; return false;
} }
if (conversion != VertexInputStateFactory::VertexStreamConversion::None && indexedDraw) {
// The current indexed setup only carries indexCount, not the maximum effective
// index. Guessing a client-memory range here can truncate the converted stream.
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
"location=%u requires an indexed vertex range", location);
return false;
}
if (conversion != VertexInputStateFactory::VertexStreamConversion::None &&
drawParams.vertexCount == 0) {
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
"location=%u has an unknown vertex range", location);
return false;
}
const Uint32 lastVertex = drawParams.vertexCount > 0 const Uint32 lastVertex = drawParams.vertexCount > 0
? drawParams.firstVertex + drawParams.vertexCount - 1 ? drawParams.firstVertex + drawParams.vertexCount - 1
: drawParams.firstVertex; : drawParams.firstVertex;
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
BufferSlice slice{}; BufferSlice slice{};
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), Bool uploaded = false;
clientData, static_cast<VkDeviceSize>(uploadSize), 16, slice)) { if (conversion == VertexInputStateFactory::VertexStreamConversion::None) {
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
uploaded = m_bufferManager.UploadTransient(
BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData,
static_cast<VkDeviceSize>(uploadSize), 16, slice);
} else {
uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize,
static_cast<SizeT>(lastVertex) + 1, slice);
}
if (!uploaded) {
MOBILEGL_ASSERT(false, MOBILEGL_ASSERT(false,
"UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu", "UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu",
binding); binding);
@@ -2116,6 +2339,64 @@ void main() {
const auto& sourceBufferShared = *sourceBufferSharedPtr; const auto& sourceBufferShared = *sourceBufferSharedPtr;
BufferSlice slice{}; BufferSlice slice{};
const SizeT sourceSize = sourceBufferShared->GetSize(); const SizeT sourceSize = sourceBufferShared->GetSize();
const SizeT baseOffset =
binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0;
MOBILEGL_ASSERT(baseOffset <= sourceSize,
"UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu",
binding, baseOffset, sourceSize);
if (conversion != VertexInputStateFactory::VertexStreamConversion::None) {
MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"UploadAndBindVertexStreams failed to resolve converted attribute location");
const auto& attr = vao.GetAttribute(bindingLocation);
const SizeT elementSize =
VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
const SizeT sourceStride =
attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || sourceStride == 0 ||
baseOffset > sourceSize || elementSize > sourceSize - baseOffset) {
MGLOG_E("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu "
"location=%u base=%zu size=%zu element=%zu stride=%zu",
binding, bindingLocation, baseOffset, sourceSize, elementSize, sourceStride);
return false;
}
sourceBufferShared->SyncPersistentMappedRange();
const SizeT elementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
const ConvertedVertexStreamKey cacheKey{
.buffer = sourceBufferShared.get(),
.changeSerial = sourceBufferShared->GetChangeSerial(),
.baseOffset = baseOffset,
.sourceStride = static_cast<Uint32>(sourceStride),
.type = attr.Type,
.size = attr.Size,
.normalized = attr.Normalized,
.isInteger = attr.IsInteger,
.conversion = conversion,
};
const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped();
auto cached = cacheable ? m_convertedVertexStreams.find(cacheKey)
: m_convertedVertexStreams.end();
if (cached != m_convertedVertexStreams.end()) {
slice = cached->second;
} else {
const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset;
if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride,
elementSize, elementCount, slice)) {
MGLOG_E("UploadAndBindVertexStreams skipped: failed to convert binding=%zu location=%u",
binding, bindingLocation);
return false;
}
if (cacheable) {
m_convertedVertexStreams.emplace(cacheKey, slice);
}
}
vkBuffers[binding] = slice.buffer;
vkOffsets[binding] = slice.offset;
continue;
}
if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) {
if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) {
MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding);
@@ -2128,11 +2409,6 @@ void main() {
} }
} }
vkBuffers[binding] = slice.buffer; vkBuffers[binding] = slice.buffer;
const SizeT baseOffset =
binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0;
MOBILEGL_ASSERT(baseOffset <= sourceSize,
"UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu",
binding, baseOffset, sourceSize);
vkOffsets[binding] = slice.offset + static_cast<VkDeviceSize>(baseOffset); vkOffsets[binding] = slice.offset + static_cast<VkDeviceSize>(baseOffset);
} }
@@ -3246,6 +3522,44 @@ void main() {
return pipeline; return pipeline;
} }
Bool VulkanRenderer::PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj) {
auto& storageTextures = m_storageImageTexturesScratch;
if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) {
MGLOG_E("%s: failed to collect storage images for program=%u",
__func__, program.GetExternalIndex());
return false;
}
if (storageTextures.empty()) {
return true;
}
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
// a classic render pass. Do this before sampler preparation as well: a texture used by
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
// same layout independent of SPIR-V reflection/binding order.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(commandBuffer);
}
for (auto* texture : storageTextures) {
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
__func__, texture->GetExternalIndex());
return false;
}
if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) {
MGLOG_E("%s: failed to prepare storage textureId=%d",
__func__, texture->GetExternalIndex());
return false;
}
}
return true;
}
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects, Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) { const IndexBufferView* pIndexBufferView) {
@@ -3273,6 +3587,11 @@ void main() {
m_lastSampledSetValid = false; m_lastSampledSetValid = false;
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("SetupDraw skipped: storage image preparation failed");
return false;
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
// Check if any of the textures to sample have pending clears, // Check if any of the textures to sample have pending clears,
@@ -3443,7 +3762,8 @@ void main() {
return false; return false;
} }
auto vtxUploadOk = UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams); auto vtxUploadOk = UploadAndBindVertexBuffers(
frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView != nullptr);
if (!vtxUploadOk) { if (!vtxUploadOk) {
MGLOG_E("SetupDraw skipped: failed to upload vertex buffers"); MGLOG_E("SetupDraw skipped: failed to upload vertex buffers");
return false; return false;
@@ -3492,6 +3812,11 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u", MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u",
@@ -3527,6 +3852,11 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
MGLOG_E("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u", MGLOG_E("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u",
@@ -5090,6 +5420,7 @@ void main() {
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
} }
} }
Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) { Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) {
@@ -5174,7 +5505,15 @@ void main() {
return; return;
} }
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4; const VkFormat srcFormat = srcBinding.format;
const SizeT sourceTexelSize = GetReadbackTexelSize(srcFormat);
if (sourceTexelSize == 0) {
MGLOG_E("DirectVulkan::ReadPixels skipped: unsupported source format=%d",
static_cast<Int>(srcFormat));
return;
}
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
static_cast<VkDeviceSize>(height) * sourceTexelSize;
VkBufferObject readback; VkBufferObject readback;
if (!readback.Create({ if (!readback.Create({
.allocator = m_allocator, .allocator = m_allocator,
@@ -5210,7 +5549,7 @@ void main() {
VkBufferImageCopy copyRegion{}; VkBufferImageCopy copyRegion{};
copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask; copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask;
copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel; copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel;
copyRegion.imageSubresource.baseArrayLayer = 0; copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
copyRegion.imageSubresource.layerCount = 1; copyRegion.imageSubresource.layerCount = 1;
copyRegion.imageOffset = {x, y, 0}; copyRegion.imageOffset = {x, y, 0};
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}; copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
@@ -5245,14 +5584,18 @@ void main() {
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer"); MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
return; return;
} }
const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM; if (!readback.Invalidate(readbackSize)) {
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to invalidate readback buffer");
return;
}
if (readIsDefaultFbo) { if (readIsDefaultFbo) {
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent(); const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
if (static_cast<Uint32>(width) == swapchainExtent.width && if (static_cast<Uint32>(width) == swapchainExtent.width &&
static_cast<Uint32>(height) == swapchainExtent.height) { static_cast<Uint32>(height) == swapchainExtent.height) {
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4); Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform, if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
sourceTexelSize,
remapped.data())) { remapped.data())) {
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels); PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
return; return;
@@ -5318,9 +5661,10 @@ void main() {
} }
if (bufSize >= 0) { if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format); const Int dstChannels = GetReadbackChannelCount(format);
if (type == GL_UNSIGNED_BYTE && dstChannels > 0) { if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
const SizeT dstComponentSize = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
const SizeT minSize = static_cast<SizeT>(width) * static_cast<SizeT>(height) * const SizeT minSize = static_cast<SizeT>(width) * static_cast<SizeT>(height) *
static_cast<SizeT>(dstChannels); static_cast<SizeT>(dstChannels) * dstComponentSize;
if (static_cast<SizeT>(bufSize) < minSize) { if (static_cast<SizeT>(bufSize) < minSize) {
MGLOG_E("DirectVulkan::GetTextureImage skipped: destination buffer is too small"); MGLOG_E("DirectVulkan::GetTextureImage skipped: destination buffer is too small");
return; return;
@@ -5328,7 +5672,14 @@ void main() {
} }
} }
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4; const SizeT sourceTexelSize = GetReadbackTexelSize(resource->format);
if (sourceTexelSize == 0) {
MGLOG_E("DirectVulkan::GetTexImage skipped: unsupported source format=%d",
static_cast<Int>(resource->format));
return;
}
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
static_cast<VkDeviceSize>(height) * sourceTexelSize;
VkBufferObject readback; VkBufferObject readback;
if (!readback.Create({ if (!readback.Create({
.allocator = m_allocator, .allocator = m_allocator,
@@ -5379,6 +5730,10 @@ void main() {
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to map readback buffer"); MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to map readback buffer");
return; return;
} }
if (!readback.Invalidate(readbackSize)) {
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
return;
}
PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels); PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels);
} }
@@ -6331,6 +6686,7 @@ void main() {
CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex()); CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex());
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
// Descriptor-set reuse cursors rewind exactly once per frame, here, // Descriptor-set reuse cursors rewind exactly once per frame, here,
// after the slot's fence wait proved its previous sets GPU-idle. (The // after the slot's fence wait proved its previous sets GPU-idle. (The
// per-draw-path lazy rewind missed frames whose recording was opened // per-draw-path lazy rewind missed frames whose recording was opened
@@ -6648,6 +7004,10 @@ void main() {
vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures); vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures);
VkPhysicalDeviceFeatures deviceFeatures{}; VkPhysicalDeviceFeatures deviceFeatures{};
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
// out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight
// repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules.
deviceFeatures.robustBufferAccess = supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
@@ -6661,6 +7021,23 @@ void main() {
deviceFeatures.wideLines = supportedDeviceFeatures.wideLines; deviceFeatures.wideLines = supportedDeviceFeatures.wideLines;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64; deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64;
// Required for desktop GL image load/store semantics. iterationRP writes storage
// images from vertex and fragment stages and uses formats outside Vulkan's small
// mandatory storage-image set.
deviceFeatures.vertexPipelineStoresAndAtomics =
supportedDeviceFeatures.vertexPipelineStoresAndAtomics;
deviceFeatures.fragmentStoresAndAtomics = supportedDeviceFeatures.fragmentStoresAndAtomics;
deviceFeatures.shaderStorageImageExtendedFormats =
supportedDeviceFeatures.shaderStorageImageExtendedFormats;
// The formatless float-storage compatibility path must be all-or-nothing: transformed
// modules declare both capabilities and image bindings may be read, written, or both.
m_unformattedFloatStorageImagesEnabled =
supportedDeviceFeatures.shaderStorageImageReadWithoutFormat == VK_TRUE &&
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat == VK_TRUE;
if (m_unformattedFloatStorageImagesEnabled) {
deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE;
deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE;
}
deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance; deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance;
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect; deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE; m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
@@ -6780,8 +7157,12 @@ void main() {
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size()); deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
"shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s", "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s "
"fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s "
"storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s "
"multiDrawIndirect=%s",
supportedDeviceFeatures.robustBufferAccess ? "true" : "false",
supportedDeviceFeatures.geometryShader ? "true" : "false", supportedDeviceFeatures.geometryShader ? "true" : "false",
supportedDeviceFeatures.independentBlend ? "true" : "false", supportedDeviceFeatures.independentBlend ? "true" : "false",
supportedDeviceFeatures.logicOp ? "true" : "false", supportedDeviceFeatures.logicOp ? "true" : "false",
@@ -6789,11 +7170,19 @@ void main() {
supportedDeviceFeatures.shaderCullDistance ? "true" : "false", supportedDeviceFeatures.shaderCullDistance ? "true" : "false",
supportedDeviceFeatures.wideLines ? "true" : "false", supportedDeviceFeatures.wideLines ? "true" : "false",
supportedDeviceFeatures.shaderInt64 ? "true" : "false", supportedDeviceFeatures.shaderInt64 ? "true" : "false",
supportedDeviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false",
supportedDeviceFeatures.fragmentStoresAndAtomics ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false",
supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false", supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false",
supportedDeviceFeatures.multiDrawIndirect ? "true" : "false"); supportedDeviceFeatures.multiDrawIndirect ? "true" : "false");
MGLOG_I("Device feature enabled: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " MGLOG_I("Device feature enabled: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
"shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s " "shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s "
"shaderDrawParameters=%s", "fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s "
"storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s "
"multiDrawIndirect=%s shaderDrawParameters=%s",
deviceFeatures.robustBufferAccess ? "true" : "false",
deviceFeatures.geometryShader ? "true" : "false", deviceFeatures.geometryShader ? "true" : "false",
deviceFeatures.independentBlend ? "true" : "false", deviceFeatures.independentBlend ? "true" : "false",
deviceFeatures.logicOp ? "true" : "false", deviceFeatures.logicOp ? "true" : "false",
@@ -6801,6 +7190,11 @@ void main() {
deviceFeatures.shaderCullDistance ? "true" : "false", deviceFeatures.shaderCullDistance ? "true" : "false",
deviceFeatures.wideLines ? "true" : "false", deviceFeatures.wideLines ? "true" : "false",
deviceFeatures.shaderInt64 ? "true" : "false", deviceFeatures.shaderInt64 ? "true" : "false",
deviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false",
deviceFeatures.fragmentStoresAndAtomics ? "true" : "false",
deviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false",
deviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false",
deviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false",
deviceFeatures.drawIndirectFirstInstance ? "true" : "false", deviceFeatures.drawIndirectFirstInstance ? "true" : "false",
deviceFeatures.multiDrawIndirect ? "true" : "false", deviceFeatures.multiDrawIndirect ? "true" : "false",
m_shaderDrawParametersFeatureEnabled ? "true" : "false"); m_shaderDrawParametersFeatureEnabled ? "true" : "false");
@@ -7127,6 +7521,7 @@ void main() {
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
} }
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
} }
} }
@@ -165,6 +165,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type, TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
@@ -365,6 +370,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_samplerAnisotropyFeatureEnabled = false; Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false;
Bool m_unformattedFloatStorageImagesEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them. // drive a runtime fallback when the device lacks them.
@@ -437,9 +443,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every // Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate. // draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch; Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch; Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch; Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch; Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
Vector<Float> m_vertexConversionScratch;
Vector<Uint8> m_vertexRepackScratch;
struct ConvertedVertexStreamKey {
const MG_State::GLState::BufferObject* buffer = nullptr;
Uint64 changeSerial = 0;
SizeT baseOffset = 0;
Uint32 sourceStride = 0;
DataType type = DataType::Float32;
Int size = 0;
Bool normalized = false;
Bool isInteger = false;
VertexInputStateFactory::VertexStreamConversion conversion =
VertexInputStateFactory::VertexStreamConversion::None;
Bool operator==(const ConvertedVertexStreamKey& other) const {
return buffer == other.buffer && changeSerial == other.changeSerial &&
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
type == other.type && size == other.size && normalized == other.normalized &&
isInteger == other.isInteger && conversion == other.conversion;
}
};
struct ConvertedVertexStreamKeyHash {
SizeT operator()(const ConvertedVertexStreamKey& key) const {
SizeT hash = std::hash<const void*>{}(key.buffer);
auto combine = [&hash](SizeT value) {
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
};
combine(std::hash<Uint64>{}(key.changeSerial));
combine(std::hash<SizeT>{}(key.baseOffset));
combine(std::hash<Uint32>{}(key.sourceStride));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
combine(std::hash<Int>{}(key.size));
combine(std::hash<Bool>{}(key.normalized));
combine(std::hash<Bool>{}(key.isInteger));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
return hash;
}
};
UnorderedMap<ConvertedVertexStreamKey, BufferSlice, ConvertedVertexStreamKeyHash>
m_convertedVertexStreams;
void CreateInstance(); void CreateInstance();
VkResult SetupDebugMessenger(); VkResult SetupDebugMessenger();
@@ -462,10 +512,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); void DestroyComputePipelines();
Bool PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams); const DrawCmdParam& drawParams, Bool indexedDraw);
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame, Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView = nullptr); const IndexBufferView* pIndexBufferView = nullptr);
@@ -998,8 +998,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
@@ -1063,7 +1063,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture,
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
+8 -5
View File
@@ -1175,10 +1175,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentInputComponents; *params = kFrontendMaxFragmentInputComponents;
return; return;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
// TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap.
*params = MG_Backend::pActiveBackendObject *params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms; : MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms;
return; return;
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS: case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
*params = kFrontendMaxFragmentUniformComponents; *params = kFrontendMaxFragmentUniformComponents;
@@ -1208,7 +1207,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryTextureImageUnits; *params = kFrontendMaxGeometryTextureImageUnits;
return; return;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
*params = 0; *params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
return; return;
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS: case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
*params = kFrontendMaxGeometryTotalOutputComponents; *params = kFrontendMaxGeometryTotalOutputComponents;
@@ -1277,7 +1278,9 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxVertexAtomicCounters; *params = kFrontendMaxVertexAtomicCounters;
return; return;
case GL_MAX_VERTEX_IMAGE_UNIFORMS: case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*params = 0; *params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return; return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO *params = 16; // TODO
@@ -470,6 +470,210 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject; return textureObject;
} }
namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
const char* caller) {
if (texture == 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Clear texture operations require a non-zero texture name.");
return nullptr;
}
auto textureObject = GetTextureObjectByName(texture, caller);
if (!textureObject) return nullptr;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Buffer textures cannot be cleared with glClearTexImage.");
return nullptr;
}
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
if (level < 0 || static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
std::format("Texture level {} is not defined.", level));
return nullptr;
}
return mipmapTexture;
}
Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
!TextureImpl::ValidateTexturePixelDataType(inputType) ||
!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
inputFormat, textureObject->GetFormat(), inputType)) {
return false;
}
clearPixel.clear();
if (data == nullptr) {
// ARB_clear_texture defines a null clear value as all zeroes. Keeping the
// pattern empty lets the region writer use a fast memset path.
return true;
}
PixelStoreParameters clearPixelStore{};
clearPixelStore.Alignment = 1;
SizeT clearPixelSize = 0;
void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
{1, 1, 1}, false, clearPixelSize);
if (!converted || clearPixelSize == 0) {
if (converted) free(converted);
return false;
}
clearPixel.resize(clearPixelSize);
Memcpy(clearPixel.data(), converted, clearPixelSize);
free(converted);
return true;
}
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
TextureUploadTarget uploadTarget, GLint level,
GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth,
const Vector<Uint8>& clearPixel, const char* caller) {
const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level has no storage.");
return false;
}
if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
width < 0 || height < 0 || depth < 0 ||
width > texelSize.x() - xoffset ||
height > texelSize.y() - yoffset ||
depth > texelSize.z() - zoffset) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
"The clear region lies outside the requested texture level.");
return false;
}
if (width == 0 || height == 0 || depth == 0) return true;
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(texelSize.z());
const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
if (byteSize == 0 || byteSize % texelCount != 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture storage cannot be cleared.");
return false;
}
const SizeT bytesPerTexel = byteSize / texelCount;
if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
RecordClearTextureError(
caller, ErrorCode::InvalidOperation,
std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
clearPixel.size(), bytesPerTexel));
return false;
}
auto* destination = static_cast<Uint8*>(
textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
if (!destination) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level could not be mapped.");
return false;
}
const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
Uint8* firstClearRow = nullptr;
for (GLsizei z = 0; z < depth; ++z) {
for (GLsizei y = 0; y < height; ++y) {
Uint8* row = destination +
static_cast<SizeT>(zoffset + z) * fullSliceBytes +
static_cast<SizeT>(yoffset + y) * fullRowBytes +
static_cast<SizeT>(xoffset) * bytesPerTexel;
if (firstClearRow) {
Memcpy(row, firstClearRow, clearRowBytes);
continue;
}
firstClearRow = row;
if (clearPixel.empty()) {
Memset(row, 0, clearRowBytes);
continue;
}
Memcpy(row, clearPixel.data(), bytesPerTexel);
SizeT filled = bytesPerTexel;
while (filled < clearRowBytes) {
const SizeT copySize = std::min(filled, clearRowBytes - filled);
Memcpy(row + filled, row, copySize);
filled += copySize;
}
}
}
textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
return true;
}
} // namespace
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
size.x(), size.y(), size.z(), clearPixel, __func__)) {
return;
}
}
}
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
const auto& uploadTargets = textureObject->GetUploadTargets();
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
if (zoffset < 0 || depth < 0 ||
static_cast<SizeT>(zoffset) > uploadTargets.size() ||
static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
RecordClearTextureError(__func__, ErrorCode::InvalidValue,
"The cube-map clear region selects invalid faces.");
return;
}
for (GLsizei face = 0; face < depth; ++face) {
if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
return;
}
}
return;
}
if (uploadTargets.empty()) {
RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
"The requested texture has no upload target.");
return;
}
ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
width, height, depth, clearPixel, __func__);
}
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum pname, GLint param, const char* caller) { GLenum pname, GLint param, const char* caller) {
const auto target = textureObject->GetTarget(); const auto target = textureObject->GetTarget();
@@ -3818,6 +4022,14 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height); CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
});
}
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
CopyTexSubImage1D_State(target, level, xoffset, x, y, width); CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
} }
@@ -11,6 +11,9 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format); GLenum format);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -95,6 +98,8 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border); GLsizei height, GLint border);
@@ -27,6 +27,13 @@ namespace {
struct FakeDriverState { struct FakeDriverState {
// Behavior knobs, configured per test before running the probe. // Behavior knobs, configured per test before running the probe.
GLint maxVertexSsboBlocks = 4; GLint maxVertexSsboBlocks = 4;
GLint glesMajorVersion = 3;
GLint glesMinorVersion = 1;
GLint maxVertexImageUniforms = 2;
GLint maxGeometryImageUniforms = 3;
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's // Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
// baseInstance word and exposes it through gl_InstanceID. // baseInstance word and exposes it through gl_InstanceID.
bool drawLeaksBaseInstanceWord = false; bool drawLeaksBaseInstanceWord = false;
@@ -88,13 +95,26 @@ namespace {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*data = g_fake.maxVertexSsboBlocks; *data = g_fake.maxVertexSsboBlocks;
break; break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms;
break;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
g_fake.maxGeometryImageUniformsQueried = true;
*data = g_fake.maxGeometryImageUniforms;
break;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
*data = g_fake.maxFragmentImageUniforms;
break;
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
*data = g_fake.maxComputeImageUniforms;
break;
// FillInGLESCapabilities reads the context version before running the // FillInGLESCapabilities reads the context version before running the
// baseInstance probe, which requires ES >= 3.1. // baseInstance probe, which requires ES >= 3.1.
case GL_MAJOR_VERSION: case GL_MAJOR_VERSION:
*data = 3; *data = g_fake.glesMajorVersion;
break; break;
case GL_MINOR_VERSION: case GL_MINOR_VERSION:
*data = 1; *data = g_fake.glesMinorVersion;
break; break;
case GL_NUM_EXTENSIONS: case GL_NUM_EXTENSIONS:
*data = static_cast<GLint>(g_fake.extensions.size()); *data = static_cast<GLint>(g_fake.extensions.size());
@@ -417,6 +437,31 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
ExpectProbeReleasedAllObjects(); ExpectProbeReleasedAllObjects();
} }
TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGeometry) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_EQ(es31Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
EXPECT_EQ(es31Caps.MaxGeometryImageUniforms, 0);
EXPECT_EQ(es31Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
EXPECT_EQ(es31Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
EXPECT_FALSE(g_fake.maxGeometryImageUniformsQueried);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_EQ(es32Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
EXPECT_EQ(es32Caps.MaxGeometryImageUniforms, g_fake.maxGeometryImageUniforms);
EXPECT_EQ(es32Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
EXPECT_EQ(es32Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising // The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear. // it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) { TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
+83
View File
@@ -7,6 +7,7 @@
// End of Source File Header // End of Source File Header
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <spirv_reflect.h>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
@@ -1325,6 +1326,88 @@ TEST_F(ProgramTest, CompileAndLinkWithExplicitVertexIn) {
<< "\")"; << "\")";
} }
TEST_F(ProgramTest, InactiveExplicitVertexBindingsDoNotReserveLocations) {
const char* vertexSource = R"(#version 430 compatibility
in vec3 Position;
in vec2 UV0;
in vec3 vaPosition;
void main() {
gl_Position = vec4(vaPosition, 1.0);
}
)";
const char* fragmentSource = R"(#version 430 compatibility
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
}
)";
GLuint vertexShader = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vertexShader, 1, &vertexSource, nullptr);
CompileShader(vertexShader);
GLint compileStatus = GL_FALSE;
GetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
CompileShader(fragmentShader);
GetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint program = CreateProgram();
AttachShader(program, vertexShader);
AttachShader(program, fragmentShader);
// Iris binds these canonical names before linking every program. Its compatibility
// transformer can inject both declarations even when the shader pack instead reads
// vaPosition. Inactive API bindings must not consume locations during the link.
BindAttribLocation(program, 0, "Position");
BindAttribLocation(program, 1, "UV0");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
EXPECT_EQ(GetAttribLocation(program, "Position"), -1);
EXPECT_EQ(GetAttribLocation(program, "UV0"), -1);
EXPECT_EQ(GetAttribLocation(program, "vaPosition"), 0);
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const Int vertexIndex = programObject->GetShaderIndexByStage(ShaderStage::Vertex);
ASSERT_GE(vertexIndex, 0);
const auto& spirvs = programObject->GetGeneratedSpirv();
ASSERT_LT(static_cast<SizeT>(vertexIndex), spirvs.size());
const auto& vertexSpirv = spirvs[vertexIndex];
spv_reflect::ShaderModule reflection(vertexSpirv.size() * sizeof(Uint), vertexSpirv.data());
ASSERT_EQ(reflection.GetResult(), SPV_REFLECT_RESULT_SUCCESS);
uint32_t inputCount = 0;
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, nullptr), SPV_REFLECT_RESULT_SUCCESS);
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
Uint32 userInputCount = 0;
Uint32 locationMask = 0;
for (const auto* input : inputs) {
if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) {
continue;
}
ASSERT_LT(input->location, 32u);
locationMask |= 1u << input->location;
++userInputCount;
}
EXPECT_EQ(userInputCount, 1u);
EXPECT_EQ(locationMask, 0x1u);
}
TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) { TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) {
char infoLog[1024] = ""; char infoLog[1024] = "";
@@ -8,7 +8,9 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstring>
#include <string> #include <string>
#include <utility>
#include "Includes.h" #include "Includes.h"
#include "Init.h" #include "Init.h"
@@ -92,6 +94,120 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
EXPECT_EQ(exactSamplerNameCount, 1u); EXPECT_EQ(exactSamplerNameCount, 1u);
} }
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(rgba16, binding = 0) uniform image2D floatImage;
layout(r32ui, binding = 1) uniform uimage2D atomicImage;
void main() {
ivec2 coordinate = ivec2(gl_GlobalInvocationID.xy);
imageStore(floatImage, coordinate, imageLoad(floatImage, coordinate));
imageAtomicAdd(atomicImage, coordinate, 1u);
}
)";
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
const auto& inputBinary = binaryResult->front();
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String inputText;
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
EXPECT_NE(inputText.find("2D 0 0 0 2 Rgba16"), String::npos) << inputText;
EXPECT_NE(inputText.find("2D 0 0 0 2 R32ui"), String::npos) << inputText;
EXPECT_EQ(inputText.find("StorageImageReadWithoutFormat"), String::npos) << inputText;
EXPECT_EQ(inputText.find("StorageImageWriteWithoutFormat"), String::npos) << inputText;
Vector<Uint32> outputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
String outputText;
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_EQ(outputText.find("2D 0 0 0 2 Rgba16"), String::npos) << outputText;
EXPECT_NE(outputText.find("2D 0 0 0 2 Unknown"), String::npos) << outputText;
EXPECT_NE(outputText.find("2D 0 0 0 2 R32ui"), String::npos) << outputText;
const auto countOccurrences = [](const String& text, const String& needle) {
SizeT count = 0;
for (SizeT offset = 0; (offset = text.find(needle, offset)) != String::npos;
offset += needle.size()) {
++count;
}
return count;
};
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageReadWithoutFormat"), 1u)
<< outputText;
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageWriteWithoutFormat"), 1u)
<< outputText;
EXPECT_TRUE(tools.Validate(outputBinary));
Vector<Uint32> secondOutputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(outputBinary, secondOutputBinary));
EXPECT_EQ(secondOutputBinary, outputBinary);
}
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepFloatAtomicImageTypesTyped) {
using namespace MG_Util::ShaderTranspiler;
const String spirvText = R"(
OpCapability Shader
OpCapability StorageImageExtendedFormats
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
OpDecorate %target DescriptorSet 0
OpDecorate %target Binding 0
%void = OpTypeVoid
%float = OpTypeFloat 32
%int = OpTypeInt 32 1
%v2int = OpTypeVector %int 2
%image = OpTypeImage %float 2D 0 0 0 2 R32f
%imageUniformPtr = OpTypePointer UniformConstant %image
%imageTexelPtr = OpTypePointer Image %float
%mainType = OpTypeFunction %void
%zero = OpConstant %int 0
%coordinate = OpConstantComposite %v2int %zero %zero
%target = OpVariable %imageUniformPtr UniformConstant
%main = OpFunction %void None %mainType
%entry = OpLabel
%texelPtr = OpImageTexelPointer %imageTexelPtr %target %coordinate %zero
OpReturn
OpFunctionEnd
)";
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
Vector<Uint32> inputBinary;
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
Vector<Uint32> outputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
String outputText;
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_NE(outputText.find("2D 0 0 0 2 R32f"), String::npos) << outputText;
EXPECT_EQ(outputText.find("StorageImageReadWithoutFormat"), String::npos) << outputText;
EXPECT_EQ(outputText.find("StorageImageWriteWithoutFormat"), String::npos) << outputText;
String validationDiagnostics;
tools.SetMessageConsumer([&validationDiagnostics](spv_message_level_t, const char*,
const spv_position_t&, const char* message) {
validationDiagnostics += message;
});
EXPECT_TRUE(tools.Validate(outputBinary)) << validationDiagnostics;
}
TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) { TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) {
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
@@ -1383,3 +1499,128 @@ void main() {
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos); EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos); EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
} }
namespace {
String MakeLinearSubgroupPrefixScanShader() {
return R"(#version 460 core
#extension GL_KHR_shader_subgroup_arithmetic : enable
layout(local_size_x = 1024) in;
shared float prefixSumCache[64];
layout(std430, binding = 0) writeonly buffer OutputBuffer {
float outputValues[];
};
void main() {
float importance = 1.0f;
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0; i < loopLength; i++) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
barrier();
float sum = prefixSumCache[0];
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
outputValues[gl_GlobalInvocationID.x] = warp;
}
)";
}
} // namespace
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeLinearSubgroupPrefixScanShader();
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
const String onceRewritten = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, onceRewritten);
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
String validationDiagnostics;
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
validationDiagnostics += message;
validationDiagnostics += '\n';
});
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
String spirvText;
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
using namespace MG_Util::ShaderTranspiler;
const String original = MakeLinearSubgroupPrefixScanShader();
for (const auto& [stage, subgroupSize] :
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
std::pair{ShaderStage::Compute, Uint32{96}}}) {
String source = original;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
EXPECT_EQ(source, original);
}
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
using namespace MG_Util::ShaderTranspiler;
const auto expectUnchanged = [](String source) {
const String original = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, original);
};
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
"local_size_x = 512");
expectUnchanged(std::move(wrongLocalSize));
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
expectUnchanged(std::move(cacheHasAnotherUse));
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
expectUnchanged(std::move(extraSubgroupBuiltin));
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
expectUnchanged(std::move(alteredBarrier));
String nestedScan = MakeLinearSubgroupPrefixScanShader();
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
ASSERT_NE(consumerEnd, String::npos);
nestedScan.insert(consumerEnd + 1, "\n }");
expectUnchanged(std::move(nestedScan));
}
+334
View File
@@ -24,9 +24,12 @@
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h> #include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h> #include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h> #include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h> #include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h> #include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
@@ -460,6 +463,58 @@ TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendSta
EXPECT_EQ(lowParams.MaxColorAttachments, 6); EXPECT_EQ(lowParams.MaxColorAttachments, 6);
} }
TEST(DirectVulkanSanity, GatesPerStageImageUniformLimitsOnPhysicalDeviceFeatures) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities caps;
caps.MaxImageUnits = 12;
caps.MaxCombinedImageUniforms = 10;
caps.MaxComputeImageUniforms = 9;
caps.SupportsVertexPipelineStoresAndAtomics = true;
caps.SupportsFragmentStoresAndAtomics = true;
caps.SupportsGeometryShader = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
const auto& withoutGeometry = backend.GetDynamicParameters();
EXPECT_EQ(withoutGeometry.MaxVertexImageUniforms, 10);
EXPECT_EQ(withoutGeometry.MaxGeometryImageUniforms, 0);
EXPECT_EQ(withoutGeometry.MaxFragmentImageUniforms, 10);
EXPECT_EQ(withoutGeometry.MaxComputeImageUniforms, 9);
caps.SupportsGeometryShader = true;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 10);
caps.SupportsVertexPipelineStoresAndAtomics = false;
caps.SupportsFragmentStoresAndAtomics = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxVertexImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxFragmentImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxComputeImageUniforms, 9);
}
TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
using namespace MobileGL;
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
MG_External::GLESCapabilities caps;
caps.MaxImageUnits = 8;
caps.MaxCombinedImageUniforms = 16;
caps.MaxVertexImageUniforms = 2;
caps.MaxGeometryImageUniforms = 3;
caps.MaxFragmentImageUniforms = 4;
caps.MaxComputeImageUniforms = 5;
backend.ApplyGLESCapabilitiesForTesting(caps);
const auto& params = backend.GetDynamicParameters();
EXPECT_EQ(params.MaxVertexImageUniforms, 2);
EXPECT_EQ(params.MaxGeometryImageUniforms, 3);
EXPECT_EQ(params.MaxFragmentImageUniforms, 4);
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
}
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
using namespace MobileGL; using namespace MobileGL;
@@ -582,6 +637,54 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
} }
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
MG_Backend::DynamicBackendParameters params;
params.MaxImageUnits = 8;
params.MaxCombinedImageUniforms = 8;
params.MaxVertexImageUniforms = 1;
params.MaxGeometryImageUniforms = 2;
params.MaxFragmentImageUniforms = 3;
params.MaxComputeImageUniforms = 4;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 1);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 2);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 3);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 4);
const String vertexImageStore = R"(#version 430 core
layout(r32ui, binding = 0) uniform uimage2D targetImages[gl_MaxVertexImageUniforms];
void main() {
imageStore(targetImages[0], ivec2(0), uvec4(1));
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
auto supported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_VERTEX_SHADER,
.sourceStr = vertexImageStore,
});
EXPECT_TRUE(supported) << (supported ? "" : supported.error().log);
params.MaxVertexImageUniforms = 0;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 0);
auto unsupported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_VERTEX_SHADER,
.sourceStr = vertexImageStore,
});
EXPECT_FALSE(unsupported);
MG_Backend::pActiveBackendObject.reset();
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL; using namespace MobileGL;
@@ -629,6 +732,68 @@ TEST(DirectVulkanSanity, CommandMemoryBarrierMakesIndirectDrawCommandsVisible) {
EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u); EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
} }
TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R8G8B8A8_UNORM), 4u);
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R16G16B16A16_SFLOAT), 8u);
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
}
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
using MobileGL::MG_Util::EncodeFloatToHalfBits;
const MobileGL::Uint8 rgba8[] = {17, 34, 51, 68, 85, 102, 119, 136};
MobileGL::Uint8 rgba8Result[sizeof(rgba8)]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
rgba8, VK_FORMAT_R8G8B8A8_UNORM, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE,
sizeof(rgba8Result), rgba8Result));
EXPECT_TRUE(std::equal(std::begin(rgba8), std::end(rgba8), std::begin(rgba8Result)));
const MobileGL::Uint8 bgra8[] = {51, 34, 17, 68};
MobileGL::Uint8 bgra8Result[4]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
bgra8, VK_FORMAT_B8G8R8A8_UNORM, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
sizeof(bgra8Result), bgra8Result));
const MobileGL::Uint8 expectedBgra8[] = {17, 34, 51, 68};
EXPECT_TRUE(std::equal(std::begin(expectedBgra8), std::end(expectedBgra8), std::begin(bgra8Result)));
const MobileGL::Uint16 rgba16f[] = {
EncodeFloatToHalfBits(-0.25f), EncodeFloatToHalfBits(0.5f), EncodeFloatToHalfBits(1.5f),
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.5f),
EncodeFloatToHalfBits(0.75f), EncodeFloatToHalfBits(0.125f), EncodeFloatToHalfBits(-1.0f),
EncodeFloatToHalfBits(2.0f), EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.75f),
EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
};
constexpr MobileGL::SizeT kDestinationRowStride = 12;
MobileGL::Uint8 rgba16fResult[kDestinationRowStride * 2];
std::fill(std::begin(rgba16fResult), std::end(rgba16fResult), 0xCD);
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
2, 2, GL_RGBA, GL_UNSIGNED_BYTE, kDestinationRowStride, rgba16fResult));
const MobileGL::Uint8 expectedRgba16fRow0[] = {0, 128, 255, 255, 64, 0, 255, 128};
const MobileGL::Uint8 expectedRgba16fRow1[] = {191, 32, 0, 255, 255, 191, 64, 0};
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow0), std::end(expectedRgba16fRow0),
std::begin(rgba16fResult)));
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow1), std::end(expectedRgba16fRow1),
std::begin(rgba16fResult) + kDestinationRowStride));
EXPECT_TRUE(std::all_of(std::begin(rgba16fResult) + 8,
std::begin(rgba16fResult) + kDestinationRowStride,
[](MobileGL::Uint8 value) { return value == 0xCD; }));
MobileGL::Float rgba16fFloatResult[16]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
2, 2, GL_RGBA, GL_FLOAT, sizeof(MobileGL::Float) * 8,
reinterpret_cast<MobileGL::Uint8*>(rgba16fFloatResult)));
EXPECT_FLOAT_EQ(rgba16fFloatResult[0], -0.25f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[1], 0.5f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[2], 1.5f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[3], 1.0f);
}
TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) { TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) {
using namespace MobileGL::MG_Backend::DirectVulkan; using namespace MobileGL::MG_Backend::DirectVulkan;
@@ -675,6 +840,175 @@ TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) {
VK_IMAGE_ASPECT_DEPTH_BIT); VK_IMAGE_ASPECT_DEPTH_BIT);
} }
TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
struct FormatCase {
SpvImageFormat spirv;
VkFormat vulkan;
};
const FormatCase cases[] = {
{SpvImageFormatUnknown, VK_FORMAT_UNDEFINED},
{SpvImageFormatRgba32f, VK_FORMAT_R32G32B32A32_SFLOAT},
{SpvImageFormatRgba16f, VK_FORMAT_R16G16B16A16_SFLOAT},
{SpvImageFormatR32f, VK_FORMAT_R32_SFLOAT},
{SpvImageFormatRgba8, VK_FORMAT_R8G8B8A8_UNORM},
{SpvImageFormatRgba8Snorm, VK_FORMAT_R8G8B8A8_SNORM},
{SpvImageFormatRg32f, VK_FORMAT_R32G32_SFLOAT},
{SpvImageFormatRg16f, VK_FORMAT_R16G16_SFLOAT},
{SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32},
{SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT},
{SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM},
{SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32},
{SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM},
{SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM},
{SpvImageFormatR16, VK_FORMAT_R16_UNORM},
{SpvImageFormatR8, VK_FORMAT_R8_UNORM},
{SpvImageFormatRgba16Snorm, VK_FORMAT_R16G16B16A16_SNORM},
{SpvImageFormatRg16Snorm, VK_FORMAT_R16G16_SNORM},
{SpvImageFormatRg8Snorm, VK_FORMAT_R8G8_SNORM},
{SpvImageFormatR16Snorm, VK_FORMAT_R16_SNORM},
{SpvImageFormatR8Snorm, VK_FORMAT_R8_SNORM},
{SpvImageFormatRgba32i, VK_FORMAT_R32G32B32A32_SINT},
{SpvImageFormatRgba16i, VK_FORMAT_R16G16B16A16_SINT},
{SpvImageFormatRgba8i, VK_FORMAT_R8G8B8A8_SINT},
{SpvImageFormatR32i, VK_FORMAT_R32_SINT},
{SpvImageFormatRg32i, VK_FORMAT_R32G32_SINT},
{SpvImageFormatRg16i, VK_FORMAT_R16G16_SINT},
{SpvImageFormatRg8i, VK_FORMAT_R8G8_SINT},
{SpvImageFormatR16i, VK_FORMAT_R16_SINT},
{SpvImageFormatR8i, VK_FORMAT_R8_SINT},
{SpvImageFormatRgba32ui, VK_FORMAT_R32G32B32A32_UINT},
{SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT},
{SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT},
{SpvImageFormatR32ui, VK_FORMAT_R32_UINT},
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32},
{SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT},
{SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT},
{SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT},
{SpvImageFormatR16ui, VK_FORMAT_R16_UINT},
{SpvImageFormatR8ui, VK_FORMAT_R8_UINT},
{SpvImageFormatR64ui, VK_FORMAT_R64_UINT},
{SpvImageFormatR64i, VK_FORMAT_R64_SINT},
};
for (const auto& testCase : cases) {
EXPECT_EQ(ProgramFactory::ConvertSpirvImageFormatToVkFormat(testCase.spirv), testCase.vulkan)
<< "SpvImageFormat=" << static_cast<int>(testCase.spirv);
}
}
TEST(DirectVulkanSanity, MutableStorageImageViewsUseVulkanCompatibilityClasses) {
using MobileGL::MG_Backend::DirectVulkan::VkTextureManager;
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R8G8B8A8_UINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_SFLOAT));
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT));
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_D32_SFLOAT));
}
TEST(DirectVulkanSanity, StorageImageViewFormatUsesBindingOnlyForFormatlessFloatPolicy) {
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R16G16B16A16_UNORM, true),
VK_FORMAT_R16G16B16A16_SFLOAT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16, VK_FORMAT_R16G16B16A16_SFLOAT, true),
VK_FORMAT_R16G16B16A16_UNORM);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_R32_UINT, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
VK_FORMAT_R32_UINT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
VK_FORMAT_R32_SFLOAT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_NONE, VK_FORMAT_R16G16B16A16_SFLOAT, true),
VK_FORMAT_UNDEFINED);
}
TEST(DirectVulkanSanity, ProgramObjectMovePreservesStorageImageFormatPolicy) {
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
ProgramFactory::VkProgramObject source;
source.storageImageFormatByBinding = {VK_FORMAT_UNDEFINED, VK_FORMAT_R32_UINT};
source.storageImageUsesBindingFormatByBinding = {true, false};
ProgramFactory::VkProgramObject moved(std::move(source));
ASSERT_EQ(moved.storageImageFormatByBinding.size(), 2u);
ASSERT_EQ(moved.storageImageUsesBindingFormatByBinding.size(), 2u);
EXPECT_EQ(moved.storageImageFormatByBinding[0], VK_FORMAT_UNDEFINED);
EXPECT_EQ(moved.storageImageFormatByBinding[1], VK_FORMAT_R32_UINT);
EXPECT_TRUE(moved.storageImageUsesBindingFormatByBinding[0]);
EXPECT_FALSE(moved.storageImageUsesBindingFormatByBinding[1]);
ProgramFactory::VkProgramObject assigned;
assigned = std::move(moved);
ASSERT_EQ(assigned.storageImageFormatByBinding.size(), 2u);
ASSERT_EQ(assigned.storageImageUsesBindingFormatByBinding.size(), 2u);
EXPECT_TRUE(assigned.storageImageUsesBindingFormatByBinding[0]);
EXPECT_FALSE(assigned.storageImageUsesBindingFormatByBinding[1]);
}
TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_2D),
SamplerNumericDomain::Float);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW),
SamplerNumericDomain::Float);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_INT_SAMPLER_2D_ARRAY),
SamplerNumericDomain::SignedInteger);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_UNSIGNED_INT_SAMPLER_2D),
SamplerNumericDomain::UnsignedInteger);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_IMAGE_2D),
SamplerNumericDomain::Unknown);
}
TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R32_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::SignedInteger),
VK_FORMAT_R32_SINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_UINT, SamplerNumericDomain::Float),
VK_FORMAT_R32_SFLOAT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R16G16B16A16_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R16G16B16A16_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R8G8B8A8_UNORM, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R8G8B8A8_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_UINT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R32_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_UNDEFINED);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_UNDEFINED);
EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
EXPECT_FALSE(VkTextureManager::AreSampledImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_UINT));
}
TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) { TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) {
using namespace MobileGL; using namespace MobileGL;
+140
View File
@@ -158,6 +158,39 @@ namespace {
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level)); return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level));
} }
class ScopedTextureBackendFunctionsOverride {
public:
ScopedTextureBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {}
~ScopedTextureBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
private:
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
};
struct CopyTexSubImage2DCall {
Bool Called = false;
GLenum Target = GL_NONE;
GLint Level = -1;
GLint XOffset = -1;
GLint YOffset = -1;
GLint X = -1;
GLint Y = -1;
GLsizei Width = -1;
GLsizei Height = -1;
GLuint BoundTexture = 0;
} g_copyTexSubImage2DCall;
void RecordCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
g_copyTexSubImage2DCall = {
true, target, level, xoffset, yoffset, x, y, width, height,
MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit())
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject()
->GetExternalIndex(),
};
}
} // namespace } // namespace
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
@@ -178,6 +211,113 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
} }
TEST_F(TextureTest, ClearTexImageNullClearsWholeNamedTextureAndMarksStorageDirty) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 initialPixels[] = {
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
mipmapObject->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, false);
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
const Uint8 zeros[sizeof(initialPixels)] = {};
EXPECT_EQ(std::memcmp(stored, zeros, sizeof(zeros)), 0);
EXPECT_TRUE(mipmapObject->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, ClearTexImageRepeatsConvertedClearPixel) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8 clearPixel[] = {17, 34, 51, 68};
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, clearPixel);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
const Uint8 expected[] = {
17, 34, 51, 68,
17, 34, 51, 68,
17, 34, 51, 68,
17, 34, 51, 68,
};
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, ClearTexSubImageClearsOnlyRequestedRectangle) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
Uint8 initialPixels[3 * 2 * 4];
std::memset(initialPixels, 0x7f, sizeof(initialPixels));
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
MG_Impl::GLImpl::ClearTexSubImage(texture, 0, 1, 0, 0, 1, 2, 1,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
for (Int y = 0; y < 2; ++y) {
for (Int x = 0; x < 3; ++x) {
for (Int channel = 0; channel < 4; ++channel) {
EXPECT_EQ(stored[(y * 3 + x) * 4 + channel], x == 1 ? 0 : 0x7f);
}
}
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
g_copyTexSubImage2DCall = {};
GLuint namedTexture = 0;
GLuint boundTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &namedTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &boundTexture);
MG_Impl::GLImpl::BindTextureUnit(0, boundTexture);
const auto boundBefore = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject();
MG_Impl::GLImpl::CopyTextureSubImage2D(namedTexture, 2, 3, 4, 5, 6, 7, 8);
EXPECT_TRUE(g_copyTexSubImage2DCall.Called);
EXPECT_EQ(g_copyTexSubImage2DCall.Target, GL_TEXTURE_2D);
EXPECT_EQ(g_copyTexSubImage2DCall.Level, 2);
EXPECT_EQ(g_copyTexSubImage2DCall.XOffset, 3);
EXPECT_EQ(g_copyTexSubImage2DCall.YOffset, 4);
EXPECT_EQ(g_copyTexSubImage2DCall.X, 5);
EXPECT_EQ(g_copyTexSubImage2DCall.Y, 6);
EXPECT_EQ(g_copyTexSubImage2DCall.Width, 7);
EXPECT_EQ(g_copyTexSubImage2DCall.Height, 8);
EXPECT_EQ(g_copyTexSubImage2DCall.BoundTexture, namedTexture);
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject(),
boundBefore);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default. // is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) { TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
@@ -865,6 +865,9 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxUniformBlockSize = 16384; GLint maxUniformBlockSize = 16384;
GLint maxImageUnits = 8; GLint maxImageUnits = 8;
GLint maxCombinedImageUniforms = 8; GLint maxCombinedImageUniforms = 8;
GLint maxVertexImageUniforms = 0;
GLint maxGeometryImageUniforms = 0;
GLint maxFragmentImageUniforms = 8;
GLint maxComputeImageUniforms = 8; GLint maxComputeImageUniforms = 8;
GLint maxDrawBuffers = 8; GLint maxDrawBuffers = 8;
GLint maxColorAttachments = 8; GLint maxColorAttachments = 8;
@@ -904,7 +907,16 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize); glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits); glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms); glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
// Geometry shaders and their image-uniform query are core only in ES 3.2. DirectGLES
// emits ESSL 3.10 on an ES 3.1 context, so reporting zero there is both legal and an
// accurate description of what the backend compiler can consume.
if (caps.GLESVersion.Major > 3 ||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
}
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments); glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
@@ -955,6 +967,9 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxUniformBlockSize = maxUniformBlockSize; caps.MaxUniformBlockSize = maxUniformBlockSize;
caps.MaxImageUnits = maxImageUnits; caps.MaxImageUnits = maxImageUnits;
caps.MaxCombinedImageUniforms = maxCombinedImageUniforms; caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
caps.MaxVertexImageUniforms = maxVertexImageUniforms;
caps.MaxGeometryImageUniforms = maxGeometryImageUniforms;
caps.MaxFragmentImageUniforms = maxFragmentImageUniforms;
caps.MaxComputeImageUniforms = maxComputeImageUniforms; caps.MaxComputeImageUniforms = maxComputeImageUniforms;
caps.MaxDrawBuffers = maxDrawBuffers; caps.MaxDrawBuffers = maxDrawBuffers;
caps.MaxColorAttachments = maxColorAttachments; caps.MaxColorAttachments = maxColorAttachments;
@@ -1000,6 +1015,9 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize); MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize);
MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits); MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits);
MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms); MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms);
MGLOG_I(" GL_MAX_VERTEX_IMAGE_UNIFORMS: %d", caps.MaxVertexImageUniforms);
MGLOG_I(" GL_MAX_GEOMETRY_IMAGE_UNIFORMS: %d", caps.MaxGeometryImageUniforms);
MGLOG_I(" GL_MAX_FRAGMENT_IMAGE_UNIFORMS: %d", caps.MaxFragmentImageUniforms);
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms); MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers); MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments); MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
@@ -1102,6 +1102,9 @@ namespace MobileGL {
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8; Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
@@ -174,6 +174,10 @@ namespace MobileGL::MG_Util::BackendLoader {
VkPhysicalDeviceFeatures supportedFeatures{}; VkPhysicalDeviceFeatures supportedFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE; caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
caps.SupportsVertexPipelineStoresAndAtomics =
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
HasUsableShaderSubgroupSupport(subgroupProps); HasUsableShaderSubgroupSupport(subgroupProps);
@@ -256,6 +260,11 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1]; caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits); caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
caps.SupportsWideLines = false; caps.SupportsWideLines = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
caps.SupportsVertexPipelineStoresAndAtomics = false;
caps.SupportsFragmentStoresAndAtomics = false;
caps.SupportsGeometryShader = false;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
caps.SupportsShaderSubgroup = false; caps.SupportsShaderSubgroup = false;
caps.SubgroupSize = 0; caps.SubgroupSize = 0;
@@ -67,6 +67,12 @@ namespace MobileGL {
Float ViewportBoundsRangeMax = 0.0f; Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0; Int ViewportSubpixelBits = 0;
Bool SupportsWideLines = false; Bool SupportsWideLines = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
// require these core Vulkan features to be enabled on the logical device.
Bool SupportsVertexPipelineStoresAndAtomics = false;
Bool SupportsFragmentStoresAndAtomics = false;
Bool SupportsGeometryShader = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Bool SupportsShaderSubgroup = false; Bool SupportsShaderSubgroup = false;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
@@ -6,6 +6,10 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header // End of Source File Header
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h" #include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
@@ -19,14 +23,15 @@
#include "spirv-tools/optimizer.hpp" #include "spirv-tools/optimizer.hpp"
#include "ShaderSourceProcessor.h" #include "ShaderSourceProcessor.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h> #include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
TBuiltInResource& GetTBuiltInResourceInstance() { TBuiltInResource BuildTBuiltInResource() {
static TBuiltInResource Resources{}; TBuiltInResource Resources{};
Resources.maxLights = 32; Resources.maxLights = 32;
Resources.maxClipPlanes = 6; Resources.maxClipPlanes = 6;
Resources.maxTextureUnits = 32; Resources.maxTextureUnits = 32;
@@ -121,6 +126,22 @@ namespace MobileGL {
Resources.maxTaskWorkGroupSizeZ_NV = 1; Resources.maxTaskWorkGroupSizeZ_NV = 1;
Resources.maxMeshViewCountNV = 4; Resources.maxMeshViewCountNV = 4;
// Resource checking must describe the same backend contract exposed through
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
// TBuiltInResource when Iris compiles shaders concurrently.
const MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters =
activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters;
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs =
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms;
Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.limits.nonInductiveForLoops = true; Resources.limits.nonInductiveForLoops = true;
Resources.limits.whileLoops = true; Resources.limits.whileLoops = true;
Resources.limits.doWhileLoops = true; Resources.limits.doWhileLoops = true;
@@ -166,7 +187,8 @@ namespace MobileGL {
tshader->setAutoMapLocations(true); tshader->setAutoMapLocations(true);
tshader->setAutoMapBindings(true); tshader->setAutoMapBindings(true);
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
if (!tshader->parse(&GetTBuiltInResourceInstance(), 460, ECoreProfile, auto resources = BuildTBuiltInResource();
if (!tshader->parse(&resources, 460, ECoreProfile,
/*forceDefaultVersionAndProfile: */ false, /*forceDefaultVersionAndProfile: */ false,
/*forwardCompatible: */ true, EShMsgDefault)) { /*forwardCompatible: */ true, EShMsgDefault)) {
ResultInfo r; ResultInfo r;
@@ -322,6 +344,136 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
} }
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) {
constexpr SizeT kSpirvHeaderWordCount = 5;
outputBinary.clear();
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
return false;
}
Vector<Uint32> floatTypeIds;
Vector<Uint32> resultTypeById(inputBinary[3], 0);
Vector<Uint32> pointerPointeeTypeById(inputBinary[3], 0);
Bool hasReadWithoutFormatCapability = false;
Bool hasWriteWithoutFormatCapability = false;
SizeT capabilityInsertOffset = kSpirvHeaderWordCount;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
const Uint32 instructionWord = inputBinary[offset];
const Uint32 wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
return false;
}
if (opcode == spv::Op::OpCapability && wordCount >= 2) {
capabilityInsertOffset = offset + wordCount;
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
hasReadWithoutFormatCapability |=
capability == spv::Capability::StorageImageReadWithoutFormat;
hasWriteWithoutFormatCapability |=
capability == spv::Capability::StorageImageWriteWithoutFormat;
} else if (opcode == spv::Op::OpTypeFloat && wordCount >= 3) {
floatTypeIds.push_back(inputBinary[offset + 1]);
} else if (opcode == spv::Op::OpTypePointer && wordCount >= 4) {
const Uint32 pointerTypeId = inputBinary[offset + 1];
if (pointerTypeId >= pointerPointeeTypeById.size()) {
return false;
}
pointerPointeeTypeById[pointerTypeId] = inputBinary[offset + 3];
}
bool hasResult = false;
bool hasResultType = false;
spv::HasResultAndType(opcode, &hasResult, &hasResultType);
if (hasResult && hasResultType && wordCount >= 3) {
const Uint32 resultTypeId = inputBinary[offset + 1];
const Uint32 resultId = inputBinary[offset + 2];
if (resultId >= resultTypeById.size()) {
return false;
}
resultTypeById[resultId] = resultTypeId;
}
offset += wordCount;
}
// OpImageTexelPointer is the bridge to image atomic instructions. Vulkan requires
// those image types to retain an atomic-compatible declared format, so exclude only
// the exact image types used by an atomic path rather than disabling formatless
// access for unrelated float images in the same module.
Vector<Uint32> atomicImageTypeIds;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
const Uint32 instructionWord = inputBinary[offset];
const Uint32 wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
if (opcode == spv::Op::OpImageTexelPointer && wordCount >= 6) {
const Uint32 imageId = inputBinary[offset + 3];
if (imageId >= resultTypeById.size()) {
return false;
}
Uint32 imageTypeId = resultTypeById[imageId];
if (imageTypeId < pointerPointeeTypeById.size() &&
pointerPointeeTypeById[imageTypeId] != 0) {
imageTypeId = pointerPointeeTypeById[imageTypeId];
}
if (imageTypeId != 0 &&
std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) ==
atomicImageTypeIds.end()) {
atomicImageTypeIds.push_back(imageTypeId);
}
}
offset += wordCount;
}
outputBinary = inputBinary;
Bool hasFloatStorageImage = false;
for (SizeT offset = kSpirvHeaderWordCount; offset < outputBinary.size();) {
const Uint32 instructionWord = outputBinary[offset];
const Uint32 wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
// OpTypeImage operands are: result id, sampled type, dim, depth, arrayed,
// multisampled, sampled, image format, and an optional access qualifier.
if (opcode == spv::Op::OpTypeImage && wordCount >= 9) {
const Uint32 imageTypeId = outputBinary[offset + 1];
const Uint32 sampledTypeId = outputBinary[offset + 2];
const Uint32 sampled = outputBinary[offset + 7];
const Bool hasFloatSampledType =
std::find(floatTypeIds.begin(), floatTypeIds.end(), sampledTypeId) != floatTypeIds.end();
const Bool usedByAtomic =
std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) !=
atomicImageTypeIds.end();
if (sampled == 2 && hasFloatSampledType && !usedByAtomic) {
outputBinary[offset + 8] = static_cast<Uint32>(spv::ImageFormat::Unknown);
hasFloatStorageImage = true;
}
}
offset += wordCount;
}
if (!hasFloatStorageImage) {
return true;
}
Vector<Uint32> addedCapabilities;
const Uint32 capabilityInstruction =
(2u << 16u) | static_cast<Uint32>(spv::Op::OpCapability);
if (!hasReadWithoutFormatCapability) {
addedCapabilities.push_back(capabilityInstruction);
addedCapabilities.push_back(
static_cast<Uint32>(spv::Capability::StorageImageReadWithoutFormat));
}
if (!hasWriteWithoutFormatCapability) {
addedCapabilities.push_back(capabilityInstruction);
addedCapabilities.push_back(
static_cast<Uint32>(spv::Capability::StorageImageWriteWithoutFormat));
}
outputBinary.insert(outputBinary.begin() + static_cast<std::ptrdiff_t>(capabilityInsertOffset),
addedCapabilities.begin(), addedCapabilities.end());
return true;
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) { Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options; spvc_compiler_options options;
session.CreateOptions(&options); session.CreateOptions(&options);
@@ -39,6 +39,14 @@ namespace MobileGL {
// which wrongly includes baseInstance). // which wrongly includes baseInstance).
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary, static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary); Vector<uint32_t>& outputBinary);
// Replaces the declared format of float storage images with Unknown and adds the
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan
// shaderStorageImage*WithoutFormat features are enabled, allowing the
// glBindImageTexture format to select the descriptor view at runtime. Integer
// storage images deliberately keep their declared format for GL-compatible bit
// reinterpretation paths (for example, R32F storage accessed as r32ui).
static bool UseUnformattedFloatStorageImagesForVulkan(
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
static Result<String> DecompileShader(SpvcSession& session); static Result<String> DecompileShader(SpvcSession& session);
}; };
} // namespace ShaderTranspiler } // namespace ShaderTranspiler
@@ -10,10 +10,14 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <initializer_list>
#include <utility>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
namespace { namespace {
using MobileGL::SizeT; using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Vector;
bool IsIdentifierChar(char ch) { bool IsIdentifierChar(char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
@@ -91,6 +95,375 @@ namespace {
return masked; return masked;
} }
struct CodeToken {
String text;
SizeT begin = 0;
SizeT end = 0;
};
Vector<CodeToken> TokenizeCode(const String& source) {
const String masked = MaskCommentsAndQuotedText(source);
Vector<CodeToken> tokens;
tokens.reserve(source.size() / 4);
SizeT pos = 0;
while (pos < masked.size()) {
const char ch = masked[pos];
if (std::isspace(static_cast<unsigned char>(ch))) {
++pos;
continue;
}
const SizeT begin = pos;
if (IsIdentifierStart(ch)) {
++pos;
while (pos < masked.size() && IsIdentifierChar(masked[pos])) {
++pos;
}
} else if (std::isdigit(static_cast<unsigned char>(ch))) {
++pos;
while (pos < masked.size()) {
const char numberChar = masked[pos];
if (!IsIdentifierChar(numberChar) && numberChar != '.') {
break;
}
++pos;
}
} else {
++pos;
if (pos < masked.size()) {
const String twoChars = masked.substr(begin, 2);
if (twoChars == "==" || twoChars == "!=" || twoChars == "<=" || twoChars == ">=" ||
twoChars == "+=" || twoChars == "-=" || twoChars == "<<" || twoChars == ">>" ||
twoChars == "++" || twoChars == "--" || twoChars == "&&" || twoChars == "||") {
++pos;
}
}
}
tokens.push_back(CodeToken{source.substr(begin, pos - begin), begin, pos});
}
return tokens;
}
bool IsIdentifierToken(const CodeToken& token) {
if (token.text.empty() || !IsIdentifierStart(token.text.front())) {
return false;
}
return std::all_of(token.text.begin() + 1, token.text.end(), IsIdentifierChar);
}
class TokenCursor {
public:
TokenCursor(const Vector<CodeToken>& tokens, SizeT position) : m_tokens(tokens), m_position(position) {}
bool Consume(const char* expected) {
if (m_position >= m_tokens.size() || m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
bool ConsumeAnyIdentifier(String& identifier) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
identifier = m_tokens[m_position++].text;
return true;
}
bool ConsumeAnyIdentifier() {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
++m_position;
return true;
}
bool ConsumeIdentifier(const String& expected) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position]) ||
m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
SizeT Position() const { return m_position; }
private:
const Vector<CodeToken>& m_tokens;
SizeT m_position;
};
SizeT CountToken(const Vector<CodeToken>& tokens, const String& tokenText) {
return static_cast<SizeT>(std::count_if(tokens.begin(), tokens.end(),
[&](const CodeToken& token) { return token.text == tokenText; }));
}
bool HasIdentifierWithPrefixOutsideAllowed(const Vector<CodeToken>& tokens, const String& prefix,
std::initializer_list<const char*> allowedIdentifiers) {
return std::any_of(tokens.begin(), tokens.end(), [&](const CodeToken& token) {
if (!IsIdentifierToken(token) || !token.text.starts_with(prefix)) {
return false;
}
return std::none_of(allowedIdentifiers.begin(), allowedIdentifiers.end(),
[&](const char* allowed) { return token.text == allowed; });
});
}
bool MatchTokenSequence(const Vector<CodeToken>& tokens, SizeT position,
std::initializer_list<const char*> expected) {
if (position + expected.size() > tokens.size()) {
return false;
}
for (const char* token : expected) {
if (tokens[position++].text != token) {
return false;
}
}
return true;
}
struct LinearPrefixScanMatch {
SizeT sharedArraySizeBegin = 0;
SizeT sharedArraySizeEnd = 0;
SizeT scanBegin = 0;
SizeT scanEnd = 0;
String cache;
String importance;
String prefixSum;
String loopLength;
String loopIndex;
String sum;
};
bool ParseLinearPrefixScanTemplate(const Vector<CodeToken>& tokens, LinearPrefixScanMatch& match) {
// The workaround deliberately recognizes one complete algorithm, not merely the
// subgroupInclusiveAdd token. Changing scratch storage is only safe when that storage is
// private to this scan and the workgroup has exactly 1024 X invocations.
SizeT localSizeDeclarationCount = 0;
for (SizeT i = 0; i < tokens.size(); ++i) {
if (MatchTokenSequence(tokens, i, {"layout", "(", "local_size_x", "=", "1024", ")", "in", ";"})) {
++localSizeDeclarationCount;
}
}
if (localSizeDeclarationCount != 1) {
return false;
}
SizeT sharedDeclarationIndex = String::npos;
SizeT sharedDeclarationCount = 0;
String cacheName;
for (SizeT i = 0; i + 6 < tokens.size(); ++i) {
if (tokens[i].text != "shared" || tokens[i + 1].text != "float" || !IsIdentifierToken(tokens[i + 2]) ||
tokens[i + 3].text != "[" || tokens[i + 4].text != "64" || tokens[i + 5].text != "]" ||
tokens[i + 6].text != ";") {
continue;
}
++sharedDeclarationCount;
sharedDeclarationIndex = i;
cacheName = tokens[i + 2].text;
}
if (sharedDeclarationCount != 1) {
return false;
}
SizeT scanTokenIndex = String::npos;
SizeT scanCount = 0;
for (SizeT i = 0; i + 7 < tokens.size(); ++i) {
if (tokens[i].text == "float" && IsIdentifierToken(tokens[i + 1]) && tokens[i + 2].text == "=" &&
tokens[i + 3].text == "subgroupInclusiveAdd" && tokens[i + 4].text == "(" &&
IsIdentifierToken(tokens[i + 5]) && tokens[i + 6].text == ")" && tokens[i + 7].text == ";") {
++scanCount;
scanTokenIndex = i;
}
}
if (scanCount != 1 || sharedDeclarationIndex >= scanTokenIndex) {
return false;
}
TokenCursor cursor(tokens, scanTokenIndex);
String prefixSum;
String importance;
String loopLength;
String loopIndex;
String sum;
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(prefixSum) || !cursor.Consume("=") ||
!cursor.Consume("subgroupInclusiveAdd") || !cursor.Consume("(") ||
!cursor.ConsumeAnyIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume(";") ||
!cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") ||
!cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("uint") ||
!cursor.ConsumeAnyIdentifier(loopLength) || !cursor.Consume("=") || !cursor.Consume("uint") ||
!cursor.Consume("(") || !cursor.Consume("findMSB") || !cursor.Consume("(") ||
!cursor.Consume("gl_NumSubgroups") || !cursor.Consume(")") || !cursor.Consume(")") ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("+=") ||
!cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") ||
!cursor.Consume("-") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") ||
!cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("for") ||
!cursor.Consume("(") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopIndex) ||
!cursor.Consume("=") || !cursor.Consume("0") || !cursor.Consume(";") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<") || !cursor.ConsumeIdentifier(loopLength) ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("++") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("&") || !cursor.Consume("(") ||
!cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume("+=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume(">>") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume("]") ||
!cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") ||
!cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") ||
!cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("barrier") || !cursor.Consume("(") ||
!cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("if") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("==") || !cursor.Consume("uint") || !cursor.Consume("(") ||
!cursor.Consume("1024") || !cursor.Consume("-") || !cursor.Consume("1") || !cursor.Consume(")") ||
!cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("float") ||
!cursor.ConsumeAnyIdentifier(sum) || !cursor.Consume("=") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume(";")) {
return false;
}
const SizeT scanEndToken = cursor.Position() - 1;
// Require the scan's immediate consumer as well. This makes the match specific to a
// linear distribution warp, and avoids changing unrelated prefix scans which may rely on
// the implementation's native subgroup partitioning.
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier() || !cursor.Consume("=") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("-") ||
!cursor.ConsumeIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume("/") ||
!cursor.ConsumeIdentifier(sum) || !cursor.Consume("-") || !cursor.Consume("float") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("+") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.Consume("/") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("1024") ||
!cursor.Consume(")") || !cursor.Consume(";")) {
return false;
}
// No other use may share the scratch array, and no additional subgroup operation or
// builtin may silently retain native-64 semantics after this module becomes virtual-32.
if (CountToken(tokens, cacheName) != 6 || CountToken(tokens, "subgroupInclusiveAdd") != 1 ||
CountToken(tokens, "gl_SubgroupInvocationID") != 2 || CountToken(tokens, "gl_SubgroupSize") != 2 ||
CountToken(tokens, "gl_SubgroupID") != 4 || CountToken(tokens, "gl_NumSubgroups") != 2 ||
CountToken(tokens, "gl_LocalInvocationID") != 2 || CountToken(tokens, "barrier") != 3 ||
CountToken(tokens, "findMSB") != 1 ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
HasIdentifierWithPrefixOutsideAllowed(
tokens, "gl_Subgroup",
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"})) {
return false;
}
// The scan must be at the top level of the sole main() body. Its existing barriers already
// require uniform control flow; this check prevents us from introducing extra barriers in
// a nested branch or loop.
SizeT mainOpenBrace = String::npos;
SizeT mainCloseBrace = String::npos;
SizeT mainCount = 0;
for (SizeT i = 0; i + 4 < tokens.size(); ++i) {
if (!MatchTokenSequence(tokens, i, {"void", "main", "(", ")", "{"})) {
continue;
}
++mainCount;
mainOpenBrace = i + 4;
int depth = 1;
for (SizeT j = mainOpenBrace + 1; j < tokens.size(); ++j) {
if (tokens[j].text == "{")
++depth;
else if (tokens[j].text == "}" && --depth == 0) {
mainCloseBrace = j;
break;
}
}
}
if (mainCount != 1 || mainCloseBrace == String::npos || scanTokenIndex <= mainOpenBrace ||
scanEndToken >= mainCloseBrace) {
return false;
}
int depthAtScan = 1;
for (SizeT i = mainOpenBrace + 1; i < scanTokenIndex; ++i) {
if (tokens[i].text == "{")
++depthAtScan;
else if (tokens[i].text == "}")
--depthAtScan;
}
if (depthAtScan != 1) {
return false;
}
constexpr const char* injectedNames[] = {"mglPrefixScanLane", "mglVirtualSubgroupInvocation",
"mglVirtualSubgroup", "mglVirtualSubgroupBase",
"mglPrefixLane", "mglVirtualSubgroupCount"};
for (const char* injectedName : injectedNames) {
if (CountToken(tokens, injectedName) != 0) {
return false;
}
}
match.sharedArraySizeBegin = tokens[sharedDeclarationIndex + 4].begin;
match.sharedArraySizeEnd = tokens[sharedDeclarationIndex + 4].end;
match.scanBegin = tokens[scanTokenIndex].begin;
match.scanEnd = tokens[scanEndToken].end;
match.cache = std::move(cacheName);
match.importance = std::move(importance);
match.prefixSum = std::move(prefixSum);
match.loopLength = std::move(loopLength);
match.loopIndex = std::move(loopIndex);
match.sum = std::move(sum);
return true;
}
String BuildLinearPrefixScanReplacement(const LinearPrefixScanMatch& match) {
String replacement;
replacement.reserve(1800);
replacement += "uint mglPrefixScanLane = gl_LocalInvocationID.x;\n";
replacement += "uint mglVirtualSubgroupInvocation = mglPrefixScanLane & 31u;\n";
replacement += "uint mglVirtualSubgroup = mglPrefixScanLane >> 5u;\n";
replacement += "const uint mglVirtualSubgroupCount = 32u;\n";
replacement += match.cache + "[mglPrefixScanLane] = " + match.importance + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.prefixSum + " = 0.0f;\n";
replacement += "uint mglVirtualSubgroupBase = mglVirtualSubgroup << 5u;\n";
replacement += "for (uint mglPrefixLane = mglVirtualSubgroupBase; "
"mglPrefixLane <= mglPrefixScanLane; ++mglPrefixLane) {\n";
replacement += match.prefixSum + " += " + match.cache + "[mglPrefixLane];\n";
replacement += "}\n";
replacement += "barrier();\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "uint " + match.loopLength + " = uint(findMSB(mglVirtualSubgroupCount));\n";
replacement +=
match.loopLength + " += uint(mglVirtualSubgroupCount - (1u << (" + match.loopLength + " - 1u)) > 0u);\n";
replacement += "for (uint " + match.loopIndex + " = 0u; " + match.loopIndex + " < " + match.loopLength +
"; ++" + match.loopIndex + ") {\n";
replacement += "if ((mglVirtualSubgroup & (1u << " + match.loopIndex + ")) > 0u) {\n";
replacement += match.prefixSum + " += " + match.cache + "[(mglVirtualSubgroup >> " + match.loopIndex + " << " +
match.loopIndex + ") - 1u];\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "}\nbarrier();\n}\n";
replacement += "if (mglPrefixScanLane == 1023u) " + match.cache + "[0] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.sum + " = " + match.cache + "[0];";
return replacement;
}
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) { while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
pos++; pos++;
@@ -576,6 +949,36 @@ namespace {
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source) {
constexpr Uint32 capturedSubgroupSize = 32;
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
nativeSubgroupSize % capturedSubgroupSize != 0) {
return false;
}
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
// wider widths which are a power-of-two multiple of the captured 32-lane model.
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
return false;
}
const Vector<CodeToken> tokens = TokenizeCode(source);
LinearPrefixScanMatch match;
if (!ParseLinearPrefixScanTemplate(tokens, match)) {
return false;
}
const String replacement = BuildLinearPrefixScanReplacement(match);
source.replace(match.scanBegin, match.scanEnd - match.scanBegin, replacement);
// The declaration occurs before the replaced scan, so its original offsets remain
// valid after the first replacement.
source.replace(match.sharedArraySizeBegin, match.sharedArraySizeEnd - match.sharedArraySizeBegin,
"1024");
return true;
}
void PreprocessShaderSource(ShaderStage stage, String& source) { void PreprocessShaderSource(ShaderStage stage, String& source) {
// Normalize while the inspector's source span still refers to the untouched input. Later passes // Normalize while the inspector's source span still refers to the untouched input. Later passes
// remove comments and directives, so any subsequent insertion re-inspects the current source. // remove comments and directives, so any subsequent insertion re-inspects the current source.
@@ -631,6 +1034,13 @@ namespace MobileGL {
RenameBuiltinShadowingFunction(source, "max3", "mg_max3"); RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
ModernizeLegacyGLSL(stage, source); ModernizeLegacyGLSL(stage, source);
InjectDepthRangeBuiltinShim(stage, source); InjectDepthRangeBuiltinShim(stage, source);
const auto& activeBackend = MG_Backend::pActiveBackendObject;
if (stage == ShaderStage::Compute && activeBackend &&
activeBackend->GetBackendType() == BackendType::DirectVulkan) {
RewriteLinearSubgroupPrefixScanForVulkan(stage, activeBackend->GetDynamicParameters().SubgroupSize,
source);
}
} }
Bool RetargetLegacyVersionDirectiveTo460(String& source) { Bool RetargetLegacyVersionDirectiveTo460(String& source) {
@@ -21,6 +21,16 @@ namespace MobileGL {
namespace ShaderTranspiler { namespace ShaderTranspiler {
void PreprocessShaderSource(ShaderStage stage, String& source); void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
// from subgroupInclusiveAdd plus a shared array of subgroup totals. Qualcomm's
// Vulkan driver miscompiles that exact float InclusiveScan path for native subgroups
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
// flow template, replace the subgroup-local scan with a shared-memory, strict
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
// template was recognized and rewritten. DirectVulkan calls this through
// PreprocessShaderSource; the explicit entry point exists for deterministic tests.
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down // Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving // from a legacy desktop version back up to "#version 460 core". Returns false (leaving
// the source untouched) for anything else: ES, compatibility, or an already-modern // the source untouched) for anything else: ES, compatibility, or an already-modern
@@ -30,4 +40,4 @@ namespace MobileGL {
Bool RetargetLegacyVersionDirectiveTo460(String& source); Bool RetargetLegacyVersionDirectiveTo460(String& source);
} // namespace ShaderTranspiler } // namespace ShaderTranspiler
} // namespace MG_Util } // namespace MG_Util
} // namespace MobileGL } // namespace MobileGL
@@ -63,7 +63,17 @@ namespace MobileGL {
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType(); const glslang::TType& type = ent.symbol->getType();
const glslang::TString& name = ent.symbol->getAccessName(); const glslang::TString& name = ent.symbol->getAccessName();
if (currentStage == EShLangVertex && type.getQualifier().isPipeInput()) { // OpenGL assigns generic vertex attribute locations only to active inputs. glslang gathers
// both live and dead declarations before mapping, so allowing the default collector to
// reserve a dead vertex input would make it consume a location that an active input should
// reuse. Other stage interfaces still need the default cross-stage matching behavior.
if (!ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
return;
}
// glBindAttribLocation only affects active inputs in the linked program. Applying an API
// binding to an inactive declaration would reserve its slot in glslang's collector and
// incorrectly push an active, automatically mapped input to a different location.
if (ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
auto it = m_explicitVertexIns.find(name.c_str()); auto it = m_explicitVertexIns.find(name.c_str());
if (it != m_explicitVertexIns.end()) { if (it != m_explicitVertexIns.end()) {
auto& writableType = ent.symbol->getWritableType(); auto& writableType = ent.symbol->getWritableType();
@@ -94,6 +104,13 @@ namespace MobileGL {
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink); TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
} }
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) {
return ent.newLocation = -1;
}
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
}
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType(); const glslang::TType& type = ent.symbol->getType();
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler && if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
@@ -37,6 +37,7 @@ namespace MobileGL {
opaqueUniformBindings) {} opaqueUniformBindings) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override; int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
protected: protected:
@@ -18,17 +18,16 @@ SUMMARY_HTML = "mobilegl-android-retrace-overview.html"
DEFAULT_ANGLE_VARIANT = "ec889e6ea831" DEFAULT_ANGLE_VARIANT = "ec889e6ea831"
BLISS_ANGLE_VARIANT = "90a62123d794" BLISS_ANGLE_VARIANT = "90a62123d794"
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world" BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
TRACE_APK_DIR = ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "trace" / "debug"
BACKENDS = { BACKENDS = {
"DirectGLES": { "DirectGLES": {
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk", "package": "top.mobilegl.plugin.trace",
"package": "top.mobilegl.plugin.espryt.trace", "use_angle": False,
"use_angle": True,
"use_pbuffer": False, "use_pbuffer": False,
}, },
"DirectVulkan": { "DirectVulkan": {
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "magmaTrace" / "debug" / "MobileGL-MagmaTrace-debug.apk", "package": "top.mobilegl.plugin.trace",
"package": "top.mobilegl.plugin.magma.trace",
"use_angle": False, "use_angle": False,
"use_pbuffer": False, "use_pbuffer": False,
}, },
@@ -45,6 +44,11 @@ def is_lfs_pointer(path):
return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1") return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1")
def find_trace_apk():
candidates = list(TRACE_APK_DIR.glob("MobileGL-plugin-trace-release-*.apk"))
return max(candidates, key=lambda path: path.stat().st_mtime) if candidates else None
def bash_path(path): def bash_path(path):
path = Path(path).resolve() path = Path(path).resolve()
drive = path.drive.rstrip(":").lower() drive = path.drive.rstrip(":").lower()
@@ -117,9 +121,13 @@ def render_summary():
def run_case(case, backend): def run_case(case, backend):
backend_info = BACKENDS[backend] backend_info = BACKENDS[backend]
apk = find_trace_apk()
trace_archive = FIXTURES / case["trace_archive"] trace_archive = FIXTURES / case["trace_archive"]
golden = FIXTURES / case["golden"] golden = FIXTURES / case["golden"]
alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None
if apk is None:
mark_skipped(case, backend, f"SKIPPED_MISSING_APK: no trace APK found under {TRACE_APK_DIR}")
return 2
if not trace_archive.exists() or is_lfs_pointer(trace_archive): if not trace_archive.exists() or is_lfs_pointer(trace_archive):
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer") mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
copy_goldens(case, backend) copy_goldens(case, backend)
@@ -134,7 +142,7 @@ def run_case(case, backend):
"C:/Program Files/Git/bin/bash.exe", "C:/Program Files/Git/bin/bash.exe",
"android-plugin/trace-replay-ci.sh", "android-plugin/trace-replay-ci.sh",
"--apk-file", "--apk-file",
bash_path(backend_info["apk"]), bash_path(apk),
"--package", "--package",
backend_info["package"], backend_info["package"],
"--backend", "--backend",
+8
View File
@@ -282,6 +282,14 @@
"target_call": 2667619, "target_call": 2667619,
"timeout_seconds": 1800, "timeout_seconds": 1800,
"coherent_as_flush": true "coherent_as_flush": true
},
{
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world",
"ci": false,
"trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.tgz",
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png",
"target_call": 202020,
"timeout_seconds": 1800
} }
] ]
} }