mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
2
Commits
piglit-gl33
...
itrp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44cf9ab26a | ||
|
|
77366d51ed |
@@ -272,6 +272,9 @@ namespace MobileGL {
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
Int MaxCombinedImageUniforms = 8;
|
||||
Int MaxVertexImageUniforms = 0;
|
||||
Int MaxGeometryImageUniforms = 0;
|
||||
Int MaxFragmentImageUniforms = 8;
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
|
||||
@@ -947,6 +947,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return m_dynamicParameters;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
|
||||
const MG_External::GLESCapabilities& capabilities) {
|
||||
m_GLESCapabilities = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
|
||||
@@ -1003,9 +1009,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
||||
const Int maxSupportedTextureUnits =
|
||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||
m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms;
|
||||
m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms;
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
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.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
|
||||
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
|
||||
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
|
||||
|
||||
private:
|
||||
void UpdateDynamicBackendParameters();
|
||||
|
||||
@@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
||||
E_GL_ARB_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_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};
|
||||
@@ -746,9 +746,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
|
||||
m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||
const Int maxPerStageImageUniforms =
|
||||
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
||||
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
|
||||
// optional device features. VulkanRenderer enables each feature whenever the physical
|
||||
// device reports it, so these are the exact limits the logical device can compile and run.
|
||||
m_dynamicParameters.MaxVertexImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
|
||||
? maxPerStageImageUniforms
|
||||
: 0;
|
||||
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||
m_dynamicParameters.MaxComputeImageUniforms =
|
||||
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
||||
const Int maxSupportedDrawBuffers =
|
||||
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||
|
||||
@@ -1197,6 +1197,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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
|
||||
|
||||
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,
|
||||
CompileOptionFlags flags) const {
|
||||
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.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
|
||||
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.storageBlockIndexByBinding.assign(m_maxBindings, -1);
|
||||
entry.globalUboBinding = -1;
|
||||
@@ -1592,10 +1713,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,
|
||||
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
|
||||
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 ||
|
||||
entry.samplerUniformLocationByBinding[binding] == location,
|
||||
"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);
|
||||
|
||||
@@ -14,8 +14,16 @@
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
#include <Includes.h>
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8 {
|
||||
Unknown = 0,
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger,
|
||||
};
|
||||
|
||||
class ProgramFactory {
|
||||
public:
|
||||
enum class DescriptorBindingKind : Uint8 {
|
||||
@@ -54,6 +62,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<String> samplerNameByBinding;
|
||||
Vector<Int> samplerUniformLocationByBinding;
|
||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||
Vector<VkFormat> storageImageFormatByBinding;
|
||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||
Vector<String> storageBlockNameByBinding;
|
||||
Vector<Int> storageBlockIndexByBinding;
|
||||
Int globalUboBinding = -1;
|
||||
@@ -82,6 +93,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||
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);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
@@ -118,6 +133,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerNameByBinding = std::move(other.samplerNameByBinding);
|
||||
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
|
||||
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);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
@@ -167,9 +186,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
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_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = device;
|
||||
}
|
||||
~ProgramFactory() = default;
|
||||
@@ -180,6 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -206,6 +229,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
|
||||
// (which needs the DrawParameters capability / gl_BaseInstance builtin).
|
||||
Bool m_shaderDrawParametersEnabled = false;
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.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/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
@@ -78,6 +79,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
@@ -276,6 +287,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
|
||||
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
|
||||
// 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
|
||||
@@ -291,23 +332,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint64 textureLifetimeId = texture->GetLifetimeId();
|
||||
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
|
||||
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;
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
memo.samplerLifetimeId = samplerLifetimeId;
|
||||
memo.samplerVersion = samplerVersion;
|
||||
memo.textureLifetimeId = textureLifetimeId;
|
||||
memo.textureParamsVersion = textureParamsVersion;
|
||||
memo.forceNearestFiltering = forceNearestFiltering;
|
||||
memo.sampler = resolvedSampler;
|
||||
memo.valid = true;
|
||||
}
|
||||
} else {
|
||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
|
||||
resolvedSampler =
|
||||
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
|
||||
}
|
||||
outImageInfo = {
|
||||
.sampler = resolvedSampler,
|
||||
.imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
|
||||
.imageView = sampledImageView,
|
||||
.imageLayout = resource->layout,
|
||||
};
|
||||
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));
|
||||
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) {
|
||||
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.imageView = view;
|
||||
@@ -632,6 +700,50 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
UboBindResult& out) const {
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
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,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -49,6 +52,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
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:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
@@ -165,9 +174,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
Uint16 samplerVersion = 0;
|
||||
Uint16 textureParamsVersion = 0;
|
||||
Bool forceNearestFiltering = false;
|
||||
Bool valid = false;
|
||||
};
|
||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
@@ -74,8 +75,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
||||
if (vkFormat == VK_FORMAT_UNDEFINED) {
|
||||
const VkFormat sourceVkFormat =
|
||||
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 "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -83,6 +85,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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);
|
||||
if (attribByteSize == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
const Uint32 stride =
|
||||
const Uint32 sourceStride =
|
||||
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 =
|
||||
(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);
|
||||
bindingAttributeLocations.push_back(location);
|
||||
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
}
|
||||
@@ -117,6 +167,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
|
||||
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
|
||||
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
|
||||
entry.bindingConversions = std::move(bindingConversions);
|
||||
entry.unsupportedAttribMask = unsupportedAttribMask;
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
@@ -282,4 +333,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const SizeT componentSize = GetComponentSize(type);
|
||||
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
|
||||
|
||||
@@ -19,6 +19,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
|
||||
enum class VertexStreamConversion : Uint8 {
|
||||
None = 0,
|
||||
Repack,
|
||||
ScaledIntegerToFloat32,
|
||||
};
|
||||
|
||||
struct BackendVertexInputState {
|
||||
HashType hash = 0;
|
||||
Vector<VkVertexInputBindingDescription> bindings;
|
||||
@@ -27,6 +33,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<SizeT> bindingBaseOffsets;
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
// 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
|
||||
// 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):
|
||||
m_config(config) {}
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
~VertexInputStateFactory() = default;
|
||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||
|
||||
@@ -56,8 +63,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
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;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
UnorderedMap<HashType, BackendVertexInputState> m_cache;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
@@ -157,6 +157,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 {
|
||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* Map();
|
||||
void Unmap();
|
||||
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; }
|
||||
VkDeviceSize GetSize() const { return m_size; }
|
||||
|
||||
@@ -65,8 +65,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 (forceNearestFiltering) return 1.0f;
|
||||
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
|
||||
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
|
||||
if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
|
||||
@@ -90,10 +92,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
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");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
|
||||
|
||||
const auto minFilter = sampler.GetMinFilter();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
|
||||
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
|
||||
// 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.
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
|
||||
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||
const auto compareMode = sampler.GetCompareMode();
|
||||
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,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture);
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering) {
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
return it->second.handle;
|
||||
@@ -136,16 +142,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSamplerCreateInfo samplerInfo{};
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
|
||||
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
|
||||
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
|
||||
: ToVkMipmapMode(sampler.GetMipmapMode());
|
||||
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
|
||||
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
|
||||
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
|
||||
samplerInfo.mipLodBias = sampler.GetLodBias();
|
||||
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two
|
||||
// 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.maxAnisotropy = maxAnisotropy;
|
||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||
|
||||
@@ -34,7 +34,8 @@ public:
|
||||
void Shutdown();
|
||||
|
||||
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture);
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering = false);
|
||||
|
||||
private:
|
||||
struct SamplerCacheEntry {
|
||||
@@ -44,7 +45,8 @@ private:
|
||||
};
|
||||
|
||||
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 VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode 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.
|
||||
// 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.
|
||||
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;
|
||||
const VulkanRendererConfig* m_config = nullptr;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (requestedSamples) {
|
||||
case 1:
|
||||
@@ -767,6 +823,180 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -1085,6 +1315,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 &&
|
||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||
@@ -1092,6 +1334,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resource.mipLevels == backingMipLevels;
|
||||
if (compatible) {
|
||||
if (resource.perMipViews.size() != backingMipLevels) {
|
||||
@@ -1112,6 +1355,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||
resource.mipLevels < backingMipLevels &&
|
||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -1123,26 +1367,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DeferResourceRelease(Move(resource));
|
||||
}
|
||||
|
||||
auto aspect = GetAspectMaskForFormat(format);
|
||||
|
||||
VkImageCreateInfo imageInfo{};
|
||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
imageInfo.flags = shapeInfo.imageFlags;
|
||||
imageInfo.imageType = shapeInfo.imageType;
|
||||
imageInfo.flags = imageCreateFlags;
|
||||
imageInfo.imageType = shapeInfo.imageType;
|
||||
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
|
||||
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
|
||||
imageInfo.extent.depth = shapeInfo.depth;
|
||||
imageInfo.extent.depth = shapeInfo.depth;
|
||||
imageInfo.mipLevels = backingMipLevels;
|
||||
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
||||
imageInfo.arrayLayers = shapeInfo.arrayLayers;
|
||||
imageInfo.format = format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
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 |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_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.samples = resolvedSampleCount;
|
||||
if (isMultisampleTexture) {
|
||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||
imageInfo.flags, &imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS ||
|
||||
(imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) {
|
||||
MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x",
|
||||
__func__, texture.GetSamples(), texture.GetExternalIndex(),
|
||||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||
"format=%d usage=0x%x",
|
||||
__func__, static_cast<Uint32>(imageInfo.flags), texture.GetSamples(),
|
||||
texture.GetExternalIndex(),
|
||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||
return false;
|
||||
@@ -1188,6 +1426,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.aspect = aspect;
|
||||
resource.viewType = shapeInfo.viewType;
|
||||
resource.sampleCount = resolvedSampleCount;
|
||||
resource.imageCreateFlags = imageCreateFlags;
|
||||
resource.syncedTextureParamsVersion = 0;
|
||||
|
||||
if (preservedResource) {
|
||||
@@ -1203,7 +1442,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
||||
if (resource.image == VK_NULL_HANDLE && resource.fullView == 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;
|
||||
}
|
||||
|
||||
@@ -1299,6 +1540,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
sampledView = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
for (const auto& [_, sampledView] : resource.alternateSampledViews) {
|
||||
DeferViewRelease(sampledView);
|
||||
}
|
||||
resource.alternateSampledViews.clear();
|
||||
|
||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
||||
@@ -1324,7 +1569,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 baseArrayLayer,
|
||||
Uint32 layerCount,
|
||||
const VkComponentMapping* components) const {
|
||||
const VkComponentMapping* components,
|
||||
VkImageUsageFlags viewUsage) const {
|
||||
VkImageViewCreateInfo viewInfo{};
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
viewInfo.image = image;
|
||||
@@ -1340,6 +1586,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
|
||||
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;
|
||||
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
|
||||
return view;
|
||||
@@ -1665,4 +1918,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
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
|
||||
|
||||
@@ -19,6 +19,8 @@ class ITextureObject;
|
||||
}
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class SamplerNumericDomain : Uint8;
|
||||
|
||||
class VkTextureManager {
|
||||
public:
|
||||
// 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;
|
||||
VmaAllocation allocation = nullptr;
|
||||
VkImageView fullView = VK_NULL_HANDLE;
|
||||
@@ -85,6 +142,8 @@ public:
|
||||
Vector<VkImageView> perMipViews;
|
||||
Vector<VkImageView> perMipSampledViews;
|
||||
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
|
||||
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
|
||||
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkExtent2D extent = {0, 0};
|
||||
Uint32 depth = 1;
|
||||
@@ -96,6 +155,7 @@ public:
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// 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->perMipSampledViews, that.perMipSampledViews);
|
||||
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->extent, that.extent);
|
||||
std::swap(this->depth, that.depth);
|
||||
@@ -126,6 +188,7 @@ public:
|
||||
std::swap(this->aspect, that.aspect);
|
||||
std::swap(this->viewType, that.viewType);
|
||||
std::swap(this->sampleCount, that.sampleCount);
|
||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
@@ -153,6 +216,16 @@ public:
|
||||
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) {
|
||||
vmaDestroyImage(s_allocator, image, allocation);
|
||||
}
|
||||
@@ -161,6 +234,8 @@ public:
|
||||
perMipViews.clear();
|
||||
perMipSampledViews.clear();
|
||||
attachmentViews.clear();
|
||||
alternateSampledViews.clear();
|
||||
storageImageViews.clear();
|
||||
image = VK_NULL_HANDLE;
|
||||
allocation = nullptr;
|
||||
layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
@@ -174,6 +249,7 @@ public:
|
||||
aspect = VK_IMAGE_ASPECT_NONE;
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
@@ -198,6 +274,9 @@ public:
|
||||
Uint32 baseArrayLayer, Uint32 layerCount,
|
||||
VkImageViewType viewType);
|
||||
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 UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject* texture,
|
||||
@@ -207,6 +286,9 @@ public:
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
|
||||
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,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
@@ -255,7 +337,8 @@ private:
|
||||
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 baseArrayLayer,
|
||||
Uint32 layerCount,
|
||||
const VkComponentMapping* components = nullptr) const;
|
||||
const VkComponentMapping* components = nullptr,
|
||||
VkImageUsageFlags viewUsage = 0) const;
|
||||
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
|
||||
TextureUploadTarget uploadTarget,
|
||||
TextureResource &outResource);
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Math/HalfFloat.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
#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) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
@@ -1041,6 +1125,7 @@ void main() {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkImageLayout* trackedLayout = nullptr;
|
||||
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
IntVec2 extent = {0, 0};
|
||||
Uint32 mipLevel = 0;
|
||||
Uint32 mipLevelCount = 1;
|
||||
@@ -1204,6 +1289,7 @@ void main() {
|
||||
outBinding.image = swapchainObject.GetImage(swapchainImageIndex);
|
||||
outBinding.trackedLayout = nullptr;
|
||||
outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
outBinding.format = swapchainObject.GetSurfaceFormat().format;
|
||||
const auto extent = swapchainObject.GetExtent();
|
||||
outBinding.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
|
||||
outBinding.mipLevel = 0;
|
||||
@@ -1252,6 +1338,7 @@ void main() {
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
outBinding.aspectMask = resource->aspect;
|
||||
outBinding.format = resource->format;
|
||||
const auto attachmentExtent = attachment.GetSize();
|
||||
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
|
||||
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
|
||||
@@ -1498,19 +1585,54 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
static Bool IsBgraVkFormat(VkFormat format) {
|
||||
switch (format) {
|
||||
static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) {
|
||||
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_SNORM:
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
case VK_FORMAT_B8G8R8A8_USCALED:
|
||||
case VK_FORMAT_B8G8R8A8_SSCALED:
|
||||
rgba[0] = static_cast<Float>(source[2]) / 255.0f;
|
||||
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;
|
||||
default:
|
||||
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
|
||||
// GL-oriented pixels (bottom-left origin) for the retrace snapshot path.
|
||||
// Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip
|
||||
@@ -1520,6 +1642,7 @@ void main() {
|
||||
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
|
||||
VkExtent2D rawExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize,
|
||||
Uint8* outPixels) {
|
||||
if (IsQuarterTurnPreTransform(preTransform)) {
|
||||
return false;
|
||||
@@ -1543,12 +1666,9 @@ void main() {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * 4;
|
||||
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * 4;
|
||||
dst[0] = src[0];
|
||||
dst[1] = src[1];
|
||||
dst[2] = src[2];
|
||||
dst[3] = src[3];
|
||||
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * texelSize;
|
||||
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * texelSize;
|
||||
Memcpy(dst, src, texelSize);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -1572,12 +1692,11 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
static void StoreReadbackPixel(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Uint8* dst) {
|
||||
const Uint8 r = srcIsBgra ? src[2] : src[0];
|
||||
const Uint8 g = src[1];
|
||||
const Uint8 b = srcIsBgra ? src[0] : src[2];
|
||||
const Uint8 a = src[3];
|
||||
|
||||
static void StoreReadbackPixel(const Float* rgba, GLenum dstFormat, Uint8* dst) {
|
||||
const Uint8 r = EncodeReadbackUnorm8(rgba[0]);
|
||||
const Uint8 g = EncodeReadbackUnorm8(rgba[1]);
|
||||
const Uint8 b = EncodeReadbackUnorm8(rgba[2]);
|
||||
const Uint8 a = EncodeReadbackUnorm8(rgba[3]);
|
||||
switch (dstFormat) {
|
||||
case GL_RGB:
|
||||
dst[0] = r;
|
||||
@@ -1606,13 +1725,11 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
static void StoreReadbackPixelFloat(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Float* dst) {
|
||||
const Float r = static_cast<Float>(srcIsBgra ? src[2] : src[0]) / 255.0f;
|
||||
const Float g = static_cast<Float>(src[1]) / 255.0f;
|
||||
const Float b = static_cast<Float>(srcIsBgra ? src[0] : src[2]) / 255.0f;
|
||||
const Float a = static_cast<Float>(src[3]) / 255.0f;
|
||||
|
||||
// TODO: extend readback packing to integer/depth formats instead of only normalized color formats.
|
||||
static void StoreReadbackPixelFloat(const Float* rgba, GLenum dstFormat, Float* dst) {
|
||||
const Float r = rgba[0];
|
||||
const Float g = rgba[1];
|
||||
const Float b = rgba[2];
|
||||
const Float a = rgba[3];
|
||||
switch (dstFormat) {
|
||||
case GL_RGB:
|
||||
dst[0] = r;
|
||||
@@ -1670,20 +1787,11 @@ void main() {
|
||||
(static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes);
|
||||
Vector<Uint8> packed(packedSize, 0);
|
||||
|
||||
const Bool srcIsBgra = IsBgraVkFormat(srcFormat);
|
||||
for (GLsizei row = 0; row < height; ++row) {
|
||||
const Uint8* srcRow = srcPixels + static_cast<SizeT>(row) * static_cast<SizeT>(width) * 4;
|
||||
Uint8* dstRow = packed.data() + dstOffset + static_cast<SizeT>(row) * dstRowStride;
|
||||
for (GLsizei col = 0; col < width; ++col) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type,
|
||||
dstRowStride, packed.data() + dstOffset)) {
|
||||
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
||||
static_cast<Int>(srcFormat));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& pixelPackBufferObject =
|
||||
@@ -1705,6 +1813,60 @@ void main() {
|
||||
}
|
||||
} // 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,
|
||||
VkDebugUtilsMessageTypeFlagsEXT messageType,
|
||||
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
|
||||
@@ -1885,7 +2047,8 @@ void main() {
|
||||
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
|
||||
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
|
||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||
m_shaderDrawParametersFeatureEnabled);
|
||||
m_shaderDrawParametersFeatureEnabled,
|
||||
m_unformattedFloatStorageImagesEnabled);
|
||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||
|
||||
m_samplerManager = MakeUnique<VkSamplerManager>();
|
||||
@@ -1905,7 +2068,7 @@ void main() {
|
||||
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
|
||||
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
|
||||
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.");
|
||||
|
||||
// Prime the first frame so Render() always targets an acquired swapchain image.
|
||||
@@ -1920,6 +2083,7 @@ void main() {
|
||||
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
|
||||
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||
m_convertedVertexStreams.clear();
|
||||
|
||||
MGLOG_D("VulkanRenderer initialized");
|
||||
}
|
||||
@@ -2038,7 +2202,8 @@ void main() {
|
||||
|
||||
Bool VulkanRenderer::UploadAndBindVertexBuffers(
|
||||
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
|
||||
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
|
||||
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
|
||||
@@ -2064,6 +2229,39 @@ void main() {
|
||||
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) {
|
||||
if (binding >= vertexInputState.bindings.size()) {
|
||||
break;
|
||||
@@ -2073,28 +2271,53 @@ void main() {
|
||||
: static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
||||
const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() &&
|
||||
vertexInputState.bindingUsesClientMemory[binding];
|
||||
const auto conversion = binding < vertexInputState.bindingConversions.size()
|
||||
? vertexInputState.bindingConversions[binding]
|
||||
: VertexInputStateFactory::VertexStreamConversion::None;
|
||||
if (usesClientMemory) {
|
||||
const Uint32 location = bindingLocation;
|
||||
MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||
"UploadAndBindVertexStreams failed to resolve client attribute location");
|
||||
|
||||
const auto& attr = vao.GetAttribute(location);
|
||||
const SizeT componentSize = VertexInputStateFactory::GetComponentSize(attr.Type);
|
||||
const SizeT elementSize = componentSize * static_cast<SizeT>(attr.Size);
|
||||
const SizeT elementSize =
|
||||
VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
const SizeT stride = attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
|
||||
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);
|
||||
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
|
||||
? drawParams.firstVertex + drawParams.vertexCount - 1
|
||||
: drawParams.firstVertex;
|
||||
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
|
||||
clientData, static_cast<VkDeviceSize>(uploadSize), 16, slice)) {
|
||||
Bool uploaded = false;
|
||||
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,
|
||||
"UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu",
|
||||
binding);
|
||||
@@ -2116,6 +2339,64 @@ void main() {
|
||||
const auto& sourceBufferShared = *sourceBufferSharedPtr;
|
||||
BufferSlice slice{};
|
||||
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 (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) {
|
||||
MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding);
|
||||
@@ -2128,11 +2409,6 @@ void main() {
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -3246,6 +3522,44 @@ void main() {
|
||||
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,
|
||||
const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
@@ -3273,6 +3587,11 @@ void main() {
|
||||
m_lastSampledSetValid = false;
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
MGLOG_E("SetupDraw skipped: storage image preparation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
|
||||
// Check if any of the textures to sample have pending clears,
|
||||
@@ -3443,7 +3762,8 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto vtxUploadOk = UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams);
|
||||
auto vtxUploadOk = UploadAndBindVertexBuffers(
|
||||
frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView != nullptr);
|
||||
if (!vtxUploadOk) {
|
||||
MGLOG_E("SetupDraw skipped: failed to upload vertex buffers");
|
||||
return false;
|
||||
@@ -3492,6 +3812,11 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u",
|
||||
@@ -3527,6 +3852,11 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
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);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) {
|
||||
@@ -5174,7 +5505,15 @@ void main() {
|
||||
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;
|
||||
if (!readback.Create({
|
||||
.allocator = m_allocator,
|
||||
@@ -5210,7 +5549,7 @@ void main() {
|
||||
VkBufferImageCopy copyRegion{};
|
||||
copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask;
|
||||
copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel;
|
||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||
copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer;
|
||||
copyRegion.imageSubresource.layerCount = 1;
|
||||
copyRegion.imageOffset = {x, y, 0};
|
||||
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");
|
||||
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) {
|
||||
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
|
||||
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
||||
if (static_cast<Uint32>(width) == swapchainExtent.width &&
|
||||
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,
|
||||
sourceTexelSize,
|
||||
remapped.data())) {
|
||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
|
||||
return;
|
||||
@@ -5318,9 +5661,10 @@ void main() {
|
||||
}
|
||||
if (bufSize >= 0) {
|
||||
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) *
|
||||
static_cast<SizeT>(dstChannels);
|
||||
static_cast<SizeT>(dstChannels) * dstComponentSize;
|
||||
if (static_cast<SizeT>(bufSize) < minSize) {
|
||||
MGLOG_E("DirectVulkan::GetTextureImage skipped: destination buffer is too small");
|
||||
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;
|
||||
if (!readback.Create({
|
||||
.allocator = m_allocator,
|
||||
@@ -5379,6 +5730,10 @@ void main() {
|
||||
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to map readback buffer");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -6331,6 +6686,7 @@ void main() {
|
||||
CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex());
|
||||
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
|
||||
m_convertedVertexStreams.clear();
|
||||
// Descriptor-set reuse cursors rewind exactly once per frame, here,
|
||||
// after the slot's fence wait proved its previous sets GPU-idle. (The
|
||||
// per-draw-path lazy rewind missed frames whose recording was opened
|
||||
@@ -6648,6 +7004,10 @@ void main() {
|
||||
vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures);
|
||||
|
||||
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.independentBlend = supportedDeviceFeatures.independentBlend;
|
||||
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
|
||||
@@ -6661,6 +7021,23 @@ void main() {
|
||||
deviceFeatures.wideLines = supportedDeviceFeatures.wideLines;
|
||||
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
|
||||
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.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
|
||||
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
|
||||
@@ -6780,8 +7157,12 @@ void main() {
|
||||
|
||||
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
|
||||
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
|
||||
MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
|
||||
"shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s",
|
||||
MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%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.independentBlend ? "true" : "false",
|
||||
supportedDeviceFeatures.logicOp ? "true" : "false",
|
||||
@@ -6789,11 +7170,19 @@ void main() {
|
||||
supportedDeviceFeatures.shaderCullDistance ? "true" : "false",
|
||||
supportedDeviceFeatures.wideLines ? "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.multiDrawIndirect ? "true" : "false");
|
||||
MGLOG_I("Device feature enabled: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
|
||||
"shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s "
|
||||
"shaderDrawParameters=%s",
|
||||
MGLOG_I("Device feature enabled: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
|
||||
"shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s "
|
||||
"fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s "
|
||||
"storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s "
|
||||
"multiDrawIndirect=%s shaderDrawParameters=%s",
|
||||
deviceFeatures.robustBufferAccess ? "true" : "false",
|
||||
deviceFeatures.geometryShader ? "true" : "false",
|
||||
deviceFeatures.independentBlend ? "true" : "false",
|
||||
deviceFeatures.logicOp ? "true" : "false",
|
||||
@@ -6801,6 +7190,11 @@ void main() {
|
||||
deviceFeatures.shaderCullDistance ? "true" : "false",
|
||||
deviceFeatures.wideLines ? "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.multiDrawIndirect ? "true" : "false",
|
||||
m_shaderDrawParametersFeatureEnabled ? "true" : "false");
|
||||
@@ -7127,6 +7521,7 @@ void main() {
|
||||
m_textureManager->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);
|
||||
void GenerateMipmap(GLenum target);
|
||||
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 GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
||||
@@ -365,6 +370,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
|
||||
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
|
||||
// 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
|
||||
// draw call and must not allocate.
|
||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
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();
|
||||
VkResult SetupDebugMessenger();
|
||||
@@ -462,10 +512,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
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,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const DrawCmdParam& drawParams);
|
||||
const DrawCmdParam& drawParams, Bool indexedDraw);
|
||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
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, 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_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_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, 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_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, 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)
|
||||
@@ -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, 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, 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_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)
|
||||
|
||||
@@ -1175,10 +1175,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxFragmentInputComponents;
|
||||
return;
|
||||
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
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms;
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms;
|
||||
return;
|
||||
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
|
||||
*params = kFrontendMaxFragmentUniformComponents;
|
||||
@@ -1208,7 +1207,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxGeometryTextureImageUnits;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
|
||||
return;
|
||||
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
|
||||
*params = kFrontendMaxGeometryTotalOutputComponents;
|
||||
@@ -1277,7 +1278,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = kFrontendMaxVertexAtomicCounters;
|
||||
return;
|
||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||
*params = 0;
|
||||
*params = MG_Backend::pActiveBackendObject
|
||||
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
|
||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||
return;
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*params = 16; // TODO
|
||||
|
||||
@@ -470,6 +470,210 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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,
|
||||
GLenum pname, GLint param, const char* caller) {
|
||||
const auto target = textureObject->GetTarget();
|
||||
@@ -3818,6 +4022,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
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) {
|
||||
CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @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,
|
||||
GLenum format);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -95,6 +98,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GLsizei width, GLsizei height);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
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 CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height, GLint border);
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace {
|
||||
struct FakeDriverState {
|
||||
// Behavior knobs, configured per test before running the probe.
|
||||
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
|
||||
// baseInstance word and exposes it through gl_InstanceID.
|
||||
bool drawLeaksBaseInstanceWord = false;
|
||||
@@ -88,13 +95,26 @@ namespace {
|
||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||
*data = g_fake.maxVertexSsboBlocks;
|
||||
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
|
||||
// baseInstance probe, which requires ES >= 3.1.
|
||||
case GL_MAJOR_VERSION:
|
||||
*data = 3;
|
||||
*data = g_fake.glesMajorVersion;
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*data = 1;
|
||||
*data = g_fake.glesMinorVersion;
|
||||
break;
|
||||
case GL_NUM_EXTENSIONS:
|
||||
*data = static_cast<GLint>(g_fake.extensions.size());
|
||||
@@ -417,6 +437,31 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
|
||||
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
|
||||
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
|
||||
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <spirv_reflect.h>
|
||||
#include <cstring>
|
||||
#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) {
|
||||
char infoLog[1024] = "";
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
@@ -92,6 +94,120 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
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) {
|
||||
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("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));
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.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/VkTextureManager.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
|
||||
#include <MG_Util/Math/HalfFloat.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
@@ -460,6 +463,58 @@ TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendSta
|
||||
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) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -582,6 +637,54 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
|
||||
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) {
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -629,6 +732,68 @@ TEST(DirectVulkanSanity, CommandMemoryBarrierMakesIndirectDrawCommandsVisible) {
|
||||
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) {
|
||||
using namespace MobileGL::MG_Backend::DirectVulkan;
|
||||
|
||||
@@ -675,6 +840,175 @@ TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) {
|
||||
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) {
|
||||
using namespace MobileGL;
|
||||
|
||||
|
||||
@@ -158,6 +158,39 @@ namespace {
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
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
|
||||
|
||||
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
|
||||
@@ -178,6 +211,113 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
|
||||
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
|
||||
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
||||
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
||||
|
||||
@@ -865,6 +865,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLint maxUniformBlockSize = 16384;
|
||||
GLint maxImageUnits = 8;
|
||||
GLint maxCombinedImageUniforms = 8;
|
||||
GLint maxVertexImageUniforms = 0;
|
||||
GLint maxGeometryImageUniforms = 0;
|
||||
GLint maxFragmentImageUniforms = 8;
|
||||
GLint maxComputeImageUniforms = 8;
|
||||
GLint maxDrawBuffers = 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_IMAGE_UNITS, &maxImageUnits);
|
||||
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);
|
||||
// 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_COLOR_ATTACHMENTS, &maxColorAttachments);
|
||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||
@@ -955,6 +967,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxUniformBlockSize = maxUniformBlockSize;
|
||||
caps.MaxImageUnits = maxImageUnits;
|
||||
caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
|
||||
caps.MaxVertexImageUniforms = maxVertexImageUniforms;
|
||||
caps.MaxGeometryImageUniforms = maxGeometryImageUniforms;
|
||||
caps.MaxFragmentImageUniforms = maxFragmentImageUniforms;
|
||||
caps.MaxComputeImageUniforms = maxComputeImageUniforms;
|
||||
caps.MaxDrawBuffers = maxDrawBuffers;
|
||||
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_IMAGE_UNITS: %d", caps.MaxImageUnits);
|
||||
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_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
|
||||
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
|
||||
|
||||
@@ -1102,6 +1102,9 @@ namespace MobileGL {
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
Int MaxCombinedImageUniforms = 8;
|
||||
Int MaxVertexImageUniforms = 0;
|
||||
Int MaxGeometryImageUniforms = 0;
|
||||
Int MaxFragmentImageUniforms = 8;
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
|
||||
@@ -174,6 +174,10 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||
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);
|
||||
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
|
||||
HasUsableShaderSubgroupSupport(subgroupProps);
|
||||
@@ -256,6 +260,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
|
||||
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
|
||||
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.SupportsShaderSubgroup = false;
|
||||
caps.SubgroupSize = 0;
|
||||
|
||||
@@ -67,6 +67,12 @@ namespace MobileGL {
|
||||
Float ViewportBoundsRangeMax = 0.0f;
|
||||
Int ViewportSubpixelBits = 0;
|
||||
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;
|
||||
Bool SupportsShaderSubgroup = false;
|
||||
Uint32 SubgroupSize = 0;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include "glslang/SPIRV/spirv.hpp11"
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
|
||||
#include "ShaderCompiler.h"
|
||||
|
||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||
@@ -19,14 +23,15 @@
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include "ShaderSourceProcessor.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
TBuiltInResource& GetTBuiltInResourceInstance() {
|
||||
static TBuiltInResource Resources{};
|
||||
TBuiltInResource BuildTBuiltInResource() {
|
||||
TBuiltInResource Resources{};
|
||||
Resources.maxLights = 32;
|
||||
Resources.maxClipPlanes = 6;
|
||||
Resources.maxTextureUnits = 32;
|
||||
@@ -121,6 +126,22 @@ namespace MobileGL {
|
||||
Resources.maxTaskWorkGroupSizeZ_NV = 1;
|
||||
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.whileLoops = true;
|
||||
Resources.limits.doWhileLoops = true;
|
||||
@@ -166,7 +187,8 @@ namespace MobileGL {
|
||||
tshader->setAutoMapLocations(true);
|
||||
tshader->setAutoMapBindings(true);
|
||||
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
|
||||
if (!tshader->parse(&GetTBuiltInResourceInstance(), 460, ECoreProfile,
|
||||
auto resources = BuildTBuiltInResource();
|
||||
if (!tshader->parse(&resources, 460, ECoreProfile,
|
||||
/*forceDefaultVersionAndProfile: */ false,
|
||||
/*forwardCompatible: */ true, EShMsgDefault)) {
|
||||
ResultInfo r;
|
||||
@@ -322,6 +344,136 @@ namespace MobileGL {
|
||||
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) {
|
||||
spvc_compiler_options options;
|
||||
session.CreateOptions(&options);
|
||||
|
||||
@@ -39,6 +39,14 @@ namespace MobileGL {
|
||||
// which wrongly includes baseInstance).
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
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);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
|
||||
@@ -10,10 +10,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <initializer_list>
|
||||
#include <utility>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace {
|
||||
using MobileGL::SizeT;
|
||||
using MobileGL::String;
|
||||
using MobileGL::Vector;
|
||||
|
||||
bool IsIdentifierChar(char ch) {
|
||||
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
|
||||
@@ -91,6 +95,375 @@ namespace {
|
||||
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) {
|
||||
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
|
||||
pos++;
|
||||
@@ -576,6 +949,36 @@ namespace {
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
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) {
|
||||
// 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.
|
||||
@@ -631,6 +1034,13 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
|
||||
ModernizeLegacyGLSL(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) {
|
||||
|
||||
@@ -21,6 +21,16 @@ namespace MobileGL {
|
||||
namespace ShaderTranspiler {
|
||||
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
|
||||
// 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
|
||||
@@ -30,4 +40,4 @@ namespace MobileGL {
|
||||
Bool RetargetLegacyVersionDirectiveTo460(String& source);
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -63,7 +63,17 @@ namespace MobileGL {
|
||||
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||
const glslang::TType& type = ent.symbol->getType();
|
||||
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());
|
||||
if (it != m_explicitVertexIns.end()) {
|
||||
auto& writableType = ent.symbol->getWritableType();
|
||||
@@ -94,6 +104,13 @@ namespace MobileGL {
|
||||
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) {
|
||||
const glslang::TType& type = ent.symbol->getType();
|
||||
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace MobileGL {
|
||||
opaqueUniformBindings) {}
|
||||
void reserverStorageSlot(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;
|
||||
|
||||
protected:
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -18,17 +18,16 @@ SUMMARY_HTML = "mobilegl-android-retrace-overview.html"
|
||||
DEFAULT_ANGLE_VARIANT = "ec889e6ea831"
|
||||
BLISS_ANGLE_VARIANT = "90a62123d794"
|
||||
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
|
||||
TRACE_APK_DIR = ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "trace" / "debug"
|
||||
|
||||
BACKENDS = {
|
||||
"DirectGLES": {
|
||||
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk",
|
||||
"package": "top.mobilegl.plugin.espryt.trace",
|
||||
"use_angle": True,
|
||||
"package": "top.mobilegl.plugin.trace",
|
||||
"use_angle": False,
|
||||
"use_pbuffer": False,
|
||||
},
|
||||
"DirectVulkan": {
|
||||
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "magmaTrace" / "debug" / "MobileGL-MagmaTrace-debug.apk",
|
||||
"package": "top.mobilegl.plugin.magma.trace",
|
||||
"package": "top.mobilegl.plugin.trace",
|
||||
"use_angle": 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")
|
||||
|
||||
|
||||
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):
|
||||
path = Path(path).resolve()
|
||||
drive = path.drive.rstrip(":").lower()
|
||||
@@ -117,9 +121,13 @@ def render_summary():
|
||||
|
||||
def run_case(case, backend):
|
||||
backend_info = BACKENDS[backend]
|
||||
apk = find_trace_apk()
|
||||
trace_archive = FIXTURES / case["trace_archive"]
|
||||
golden = FIXTURES / case["golden"]
|
||||
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):
|
||||
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
|
||||
copy_goldens(case, backend)
|
||||
@@ -134,7 +142,7 @@ def run_case(case, backend):
|
||||
"C:/Program Files/Git/bin/bash.exe",
|
||||
"android-plugin/trace-replay-ci.sh",
|
||||
"--apk-file",
|
||||
bash_path(backend_info["apk"]),
|
||||
bash_path(apk),
|
||||
"--package",
|
||||
backend_info["package"],
|
||||
"--backend",
|
||||
|
||||
@@ -282,6 +282,14 @@
|
||||
"target_call": 2667619,
|
||||
"timeout_seconds": 1800,
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user