Compare commits

..
4 Commits
62 changed files with 1234 additions and 3192 deletions
-3
View File
@@ -272,9 +272,6 @@ namespace MobileGL {
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8; Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
@@ -947,12 +947,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_dynamicParameters; return m_dynamicParameters;
} }
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
const MG_External::GLESCapabilities& capabilities) {
m_GLESCapabilities = capabilities;
UpdateDynamicBackendParameters();
}
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
@@ -1009,21 +1003,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
const Int maxSupportedTextureUnits = const Int maxSupportedTextureUnits =
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS); static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
m_dynamicParameters.MaxImageUnits = m_dynamicParameters.MaxImageUnits = std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits);
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxCombinedImageUniforms = m_GLESCapabilities.MaxCombinedImageUniforms;
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0); m_dynamicParameters.MaxComputeImageUniforms = m_GLESCapabilities.MaxComputeImageUniforms;
const auto clampStageImageUniforms = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
m_dynamicParameters.MaxCombinedImageUniforms});
};
m_dynamicParameters.MaxVertexImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxGeometryImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
m_dynamicParameters.MaxFragmentImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
m_dynamicParameters.MaxComputeImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
@@ -41,7 +41,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESFunctionsTable& GetGLESFunctions() const; const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
const MG_External::EGLFunctionsTable& GetEGLFunctions() const; const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
private: private:
void UpdateDynamicBackendParameters(); void UpdateDynamicBackendParameters();
@@ -504,7 +504,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size}; E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
@@ -746,24 +746,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits = m_dynamicParameters.MaxImageUnits = std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits);
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxCombinedImageUniforms = m_vulkanCaps.MaxCombinedImageUniforms;
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0); m_dynamicParameters.MaxComputeImageUniforms = m_vulkanCaps.MaxComputeImageUniforms;
const Int maxPerStageImageUniforms =
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
// Vulkan uses one descriptor limit for every stage, but non-compute stores/atomics are
// optional device features. VulkanRenderer enables each feature whenever the physical
// device reports it, so these are the exact limits the logical device can compile and run.
m_dynamicParameters.MaxVertexImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxGeometryImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
? maxPerStageImageUniforms
: 0;
m_dynamicParameters.MaxFragmentImageUniforms =
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
m_dynamicParameters.MaxComputeImageUniforms =
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
const Int maxSupportedDrawBuffers = const Int maxSupportedDrawBuffers =
static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
@@ -8,6 +8,7 @@
#include "PipelineFactory.h" #include "PipelineFactory.h"
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) { static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) { switch (topology) {
@@ -108,6 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache"); "vkCreatePipelineCache");
} }
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
PipelineFactory::~PipelineFactory() { PipelineFactory::~PipelineFactory() {
DestroyAll(); DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) { if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -258,6 +263,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i]; colorAttachments[i] = payload.colorBlendAttachments[i];
} }
// Suppress depth writes on blended pipelines when the active driver cannot keep
// vertex positions invariant across the pipelines of a multi-pass depth-equality
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
// and the equality-dependent prepass pattern is exactly the case that breaks.
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
if (colorAttachments[i].blendEnable == VK_TRUE) {
depthStencil.depthWriteEnable = VK_FALSE;
break;
}
}
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE; blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp; blend.logicOp = payload.logicOp;
@@ -62,6 +62,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload); VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll(); void DestroyAll();
// Driver quirk: suppress depth writes on blended pipelines. Multi-pass depth-equality
// rendering (a blended prepass writes depth that later passes re-test with an
// equality-inclusive compare on the re-rasterized geometry) requires cross-pipeline
// position invariance that some mobile compilers do not provide, even with the
// SPIR-V Invariant decoration; whole primitives then drop out of the later passes.
// Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
private: private:
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const; VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
@@ -70,5 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, VkPipeline> m_cache; UnorderedMap<HashType, VkPipeline> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1197,25 +1197,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return TextureTarget::Unknown; return TextureTarget::Unknown;
} }
} }
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
switch (uniformType) {
case GL_IMAGE_1D:
case GL_IMAGE_2D:
case GL_IMAGE_3D:
case GL_IMAGE_2D_RECT:
case GL_IMAGE_CUBE:
case GL_IMAGE_BUFFER:
case GL_IMAGE_1D_ARRAY:
case GL_IMAGE_2D_ARRAY:
case GL_IMAGE_CUBE_MAP_ARRAY:
case GL_IMAGE_2D_MULTISAMPLE:
case GL_IMAGE_2D_MULTISAMPLE_ARRAY:
return true;
default:
return false;
}
}
} // namespace } // namespace
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
@@ -1237,105 +1218,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) {
switch (format) {
case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED;
case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT;
case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT;
case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT;
case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM;
case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM;
case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT;
case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT;
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
case SpvImageFormatR8: return VK_FORMAT_R8_UNORM;
case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM;
case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM;
case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM;
case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM;
case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM;
case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT;
case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT;
case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT;
case SpvImageFormatR32i: return VK_FORMAT_R32_SINT;
case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT;
case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT;
case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT;
case SpvImageFormatR16i: return VK_FORMAT_R16_SINT;
case SpvImageFormatR8i: return VK_FORMAT_R8_SINT;
case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT;
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT;
case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT;
case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT;
case SpvImageFormatR64i: return VK_FORMAT_R64_SINT;
case SpvImageFormatMax: return VK_FORMAT_UNDEFINED;
}
return VK_FORMAT_UNDEFINED;
}
SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) {
switch (glType) {
case GL_INT_SAMPLER_1D:
case GL_INT_SAMPLER_2D:
case GL_INT_SAMPLER_3D:
case GL_INT_SAMPLER_CUBE:
case GL_INT_SAMPLER_2D_RECT:
case GL_INT_SAMPLER_1D_ARRAY:
case GL_INT_SAMPLER_2D_ARRAY:
case GL_INT_SAMPLER_BUFFER:
case GL_INT_SAMPLER_2D_MULTISAMPLE:
case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_INT_SAMPLER_CUBE_MAP_ARRAY:
return SamplerNumericDomain::SignedInteger;
case GL_UNSIGNED_INT_SAMPLER_1D:
case GL_UNSIGNED_INT_SAMPLER_2D:
case GL_UNSIGNED_INT_SAMPLER_3D:
case GL_UNSIGNED_INT_SAMPLER_CUBE:
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_BUFFER:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY:
return SamplerNumericDomain::UnsignedInteger;
case GL_SAMPLER_1D:
case GL_SAMPLER_2D:
case GL_SAMPLER_3D:
case GL_SAMPLER_CUBE:
case GL_SAMPLER_2D_RECT:
case GL_SAMPLER_1D_ARRAY:
case GL_SAMPLER_2D_ARRAY:
case GL_SAMPLER_BUFFER:
case GL_SAMPLER_2D_MULTISAMPLE:
case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
case GL_SAMPLER_CUBE_MAP_ARRAY:
case GL_SAMPLER_1D_SHADOW:
case GL_SAMPLER_2D_SHADOW:
case GL_SAMPLER_CUBE_SHADOW:
case GL_SAMPLER_2D_RECT_SHADOW:
case GL_SAMPLER_1D_ARRAY_SHADOW:
case GL_SAMPLER_2D_ARRAY_SHADOW:
case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW:
return SamplerNumericDomain::Float;
default:
return SamplerNumericDomain::Unknown;
}
}
ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program,
CompileOptionFlags flags) const { CompileOptionFlags flags) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
@@ -1583,9 +1465,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.samplerNameByBinding.assign(m_maxBindings, String()); entry.samplerNameByBinding.assign(m_maxBindings, String());
entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1); entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1);
entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D); entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D);
entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown);
entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED);
entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false);
entry.storageBlockNameByBinding.assign(m_maxBindings, String()); entry.storageBlockNameByBinding.assign(m_maxBindings, String());
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1); entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
entry.globalUboBinding = -1; entry.globalUboBinding = -1;
@@ -1713,54 +1592,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const GLenum uniformType = program.GetUniformType(static_cast<Uint>(location)); const TextureTarget target = UniformTypeToTextureTarget(program.GetUniformType(static_cast<Uint>(location)));
if (descriptorKind == DescriptorBindingKind::StorageImage) {
const VkFormat reflectedFormat =
ConvertSpirvImageFormatToVkFormat(sampler->image.image_format);
VkFormat& existingFormat = entry.storageImageFormatByBinding[binding];
MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED ||
reflectedFormat == VK_FORMAT_UNDEFINED ||
existingFormat == reflectedFormat,
"ProgramFactory::ReflectLayout: storage image binding %u ('%s') has "
"conflicting reflected formats (%d vs %d)",
binding, uniformName.c_str(), static_cast<Int>(existingFormat),
static_cast<Int>(reflectedFormat));
if (existingFormat == VK_FORMAT_UNDEFINED) {
existingFormat = reflectedFormat;
}
if (m_unformattedFloatStorageImagesEnabled &&
existingFormat == VK_FORMAT_UNDEFINED &&
IsFloatStorageImageUniformType(uniformType)) {
entry.storageImageUsesBindingFormatByBinding[binding] = true;
} else if (reflectedFormat != VK_FORMAT_UNDEFINED) {
// A typed declaration in any stage wins for the entire binding. This is
// required when another stage reaches the same image through an atomic
// path and therefore could not be made formatless.
entry.storageImageUsesBindingFormatByBinding[binding] = false;
}
}
const TextureTarget target = UniformTypeToTextureTarget(uniformType);
MOBILEGL_ASSERT(target != TextureTarget::Unknown, MOBILEGL_ASSERT(target != TextureTarget::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'", "ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'",
uniformName.c_str()); uniformName.c_str());
if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) {
const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType);
MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown,
"ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain "
"for '%s' (uniformType=0x%x)",
uniformName.c_str(), uniformType);
MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] ==
SamplerNumericDomain::Unknown ||
entry.samplerNumericDomainByBinding[binding] == numericDomain,
"ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting "
"numeric domains (%d vs %d)",
binding, uniformName.c_str(),
static_cast<Int>(entry.samplerNumericDomainByBinding[binding]),
static_cast<Int>(numericDomain));
entry.samplerNumericDomainByBinding[binding] = numericDomain;
}
MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 || MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 ||
entry.samplerUniformLocationByBinding[binding] == location, entry.samplerUniformLocationByBinding[binding] == location,
"ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)", "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)",
@@ -1862,6 +1697,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i] = spv; moduleSpirvs[i] = spv;
} }
// GL apps depend on cross-program position invariance for multi-pass equality
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote); decorate Position outputs Invariant so
// per-pipeline compilers cannot vary the position math between passes.
{
Vector<Uint> invariantSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
moduleSpirvs[i], invariantSpirv)) {
moduleSpirvs[i] = std::move(invariantSpirv);
} else {
MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; "
"keeping the original module",
program.GetExternalIndex());
}
}
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's // glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage // gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection // loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
@@ -1886,23 +1737,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
} }
// When Vulkan can legally access storage images without a statically declared
// format, let GL's glBindImageTexture format select the runtime image view. This
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
// float image qualifier can disagree with the bound render-target format. Integer
// storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the
// exact descriptor format required by their shader operations.
if (m_unformattedFloatStorageImagesEnabled) {
Vector<Uint> unformattedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
moduleSpirvs[i], unformattedSpirv)) {
moduleSpirvs[i] = std::move(unformattedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u",
program.GetExternalIndex());
}
}
} }
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
@@ -14,16 +14,8 @@
#include "MG_State/GLState/TextureState/TextureEnum.h" #include "MG_State/GLState/TextureState/TextureEnum.h"
#include <Includes.h> #include <Includes.h>
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8 {
Unknown = 0,
Float,
SignedInteger,
UnsignedInteger,
};
class ProgramFactory { class ProgramFactory {
public: public:
enum class DescriptorBindingKind : Uint8 { enum class DescriptorBindingKind : Uint8 {
@@ -62,9 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<String> samplerNameByBinding; Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding; Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding; Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding; Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding; Vector<Int> storageBlockIndexByBinding;
Int globalUboBinding = -1; Int globalUboBinding = -1;
@@ -93,10 +82,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
@@ -133,10 +118,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerNameByBinding = std::move(other.samplerNameByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
samplerNumericDomainByBinding = std::move(other.samplerNumericDomainByBinding);
storageImageFormatByBinding = std::move(other.storageImageFormatByBinding);
storageImageUsesBindingFormatByBinding =
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
@@ -186,11 +167,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false, Bool shaderDrawParametersEnabled = false)
Bool unformattedFloatStorageImagesEnabled = false)
: m_device(device), m_maxBindings(maxBindings), m_config(config), : m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled), m_shaderDrawParametersEnabled(shaderDrawParametersEnabled) {
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
VkProgramObject::s_device = device; VkProgramObject::s_device = device;
} }
~ProgramFactory() = default; ~ProgramFactory() = default;
@@ -201,8 +180,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
static VkShaderStageFlagBits ToVkStage(ShaderStage stage); static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
private: private:
struct ProgramLookupCache { struct ProgramLookupCache {
@@ -229,9 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass // True when the device enabled shaderDrawParameters; gates the InstanceIndex rebase pass
// (which needs the DrawParameters capability / gl_BaseInstance builtin). // (which needs the DrawParameters capability / gl_BaseInstance builtin).
Bool m_shaderDrawParametersEnabled = false; Bool m_shaderDrawParametersEnabled = false;
// True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup; mutable ProgramLookupCache m_lastLookup;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
@@ -13,7 +13,6 @@
#include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h" #include "MG_State/GLState/TextureState/TextureObject2D.h"
#include "MG_State/GLState/TextureState/TextureObjectBuffer.h" #include "MG_State/GLState/TextureState/TextureObjectBuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
@@ -79,16 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return uniformUnit >= 0 ? uniformUnit : 0; return uniformUnit >= 0 ? uniformUnit : 0;
} }
VkFormat UniformManager::ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat) {
if (useBindingFormat) {
const TextureInternalFormat bindingInternalFormat =
MG_Util::ConvertGLEnumToTextureInternalFormat(bindingFormat);
return MG_Util::ConvertTextureInternalFormatToVkEnum(bindingInternalFormat);
}
return reflectedFormat != VK_FORMAT_UNDEFINED ? reflectedFormat : resourceFormat;
}
Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager, Bool UniformManager::Initialize(VkDevice device, VkBufferManager* bufferManager,
ProgramFactory* programFactory, ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
@@ -287,36 +276,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u", "ResolveSamplerDescriptor: invalid sampled image layout=%d for textureId=%d, binding=%u",
static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding); static_cast<Int>(resource->layout), texture->GetExternalIndex(), binding);
} }
MOBILEGL_ASSERT(binding < programObj.samplerNumericDomainByBinding.size(),
"ResolveSamplerDescriptor: sampler numeric-domain binding %u out of range", binding);
const SamplerNumericDomain numericDomain = programObj.samplerNumericDomainByBinding[binding];
// Vulkan forbids linear filtering and anisotropy for integer sampled-image formats.
// Some desktop GL shader packs deliberately bit-read a mutable float texture through a
// usampler and still leave the texture's ordinary linear parameters in place; texelFetch
// ignores filtering, so a nearest VkSampler preserves the operation while keeping the
// descriptor valid.
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
numericDomain == SamplerNumericDomain::UnsignedInteger;
const VkFormat sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
return false;
}
const VkImageView sampledImageView =
m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
binding, programObj.samplerNameByBinding[binding].c_str(), texture->GetExternalIndex(),
static_cast<Int>(resource->format), static_cast<Int>(sampledViewFormat),
static_cast<Int>(numericDomain));
return false;
}
// Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's // Skip GetOrCreateSampler's per-draw key hash + map lookup when this binding's
// sampler object and texture (both by lifetime id + version) are unchanged from the // sampler object and texture (both by lifetime id + version) are unchanged from the
// last draw that resolved it: the resulting sampler key, and therefore the VkSampler // last draw that resolved it: the resulting sampler key, and therefore the VkSampler
@@ -332,27 +291,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint64 textureLifetimeId = texture->GetLifetimeId(); const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion && if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion && memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion) {
memo.forceNearestFiltering == forceNearestFiltering) {
resolvedSampler = memo.sampler; resolvedSampler = memo.sampler;
} else { } else {
resolvedSampler = resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
memo.samplerLifetimeId = samplerLifetimeId; memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion; memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId; memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion; memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering;
memo.sampler = resolvedSampler; memo.sampler = resolvedSampler;
memo.valid = true; memo.valid = true;
} }
} else { } else {
resolvedSampler = resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture);
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
} }
outImageInfo = { outImageInfo = {
.sampler = resolvedSampler, .sampler = resolvedSampler,
.imageView = sampledImageView, .imageView = resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView,
.imageLayout = resource->layout, .imageLayout = resource->layout,
}; };
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
@@ -617,32 +572,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level)); const Uint32 mipLevel = static_cast<Uint32>(std::max<GLint>(0, imageBinding.Level));
MOBILEGL_ASSERT(binding < programObj.storageImageFormatByBinding.size(), VkImageView view = m_textureManager->GetOrCreateViewAtMipLevel(*imageBinding.Texture, mipLevel);
"ResolveStorageImageDescriptor: storage image format binding %u out of range", binding);
MOBILEGL_ASSERT(binding < programObj.storageImageUsesBindingFormatByBinding.size(),
"ResolveStorageImageDescriptor: storage image format policy binding %u out of range",
binding);
const VkFormat reflectedFormat = programObj.storageImageFormatByBinding[binding];
const Bool useBindingFormat = programObj.storageImageUsesBindingFormatByBinding[binding];
const VkFormat viewFormat = ResolveStorageImageViewFormat(
reflectedFormat, imageBinding.Format, resource->format, useBindingFormat);
if (viewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveStorageImageDescriptor: unsupported glBindImageTexture format=0x%x "
"for binding=%u imageUnit=%d textureId=%d bindingPolicy=%s",
imageBinding.Format, binding, imageUnit, imageBinding.Texture->GetExternalIndex(),
useBindingFormat ? "true" : "false");
return false;
}
const VkImageView view = m_textureManager->GetOrCreateStorageImageView(
*imageBinding.Texture, mipLevel, viewFormat, imageBinding.Layered != GL_FALSE, imageBinding.Layer);
if (view == VK_NULL_HANDLE) { if (view == VK_NULL_HANDLE) {
MGLOG_E("ResolveStorageImageDescriptor: failed to resolve storage view textureId=%d mip=%u " view = resource->fullView;
"bindingFormat=0x%x imageFormat=%d reflectedFormat=%d selectedFormat=%d bindingPolicy=%s",
imageBinding.Texture->GetExternalIndex(), mipLevel, imageBinding.Format,
static_cast<Int>(resource->format), static_cast<Int>(reflectedFormat),
static_cast<Int>(viewFormat),
useBindingFormat ? "true" : "false");
return false;
} }
outImageInfo.sampler = VK_NULL_HANDLE; outImageInfo.sampler = VK_NULL_HANDLE;
outImageInfo.imageView = view; outImageInfo.imageView = view;
@@ -700,50 +632,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool UniformManager::CollectStorageImageTextures(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const {
outTextures.clear();
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
"CollectStorageImageTextures: GL context is null");
const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
continue;
}
if (binding >= programObj.samplerUniformLocationByBinding.size()) {
MGLOG_E("CollectStorageImageTextures: binding %u has no uniform-location mapping", binding);
return false;
}
const Int location = programObj.samplerUniformLocationByBinding[binding];
if (location < 0) {
MGLOG_E("CollectStorageImageTextures: binding %u has no image uniform location", binding);
return false;
}
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
MGLOG_E("CollectStorageImageTextures: image unit %d is invalid for binding %u",
imageUnit, binding);
return false;
}
auto* texture = MG_State::pGLContext->GetImageTextureBinding(imageUnit).Texture.get();
if (texture == nullptr) {
MGLOG_E("CollectStorageImageTextures: image unit %d is unbound for binding %u",
imageUnit, binding);
return false;
}
if (std::find(outTextures.begin(), outTextures.end(), texture) == outTextures.end()) {
outTextures.push_back(texture);
}
}
return true;
}
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const { UboBindResult& out) const {
@@ -42,9 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures); Vector<MG_State::GLState::ITextureObject*>& outTextures);
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
@@ -52,12 +49,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr); const SamplerBindingOverride* samplerBindingOverride = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
// format and never silently fall back to the backing image format.
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
private: private:
struct DescriptorPoolBucket { struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE; VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -174,9 +165,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampler sampler = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE;
Uint16 samplerVersion = 0; Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0; Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false; Bool valid = false;
}; };
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo; mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -66,7 +66,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Uint32 unsupportedAttribMask = 0; Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
@@ -75,9 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const VkFormat sourceVkFormat = const auto vkFormat = ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra); if (vkFormat == VK_FORMAT_UNDEFINED) {
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat", "enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -85,33 +83,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None;
if (!SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
}
}
if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
}
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) { if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is " MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
@@ -121,28 +92,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const Uint32 sourceStride = const Uint32 stride =
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize); attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
const VkVertexInputRate inputRate = const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -152,7 +103,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0); bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
bindingAttributeLocations.push_back(location); bindingAttributeLocations.push_back(location);
bindingUsesClientMemory.push_back(attr.Buffer == nullptr); bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate); builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0); builder.AddAttribute(location, binding, vkFormat, 0);
} }
@@ -167,7 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.bindingBaseOffsets = std::move(bindingBaseOffsets); entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations); entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory); entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask; entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state; entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data(); entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
@@ -333,47 +282,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT componentSize = GetComponentSize(type); const SizeT componentSize = GetComponentSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size); return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
} }
Bool VertexInputStateFactory::IsScaledIntegerVertexFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_USCALED:
case VK_FORMAT_R8_SSCALED:
case VK_FORMAT_R8G8_USCALED:
case VK_FORMAT_R8G8_SSCALED:
case VK_FORMAT_R8G8B8_USCALED:
case VK_FORMAT_R8G8B8_SSCALED:
case VK_FORMAT_R8G8B8A8_USCALED:
case VK_FORMAT_R8G8B8A8_SSCALED:
case VK_FORMAT_R16_USCALED:
case VK_FORMAT_R16_SSCALED:
case VK_FORMAT_R16G16_USCALED:
case VK_FORMAT_R16G16_SSCALED:
case VK_FORMAT_R16G16B16_USCALED:
case VK_FORMAT_R16G16B16_SSCALED:
case VK_FORMAT_R16G16B16A16_USCALED:
case VK_FORMAT_R16G16B16A16_SSCALED:
return true;
default:
return false;
}
}
VkFormat VertexInputStateFactory::ToFloat32VertexFormat(Int componentCount) {
switch (componentCount) {
case 1: return VK_FORMAT_R32_SFLOAT;
case 2: return VK_FORMAT_R32G32_SFLOAT;
case 3: return VK_FORMAT_R32G32B32_SFLOAT;
case 4: return VK_FORMAT_R32G32B32A32_SFLOAT;
default: return VK_FORMAT_UNDEFINED;
}
}
Bool VertexInputStateFactory::SupportsVertexBufferFormat(VkFormat format) const {
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
return false;
}
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
}
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,12 +19,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public: public:
using HashType = Uint64; using HashType = Uint64;
enum class VertexStreamConversion : Uint8 {
None = 0,
Repack,
ScaledIntegerToFloat32,
};
struct BackendVertexInputState { struct BackendVertexInputState {
HashType hash = 0; HashType hash = 0;
Vector<VkVertexInputBindingDescription> bindings; Vector<VkVertexInputBindingDescription> bindings;
@@ -33,7 +27,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SizeT> bindingBaseOffsets; Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations; Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory; Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are // Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
// absent from `attributes`, so without this mask the draw path cannot tell them apart from // absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value. // a genuinely disabled array and would silently feed the shader the current attribute value.
@@ -43,8 +36,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
}; };
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice): explicit VertexInputStateFactory(const VulkanRendererConfig& config):
m_config(config), m_physicalDevice(physicalDevice) {} m_config(config) {}
~VertexInputStateFactory() = default; ~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete; VertexInputStateFactory(const VertexInputStateFactory&) = delete;
@@ -63,12 +56,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private: private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false); static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
static Bool IsScaledIntegerVertexFormat(VkFormat format);
static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const;
const VulkanRendererConfig& m_config; const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache; UnorderedMap<HashType, BackendVertexInputState> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
@@ -157,25 +157,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Bool VkBufferObject::Invalidate(VkDeviceSize size, VkDeviceSize offset) {
MOBILEGL_ASSERT(IsValid(), "VkBufferObject::Invalidate called on invalid buffer");
MOBILEGL_ASSERT(IsMapped(), "VkBufferObject::Invalidate requires mapped memory");
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::Invalidate offset out of range");
const VkDeviceSize resolvedSize = size == VK_WHOLE_SIZE ? m_size - offset : size;
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::Invalidate range out of bounds");
if (resolvedSize == 0) {
return true;
}
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false;
}
return true;
}
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const { BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range"); MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size; const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
@@ -44,7 +44,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* Map(); void* Map();
void Unmap(); void Unmap();
Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0); Bool Upload(const void* data, VkDeviceSize size, VkDeviceSize offset = 0);
Bool Invalidate(VkDeviceSize size = VK_WHOLE_SIZE, VkDeviceSize offset = 0);
VkBuffer GetHandle() const { return m_buffer; } VkBuffer GetHandle() const { return m_buffer; }
VkDeviceSize GetSize() const { return m_size; } VkDeviceSize GetSize() const { return m_size; }
@@ -65,10 +65,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true; return true;
} }
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const {
Bool forceNearestFiltering) const {
if (!m_samplerAnisotropySupported) return 1.0f; if (!m_samplerAnisotropySupported) return 1.0f;
if (forceNearestFiltering) return 1.0f;
// VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to // VUID-VkSamplerCreateInfo-anisotropyEnable-01071/01072: anisotropy requires both filters to
// be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy]. // be LINEAR and the value to sit within [1, limits.maxSamplerAnisotropy].
if (sampler.GetMinFilter() != SamplerFilterMode::Linear || if (sampler.GetMinFilter() != SamplerFilterMode::Linear ||
@@ -92,13 +90,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) const {
Bool forceNearestFiltering) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
const auto minFilter = sampler.GetMinFilter(); const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter(); const auto magFilter = sampler.GetMagFilter();
@@ -120,7 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan // The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one // will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's. // VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy))); XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode(); const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
@@ -132,9 +127,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) {
Bool forceNearestFiltering) { const Uint64 key = BuildSamplerKey(sampler, texture);
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
auto it = m_samplers.find(key); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
return it->second.handle; return it->second.handle;
@@ -142,17 +136,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSamplerCreateInfo samplerInfo{}; VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter()); samplerInfo.magFilter = ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter()); samplerInfo.minFilter = ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST samplerInfo.mipmapMode = ToVkMipmapMode(sampler.GetMipmapMode());
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS()); samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR()); samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
samplerInfo.mipLodBias = sampler.GetLodBias(); samplerInfo.mipLodBias = sampler.GetLodBias();
// Must use the same resolver as BuildSamplerKey - a divergence would either collide two // Must use the same resolver as BuildSamplerKey - a divergence would either collide two
// different samplers or silently create duplicates. // different samplers or silently create duplicates.
const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering); const Float maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler);
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE; samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
samplerInfo.maxAnisotropy = maxAnisotropy; samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
@@ -34,8 +34,7 @@ public:
void Shutdown(); void Shutdown();
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler, VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture);
Bool forceNearestFiltering = false);
private: private:
struct SamplerCacheEntry { struct SamplerCacheEntry {
@@ -45,8 +44,7 @@ private:
}; };
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture) const;
Bool forceNearestFiltering) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -59,8 +57,7 @@ private:
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw. // that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler, Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler) const;
Bool forceNearestFiltering) const;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig* m_config = nullptr; const VulkanRendererConfig* m_config = nullptr;
@@ -8,8 +8,6 @@
#include "VkTextureManager.h" #include "VkTextureManager.h"
#include "ProgramFactory.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
@@ -19,7 +17,6 @@
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <vulkan/utility/vk_format_utils.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule // Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
@@ -66,59 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
target == TextureUploadTarget::ProxyTexture2DMultisampleArray; target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
} }
static Bool IsMutableStorageImageFormat(VkFormat format) {
if (!vkuFormatIsColor(format) || vkuFormatIsCompressed(format)) {
return false;
}
// These are the uncompressed color compatibility classes covered by the core GLSL/SPIR-V
// storage-image formats. OpenGL mutable texture storage uses image-format compatibility by
// size, so a shader may legally reinterpret (for example) RGBA16_UNORM storage as rgba16f. Vulkan
// requires the image to be mutable and the view formats to share this exact compatibility
// class for the equivalent operation.
switch (vkuFormatCompatibilityClass(format)) {
case VKU_FORMAT_COMPATIBILITY_CLASS_8BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_16BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_32BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_64BIT:
case VKU_FORMAT_COMPATIBILITY_CLASS_128BIT:
return true;
default:
return false;
}
}
static Bool HasMatchingColorComponentLayout(VkFormat lhs, VkFormat rhs) {
const VKU_FORMAT_INFO lhsInfo = vkuGetFormatInfo(lhs);
const VKU_FORMAT_INFO rhsInfo = vkuGetFormatInfo(rhs);
if (lhsInfo.component_count == 0 || lhsInfo.component_count != rhsInfo.component_count ||
lhsInfo.texel_block_size != rhsInfo.texel_block_size ||
lhsInfo.texels_per_block != 1 || rhsInfo.texels_per_block != 1) {
return false;
}
for (Uint32 component = 0; component < lhsInfo.component_count; ++component) {
if (lhsInfo.components[component].type != rhsInfo.components[component].type ||
lhsInfo.components[component].size != rhsInfo.components[component].size) {
return false;
}
}
return true;
}
static Bool FormatMatchesSamplerNumericDomain(VkFormat format, SamplerNumericDomain numericDomain) {
switch (numericDomain) {
case SamplerNumericDomain::Float:
return vkuFormatIsSampledFloat(format);
case SamplerNumericDomain::SignedInteger:
return vkuFormatIsSINT(format);
case SamplerNumericDomain::UnsignedInteger:
return vkuFormatIsUINT(format);
case SamplerNumericDomain::Unknown:
return true;
}
return false;
}
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) { static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples) { switch (requestedSamples) {
case 1: case 1:
@@ -823,180 +767,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return perMipSampledView; return perMipSampledView;
} }
VkImageView VkTextureManager::GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture,
VkFormat format) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE ||
resource->sampledView == VK_NULL_HANDLE) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED || format == resource->format) {
return resource->sampledView;
}
if (!AreSampledImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible sampled image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for sampled view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
const TextureResource::SampledImageViewKey key{
.baseMipLevel = resource->sampledBaseMipLevel,
.levelCount = resource->sampledLevelCount,
.viewType = resource->viewType,
.format = format,
};
const auto existing = resource->alternateSampledViews.find(key);
if (existing != resource->alternateSampledViews.end()) {
return existing->second;
}
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) {
MGLOG_E("%s: sampled image view format=%d lacks VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT "
"for textureId=%d (available=0x%x)",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Uint32>(formatProperties.optimalTilingFeatures));
return VK_NULL_HANDLE;
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageView view = CreateImageView(
resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, resource->viewType,
resource->sampledBaseMipLevel, resource->sampledLevelCount, 0, resource->arrayLayers,
&sampledComponents, VK_IMAGE_USAGE_SAMPLED_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create sampled image view textureId=%d imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
resource->alternateSampledViews.emplace(key, view);
MGLOG_D("%s: created sampled image view textureId=%d imageFormat=%d viewFormat=%d mip=[%u,%u)",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format), resource->sampledBaseMipLevel,
resource->sampledBaseMipLevel + resource->sampledLevelCount);
return view;
}
VkImageView VkTextureManager::GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, VkFormat format,
Bool layered, Int32 layer) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels ||
resource->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
(resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
return VK_NULL_HANDLE;
}
if (format == VK_FORMAT_UNDEFINED) {
format = resource->format;
}
if (!AreStorageImageViewFormatsCompatible(resource->format, format)) {
MGLOG_E("%s: incompatible storage image view format=%d for textureId=%d imageFormat=%d",
__func__, static_cast<Int>(format), texture.GetExternalIndex(),
static_cast<Int>(resource->format));
return VK_NULL_HANDLE;
}
if (format != resource->format &&
(resource->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
MGLOG_E("%s: textureId=%d needs mutable image format=%d for storage view format=%d",
__func__, texture.GetExternalIndex(), static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
Uint32 baseArrayLayer = 0;
Uint32 layerCount = resource->arrayLayers;
VkImageViewType viewType = resource->viewType;
if (!layered) {
switch (resource->viewType) {
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
viewType = VK_IMAGE_VIEW_TYPE_1D;
break;
case VK_IMAGE_VIEW_TYPE_2D_ARRAY:
case VK_IMAGE_VIEW_TYPE_CUBE:
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
viewType = VK_IMAGE_VIEW_TYPE_2D;
break;
case VK_IMAGE_VIEW_TYPE_3D:
MGLOG_E("%s: non-layered 3D storage views are unsupported for textureId=%d",
__func__, texture.GetExternalIndex());
return VK_NULL_HANDLE;
default:
break;
}
if (viewType != resource->viewType) {
if (layer < 0 || static_cast<Uint32>(layer) >= resource->arrayLayers) {
MGLOG_E("%s: storage image layer=%d is out of range for textureId=%d arrayLayers=%u",
__func__, layer, texture.GetExternalIndex(), resource->arrayLayers);
return VK_NULL_HANDLE;
}
baseArrayLayer = static_cast<Uint32>(layer);
layerCount = 1;
}
}
const Bool isFullResourceView = baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType;
if (format == resource->format && isFullResourceView) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
const TextureResource::StorageImageViewKey key{
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
.format = format,
};
auto it = resource->storageImageViews.find(key);
if (it != resource->storageImageViews.end()) {
return it->second;
}
VkFormatFeatureFlags requiredFormatFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
if (format != resource->format &&
(format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT)) {
requiredFormatFeatures |= VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT;
}
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
if ((formatProperties.optimalTilingFeatures & requiredFormatFeatures) != requiredFormatFeatures) {
MGLOG_E("%s: storage image view format=%d lacks required features=0x%x for textureId=%d "
"(available=0x%x)",
__func__, static_cast<Int>(format), static_cast<Uint32>(requiredFormatFeatures),
texture.GetExternalIndex(), static_cast<Uint32>(formatProperties.optimalTilingFeatures));
return VK_NULL_HANDLE;
}
const VkImageView view = CreateImageView(resource->image, format, VK_IMAGE_ASPECT_COLOR_BIT, viewType,
mipLevel, 1, baseArrayLayer, layerCount, nullptr,
VK_IMAGE_USAGE_STORAGE_BIT);
if (view == VK_NULL_HANDLE) {
MGLOG_E("%s: failed to create storage image view for textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return VK_NULL_HANDLE;
}
resource->storageImageViews.emplace(key, view);
MGLOG_D("%s: created storage image view textureId=%d mip=%u imageFormat=%d viewFormat=%d",
__func__, texture.GetExternalIndex(), mipLevel, static_cast<Int>(resource->format),
static_cast<Int>(format));
return view;
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) { void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null"); MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture)); auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1315,18 +1085,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
if (supportsStorageImage && IsMutableStorageImageFormat(format)) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) && resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) && resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
@@ -1334,7 +1092,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.arrayLayers == shapeInfo.arrayLayers && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType && resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resource.mipLevels == backingMipLevels; resource.mipLevels == backingMipLevels;
if (compatible) { if (compatible) {
if (resource.perMipViews.size() != backingMipLevels) { if (resource.perMipViews.size() != backingMipLevels) {
@@ -1355,7 +1112,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.arrayLayers == shapeInfo.arrayLayers && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType && resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
resource.mipLevels < backingMipLevels && resource.mipLevels < backingMipLevels &&
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
@@ -1367,18 +1123,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DeferResourceRelease(Move(resource)); DeferResourceRelease(Move(resource));
} }
auto aspect = GetAspectMaskForFormat(format);
VkImageCreateInfo imageInfo{}; VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = imageCreateFlags; imageInfo.flags = shapeInfo.imageFlags;
imageInfo.imageType = shapeInfo.imageType; imageInfo.imageType = shapeInfo.imageType;
imageInfo.extent.width = static_cast<Uint32>(texelSize.x()); imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
imageInfo.extent.height = static_cast<Uint32>(texelSize.y()); imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
imageInfo.extent.depth = shapeInfo.depth; imageInfo.extent.depth = shapeInfo.depth;
imageInfo.mipLevels = backingMipLevels; imageInfo.mipLevels = backingMipLevels;
imageInfo.arrayLayers = shapeInfo.arrayLayers; imageInfo.arrayLayers = shapeInfo.arrayLayers;
imageInfo.format = format; imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
@@ -1389,17 +1153,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
} }
imageInfo.samples = resolvedSampleCount; imageInfo.samples = resolvedSampleCount;
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { if (isMultisampleTexture) {
VkImageFormatProperties imageFormatProperties{}; VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties); imageInfo.flags, &imageFormatProperties);
if (imageFormatResult != VK_SUCCESS || if (imageFormatResult != VK_SUCCESS ||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) { (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) {
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s " MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x",
"format=%d usage=0x%x", __func__, texture.GetSamples(), texture.GetExternalIndex(),
__func__, static_cast<Uint32>(imageInfo.flags), texture.GetSamples(),
texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(), MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage)); static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
return false; return false;
@@ -1426,7 +1188,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.aspect = aspect; resource.aspect = aspect;
resource.viewType = shapeInfo.viewType; resource.viewType = shapeInfo.viewType;
resource.sampleCount = resolvedSampleCount; resource.sampleCount = resolvedSampleCount;
resource.imageCreateFlags = imageCreateFlags;
resource.syncedTextureParamsVersion = 0; resource.syncedTextureParamsVersion = 0;
if (preservedResource) { if (preservedResource) {
@@ -1442,9 +1203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) { void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE && if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
resource.sampledView == VK_NULL_HANDLE && resource.sampledView == VK_NULL_HANDLE &&
resource.perMipViews.empty() && resource.perMipSampledViews.empty() && resource.perMipViews.empty() && resource.perMipSampledViews.empty()) {
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
resource.storageImageViews.empty()) {
return; return;
} }
@@ -1540,10 +1299,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampledView = VK_NULL_HANDLE; sampledView = VK_NULL_HANDLE;
} }
} }
for (const auto& [_, sampledView] : resource.alternateSampledViews) {
DeferViewRelease(sampledView);
}
resource.alternateSampledViews.clear();
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
@@ -1569,8 +1324,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer, Uint32 baseArrayLayer,
Uint32 layerCount, Uint32 layerCount,
const VkComponentMapping* components, const VkComponentMapping* components) const {
VkImageUsageFlags viewUsage) const {
VkImageViewCreateInfo viewInfo{}; VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image; viewInfo.image = image;
@@ -1586,13 +1340,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer; viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
viewInfo.subresourceRange.layerCount = layerCount; viewInfo.subresourceRange.layerCount = layerCount;
VkImageViewUsageCreateInfo usageInfo{};
if (viewUsage != 0) {
usageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO;
usageInfo.usage = viewUsage;
viewInfo.pNext = &usageInfo;
}
VkImageView view = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)"); VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
return view; return view;
@@ -1918,55 +1665,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
return imageAspect; return imageAspect;
} }
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
SamplerNumericDomain numericDomain) {
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
return imageFormat;
}
if (!IsMutableStorageImageFormat(imageFormat)) {
return VK_FORMAT_UNDEFINED;
}
// Preserve component ordering and bit widths. This selects R32_UINT for an R32_SFLOAT
// texture sampled by a usampler rather than an arbitrary member (such as
// R8G8B8A8_UINT) of Vulkan's broad 32-bit compatibility class.
for (Int candidateValue = static_cast<Int>(VK_FORMAT_R4G4_UNORM_PACK8);
candidateValue <= static_cast<Int>(VK_FORMAT_ASTC_12x12_SRGB_BLOCK);
++candidateValue) {
const VkFormat candidate = static_cast<VkFormat>(candidateValue);
if (!IsMutableStorageImageFormat(candidate) ||
!FormatMatchesSamplerNumericDomain(candidate, numericDomain) ||
!HasMatchingColorComponentLayout(imageFormat, candidate) ||
!AreSampledImageViewFormatsCompatible(imageFormat, candidate)) {
continue;
}
// If an integer backing is intentionally bit-read through a float sampler, require
// a true floating-point view. Normalized/scaled views satisfy OpTypeFloat but apply
// an unrelated numeric conversion to those bits.
if (numericDomain == SamplerNumericDomain::Float && !vkuFormatIsSFLOAT(candidate)) {
continue;
}
return candidate;
}
return VK_FORMAT_UNDEFINED;
}
Bool VkTextureManager::AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
if (imageFormat == viewFormat) {
return true;
}
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
}
Bool VkTextureManager::AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat) {
if (imageFormat == viewFormat) {
return true;
}
return IsMutableStorageImageFormat(imageFormat) && IsMutableStorageImageFormat(viewFormat) &&
vkuFormatCompatibilityClass(imageFormat) == vkuFormatCompatibilityClass(viewFormat);
}
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,8 +19,6 @@ class ITextureObject;
} }
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
class VkTextureManager { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -80,61 +78,6 @@ public:
} }
}; };
struct StorageImageViewKey {
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const StorageImageViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKey {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED;
Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount &&
viewType == other.viewType &&
format == other.format;
}
};
struct SampledImageViewKeyHash {
SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct StorageImageViewKeyHash {
SizeT operator()(const StorageImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.mipLevel);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr; VmaAllocation allocation = nullptr;
VkImageView fullView = VK_NULL_HANDLE; VkImageView fullView = VK_NULL_HANDLE;
@@ -142,8 +85,6 @@ public:
Vector<VkImageView> perMipViews; Vector<VkImageView> perMipViews;
Vector<VkImageView> perMipSampledViews; Vector<VkImageView> perMipSampledViews;
UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews; UnorderedMap<AttachmentViewKey, VkImageView, AttachmentViewKeyHash> attachmentViews;
UnorderedMap<SampledImageViewKey, VkImageView, SampledImageViewKeyHash> alternateSampledViews;
UnorderedMap<StorageImageViewKey, VkImageView, StorageImageViewKeyHash> storageImageViews;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExtent2D extent = {0, 0}; VkExtent2D extent = {0, 0};
Uint32 depth = 1; Uint32 depth = 1;
@@ -155,7 +96,6 @@ public:
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
Uint16 syncedTextureParamsVersion = 0; Uint16 syncedTextureParamsVersion = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged. // lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
@@ -175,8 +115,6 @@ public:
std::swap(this->perMipViews, that.perMipViews); std::swap(this->perMipViews, that.perMipViews);
std::swap(this->perMipSampledViews, that.perMipSampledViews); std::swap(this->perMipSampledViews, that.perMipSampledViews);
std::swap(this->attachmentViews, that.attachmentViews); std::swap(this->attachmentViews, that.attachmentViews);
std::swap(this->alternateSampledViews, that.alternateSampledViews);
std::swap(this->storageImageViews, that.storageImageViews);
std::swap(this->layout, that.layout); std::swap(this->layout, that.layout);
std::swap(this->extent, that.extent); std::swap(this->extent, that.extent);
std::swap(this->depth, that.depth); std::swap(this->depth, that.depth);
@@ -188,7 +126,6 @@ public:
std::swap(this->aspect, that.aspect); std::swap(this->aspect, that.aspect);
std::swap(this->viewType, that.viewType); std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount); std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
@@ -216,16 +153,6 @@ public:
vkDestroyImageView(s_device, attachmentView, nullptr); vkDestroyImageView(s_device, attachmentView, nullptr);
} }
} }
for (const auto& [_, sampledView] : alternateSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
}
}
for (const auto& [_, storageImageView] : storageImageViews) {
if (storageImageView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, storageImageView, nullptr);
}
}
if (image != VK_NULL_HANDLE && allocation != nullptr) { if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator, image, allocation); vmaDestroyImage(s_allocator, image, allocation);
} }
@@ -234,8 +161,6 @@ public:
perMipViews.clear(); perMipViews.clear();
perMipSampledViews.clear(); perMipSampledViews.clear();
attachmentViews.clear(); attachmentViews.clear();
alternateSampledViews.clear();
storageImageViews.clear();
image = VK_NULL_HANDLE; image = VK_NULL_HANDLE;
allocation = nullptr; allocation = nullptr;
layout = VK_IMAGE_LAYOUT_UNDEFINED; layout = VK_IMAGE_LAYOUT_UNDEFINED;
@@ -249,7 +174,6 @@ public:
aspect = VK_IMAGE_ASPECT_NONE; aspect = VK_IMAGE_ASPECT_NONE;
viewType = VK_IMAGE_VIEW_TYPE_2D; viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT; sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
@@ -274,9 +198,6 @@ public:
Uint32 baseArrayLayer, Uint32 layerCount, Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType); VkImageViewType viewType);
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
VkFormat format, Bool layered, Int32 layer);
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout); void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer, void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture, MG_State::GLState::ITextureObject* texture,
@@ -286,9 +207,6 @@ public:
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
@@ -337,8 +255,7 @@ private:
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount, VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer, Uint32 baseArrayLayer,
Uint32 layerCount, Uint32 layerCount,
const VkComponentMapping* components = nullptr, const VkComponentMapping* components = nullptr) const;
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture, Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget, TextureUploadTarget uploadTarget,
TextureResource &outResource); TextureResource &outResource);
@@ -19,14 +19,15 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h" #include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Math/HalfFloat.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h> #include <Config.h>
#include <algorithm> #include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <vulkan/utility/vk_format_utils.h>
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
#if defined(__APPLE__) #if defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h> #include <CoreGraphics/CoreGraphics.h>
@@ -578,88 +579,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
template <typename ComponentT>
static Float ConvertIntegerVertexComponentToFloat(ComponentT value, Bool normalized) {
if (!normalized) {
return static_cast<Float>(value);
}
if constexpr (std::is_signed_v<ComponentT>) {
const Float scaled = static_cast<Float>(value) /
static_cast<Float>(std::numeric_limits<ComponentT>::max());
return std::max<Float>(-1.0f, scaled);
} else {
return static_cast<Float>(value) /
static_cast<Float>(std::numeric_limits<ComponentT>::max());
}
}
template <typename ComponentT>
static Bool ConvertIntegerVertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
SizeT sourceStride,
SizeT elementCount,
Vector<Float>& outData) {
if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attribute.Size);
outData.resize(elementCount * componentCount);
for (SizeT element = 0; element < elementCount; ++element) {
const Uint8* sourceElement = sourceData + element * sourceStride;
Float* destinationElement = outData.data() + element * componentCount;
for (SizeT component = 0; component < componentCount; ++component) {
ComponentT value{};
Memcpy(&value, sourceElement + component * sizeof(ComponentT), sizeof(ComponentT));
destinationElement[component] =
ConvertIntegerVertexComponentToFloat(value, attribute.Normalized);
}
}
return true;
}
static Bool ConvertScaledIntegerVertexStreamToFloat32(
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData,
SizeT sourceStride,
SizeT elementCount,
Vector<Float>& outData) {
switch (attribute.Type) {
case DataType::Int8:
return ConvertIntegerVertexStreamToFloat32<Int8>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Uint8:
return ConvertIntegerVertexStreamToFloat32<Uint8>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Int16:
return ConvertIntegerVertexStreamToFloat32<Int16>(
attribute, sourceData, sourceStride, elementCount, outData);
case DataType::Uint16:
return ConvertIntegerVertexStreamToFloat32<Uint16>(
attribute, sourceData, sourceStride, elementCount, outData);
default:
return false;
}
}
static Bool RepackVertexStream(const Uint8* sourceData,
SizeT sourceStride,
SizeT elementSize,
SizeT elementCount,
Vector<Uint8>& outData) {
if (sourceData == nullptr || sourceStride == 0 || elementSize == 0) {
return false;
}
outData.resize(elementCount * elementSize);
for (SizeT element = 0; element < elementCount; ++element) {
Memcpy(outData.data() + element * elementSize,
sourceData + element * sourceStride,
elementSize);
}
return true;
}
static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) {
switch (format) { switch (format) {
case TextureInternalFormat::R8I: case TextureInternalFormat::R8I:
@@ -1031,6 +950,7 @@ void main() {
} }
)"; )";
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>( Int maxDimension = std::max<Int>(
baseTexelSize.x(), baseTexelSize.x(),
@@ -1125,7 +1045,6 @@ void main() {
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VkImageLayout* trackedLayout = nullptr; VkImageLayout* trackedLayout = nullptr;
VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE; VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE;
VkFormat format = VK_FORMAT_UNDEFINED;
IntVec2 extent = {0, 0}; IntVec2 extent = {0, 0};
Uint32 mipLevel = 0; Uint32 mipLevel = 0;
Uint32 mipLevelCount = 1; Uint32 mipLevelCount = 1;
@@ -1289,7 +1208,6 @@ void main() {
outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.image = swapchainObject.GetImage(swapchainImageIndex);
outBinding.trackedLayout = nullptr; outBinding.trackedLayout = nullptr;
outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
outBinding.format = swapchainObject.GetSurfaceFormat().format;
const auto extent = swapchainObject.GetExtent(); const auto extent = swapchainObject.GetExtent();
outBinding.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)}; outBinding.extent = {static_cast<Int>(extent.width), static_cast<Int>(extent.height)};
outBinding.mipLevel = 0; outBinding.mipLevel = 0;
@@ -1338,7 +1256,6 @@ void main() {
outBinding.image = resource->image; outBinding.image = resource->image;
outBinding.trackedLayout = &resource->layout; outBinding.trackedLayout = &resource->layout;
outBinding.aspectMask = resource->aspect; outBinding.aspectMask = resource->aspect;
outBinding.format = resource->format;
const auto attachmentExtent = attachment.GetSize(); const auto attachmentExtent = attachment.GetSize();
outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()};
outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0)); outBinding.mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
@@ -1585,54 +1502,19 @@ void main() {
} }
} }
static Bool DecodeReadbackPixel(const Uint8* source, VkFormat sourceFormat, Float* rgba) { static Bool IsBgraVkFormat(VkFormat format) {
switch (sourceFormat) { switch (format) {
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
rgba[0] = static_cast<Float>(source[0]) / 255.0f;
rgba[1] = static_cast<Float>(source[1]) / 255.0f;
rgba[2] = static_cast<Float>(source[2]) / 255.0f;
rgba[3] = static_cast<Float>(source[3]) / 255.0f;
return true;
case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SNORM:
case VK_FORMAT_B8G8R8A8_SRGB: case VK_FORMAT_B8G8R8A8_SRGB:
rgba[0] = static_cast<Float>(source[2]) / 255.0f; case VK_FORMAT_B8G8R8A8_USCALED:
rgba[1] = static_cast<Float>(source[1]) / 255.0f; case VK_FORMAT_B8G8R8A8_SSCALED:
rgba[2] = static_cast<Float>(source[0]) / 255.0f;
rgba[3] = static_cast<Float>(source[3]) / 255.0f;
return true;
case VK_FORMAT_R16G16B16A16_UNORM:
for (SizeT component = 0; component < 4; ++component) {
Uint16 value = 0;
Memcpy(&value, source + component * sizeof(value), sizeof(value));
rgba[component] = static_cast<Float>(value) / 65535.0f;
}
return true;
case VK_FORMAT_R16G16B16A16_SFLOAT:
for (SizeT component = 0; component < 4; ++component) {
Uint16 value = 0;
Memcpy(&value, source + component * sizeof(value), sizeof(value));
rgba[component] = MG_Util::DecodeHalfBitsToFloat(value);
}
return true;
case VK_FORMAT_R32G32B32A32_SFLOAT:
Memcpy(rgba, source, sizeof(Float) * 4);
return true; return true;
default: default:
return false; return false;
} }
} }
static Uint8 EncodeReadbackUnorm8(Float value) {
if (!(value > 0.0f)) {
return 0;
}
if (value >= 1.0f) {
return 255;
}
return static_cast<Uint8>(value * 255.0f + 0.5f);
}
// Remap raw swapchain pixels (top-left origin, preTransform-rotated) into // Remap raw swapchain pixels (top-left origin, preTransform-rotated) into
// GL-oriented pixels (bottom-left origin) for the retrace snapshot path. // GL-oriented pixels (bottom-left origin) for the retrace snapshot path.
// Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip // Mirrors the removed GetPresentedDumpPixel mapping plus the Y-origin flip
@@ -1642,7 +1524,6 @@ void main() {
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels, static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
VkExtent2D rawExtent, VkExtent2D rawExtent,
VkSurfaceTransformFlagBitsKHR preTransform, VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize,
Uint8* outPixels) { Uint8* outPixels) {
if (IsQuarterTurnPreTransform(preTransform)) { if (IsQuarterTurnPreTransform(preTransform)) {
return false; return false;
@@ -1666,9 +1547,12 @@ void main() {
default: default:
break; break;
} }
const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * texelSize; const Uint8* src = rawPixels + (static_cast<SizeT>(rawY) * w + rawX) * 4;
Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * texelSize; Uint8* dst = outPixels + (static_cast<SizeT>(outY) * w + outX) * 4;
Memcpy(dst, src, texelSize); dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
} }
} }
return true; return true;
@@ -1692,11 +1576,12 @@ void main() {
} }
} }
static void StoreReadbackPixel(const Float* rgba, GLenum dstFormat, Uint8* dst) { static void StoreReadbackPixel(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Uint8* dst) {
const Uint8 r = EncodeReadbackUnorm8(rgba[0]); const Uint8 r = srcIsBgra ? src[2] : src[0];
const Uint8 g = EncodeReadbackUnorm8(rgba[1]); const Uint8 g = src[1];
const Uint8 b = EncodeReadbackUnorm8(rgba[2]); const Uint8 b = srcIsBgra ? src[0] : src[2];
const Uint8 a = EncodeReadbackUnorm8(rgba[3]); const Uint8 a = src[3];
switch (dstFormat) { switch (dstFormat) {
case GL_RGB: case GL_RGB:
dst[0] = r; dst[0] = r;
@@ -1725,11 +1610,13 @@ void main() {
} }
} }
static void StoreReadbackPixelFloat(const Float* rgba, GLenum dstFormat, Float* dst) { static void StoreReadbackPixelFloat(const Uint8* src, Bool srcIsBgra, GLenum dstFormat, Float* dst) {
const Float r = rgba[0]; const Float r = static_cast<Float>(srcIsBgra ? src[2] : src[0]) / 255.0f;
const Float g = rgba[1]; const Float g = static_cast<Float>(src[1]) / 255.0f;
const Float b = rgba[2]; const Float b = static_cast<Float>(srcIsBgra ? src[0] : src[2]) / 255.0f;
const Float a = rgba[3]; const Float a = static_cast<Float>(src[3]) / 255.0f;
// TODO: extend readback packing to integer/depth formats instead of only normalized color formats.
switch (dstFormat) { switch (dstFormat) {
case GL_RGB: case GL_RGB:
dst[0] = r; dst[0] = r;
@@ -1787,11 +1674,20 @@ void main() {
(static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes); (static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes);
Vector<Uint8> packed(packedSize, 0); Vector<Uint8> packed(packedSize, 0);
if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type, const Bool srcIsBgra = IsBgraVkFormat(srcFormat);
dstRowStride, packed.data() + dstOffset)) { for (GLsizei row = 0; row < height; ++row) {
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d", const Uint8* srcRow = srcPixels + static_cast<SizeT>(row) * static_cast<SizeT>(width) * 4;
static_cast<Int>(srcFormat)); Uint8* dstRow = packed.data() + dstOffset + static_cast<SizeT>(row) * dstRowStride;
return false; 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);
}
}
} }
const auto& pixelPackBufferObject = const auto& pixelPackBufferObject =
@@ -1813,60 +1709,6 @@ void main() {
} }
} // namespace } // namespace
SizeT VulkanRenderer::GetReadbackTexelSize(VkFormat sourceFormat) {
const VKU_FORMAT_INFO formatInfo = vkuGetFormatInfo(sourceFormat);
if (formatInfo.texels_per_block != 1) {
return 0;
}
return formatInfo.texel_block_size;
}
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels) {
if (width <= 0 || height <= 0) {
return true;
}
if (sourcePixels == nullptr || destinationPixels == nullptr) {
return false;
}
const SizeT sourceTexelSize = GetReadbackTexelSize(sourceFormat);
const Int destinationChannels = GetReadbackChannelCount(destinationFormat);
if (sourceTexelSize == 0 || destinationChannels == 0 ||
(destinationType != GL_UNSIGNED_BYTE && destinationType != GL_FLOAT)) {
return false;
}
const SizeT destinationComponentSize = destinationType == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
const SizeT destinationPixelSize = static_cast<SizeT>(destinationChannels) * destinationComponentSize;
if (destinationRowStride < static_cast<SizeT>(width) * destinationPixelSize) {
return false;
}
for (GLsizei row = 0; row < height; ++row) {
const Uint8* sourceRow = sourcePixels +
static_cast<SizeT>(row) * static_cast<SizeT>(width) * sourceTexelSize;
Uint8* destinationRow = destinationPixels + static_cast<SizeT>(row) * destinationRowStride;
for (GLsizei column = 0; column < width; ++column) {
const Uint8* source = sourceRow + static_cast<SizeT>(column) * sourceTexelSize;
Uint8* destination = destinationRow + static_cast<SizeT>(column) * destinationPixelSize;
Float rgba[4]{};
if (!DecodeReadbackPixel(source, sourceFormat, rgba)) {
return false;
}
if (destinationType == GL_FLOAT) {
Float converted[4]{};
StoreReadbackPixelFloat(rgba, destinationFormat, converted);
Memcpy(destination, converted, destinationPixelSize);
} else {
StoreReadbackPixel(rgba, destinationFormat, destination);
}
}
}
return true;
}
VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType, VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
@@ -2046,9 +1888,30 @@ void main() {
m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config); m_pipelineFactory = MakeUnique<PipelineFactory>(m_device, m_config);
MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed.");
{
// Qualcomm's pipeline compiler does not keep vertex positions invariant across
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
// Invariant decoration), so a blended depth-writing prepass makes later
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
// clouds flicker black). Suppress blended depth writes there; the env variable
// forces the quirk on ("0") or off ("1") on any driver.
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm;
if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) {
if (env[0] == '0') {
suppressBlendedDepthWrite = true;
} else if (env[0] == '1') {
suppressBlendedDepthWrite = false;
}
}
if (suppressBlendedDepthWrite) {
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
"(driver lacks cross-pipeline position invariance)");
}
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
}
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings, m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled, m_shaderDrawParametersFeatureEnabled);
m_unformattedFloatStorageImagesEnabled);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
m_samplerManager = MakeUnique<VkSamplerManager>(); m_samplerManager = MakeUnique<VkSamplerManager>();
@@ -2068,7 +1931,7 @@ void main() {
m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight, m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight,
maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get()); maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get());
MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed.");
m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config, m_physicalDevice.handle); m_vertexInputStateFactory = MakeUnique<VertexInputStateFactory>(m_config);
MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed.");
// Prime the first frame so Render() always targets an acquired swapchain image. // Prime the first frame so Render() always targets an acquired swapchain image.
@@ -2083,7 +1946,6 @@ void main() {
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
MGLOG_D("VulkanRenderer initialized"); MGLOG_D("VulkanRenderer initialized");
} }
@@ -2202,8 +2064,7 @@ void main() {
Bool VulkanRenderer::UploadAndBindVertexBuffers( Bool VulkanRenderer::UploadAndBindVertexBuffers(
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams, const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams) {
Bool indexedDraw) {
// programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat // programObj is resolved once in SetupDraw and passed in; re-resolving it here would repeat
// the GetCurrentProgram + GetOrCreateProgram hash lookup every draw. // the GetCurrentProgram + GetOrCreateProgram hash lookup every draw.
auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao);
@@ -2229,39 +2090,6 @@ void main() {
return nullptr; return nullptr;
}; };
auto uploadConvertedStream = [&](VertexInputStateFactory::VertexStreamConversion conversion,
const MG_State::GLState::VertexAttribute& attribute,
const Uint8* sourceData, SizeT sourceStride,
SizeT elementSize, SizeT elementCount,
BufferSlice& outSlice) -> Bool {
const void* uploadData = nullptr;
VkDeviceSize uploadSize = 0;
switch (conversion) {
case VertexInputStateFactory::VertexStreamConversion::Repack:
if (!RepackVertexStream(sourceData, sourceStride, elementSize, elementCount,
m_vertexRepackScratch)) {
return false;
}
uploadData = m_vertexRepackScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexRepackScratch.size());
break;
case VertexInputStateFactory::VertexStreamConversion::ScaledIntegerToFloat32:
if (!ConvertScaledIntegerVertexStreamToFloat32(attribute, sourceData, sourceStride,
elementCount, m_vertexConversionScratch)) {
return false;
}
uploadData = m_vertexConversionScratch.data();
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
break;
case VertexInputStateFactory::VertexStreamConversion::None:
return false;
}
return uploadSize > 0 &&
m_bufferManager.UploadTransient(BufferKind::Vertex,
m_frameContext.GetCurrentFrameIndex(),
uploadData, uploadSize, 16, outSlice);
};
for (SizeT binding = 0; binding < bindingCount; ++binding) { for (SizeT binding = 0; binding < bindingCount; ++binding) {
if (binding >= vertexInputState.bindings.size()) { if (binding >= vertexInputState.bindings.size()) {
break; break;
@@ -2271,53 +2099,28 @@ void main() {
: static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); : static_cast<Uint32>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() && const Bool usesClientMemory = binding < vertexInputState.bindingUsesClientMemory.size() &&
vertexInputState.bindingUsesClientMemory[binding]; vertexInputState.bindingUsesClientMemory[binding];
const auto conversion = binding < vertexInputState.bindingConversions.size()
? vertexInputState.bindingConversions[binding]
: VertexInputStateFactory::VertexStreamConversion::None;
if (usesClientMemory) { if (usesClientMemory) {
const Uint32 location = bindingLocation; const Uint32 location = bindingLocation;
MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS, MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"UploadAndBindVertexStreams failed to resolve client attribute location"); "UploadAndBindVertexStreams failed to resolve client attribute location");
const auto& attr = vao.GetAttribute(location); const auto& attr = vao.GetAttribute(location);
const SizeT elementSize = const SizeT componentSize = VertexInputStateFactory::GetComponentSize(attr.Type);
VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT elementSize = componentSize * static_cast<SizeT>(attr.Size);
const SizeT stride = attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize; const SizeT stride = attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
const auto* clientData = reinterpret_cast<const Uint8*>(attr.Offset); const auto* clientData = reinterpret_cast<const Uint8*>(attr.Offset);
if (!clientData || elementSize == 0 || stride == 0) { if (!clientData || componentSize == 0 || elementSize == 0 || stride == 0) {
MGLOG_E("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location); MGLOG_E("UploadAndBindVertexStreams skipped: invalid client vertex attribute at location %u", location);
return false; return false;
} }
if (conversion != VertexInputStateFactory::VertexStreamConversion::None && indexedDraw) {
// The current indexed setup only carries indexCount, not the maximum effective
// index. Guessing a client-memory range here can truncate the converted stream.
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
"location=%u requires an indexed vertex range", location);
return false;
}
if (conversion != VertexInputStateFactory::VertexStreamConversion::None &&
drawParams.vertexCount == 0) {
MGLOG_E("UploadAndBindVertexStreams skipped: converted client-memory attribute "
"location=%u has an unknown vertex range", location);
return false;
}
const Uint32 lastVertex = drawParams.vertexCount > 0 const Uint32 lastVertex = drawParams.vertexCount > 0
? drawParams.firstVertex + drawParams.vertexCount - 1 ? drawParams.firstVertex + drawParams.vertexCount - 1
: drawParams.firstVertex; : drawParams.firstVertex;
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
BufferSlice slice{}; BufferSlice slice{};
Bool uploaded = false; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(),
if (conversion == VertexInputStateFactory::VertexStreamConversion::None) { clientData, static_cast<VkDeviceSize>(uploadSize), 16, slice)) {
const SizeT uploadSize = static_cast<SizeT>(lastVertex) * stride + elementSize;
uploaded = m_bufferManager.UploadTransient(
BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), clientData,
static_cast<VkDeviceSize>(uploadSize), 16, slice);
} else {
uploaded = uploadConvertedStream(conversion, attr, clientData, stride, elementSize,
static_cast<SizeT>(lastVertex) + 1, slice);
}
if (!uploaded) {
MOBILEGL_ASSERT(false, MOBILEGL_ASSERT(false,
"UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu", "UploadAndBindVertexStreams skipped: failed to upload client attribute binding %zu",
binding); binding);
@@ -2339,64 +2142,6 @@ void main() {
const auto& sourceBufferShared = *sourceBufferSharedPtr; const auto& sourceBufferShared = *sourceBufferSharedPtr;
BufferSlice slice{}; BufferSlice slice{};
const SizeT sourceSize = sourceBufferShared->GetSize(); const SizeT sourceSize = sourceBufferShared->GetSize();
const SizeT baseOffset =
binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0;
MOBILEGL_ASSERT(baseOffset <= sourceSize,
"UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu",
binding, baseOffset, sourceSize);
if (conversion != VertexInputStateFactory::VertexStreamConversion::None) {
MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"UploadAndBindVertexStreams failed to resolve converted attribute location");
const auto& attr = vao.GetAttribute(bindingLocation);
const SizeT elementSize =
VertexInputStateFactory::GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
const SizeT sourceStride =
attr.Stride > 0 ? static_cast<SizeT>(attr.Stride) : elementSize;
if (sourceBufferShared->MappedData() == nullptr || elementSize == 0 || sourceStride == 0 ||
baseOffset > sourceSize || elementSize > sourceSize - baseOffset) {
MGLOG_E("UploadAndBindVertexStreams skipped: invalid converted source binding=%zu "
"location=%u base=%zu size=%zu element=%zu stride=%zu",
binding, bindingLocation, baseOffset, sourceSize, elementSize, sourceStride);
return false;
}
sourceBufferShared->SyncPersistentMappedRange();
const SizeT elementCount = 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
const ConvertedVertexStreamKey cacheKey{
.buffer = sourceBufferShared.get(),
.changeSerial = sourceBufferShared->GetChangeSerial(),
.baseOffset = baseOffset,
.sourceStride = static_cast<Uint32>(sourceStride),
.type = attr.Type,
.size = attr.Size,
.normalized = attr.Normalized,
.isInteger = attr.IsInteger,
.conversion = conversion,
};
const Bool cacheable = !sourceBufferShared->IsBackendPersistentMapped();
auto cached = cacheable ? m_convertedVertexStreams.find(cacheKey)
: m_convertedVertexStreams.end();
if (cached != m_convertedVertexStreams.end()) {
slice = cached->second;
} else {
const Uint8* sourceData = sourceBufferShared->MappedData() + baseOffset;
if (!uploadConvertedStream(conversion, attr, sourceData, sourceStride,
elementSize, elementCount, slice)) {
MGLOG_E("UploadAndBindVertexStreams skipped: failed to convert binding=%zu location=%u",
binding, bindingLocation);
return false;
}
if (cacheable) {
m_convertedVertexStreams.emplace(cacheKey, slice);
}
}
vkBuffers[binding] = slice.buffer;
vkOffsets[binding] = slice.offset;
continue;
}
if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) { if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared)) {
if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) { if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Vertex, sourceBufferShared, slice)) {
MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding);
@@ -2409,6 +2154,11 @@ void main() {
} }
} }
vkBuffers[binding] = slice.buffer; vkBuffers[binding] = slice.buffer;
const SizeT baseOffset =
binding < vertexInputState.bindingBaseOffsets.size() ? vertexInputState.bindingBaseOffsets[binding] : 0;
MOBILEGL_ASSERT(baseOffset <= sourceSize,
"UploadAndBindVertexStreams skipped: binding %zu base offset %zu exceeds buffer size %zu",
binding, baseOffset, sourceSize);
vkOffsets[binding] = slice.offset + static_cast<VkDeviceSize>(baseOffset); vkOffsets[binding] = slice.offset + static_cast<VkDeviceSize>(baseOffset);
} }
@@ -3522,44 +3272,6 @@ void main() {
return pipeline; return pipeline;
} }
Bool VulkanRenderer::PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj) {
auto& storageTextures = m_storageImageTexturesScratch;
if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) {
MGLOG_E("%s: failed to collect storage images for program=%u",
__func__, program.GetExternalIndex());
return false;
}
if (storageTextures.empty()) {
return true;
}
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
// a classic render pass. Do this before sampler preparation as well: a texture used by
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
// same layout independent of SPIR-V reflection/binding order.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(commandBuffer);
}
for (auto* texture : storageTextures) {
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
__func__, texture->GetExternalIndex());
return false;
}
if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) {
MGLOG_E("%s: failed to prepare storage textureId=%d",
__func__, texture->GetExternalIndex());
return false;
}
}
return true;
}
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects, Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) { const IndexBufferView* pIndexBufferView) {
@@ -3587,11 +3299,6 @@ void main() {
m_lastSampledSetValid = false; m_lastSampledSetValid = false;
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("SetupDraw skipped: storage image preparation failed");
return false;
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
// Check if any of the textures to sample have pending clears, // Check if any of the textures to sample have pending clears,
@@ -3762,8 +3469,7 @@ void main() {
return false; return false;
} }
auto vtxUploadOk = UploadAndBindVertexBuffers( auto vtxUploadOk = UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams);
frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView != nullptr);
if (!vtxUploadOk) { if (!vtxUploadOk) {
MGLOG_E("SetupDraw skipped: failed to upload vertex buffers"); MGLOG_E("SetupDraw skipped: failed to upload vertex buffers");
return false; return false;
@@ -3812,11 +3518,6 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u", MGLOG_E("DispatchCompute skipped: compute pipeline creation failed for program=%u",
@@ -3852,11 +3553,6 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
return;
}
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
if (pipeline == VK_NULL_HANDLE) { if (pipeline == VK_NULL_HANDLE) {
MGLOG_E("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u", MGLOG_E("DispatchComputeIndirect skipped: compute pipeline creation failed for program=%u",
@@ -5420,7 +5116,6 @@ void main() {
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
} }
} }
Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) { Bool VulkanRenderer::SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame) {
@@ -5505,15 +5200,7 @@ void main() {
return; return;
} }
const VkFormat srcFormat = srcBinding.format; const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4;
const SizeT sourceTexelSize = GetReadbackTexelSize(srcFormat);
if (sourceTexelSize == 0) {
MGLOG_E("DirectVulkan::ReadPixels skipped: unsupported source format=%d",
static_cast<Int>(srcFormat));
return;
}
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
static_cast<VkDeviceSize>(height) * sourceTexelSize;
VkBufferObject readback; VkBufferObject readback;
if (!readback.Create({ if (!readback.Create({
.allocator = m_allocator, .allocator = m_allocator,
@@ -5549,7 +5236,7 @@ void main() {
VkBufferImageCopy copyRegion{}; VkBufferImageCopy copyRegion{};
copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask; copyRegion.imageSubresource.aspectMask = srcBinding.aspectMask;
copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel; copyRegion.imageSubresource.mipLevel = srcBinding.mipLevel;
copyRegion.imageSubresource.baseArrayLayer = srcBinding.baseArrayLayer; copyRegion.imageSubresource.baseArrayLayer = 0;
copyRegion.imageSubresource.layerCount = 1; copyRegion.imageSubresource.layerCount = 1;
copyRegion.imageOffset = {x, y, 0}; copyRegion.imageOffset = {x, y, 0};
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}; copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
@@ -5584,18 +5271,14 @@ void main() {
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer"); MGLOG_E("DirectVulkan::ReadPixels skipped: failed to map readback buffer");
return; return;
} }
if (!readback.Invalidate(readbackSize)) { const VkFormat srcFormat = readIsDefaultFbo ? m_swapchainObject.GetSurfaceFormat().format : VK_FORMAT_R8G8B8A8_UNORM;
MGLOG_E("DirectVulkan::ReadPixels skipped: failed to invalidate readback buffer");
return;
}
if (readIsDefaultFbo) { if (readIsDefaultFbo) {
const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent(); const VkExtent2D swapchainExtent = m_swapchainObject.GetExtent();
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
if (static_cast<Uint32>(width) == swapchainExtent.width && if (static_cast<Uint32>(width) == swapchainExtent.width &&
static_cast<Uint32>(height) == swapchainExtent.height) { static_cast<Uint32>(height) == swapchainExtent.height) {
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize); Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * 4);
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform, if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
sourceTexelSize,
remapped.data())) { remapped.data())) {
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels); PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
return; return;
@@ -5661,10 +5344,9 @@ void main() {
} }
if (bufSize >= 0) { if (bufSize >= 0) {
const Int dstChannels = GetReadbackChannelCount(format); const Int dstChannels = GetReadbackChannelCount(format);
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) { if (type == GL_UNSIGNED_BYTE && dstChannels > 0) {
const SizeT dstComponentSize = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
const SizeT minSize = static_cast<SizeT>(width) * static_cast<SizeT>(height) * const SizeT minSize = static_cast<SizeT>(width) * static_cast<SizeT>(height) *
static_cast<SizeT>(dstChannels) * dstComponentSize; static_cast<SizeT>(dstChannels);
if (static_cast<SizeT>(bufSize) < minSize) { if (static_cast<SizeT>(bufSize) < minSize) {
MGLOG_E("DirectVulkan::GetTextureImage skipped: destination buffer is too small"); MGLOG_E("DirectVulkan::GetTextureImage skipped: destination buffer is too small");
return; return;
@@ -5672,14 +5354,7 @@ void main() {
} }
} }
const SizeT sourceTexelSize = GetReadbackTexelSize(resource->format); const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4;
if (sourceTexelSize == 0) {
MGLOG_E("DirectVulkan::GetTexImage skipped: unsupported source format=%d",
static_cast<Int>(resource->format));
return;
}
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
static_cast<VkDeviceSize>(height) * sourceTexelSize;
VkBufferObject readback; VkBufferObject readback;
if (!readback.Create({ if (!readback.Create({
.allocator = m_allocator, .allocator = m_allocator,
@@ -5730,10 +5405,6 @@ void main() {
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to map readback buffer"); MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to map readback buffer");
return; return;
} }
if (!readback.Invalidate(readbackSize)) {
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
return;
}
PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels); PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels);
} }
@@ -6686,7 +6357,6 @@ void main() {
CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex()); CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex());
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
// Descriptor-set reuse cursors rewind exactly once per frame, here, // Descriptor-set reuse cursors rewind exactly once per frame, here,
// after the slot's fence wait proved its previous sets GPU-idle. (The // after the slot's fence wait proved its previous sets GPU-idle. (The
// per-draw-path lazy rewind missed frames whose recording was opened // per-draw-path lazy rewind missed frames whose recording was opened
@@ -7004,10 +6674,6 @@ void main() {
vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures); vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures);
VkPhysicalDeviceFeatures deviceFeatures{}; VkPhysicalDeviceFeatures deviceFeatures{};
// Match GL's robust buffer-fetch behavior where the Vulkan device supports it. This covers
// out-of-range fetches; arbitrary GL vertex strides/offsets still need the explicit tight
// repack in VertexInputStateFactory when they violate Vulkan's address-alignment rules.
deviceFeatures.robustBufferAccess = supportedDeviceFeatures.robustBufferAccess;
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE; m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
@@ -7021,23 +6687,6 @@ void main() {
deviceFeatures.wideLines = supportedDeviceFeatures.wideLines; deviceFeatures.wideLines = supportedDeviceFeatures.wideLines;
m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE; m_logicOpFeatureEnabled = deviceFeatures.logicOp == VK_TRUE;
deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64; deviceFeatures.shaderInt64 = supportedDeviceFeatures.shaderInt64;
// Required for desktop GL image load/store semantics. iterationRP writes storage
// images from vertex and fragment stages and uses formats outside Vulkan's small
// mandatory storage-image set.
deviceFeatures.vertexPipelineStoresAndAtomics =
supportedDeviceFeatures.vertexPipelineStoresAndAtomics;
deviceFeatures.fragmentStoresAndAtomics = supportedDeviceFeatures.fragmentStoresAndAtomics;
deviceFeatures.shaderStorageImageExtendedFormats =
supportedDeviceFeatures.shaderStorageImageExtendedFormats;
// The formatless float-storage compatibility path must be all-or-nothing: transformed
// modules declare both capabilities and image bindings may be read, written, or both.
m_unformattedFloatStorageImagesEnabled =
supportedDeviceFeatures.shaderStorageImageReadWithoutFormat == VK_TRUE &&
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat == VK_TRUE;
if (m_unformattedFloatStorageImagesEnabled) {
deviceFeatures.shaderStorageImageReadWithoutFormat = VK_TRUE;
deviceFeatures.shaderStorageImageWriteWithoutFormat = VK_TRUE;
}
deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance; deviceFeatures.drawIndirectFirstInstance = supportedDeviceFeatures.drawIndirectFirstInstance;
deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect; deviceFeatures.multiDrawIndirect = supportedDeviceFeatures.multiDrawIndirect;
m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE; m_multiDrawIndirectFeatureEnabled = deviceFeatures.multiDrawIndirect == VK_TRUE;
@@ -7157,12 +6806,8 @@ void main() {
deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size()); deviceCreateInfo.enabledExtensionCount = static_cast<Uint32>(enabledDeviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
MGLOG_I("Device feature support: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
"shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " "shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s",
"fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s "
"storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s "
"multiDrawIndirect=%s",
supportedDeviceFeatures.robustBufferAccess ? "true" : "false",
supportedDeviceFeatures.geometryShader ? "true" : "false", supportedDeviceFeatures.geometryShader ? "true" : "false",
supportedDeviceFeatures.independentBlend ? "true" : "false", supportedDeviceFeatures.independentBlend ? "true" : "false",
supportedDeviceFeatures.logicOp ? "true" : "false", supportedDeviceFeatures.logicOp ? "true" : "false",
@@ -7170,19 +6815,11 @@ void main() {
supportedDeviceFeatures.shaderCullDistance ? "true" : "false", supportedDeviceFeatures.shaderCullDistance ? "true" : "false",
supportedDeviceFeatures.wideLines ? "true" : "false", supportedDeviceFeatures.wideLines ? "true" : "false",
supportedDeviceFeatures.shaderInt64 ? "true" : "false", supportedDeviceFeatures.shaderInt64 ? "true" : "false",
supportedDeviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false",
supportedDeviceFeatures.fragmentStoresAndAtomics ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false",
supportedDeviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false",
supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false", supportedDeviceFeatures.drawIndirectFirstInstance ? "true" : "false",
supportedDeviceFeatures.multiDrawIndirect ? "true" : "false"); supportedDeviceFeatures.multiDrawIndirect ? "true" : "false");
MGLOG_I("Device feature enabled: robustBufferAccess=%s geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s " MGLOG_I("Device feature enabled: geometryShader=%s independentBlend=%s logicOp=%s shaderClipDistance=%s "
"shaderCullDistance=%s wideLines=%s shaderInt64=%s vertexStoresAtomics=%s " "shaderCullDistance=%s wideLines=%s shaderInt64=%s drawIndirectFirstInstance=%s multiDrawIndirect=%s "
"fragmentStoresAtomics=%s storageImageExtendedFormats=%s storageImageReadWithoutFormat=%s " "shaderDrawParameters=%s",
"storageImageWriteWithoutFormat=%s drawIndirectFirstInstance=%s "
"multiDrawIndirect=%s shaderDrawParameters=%s",
deviceFeatures.robustBufferAccess ? "true" : "false",
deviceFeatures.geometryShader ? "true" : "false", deviceFeatures.geometryShader ? "true" : "false",
deviceFeatures.independentBlend ? "true" : "false", deviceFeatures.independentBlend ? "true" : "false",
deviceFeatures.logicOp ? "true" : "false", deviceFeatures.logicOp ? "true" : "false",
@@ -7190,11 +6827,6 @@ void main() {
deviceFeatures.shaderCullDistance ? "true" : "false", deviceFeatures.shaderCullDistance ? "true" : "false",
deviceFeatures.wideLines ? "true" : "false", deviceFeatures.wideLines ? "true" : "false",
deviceFeatures.shaderInt64 ? "true" : "false", deviceFeatures.shaderInt64 ? "true" : "false",
deviceFeatures.vertexPipelineStoresAndAtomics ? "true" : "false",
deviceFeatures.fragmentStoresAndAtomics ? "true" : "false",
deviceFeatures.shaderStorageImageExtendedFormats ? "true" : "false",
deviceFeatures.shaderStorageImageReadWithoutFormat ? "true" : "false",
deviceFeatures.shaderStorageImageWriteWithoutFormat ? "true" : "false",
deviceFeatures.drawIndirectFirstInstance ? "true" : "false", deviceFeatures.drawIndirectFirstInstance ? "true" : "false",
deviceFeatures.multiDrawIndirect ? "true" : "false", deviceFeatures.multiDrawIndirect ? "true" : "false",
m_shaderDrawParametersFeatureEnabled ? "true" : "false"); m_shaderDrawParametersFeatureEnabled ? "true" : "false");
@@ -7521,7 +7153,6 @@ void main() {
m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex());
} }
m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex());
m_convertedVertexStreams.clear();
} }
} }
@@ -165,11 +165,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride,
Uint8* destinationPixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type, TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
@@ -370,7 +365,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_samplerAnisotropyFeatureEnabled = false; Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false;
Bool m_unformattedFloatStorageImagesEnabled = false;
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them. // drive a runtime fallback when the device lacks them.
@@ -443,53 +437,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every // Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate. // draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch; Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch; Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch; Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch; Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
Vector<Float> m_vertexConversionScratch;
Vector<Uint8> m_vertexRepackScratch;
struct ConvertedVertexStreamKey {
const MG_State::GLState::BufferObject* buffer = nullptr;
Uint64 changeSerial = 0;
SizeT baseOffset = 0;
Uint32 sourceStride = 0;
DataType type = DataType::Float32;
Int size = 0;
Bool normalized = false;
Bool isInteger = false;
VertexInputStateFactory::VertexStreamConversion conversion =
VertexInputStateFactory::VertexStreamConversion::None;
Bool operator==(const ConvertedVertexStreamKey& other) const {
return buffer == other.buffer && changeSerial == other.changeSerial &&
baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
type == other.type && size == other.size && normalized == other.normalized &&
isInteger == other.isInteger && conversion == other.conversion;
}
};
struct ConvertedVertexStreamKeyHash {
SizeT operator()(const ConvertedVertexStreamKey& key) const {
SizeT hash = std::hash<const void*>{}(key.buffer);
auto combine = [&hash](SizeT value) {
hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
};
combine(std::hash<Uint64>{}(key.changeSerial));
combine(std::hash<SizeT>{}(key.baseOffset));
combine(std::hash<Uint32>{}(key.sourceStride));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
combine(std::hash<Int>{}(key.size));
combine(std::hash<Bool>{}(key.normalized));
combine(std::hash<Bool>{}(key.isInteger));
combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
return hash;
}
};
UnorderedMap<ConvertedVertexStreamKey, BufferSlice, ConvertedVertexStreamKeyHash>
m_convertedVertexStreams;
void CreateInstance(); void CreateInstance();
VkResult SetupDebugMessenger(); VkResult SetupDebugMessenger();
@@ -512,14 +462,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); void DestroyComputePipelines();
Bool PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams, Bool indexedDraw); const DrawCmdParam& drawParams);
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame, Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView = nullptr); const IndexBufferView* pIndexBufferView = nullptr);
@@ -998,8 +998,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, 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_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, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures) DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures)
@@ -1063,7 +1063,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture,
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_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, 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_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param) DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
+5 -8
View File
@@ -1175,9 +1175,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxFragmentInputComponents; *params = kFrontendMaxFragmentInputComponents;
return; return;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS: case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
// TODO: Track per-stage image uniform limits separately instead of reusing the compute/backend stage cap.
*params = MG_Backend::pActiveBackendObject *params = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxFragmentImageUniforms ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxComputeImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxFragmentImageUniforms; : MG_Backend::DynamicBackendParameters{}.MaxComputeImageUniforms;
return; return;
case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS: case GL_MAX_FRAGMENT_UNIFORM_COMPONENTS:
*params = kFrontendMaxFragmentUniformComponents; *params = kFrontendMaxFragmentUniformComponents;
@@ -1207,9 +1208,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxGeometryTextureImageUnits; *params = kFrontendMaxGeometryTextureImageUnits;
return; return;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS: case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject *params = 0;
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxGeometryImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxGeometryImageUniforms;
return; return;
case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS: case GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS:
*params = kFrontendMaxGeometryTotalOutputComponents; *params = kFrontendMaxGeometryTotalOutputComponents;
@@ -1278,9 +1277,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = kFrontendMaxVertexAtomicCounters; *params = kFrontendMaxVertexAtomicCounters;
return; return;
case GL_MAX_VERTEX_IMAGE_UNIFORMS: case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*params = MG_Backend::pActiveBackendObject *params = 0;
? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexImageUniforms
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
return; return;
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*params = 16; // TODO *params = 16; // TODO
@@ -470,210 +470,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return textureObject; return textureObject;
} }
namespace {
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
MG_State::pGLContext->RecordError(
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
const char* caller) {
if (texture == 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Clear texture operations require a non-zero texture name.");
return nullptr;
}
auto textureObject = GetTextureObjectByName(texture, caller);
if (!textureObject) return nullptr;
if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"Buffer textures cannot be cleared with glClearTexImage.");
return nullptr;
}
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
if (level < 0 || static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
std::format("Texture level {} is not defined.", level));
return nullptr;
}
return mipmapTexture;
}
Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
!TextureImpl::ValidateTexturePixelDataType(inputType) ||
!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
inputFormat, textureObject->GetFormat(), inputType)) {
return false;
}
clearPixel.clear();
if (data == nullptr) {
// ARB_clear_texture defines a null clear value as all zeroes. Keeping the
// pattern empty lets the region writer use a fast memset path.
return true;
}
PixelStoreParameters clearPixelStore{};
clearPixelStore.Alignment = 1;
SizeT clearPixelSize = 0;
void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
{1, 1, 1}, false, clearPixelSize);
if (!converted || clearPixelSize == 0) {
if (converted) free(converted);
return false;
}
clearPixel.resize(clearPixelSize);
Memcpy(clearPixel.data(), converted, clearPixelSize);
free(converted);
return true;
}
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
TextureUploadTarget uploadTarget, GLint level,
GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth,
const Vector<Uint8>& clearPixel, const char* caller) {
const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level has no storage.");
return false;
}
if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
width < 0 || height < 0 || depth < 0 ||
width > texelSize.x() - xoffset ||
height > texelSize.y() - yoffset ||
depth > texelSize.z() - zoffset) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
"The clear region lies outside the requested texture level.");
return false;
}
if (width == 0 || height == 0 || depth == 0) return true;
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(texelSize.z());
const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
if (byteSize == 0 || byteSize % texelCount != 0) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture storage cannot be cleared.");
return false;
}
const SizeT bytesPerTexel = byteSize / texelCount;
if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
RecordClearTextureError(
caller, ErrorCode::InvalidOperation,
std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
clearPixel.size(), bytesPerTexel));
return false;
}
auto* destination = static_cast<Uint8*>(
textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
if (!destination) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
"The requested texture level could not be mapped.");
return false;
}
const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
Uint8* firstClearRow = nullptr;
for (GLsizei z = 0; z < depth; ++z) {
for (GLsizei y = 0; y < height; ++y) {
Uint8* row = destination +
static_cast<SizeT>(zoffset + z) * fullSliceBytes +
static_cast<SizeT>(yoffset + y) * fullRowBytes +
static_cast<SizeT>(xoffset) * bytesPerTexel;
if (firstClearRow) {
Memcpy(row, firstClearRow, clearRowBytes);
continue;
}
firstClearRow = row;
if (clearPixel.empty()) {
Memset(row, 0, clearRowBytes);
continue;
}
Memcpy(row, clearPixel.data(), bytesPerTexel);
SizeT filled = bytesPerTexel;
while (filled < clearRowBytes) {
const SizeT copySize = std::min(filled, clearRowBytes - filled);
Memcpy(row + filled, row, copySize);
filled += copySize;
}
}
}
textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
return true;
}
} // namespace
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
size.x(), size.y(), size.z(), clearPixel, __func__)) {
return;
}
}
}
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
const void* data) {
auto textureObject = GetClearTextureObject(texture, level, __func__);
if (!textureObject) return;
Vector<Uint8> clearPixel;
if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;
const auto& uploadTargets = textureObject->GetUploadTargets();
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
if (zoffset < 0 || depth < 0 ||
static_cast<SizeT>(zoffset) > uploadTargets.size() ||
static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
RecordClearTextureError(__func__, ErrorCode::InvalidValue,
"The cube-map clear region selects invalid faces.");
return;
}
for (GLsizei face = 0; face < depth; ++face) {
if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
return;
}
}
return;
}
if (uploadTargets.empty()) {
RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
"The requested texture has no upload target.");
return;
}
ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
width, height, depth, clearPixel, __func__);
}
Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum pname, GLint param, const char* caller) { GLenum pname, GLint param, const char* caller) {
const auto target = textureObject->GetTarget(); const auto target = textureObject->GetTarget();
@@ -4022,14 +3818,6 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height); CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
});
}
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) { void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
CopyTexSubImage1D_State(target, level, xoffset, x, y, width); CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
} }
@@ -11,9 +11,6 @@
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format); GLenum format);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -98,8 +95,6 @@ namespace MobileGL::MG_Impl::GLImpl {
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border); GLsizei height, GLint border);
@@ -27,13 +27,6 @@ namespace {
struct FakeDriverState { struct FakeDriverState {
// Behavior knobs, configured per test before running the probe. // Behavior knobs, configured per test before running the probe.
GLint maxVertexSsboBlocks = 4; GLint maxVertexSsboBlocks = 4;
GLint glesMajorVersion = 3;
GLint glesMinorVersion = 1;
GLint maxVertexImageUniforms = 2;
GLint maxGeometryImageUniforms = 3;
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's // Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
// baseInstance word and exposes it through gl_InstanceID. // baseInstance word and exposes it through gl_InstanceID.
bool drawLeaksBaseInstanceWord = false; bool drawLeaksBaseInstanceWord = false;
@@ -95,26 +88,13 @@ namespace {
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
*data = g_fake.maxVertexSsboBlocks; *data = g_fake.maxVertexSsboBlocks;
break; break;
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
*data = g_fake.maxVertexImageUniforms;
break;
case GL_MAX_GEOMETRY_IMAGE_UNIFORMS:
g_fake.maxGeometryImageUniformsQueried = true;
*data = g_fake.maxGeometryImageUniforms;
break;
case GL_MAX_FRAGMENT_IMAGE_UNIFORMS:
*data = g_fake.maxFragmentImageUniforms;
break;
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
*data = g_fake.maxComputeImageUniforms;
break;
// FillInGLESCapabilities reads the context version before running the // FillInGLESCapabilities reads the context version before running the
// baseInstance probe, which requires ES >= 3.1. // baseInstance probe, which requires ES >= 3.1.
case GL_MAJOR_VERSION: case GL_MAJOR_VERSION:
*data = g_fake.glesMajorVersion; *data = 3;
break; break;
case GL_MINOR_VERSION: case GL_MINOR_VERSION:
*data = g_fake.glesMinorVersion; *data = 1;
break; break;
case GL_NUM_EXTENSIONS: case GL_NUM_EXTENSIONS:
*data = static_cast<GLint>(g_fake.extensions.size()); *data = static_cast<GLint>(g_fake.extensions.size());
@@ -437,31 +417,6 @@ TEST(IndirectInstanceIdProbe, FillInCapabilitiesWiresProbeResult) {
ExpectProbeReleasedAllObjects(); ExpectProbeReleasedAllObjects();
} }
TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGeometry) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities es31Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
EXPECT_EQ(es31Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
EXPECT_EQ(es31Caps.MaxGeometryImageUniforms, 0);
EXPECT_EQ(es31Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
EXPECT_EQ(es31Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
EXPECT_FALSE(g_fake.maxGeometryImageUniformsQueried);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.glesMinorVersion = 2;
MobileGL::MG_External::GLESCapabilities es32Caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
EXPECT_EQ(es32Caps.MaxVertexImageUniforms, g_fake.maxVertexImageUniforms);
EXPECT_EQ(es32Caps.MaxGeometryImageUniforms, g_fake.maxGeometryImageUniforms);
EXPECT_EQ(es32Caps.MaxFragmentImageUniforms, g_fake.maxFragmentImageUniforms);
EXPECT_EQ(es32Caps.MaxComputeImageUniforms, g_fake.maxComputeImageUniforms);
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising // The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear. // it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) { TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
-83
View File
@@ -7,7 +7,6 @@
// End of Source File Header // End of Source File Header
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <spirv_reflect.h>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
@@ -1326,88 +1325,6 @@ TEST_F(ProgramTest, CompileAndLinkWithExplicitVertexIn) {
<< "\")"; << "\")";
} }
TEST_F(ProgramTest, InactiveExplicitVertexBindingsDoNotReserveLocations) {
const char* vertexSource = R"(#version 430 compatibility
in vec3 Position;
in vec2 UV0;
in vec3 vaPosition;
void main() {
gl_Position = vec4(vaPosition, 1.0);
}
)";
const char* fragmentSource = R"(#version 430 compatibility
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
}
)";
GLuint vertexShader = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vertexShader, 1, &vertexSource, nullptr);
CompileShader(vertexShader);
GLint compileStatus = GL_FALSE;
GetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
CompileShader(fragmentShader);
GetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint program = CreateProgram();
AttachShader(program, vertexShader);
AttachShader(program, fragmentShader);
// Iris binds these canonical names before linking every program. Its compatibility
// transformer can inject both declarations even when the shader pack instead reads
// vaPosition. Inactive API bindings must not consume locations during the link.
BindAttribLocation(program, 0, "Position");
BindAttribLocation(program, 1, "UV0");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
EXPECT_EQ(GetAttribLocation(program, "Position"), -1);
EXPECT_EQ(GetAttribLocation(program, "UV0"), -1);
EXPECT_EQ(GetAttribLocation(program, "vaPosition"), 0);
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const Int vertexIndex = programObject->GetShaderIndexByStage(ShaderStage::Vertex);
ASSERT_GE(vertexIndex, 0);
const auto& spirvs = programObject->GetGeneratedSpirv();
ASSERT_LT(static_cast<SizeT>(vertexIndex), spirvs.size());
const auto& vertexSpirv = spirvs[vertexIndex];
spv_reflect::ShaderModule reflection(vertexSpirv.size() * sizeof(Uint), vertexSpirv.data());
ASSERT_EQ(reflection.GetResult(), SPV_REFLECT_RESULT_SUCCESS);
uint32_t inputCount = 0;
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, nullptr), SPV_REFLECT_RESULT_SUCCESS);
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
Uint32 userInputCount = 0;
Uint32 locationMask = 0;
for (const auto* input : inputs) {
if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) {
continue;
}
ASSERT_LT(input->location, 32u);
locationMask |= 1u << input->location;
++userInputCount;
}
EXPECT_EQ(userInputCount, 1u);
EXPECT_EQ(locationMask, 0x1u);
}
TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) { TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) {
char infoLog[1024] = ""; char infoLog[1024] = "";
@@ -8,9 +8,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstring>
#include <string> #include <string>
#include <utility>
#include "Includes.h" #include "Includes.h"
#include "Init.h" #include "Init.h"
@@ -94,120 +92,6 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
EXPECT_EQ(exactSamplerNameCount, 1u); EXPECT_EQ(exactSamplerNameCount, 1u);
} }
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
using namespace MG_Util::ShaderTranspiler;
const String source = R"(#version 430 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(rgba16, binding = 0) uniform image2D floatImage;
layout(r32ui, binding = 1) uniform uimage2D atomicImage;
void main() {
ivec2 coordinate = ivec2(gl_GlobalInvocationID.xy);
imageStore(floatImage, coordinate, imageLoad(floatImage, coordinate));
imageAtomicAdd(atomicImage, coordinate, 1u);
}
)";
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
const auto& inputBinary = binaryResult->front();
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String inputText;
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
EXPECT_NE(inputText.find("2D 0 0 0 2 Rgba16"), String::npos) << inputText;
EXPECT_NE(inputText.find("2D 0 0 0 2 R32ui"), String::npos) << inputText;
EXPECT_EQ(inputText.find("StorageImageReadWithoutFormat"), String::npos) << inputText;
EXPECT_EQ(inputText.find("StorageImageWriteWithoutFormat"), String::npos) << inputText;
Vector<Uint32> outputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
String outputText;
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_EQ(outputText.find("2D 0 0 0 2 Rgba16"), String::npos) << outputText;
EXPECT_NE(outputText.find("2D 0 0 0 2 Unknown"), String::npos) << outputText;
EXPECT_NE(outputText.find("2D 0 0 0 2 R32ui"), String::npos) << outputText;
const auto countOccurrences = [](const String& text, const String& needle) {
SizeT count = 0;
for (SizeT offset = 0; (offset = text.find(needle, offset)) != String::npos;
offset += needle.size()) {
++count;
}
return count;
};
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageReadWithoutFormat"), 1u)
<< outputText;
EXPECT_EQ(countOccurrences(outputText, "OpCapability StorageImageWriteWithoutFormat"), 1u)
<< outputText;
EXPECT_TRUE(tools.Validate(outputBinary));
Vector<Uint32> secondOutputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(outputBinary, secondOutputBinary));
EXPECT_EQ(secondOutputBinary, outputBinary);
}
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepFloatAtomicImageTypesTyped) {
using namespace MG_Util::ShaderTranspiler;
const String spirvText = R"(
OpCapability Shader
OpCapability StorageImageExtendedFormats
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
OpDecorate %target DescriptorSet 0
OpDecorate %target Binding 0
%void = OpTypeVoid
%float = OpTypeFloat 32
%int = OpTypeInt 32 1
%v2int = OpTypeVector %int 2
%image = OpTypeImage %float 2D 0 0 0 2 R32f
%imageUniformPtr = OpTypePointer UniformConstant %image
%imageTexelPtr = OpTypePointer Image %float
%mainType = OpTypeFunction %void
%zero = OpConstant %int 0
%coordinate = OpConstantComposite %v2int %zero %zero
%target = OpVariable %imageUniformPtr UniformConstant
%main = OpFunction %void None %mainType
%entry = OpLabel
%texelPtr = OpImageTexelPointer %imageTexelPtr %target %coordinate %zero
OpReturn
OpFunctionEnd
)";
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
Vector<Uint32> inputBinary;
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
Vector<Uint32> outputBinary;
ASSERT_TRUE(ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(inputBinary, outputBinary));
String outputText;
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_NE(outputText.find("2D 0 0 0 2 R32f"), String::npos) << outputText;
EXPECT_EQ(outputText.find("StorageImageReadWithoutFormat"), String::npos) << outputText;
EXPECT_EQ(outputText.find("StorageImageWriteWithoutFormat"), String::npos) << outputText;
String validationDiagnostics;
tools.SetMessageConsumer([&validationDiagnostics](spv_message_level_t, const char*,
const spv_position_t&, const char* message) {
validationDiagnostics += message;
});
EXPECT_TRUE(tools.Validate(outputBinary)) << validationDiagnostics;
}
TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) { TEST_F(ProgramUtilTest, PreprocessLegacyVertexShaderModernizesGlmarkStyleSource) {
using namespace MG_Util::ShaderTranspiler; using namespace MG_Util::ShaderTranspiler;
@@ -1499,128 +1383,3 @@ void main() {
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos); EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos); EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
} }
namespace {
String MakeLinearSubgroupPrefixScanShader() {
return R"(#version 460 core
#extension GL_KHR_shader_subgroup_arithmetic : enable
layout(local_size_x = 1024) in;
shared float prefixSumCache[64];
layout(std430, binding = 0) writeonly buffer OutputBuffer {
float outputValues[];
};
void main() {
float importance = 1.0f;
float prefixSum = subgroupInclusiveAdd(importance);
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
barrier();
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint i = 0; i < loopLength; i++) {
if ((gl_SubgroupID & (1u << i)) > 0u) {
prefixSum += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) prefixSumCache[gl_SubgroupID] = prefixSum;
}
barrier();
}
if (gl_LocalInvocationID.x == uint(1024 - 1)) prefixSumCache[0] = prefixSum;
barrier();
float sum = prefixSumCache[0];
float warp = (prefixSum - importance) / sum - float(gl_LocalInvocationID.x + 1u) / float(1024);
outputValues[gl_GlobalInvocationID.x] = warp;
}
)";
}
} // namespace
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanUsesSharedMemoryAndProducesValidSpirv) {
using namespace MG_Util::ShaderTranspiler;
String source = MakeLinearSubgroupPrefixScanShader();
ASSERT_TRUE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_NE(source.find("shared float prefixSumCache[1024]"), String::npos) << source;
EXPECT_NE(source.find("mglVirtualSubgroupInvocation"), String::npos) << source;
EXPECT_NE(source.find("for (uint mglPrefixLane"), String::npos) << source;
EXPECT_EQ(source.find("subgroupInclusiveAdd"), String::npos) << source;
EXPECT_EQ(source.find("gl_Subgroup"), String::npos) << source;
const String onceRewritten = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, onceRewritten);
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
ASSERT_TRUE(shaderResult) << shaderResult.error().log << "\nsource:\n" << source;
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
ASSERT_TRUE(programResult) << programResult.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
ASSERT_EQ(binaryResult->size(), 1u);
String validationDiagnostics;
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
validationDiagnostics += message;
validationDiagnostics += '\n';
});
EXPECT_TRUE(tools.Validate(binaryResult->front())) << validationDiagnostics;
String spirvText;
ASSERT_TRUE(tools.Disassemble(binaryResult->front(), &spirvText));
EXPECT_EQ(spirvText.find("OpGroupNonUniform"), String::npos) << spirvText;
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsOtherStagesAndSubgroupWidths) {
using namespace MG_Util::ShaderTranspiler;
const String original = MakeLinearSubgroupPrefixScanShader();
for (const auto& [stage, subgroupSize] :
{std::pair{ShaderStage::Compute, Uint32{32}}, std::pair{ShaderStage::Fragment, Uint32{64}},
std::pair{ShaderStage::Compute, Uint32{96}}}) {
String source = original;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(stage, subgroupSize, source));
EXPECT_EQ(source, original);
}
}
TEST_F(ProgramUtilTest, RewriteLinearSubgroupPrefixScanRejectsPartialOrUnsafeTemplateMatches) {
using namespace MG_Util::ShaderTranspiler;
const auto expectUnchanged = [](String source) {
const String original = source;
EXPECT_FALSE(RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage::Compute, 64, source));
EXPECT_EQ(source, original);
};
String wrongLocalSize = MakeLinearSubgroupPrefixScanShader();
wrongLocalSize.replace(wrongLocalSize.find("local_size_x = 1024"), std::strlen("local_size_x = 1024"),
"local_size_x = 512");
expectUnchanged(std::move(wrongLocalSize));
String cacheHasAnotherUse = MakeLinearSubgroupPrefixScanShader();
cacheHasAnotherUse.insert(cacheHasAnotherUse.find("float importance"), "prefixSumCache[0] = 0.0f;\n ");
expectUnchanged(std::move(cacheHasAnotherUse));
String extraSubgroupBuiltin = MakeLinearSubgroupPrefixScanShader();
extraSubgroupBuiltin.insert(extraSubgroupBuiltin.find("float importance"),
"uvec4 extraMask = gl_SubgroupEqMask;\n ");
expectUnchanged(std::move(extraSubgroupBuiltin));
String alteredBarrier = MakeLinearSubgroupPrefixScanShader();
alteredBarrier.replace(alteredBarrier.find("barrier();"), std::strlen("barrier();"), "memoryBarrierShared();");
expectUnchanged(std::move(alteredBarrier));
String nestedScan = MakeLinearSubgroupPrefixScanShader();
nestedScan.insert(nestedScan.find("float prefixSum ="), "if (importance > 0.0f) {\n ");
const SizeT consumerEnd = nestedScan.find(';', nestedScan.find("float warp ="));
ASSERT_NE(consumerEnd, String::npos);
nestedScan.insert(consumerEnd + 1, "\n }");
expectUnchanged(std::move(nestedScan));
}
-334
View File
@@ -24,12 +24,9 @@
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h> #include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureState.h> #include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/UniformManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h> #include <MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h> #include <MG_Backend/DirectVulkan/Renderer/VkTextureManager.h>
#include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h> #include <MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h> #include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/Debug/Log.h> #include <MG_Util/Debug/Log.h>
@@ -463,58 +460,6 @@ TEST(DirectVulkanSanity, ClampsAdvertisedTextureAndDrawBufferLimitsToFrontendSta
EXPECT_EQ(lowParams.MaxColorAttachments, 6); EXPECT_EQ(lowParams.MaxColorAttachments, 6);
} }
TEST(DirectVulkanSanity, GatesPerStageImageUniformLimitsOnPhysicalDeviceFeatures) {
using namespace MobileGL;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
MG_External::VulkanCapabilities caps;
caps.MaxImageUnits = 12;
caps.MaxCombinedImageUniforms = 10;
caps.MaxComputeImageUniforms = 9;
caps.SupportsVertexPipelineStoresAndAtomics = true;
caps.SupportsFragmentStoresAndAtomics = true;
caps.SupportsGeometryShader = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
const auto& withoutGeometry = backend.GetDynamicParameters();
EXPECT_EQ(withoutGeometry.MaxVertexImageUniforms, 10);
EXPECT_EQ(withoutGeometry.MaxGeometryImageUniforms, 0);
EXPECT_EQ(withoutGeometry.MaxFragmentImageUniforms, 10);
EXPECT_EQ(withoutGeometry.MaxComputeImageUniforms, 9);
caps.SupportsGeometryShader = true;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 10);
caps.SupportsVertexPipelineStoresAndAtomics = false;
caps.SupportsFragmentStoresAndAtomics = false;
backend.ApplyVulkanCapabilitiesForTesting(caps);
EXPECT_EQ(backend.GetDynamicParameters().MaxVertexImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxGeometryImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxFragmentImageUniforms, 0);
EXPECT_EQ(backend.GetDynamicParameters().MaxComputeImageUniforms, 9);
}
TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
using namespace MobileGL;
MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
MG_External::GLESCapabilities caps;
caps.MaxImageUnits = 8;
caps.MaxCombinedImageUniforms = 16;
caps.MaxVertexImageUniforms = 2;
caps.MaxGeometryImageUniforms = 3;
caps.MaxFragmentImageUniforms = 4;
caps.MaxComputeImageUniforms = 5;
backend.ApplyGLESCapabilitiesForTesting(caps);
const auto& params = backend.GetDynamicParameters();
EXPECT_EQ(params.MaxVertexImageUniforms, 2);
EXPECT_EQ(params.MaxGeometryImageUniforms, 3);
EXPECT_EQ(params.MaxFragmentImageUniforms, 4);
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
}
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) { TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
using namespace MobileGL; using namespace MobileGL;
@@ -637,54 +582,6 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
} }
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
MG_Backend::DynamicBackendParameters params;
params.MaxImageUnits = 8;
params.MaxCombinedImageUniforms = 8;
params.MaxVertexImageUniforms = 1;
params.MaxGeometryImageUniforms = 2;
params.MaxFragmentImageUniforms = 3;
params.MaxComputeImageUniforms = 4;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLint reported = -1;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 1);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 2);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 3);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 4);
const String vertexImageStore = R"(#version 430 core
layout(r32ui, binding = 0) uniform uimage2D targetImages[gl_MaxVertexImageUniforms];
void main() {
imageStore(targetImages[0], ivec2(0), uvec4(1));
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
auto supported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_VERTEX_SHADER,
.sourceStr = vertexImageStore,
});
EXPECT_TRUE(supported) << (supported ? "" : supported.error().log);
params.MaxVertexImageUniforms = 0;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &reported);
EXPECT_EQ(reported, 0);
auto unsupported = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({
.shaderType = GL_VERTEX_SHADER,
.sourceStr = vertexImageStore,
});
EXPECT_FALSE(unsupported);
MG_Backend::pActiveBackendObject.reset();
}
TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) {
using namespace MobileGL; using namespace MobileGL;
@@ -732,68 +629,6 @@ TEST(DirectVulkanSanity, CommandMemoryBarrierMakesIndirectDrawCommandsVisible) {
EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u); EXPECT_EQ(storageOnlyBarrier.dstAccessMask & VK_ACCESS_INDIRECT_COMMAND_READ_BIT, 0u);
} }
TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R8G8B8A8_UNORM), 4u);
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R16G16B16A16_SFLOAT), 8u);
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
}
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
using MobileGL::MG_Util::EncodeFloatToHalfBits;
const MobileGL::Uint8 rgba8[] = {17, 34, 51, 68, 85, 102, 119, 136};
MobileGL::Uint8 rgba8Result[sizeof(rgba8)]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
rgba8, VK_FORMAT_R8G8B8A8_UNORM, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE,
sizeof(rgba8Result), rgba8Result));
EXPECT_TRUE(std::equal(std::begin(rgba8), std::end(rgba8), std::begin(rgba8Result)));
const MobileGL::Uint8 bgra8[] = {51, 34, 17, 68};
MobileGL::Uint8 bgra8Result[4]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
bgra8, VK_FORMAT_B8G8R8A8_UNORM, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
sizeof(bgra8Result), bgra8Result));
const MobileGL::Uint8 expectedBgra8[] = {17, 34, 51, 68};
EXPECT_TRUE(std::equal(std::begin(expectedBgra8), std::end(expectedBgra8), std::begin(bgra8Result)));
const MobileGL::Uint16 rgba16f[] = {
EncodeFloatToHalfBits(-0.25f), EncodeFloatToHalfBits(0.5f), EncodeFloatToHalfBits(1.5f),
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.5f),
EncodeFloatToHalfBits(0.75f), EncodeFloatToHalfBits(0.125f), EncodeFloatToHalfBits(-1.0f),
EncodeFloatToHalfBits(2.0f), EncodeFloatToHalfBits(1.0f), EncodeFloatToHalfBits(0.75f),
EncodeFloatToHalfBits(0.25f), EncodeFloatToHalfBits(0.0f),
};
constexpr MobileGL::SizeT kDestinationRowStride = 12;
MobileGL::Uint8 rgba16fResult[kDestinationRowStride * 2];
std::fill(std::begin(rgba16fResult), std::end(rgba16fResult), 0xCD);
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
2, 2, GL_RGBA, GL_UNSIGNED_BYTE, kDestinationRowStride, rgba16fResult));
const MobileGL::Uint8 expectedRgba16fRow0[] = {0, 128, 255, 255, 64, 0, 255, 128};
const MobileGL::Uint8 expectedRgba16fRow1[] = {191, 32, 0, 255, 255, 191, 64, 0};
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow0), std::end(expectedRgba16fRow0),
std::begin(rgba16fResult)));
EXPECT_TRUE(std::equal(std::begin(expectedRgba16fRow1), std::end(expectedRgba16fRow1),
std::begin(rgba16fResult) + kDestinationRowStride));
EXPECT_TRUE(std::all_of(std::begin(rgba16fResult) + 8,
std::begin(rgba16fResult) + kDestinationRowStride,
[](MobileGL::Uint8 value) { return value == 0xCD; }));
MobileGL::Float rgba16fFloatResult[16]{};
ASSERT_TRUE(VulkanRenderer::ConvertReadbackPixels(
reinterpret_cast<const MobileGL::Uint8*>(rgba16f), VK_FORMAT_R16G16B16A16_SFLOAT,
2, 2, GL_RGBA, GL_FLOAT, sizeof(MobileGL::Float) * 8,
reinterpret_cast<MobileGL::Uint8*>(rgba16fFloatResult)));
EXPECT_FLOAT_EQ(rgba16fFloatResult[0], -0.25f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[1], 0.5f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[2], 1.5f);
EXPECT_FLOAT_EQ(rgba16fFloatResult[3], 1.0f);
}
TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) { TEST(DirectVulkanSanity, DrawIndexedIndirectCommandMatchesGlAndVulkanLayout) {
using namespace MobileGL::MG_Backend::DirectVulkan; using namespace MobileGL::MG_Backend::DirectVulkan;
@@ -840,175 +675,6 @@ TEST(DirectVulkanSanity, SampledDepthStencilViewUsesSingleDepthAspect) {
VK_IMAGE_ASPECT_DEPTH_BIT); VK_IMAGE_ASPECT_DEPTH_BIT);
} }
TEST(DirectVulkanSanity, SpirvStorageImageFormatsMapToVulkanFormats) {
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
struct FormatCase {
SpvImageFormat spirv;
VkFormat vulkan;
};
const FormatCase cases[] = {
{SpvImageFormatUnknown, VK_FORMAT_UNDEFINED},
{SpvImageFormatRgba32f, VK_FORMAT_R32G32B32A32_SFLOAT},
{SpvImageFormatRgba16f, VK_FORMAT_R16G16B16A16_SFLOAT},
{SpvImageFormatR32f, VK_FORMAT_R32_SFLOAT},
{SpvImageFormatRgba8, VK_FORMAT_R8G8B8A8_UNORM},
{SpvImageFormatRgba8Snorm, VK_FORMAT_R8G8B8A8_SNORM},
{SpvImageFormatRg32f, VK_FORMAT_R32G32_SFLOAT},
{SpvImageFormatRg16f, VK_FORMAT_R16G16_SFLOAT},
{SpvImageFormatR11fG11fB10f, VK_FORMAT_B10G11R11_UFLOAT_PACK32},
{SpvImageFormatR16f, VK_FORMAT_R16_SFLOAT},
{SpvImageFormatRgba16, VK_FORMAT_R16G16B16A16_UNORM},
{SpvImageFormatRgb10A2, VK_FORMAT_A2R10G10B10_UNORM_PACK32},
{SpvImageFormatRg16, VK_FORMAT_R16G16_UNORM},
{SpvImageFormatRg8, VK_FORMAT_R8G8_UNORM},
{SpvImageFormatR16, VK_FORMAT_R16_UNORM},
{SpvImageFormatR8, VK_FORMAT_R8_UNORM},
{SpvImageFormatRgba16Snorm, VK_FORMAT_R16G16B16A16_SNORM},
{SpvImageFormatRg16Snorm, VK_FORMAT_R16G16_SNORM},
{SpvImageFormatRg8Snorm, VK_FORMAT_R8G8_SNORM},
{SpvImageFormatR16Snorm, VK_FORMAT_R16_SNORM},
{SpvImageFormatR8Snorm, VK_FORMAT_R8_SNORM},
{SpvImageFormatRgba32i, VK_FORMAT_R32G32B32A32_SINT},
{SpvImageFormatRgba16i, VK_FORMAT_R16G16B16A16_SINT},
{SpvImageFormatRgba8i, VK_FORMAT_R8G8B8A8_SINT},
{SpvImageFormatR32i, VK_FORMAT_R32_SINT},
{SpvImageFormatRg32i, VK_FORMAT_R32G32_SINT},
{SpvImageFormatRg16i, VK_FORMAT_R16G16_SINT},
{SpvImageFormatRg8i, VK_FORMAT_R8G8_SINT},
{SpvImageFormatR16i, VK_FORMAT_R16_SINT},
{SpvImageFormatR8i, VK_FORMAT_R8_SINT},
{SpvImageFormatRgba32ui, VK_FORMAT_R32G32B32A32_UINT},
{SpvImageFormatRgba16ui, VK_FORMAT_R16G16B16A16_UINT},
{SpvImageFormatRgba8ui, VK_FORMAT_R8G8B8A8_UINT},
{SpvImageFormatR32ui, VK_FORMAT_R32_UINT},
{SpvImageFormatRgb10a2ui, VK_FORMAT_A2R10G10B10_UINT_PACK32},
{SpvImageFormatRg32ui, VK_FORMAT_R32G32_UINT},
{SpvImageFormatRg16ui, VK_FORMAT_R16G16_UINT},
{SpvImageFormatRg8ui, VK_FORMAT_R8G8_UINT},
{SpvImageFormatR16ui, VK_FORMAT_R16_UINT},
{SpvImageFormatR8ui, VK_FORMAT_R8_UINT},
{SpvImageFormatR64ui, VK_FORMAT_R64_UINT},
{SpvImageFormatR64i, VK_FORMAT_R64_SINT},
};
for (const auto& testCase : cases) {
EXPECT_EQ(ProgramFactory::ConvertSpirvImageFormatToVkFormat(testCase.spirv), testCase.vulkan)
<< "SpvImageFormat=" << static_cast<int>(testCase.spirv);
}
}
TEST(DirectVulkanSanity, MutableStorageImageViewsUseVulkanCompatibilityClasses) {
using MobileGL::MG_Backend::DirectVulkan::VkTextureManager;
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_UINT, VK_FORMAT_R32_SINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R8G8B8A8_UINT));
EXPECT_TRUE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_SFLOAT));
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT));
EXPECT_FALSE(VkTextureManager::AreStorageImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_D32_SFLOAT));
}
TEST(DirectVulkanSanity, StorageImageViewFormatUsesBindingOnlyForFormatlessFloatPolicy) {
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R16G16B16A16_UNORM, true),
VK_FORMAT_R16G16B16A16_SFLOAT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16, VK_FORMAT_R16G16B16A16_SFLOAT, true),
VK_FORMAT_R16G16B16A16_UNORM);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_R32_UINT, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
VK_FORMAT_R32_UINT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_RGBA16F, VK_FORMAT_R32_SFLOAT, false),
VK_FORMAT_R32_SFLOAT);
EXPECT_EQ(UniformManager::ResolveStorageImageViewFormat(
VK_FORMAT_UNDEFINED, GL_NONE, VK_FORMAT_R16G16B16A16_SFLOAT, true),
VK_FORMAT_UNDEFINED);
}
TEST(DirectVulkanSanity, ProgramObjectMovePreservesStorageImageFormatPolicy) {
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
ProgramFactory::VkProgramObject source;
source.storageImageFormatByBinding = {VK_FORMAT_UNDEFINED, VK_FORMAT_R32_UINT};
source.storageImageUsesBindingFormatByBinding = {true, false};
ProgramFactory::VkProgramObject moved(std::move(source));
ASSERT_EQ(moved.storageImageFormatByBinding.size(), 2u);
ASSERT_EQ(moved.storageImageUsesBindingFormatByBinding.size(), 2u);
EXPECT_EQ(moved.storageImageFormatByBinding[0], VK_FORMAT_UNDEFINED);
EXPECT_EQ(moved.storageImageFormatByBinding[1], VK_FORMAT_R32_UINT);
EXPECT_TRUE(moved.storageImageUsesBindingFormatByBinding[0]);
EXPECT_FALSE(moved.storageImageUsesBindingFormatByBinding[1]);
ProgramFactory::VkProgramObject assigned;
assigned = std::move(moved);
ASSERT_EQ(assigned.storageImageFormatByBinding.size(), 2u);
ASSERT_EQ(assigned.storageImageUsesBindingFormatByBinding.size(), 2u);
EXPECT_TRUE(assigned.storageImageUsesBindingFormatByBinding[0]);
EXPECT_FALSE(assigned.storageImageUsesBindingFormatByBinding[1]);
}
TEST(DirectVulkanSanity, SamplerUniformTypesPreserveTheirNumericDomain) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_2D),
SamplerNumericDomain::Float);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW),
SamplerNumericDomain::Float);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_INT_SAMPLER_2D_ARRAY),
SamplerNumericDomain::SignedInteger);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_UNSIGNED_INT_SAMPLER_2D),
SamplerNumericDomain::UnsignedInteger);
EXPECT_EQ(ProgramFactory::UniformTypeToSamplerNumericDomain(GL_IMAGE_2D),
SamplerNumericDomain::Unknown);
}
TEST(DirectVulkanSanity, SampledViewFormatMatchesSamplerNumericDomainWithoutChangingComponentLayout) {
using namespace MobileGL::MG_Backend::DirectVulkan;
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R32_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_SFLOAT, SamplerNumericDomain::SignedInteger),
VK_FORMAT_R32_SINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_UINT, SamplerNumericDomain::Float),
VK_FORMAT_R32_SFLOAT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R16G16B16A16_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R16G16B16A16_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R8G8B8A8_UNORM, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R8G8B8A8_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_R32_UINT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_R32_UINT);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_B10G11R11_UFLOAT_PACK32, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_UNDEFINED);
EXPECT_EQ(VkTextureManager::ResolveSampledImageViewFormat(
VK_FORMAT_D32_SFLOAT, SamplerNumericDomain::UnsignedInteger),
VK_FORMAT_UNDEFINED);
EXPECT_TRUE(VkTextureManager::AreSampledImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_UINT));
EXPECT_FALSE(VkTextureManager::AreSampledImageViewFormatsCompatible(
VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16G16B16A16_UINT));
}
TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) { TEST(RenderStateSanity, ProvokingVertexUpdatesStateAndValidatesEnum) {
using namespace MobileGL; using namespace MobileGL;
-140
View File
@@ -158,39 +158,6 @@ namespace {
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()); auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level)); return static_cast<const Uint8*>(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, level));
} }
class ScopedTextureBackendFunctionsOverride {
public:
ScopedTextureBackendFunctionsOverride(): m_snapshot(MG_Backend::gBackendFunctionsTable) {}
~ScopedTextureBackendFunctionsOverride() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
private:
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
};
struct CopyTexSubImage2DCall {
Bool Called = false;
GLenum Target = GL_NONE;
GLint Level = -1;
GLint XOffset = -1;
GLint YOffset = -1;
GLint X = -1;
GLint Y = -1;
GLsizei Width = -1;
GLsizei Height = -1;
GLuint BoundTexture = 0;
} g_copyTexSubImage2DCall;
void RecordCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
g_copyTexSubImage2DCall = {
true, target, level, xoffset, yoffset, x, y, width, height,
MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit())
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject()
->GetExternalIndex(),
};
}
} // namespace } // namespace
TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
@@ -211,113 +178,6 @@ TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
} }
TEST_F(TextureTest, ClearTexImageNullClearsWholeNamedTextureAndMarksStorageDirty) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const Uint8 initialPixels[] = {
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
};
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
mipmapObject->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, false);
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
const Uint8 zeros[sizeof(initialPixels)] = {};
EXPECT_EQ(std::memcmp(stored, zeros, sizeof(zeros)), 0);
EXPECT_TRUE(mipmapObject->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, ClearTexImageRepeatsConvertedClearPixel) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8 clearPixel[] = {17, 34, 51, 68};
MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, clearPixel);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
const Uint8 expected[] = {
17, 34, 51, 68,
17, 34, 51, 68,
17, 34, 51, 68,
17, 34, 51, 68,
};
EXPECT_EQ(std::memcmp(stored, expected, sizeof(expected)), 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, ClearTexSubImageClearsOnlyRequestedRectangle) {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
Uint8 initialPixels[3 * 2 * 4];
std::memset(initialPixels, 0x7f, sizeof(initialPixels));
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 3, 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, initialPixels);
MG_Impl::GLImpl::ClearTexSubImage(texture, 0, 1, 0, 0, 1, 2, 1,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const Uint8* stored = GetBoundTexture2DLevelBytes(texture);
ASSERT_NE(stored, nullptr);
for (Int y = 0; y < 2; ++y) {
for (Int x = 0; x < 3; ++x) {
for (Int channel = 0; channel < 4; ++channel) {
EXPECT_EQ(stored[(y * 3 + x) * 4 + channel], x == 1 ? 0 : 0x7f);
}
}
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
g_copyTexSubImage2DCall = {};
GLuint namedTexture = 0;
GLuint boundTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &namedTexture);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &boundTexture);
MG_Impl::GLImpl::BindTextureUnit(0, boundTexture);
const auto boundBefore = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject();
MG_Impl::GLImpl::CopyTextureSubImage2D(namedTexture, 2, 3, 4, 5, 6, 7, 8);
EXPECT_TRUE(g_copyTexSubImage2DCall.Called);
EXPECT_EQ(g_copyTexSubImage2DCall.Target, GL_TEXTURE_2D);
EXPECT_EQ(g_copyTexSubImage2DCall.Level, 2);
EXPECT_EQ(g_copyTexSubImage2DCall.XOffset, 3);
EXPECT_EQ(g_copyTexSubImage2DCall.YOffset, 4);
EXPECT_EQ(g_copyTexSubImage2DCall.X, 5);
EXPECT_EQ(g_copyTexSubImage2DCall.Y, 6);
EXPECT_EQ(g_copyTexSubImage2DCall.Width, 7);
EXPECT_EQ(g_copyTexSubImage2DCall.Height, 8);
EXPECT_EQ(g_copyTexSubImage2DCall.BoundTexture, namedTexture);
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject(),
boundBefore);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default. // is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) { TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
@@ -865,9 +865,6 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxUniformBlockSize = 16384; GLint maxUniformBlockSize = 16384;
GLint maxImageUnits = 8; GLint maxImageUnits = 8;
GLint maxCombinedImageUniforms = 8; GLint maxCombinedImageUniforms = 8;
GLint maxVertexImageUniforms = 0;
GLint maxGeometryImageUniforms = 0;
GLint maxFragmentImageUniforms = 8;
GLint maxComputeImageUniforms = 8; GLint maxComputeImageUniforms = 8;
GLint maxDrawBuffers = 8; GLint maxDrawBuffers = 8;
GLint maxColorAttachments = 8; GLint maxColorAttachments = 8;
@@ -907,16 +904,7 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize); glesFuncs.glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &maxUniformBlockSize);
glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits); glesFuncs.glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms); glesFuncs.glGetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_IMAGE_UNIFORMS, &maxVertexImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms); glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
// Geometry shaders and their image-uniform query are core only in ES 3.2. DirectGLES
// emits ESSL 3.10 on an ES 3.1 context, so reporting zero there is both legal and an
// accurate description of what the backend compiler can consume.
if (caps.GLESVersion.Major > 3 ||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
}
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments); glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
@@ -967,9 +955,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.MaxUniformBlockSize = maxUniformBlockSize; caps.MaxUniformBlockSize = maxUniformBlockSize;
caps.MaxImageUnits = maxImageUnits; caps.MaxImageUnits = maxImageUnits;
caps.MaxCombinedImageUniforms = maxCombinedImageUniforms; caps.MaxCombinedImageUniforms = maxCombinedImageUniforms;
caps.MaxVertexImageUniforms = maxVertexImageUniforms;
caps.MaxGeometryImageUniforms = maxGeometryImageUniforms;
caps.MaxFragmentImageUniforms = maxFragmentImageUniforms;
caps.MaxComputeImageUniforms = maxComputeImageUniforms; caps.MaxComputeImageUniforms = maxComputeImageUniforms;
caps.MaxDrawBuffers = maxDrawBuffers; caps.MaxDrawBuffers = maxDrawBuffers;
caps.MaxColorAttachments = maxColorAttachments; caps.MaxColorAttachments = maxColorAttachments;
@@ -1015,9 +1000,6 @@ namespace MobileGL::MG_Util::BackendLoader {
MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize); MGLOG_I(" GL_MAX_UNIFORM_BLOCK_SIZE: %d", caps.MaxUniformBlockSize);
MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits); MGLOG_I(" GL_MAX_IMAGE_UNITS: %d", caps.MaxImageUnits);
MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms); MGLOG_I(" GL_MAX_COMBINED_IMAGE_UNIFORMS: %d", caps.MaxCombinedImageUniforms);
MGLOG_I(" GL_MAX_VERTEX_IMAGE_UNIFORMS: %d", caps.MaxVertexImageUniforms);
MGLOG_I(" GL_MAX_GEOMETRY_IMAGE_UNIFORMS: %d", caps.MaxGeometryImageUniforms);
MGLOG_I(" GL_MAX_FRAGMENT_IMAGE_UNIFORMS: %d", caps.MaxFragmentImageUniforms);
MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms); MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms);
MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers); MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers);
MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments); MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments);
@@ -1102,9 +1102,6 @@ namespace MobileGL {
Int MaxUniformBlockSize = 16384; Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8; Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8; Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
@@ -174,10 +174,6 @@ namespace MobileGL::MG_Util::BackendLoader {
VkPhysicalDeviceFeatures supportedFeatures{}; VkPhysicalDeviceFeatures supportedFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE; caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
caps.SupportsVertexPipelineStoresAndAtomics =
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(p.limits.maxStorageBufferRange);
const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 &&
HasUsableShaderSubgroupSupport(subgroupProps); HasUsableShaderSubgroupSupport(subgroupProps);
@@ -260,11 +256,6 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1]; caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits); caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
caps.SupportsWideLines = false; caps.SupportsWideLines = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
caps.SupportsVertexPipelineStoresAndAtomics = false;
caps.SupportsFragmentStoresAndAtomics = false;
caps.SupportsGeometryShader = false;
caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange); caps.MaxShaderStorageBlockSize = static_cast<SizeT>(properties.limits.maxStorageBufferRange);
caps.SupportsShaderSubgroup = false; caps.SupportsShaderSubgroup = false;
caps.SubgroupSize = 0; caps.SubgroupSize = 0;
@@ -67,12 +67,6 @@ namespace MobileGL {
Float ViewportBoundsRangeMax = 0.0f; Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0; Int ViewportSubpixelBits = 0;
Bool SupportsWideLines = false; Bool SupportsWideLines = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
// require these core Vulkan features to be enabled on the logical device.
Bool SupportsVertexPipelineStoresAndAtomics = false;
Bool SupportsFragmentStoresAndAtomics = false;
Bool SupportsGeometryShader = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Bool SupportsShaderSubgroup = false; Bool SupportsShaderSubgroup = false;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
@@ -6,10 +6,6 @@
// SPDX-License-Identifier: LGPL-3.0-only // SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header // End of Source File Header
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h" #include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
@@ -23,15 +19,15 @@
#include "spirv-tools/optimizer.hpp" #include "spirv-tools/optimizer.hpp"
#include "ShaderSourceProcessor.h" #include "ShaderSourceProcessor.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h> #include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
#include <cstdlib>
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
TBuiltInResource BuildTBuiltInResource() { TBuiltInResource& GetTBuiltInResourceInstance() {
TBuiltInResource Resources{}; static TBuiltInResource Resources{};
Resources.maxLights = 32; Resources.maxLights = 32;
Resources.maxClipPlanes = 6; Resources.maxClipPlanes = 6;
Resources.maxTextureUnits = 32; Resources.maxTextureUnits = 32;
@@ -126,22 +122,6 @@ namespace MobileGL {
Resources.maxTaskWorkGroupSizeZ_NV = 1; Resources.maxTaskWorkGroupSizeZ_NV = 1;
Resources.maxMeshViewCountNV = 4; Resources.maxMeshViewCountNV = 4;
// Resource checking must describe the same backend contract exposed through
// glGetIntegerv. Keeping this copy local also avoids racing on a process-global
// TBuiltInResource when Iris compiles shaders concurrently.
const MG_Backend::DynamicBackendParameters fallbackParameters{};
const auto& activeBackend = MG_Backend::pActiveBackendObject;
const auto& dynamicParameters =
activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters;
Resources.maxImageUnits = dynamicParameters.MaxImageUnits;
Resources.maxCombinedImageUnitsAndFragmentOutputs =
dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers;
Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms;
Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms;
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.limits.nonInductiveForLoops = true; Resources.limits.nonInductiveForLoops = true;
Resources.limits.whileLoops = true; Resources.limits.whileLoops = true;
Resources.limits.doWhileLoops = true; Resources.limits.doWhileLoops = true;
@@ -187,8 +167,7 @@ namespace MobileGL {
tshader->setAutoMapLocations(true); tshader->setAutoMapLocations(true);
tshader->setAutoMapBindings(true); tshader->setAutoMapBindings(true);
tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME);
auto resources = BuildTBuiltInResource(); if (!tshader->parse(&GetTBuiltInResourceInstance(), 460, ECoreProfile,
if (!tshader->parse(&resources, 460, ECoreProfile,
/*forceDefaultVersionAndProfile: */ false, /*forceDefaultVersionAndProfile: */ false,
/*forwardCompatible: */ true, EShMsgDefault)) { /*forwardCompatible: */ true, EShMsgDefault)) {
ResultInfo r; ResultInfo r;
@@ -344,133 +323,72 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
} }
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary) { Vector<uint32_t>& outputBinary) {
constexpr SizeT kSpirvHeaderWordCount = 5; static constexpr Uint32 kHeaderWords = 5;
outputBinary.clear(); static constexpr Uint32 kOpDecorate = 71;
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) { static constexpr Uint32 kOpMemberDecorate = 72;
static constexpr Uint32 kDecorationInvariant = 18;
static constexpr Uint32 kDecorationBuiltIn = 11;
static constexpr Uint32 kBuiltInPosition = 0;
if (inputBinary.size() < kHeaderWords) {
return false; return false;
} }
Vector<Uint32> floatTypeIds; // First pass: find targets that already carry Invariant so we never duplicate.
Vector<Uint32> resultTypeById(inputBinary[3], 0); struct MemberKey {
Vector<Uint32> pointerPointeeTypeById(inputBinary[3], 0); Uint32 id;
Bool hasReadWithoutFormatCapability = false; Uint32 member;
Bool hasWriteWithoutFormatCapability = false; bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; }
SizeT capabilityInsertOffset = kSpirvHeaderWordCount; };
Vector<Uint32> invariantIds;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { Vector<MemberKey> invariantMembers;
const Uint32 instructionWord = inputBinary[offset]; for (SizeT i = kHeaderWords; i < inputBinary.size();) {
const Uint32 wordCount = instructionWord >> 16u; const Uint32 word0 = inputBinary[i];
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu); const Uint32 opcode = word0 & 0xFFFFu;
if (wordCount == 0 || offset + wordCount > inputBinary.size()) { const Uint32 length = word0 >> 16;
if (length == 0 || i + length > inputBinary.size()) {
return false; return false;
} }
if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) {
if (opcode == spv::Op::OpCapability && wordCount >= 2) { invariantIds.push_back(inputBinary[i + 1]);
capabilityInsertOffset = offset + wordCount; } else if (opcode == kOpMemberDecorate && length >= 4 &&
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]); inputBinary[i + 3] == kDecorationInvariant) {
hasReadWithoutFormatCapability |= invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]});
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];
} }
i += length;
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 outputBinary.clear();
// those image types to retain an atomic-compatible declared format, so exclude only outputBinary.reserve(inputBinary.size() + 8);
// the exact image types used by an atomic path rather than disabling formatless outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords);
// access for unrelated float images in the same module. for (SizeT i = kHeaderWords; i < inputBinary.size();) {
Vector<Uint32> atomicImageTypeIds; const Uint32 word0 = inputBinary[i];
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { const Uint32 opcode = word0 & 0xFFFFu;
const Uint32 instructionWord = inputBinary[offset]; const Uint32 length = word0 >> 16;
const Uint32 wordCount = instructionWord >> 16u; outputBinary.insert(outputBinary.end(), inputBinary.begin() + i,
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu); inputBinary.begin() + i + length);
if (opcode == spv::Op::OpImageTexelPointer && wordCount >= 6) { if (opcode == kOpDecorate && length == 4 &&
const Uint32 imageId = inputBinary[offset + 3]; inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) {
if (imageId >= resultTypeById.size()) { const Uint32 target = inputBinary[i + 1];
return false; if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) {
outputBinary.push_back((3u << 16) | kOpDecorate);
outputBinary.push_back(target);
outputBinary.push_back(kDecorationInvariant);
} }
Uint32 imageTypeId = resultTypeById[imageId]; } else if (opcode == kOpMemberDecorate && length == 5 &&
if (imageTypeId < pointerPointeeTypeById.size() && inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) {
pointerPointeeTypeById[imageTypeId] != 0) { const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]};
imageTypeId = pointerPointeeTypeById[imageTypeId]; if (std::find(invariantMembers.begin(), invariantMembers.end(), key) ==
} invariantMembers.end()) {
if (imageTypeId != 0 && outputBinary.push_back((4u << 16) | kOpMemberDecorate);
std::find(atomicImageTypeIds.begin(), atomicImageTypeIds.end(), imageTypeId) == outputBinary.push_back(key.id);
atomicImageTypeIds.end()) { outputBinary.push_back(key.member);
atomicImageTypeIds.push_back(imageTypeId); outputBinary.push_back(kDecorationInvariant);
} }
} }
offset += wordCount; i += length;
} }
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; return true;
} }
@@ -39,14 +39,13 @@ namespace MobileGL {
// which wrongly includes baseInstance). // which wrongly includes baseInstance).
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary, static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary); Vector<uint32_t>& outputBinary);
// Replaces the declared format of float storage images with Unknown and adds the // Adds the Invariant decoration to every Position builtin output. GL apps
// matching SPIR-V capabilities. DirectVulkan uses this only when both Vulkan // routinely rely on cross-program position invariance for multi-pass
// shaderStorageImage*WithoutFormat features are enabled, allowing the // equality depth tests (e.g. GEQUAL re-draws of the same geometry), and
// glBindImageTexture format to select the descriptor view at runtime. Integer // mobile drivers that optimize per-pipeline break that without the
// storage images deliberately keep their declared format for GL-compatible bit // decoration. DirectVulkan only.
// reinterpretation paths (for example, R32F storage accessed as r32ui). static bool DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
static bool UseUnformattedFloatStorageImagesForVulkan( Vector<uint32_t>& outputBinary);
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
static Result<String> DecompileShader(SpvcSession& session); static Result<String> DecompileShader(SpvcSession& session);
}; };
} // namespace ShaderTranspiler } // namespace ShaderTranspiler
@@ -10,14 +10,10 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <initializer_list>
#include <utility>
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
namespace { namespace {
using MobileGL::SizeT; using MobileGL::SizeT;
using MobileGL::String;
using MobileGL::Vector;
bool IsIdentifierChar(char ch) { bool IsIdentifierChar(char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'; return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_';
@@ -95,375 +91,6 @@ namespace {
return masked; return masked;
} }
struct CodeToken {
String text;
SizeT begin = 0;
SizeT end = 0;
};
Vector<CodeToken> TokenizeCode(const String& source) {
const String masked = MaskCommentsAndQuotedText(source);
Vector<CodeToken> tokens;
tokens.reserve(source.size() / 4);
SizeT pos = 0;
while (pos < masked.size()) {
const char ch = masked[pos];
if (std::isspace(static_cast<unsigned char>(ch))) {
++pos;
continue;
}
const SizeT begin = pos;
if (IsIdentifierStart(ch)) {
++pos;
while (pos < masked.size() && IsIdentifierChar(masked[pos])) {
++pos;
}
} else if (std::isdigit(static_cast<unsigned char>(ch))) {
++pos;
while (pos < masked.size()) {
const char numberChar = masked[pos];
if (!IsIdentifierChar(numberChar) && numberChar != '.') {
break;
}
++pos;
}
} else {
++pos;
if (pos < masked.size()) {
const String twoChars = masked.substr(begin, 2);
if (twoChars == "==" || twoChars == "!=" || twoChars == "<=" || twoChars == ">=" ||
twoChars == "+=" || twoChars == "-=" || twoChars == "<<" || twoChars == ">>" ||
twoChars == "++" || twoChars == "--" || twoChars == "&&" || twoChars == "||") {
++pos;
}
}
}
tokens.push_back(CodeToken{source.substr(begin, pos - begin), begin, pos});
}
return tokens;
}
bool IsIdentifierToken(const CodeToken& token) {
if (token.text.empty() || !IsIdentifierStart(token.text.front())) {
return false;
}
return std::all_of(token.text.begin() + 1, token.text.end(), IsIdentifierChar);
}
class TokenCursor {
public:
TokenCursor(const Vector<CodeToken>& tokens, SizeT position) : m_tokens(tokens), m_position(position) {}
bool Consume(const char* expected) {
if (m_position >= m_tokens.size() || m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
bool ConsumeAnyIdentifier(String& identifier) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
identifier = m_tokens[m_position++].text;
return true;
}
bool ConsumeAnyIdentifier() {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position])) {
return false;
}
++m_position;
return true;
}
bool ConsumeIdentifier(const String& expected) {
if (m_position >= m_tokens.size() || !IsIdentifierToken(m_tokens[m_position]) ||
m_tokens[m_position].text != expected) {
return false;
}
++m_position;
return true;
}
SizeT Position() const { return m_position; }
private:
const Vector<CodeToken>& m_tokens;
SizeT m_position;
};
SizeT CountToken(const Vector<CodeToken>& tokens, const String& tokenText) {
return static_cast<SizeT>(std::count_if(tokens.begin(), tokens.end(),
[&](const CodeToken& token) { return token.text == tokenText; }));
}
bool HasIdentifierWithPrefixOutsideAllowed(const Vector<CodeToken>& tokens, const String& prefix,
std::initializer_list<const char*> allowedIdentifiers) {
return std::any_of(tokens.begin(), tokens.end(), [&](const CodeToken& token) {
if (!IsIdentifierToken(token) || !token.text.starts_with(prefix)) {
return false;
}
return std::none_of(allowedIdentifiers.begin(), allowedIdentifiers.end(),
[&](const char* allowed) { return token.text == allowed; });
});
}
bool MatchTokenSequence(const Vector<CodeToken>& tokens, SizeT position,
std::initializer_list<const char*> expected) {
if (position + expected.size() > tokens.size()) {
return false;
}
for (const char* token : expected) {
if (tokens[position++].text != token) {
return false;
}
}
return true;
}
struct LinearPrefixScanMatch {
SizeT sharedArraySizeBegin = 0;
SizeT sharedArraySizeEnd = 0;
SizeT scanBegin = 0;
SizeT scanEnd = 0;
String cache;
String importance;
String prefixSum;
String loopLength;
String loopIndex;
String sum;
};
bool ParseLinearPrefixScanTemplate(const Vector<CodeToken>& tokens, LinearPrefixScanMatch& match) {
// The workaround deliberately recognizes one complete algorithm, not merely the
// subgroupInclusiveAdd token. Changing scratch storage is only safe when that storage is
// private to this scan and the workgroup has exactly 1024 X invocations.
SizeT localSizeDeclarationCount = 0;
for (SizeT i = 0; i < tokens.size(); ++i) {
if (MatchTokenSequence(tokens, i, {"layout", "(", "local_size_x", "=", "1024", ")", "in", ";"})) {
++localSizeDeclarationCount;
}
}
if (localSizeDeclarationCount != 1) {
return false;
}
SizeT sharedDeclarationIndex = String::npos;
SizeT sharedDeclarationCount = 0;
String cacheName;
for (SizeT i = 0; i + 6 < tokens.size(); ++i) {
if (tokens[i].text != "shared" || tokens[i + 1].text != "float" || !IsIdentifierToken(tokens[i + 2]) ||
tokens[i + 3].text != "[" || tokens[i + 4].text != "64" || tokens[i + 5].text != "]" ||
tokens[i + 6].text != ";") {
continue;
}
++sharedDeclarationCount;
sharedDeclarationIndex = i;
cacheName = tokens[i + 2].text;
}
if (sharedDeclarationCount != 1) {
return false;
}
SizeT scanTokenIndex = String::npos;
SizeT scanCount = 0;
for (SizeT i = 0; i + 7 < tokens.size(); ++i) {
if (tokens[i].text == "float" && IsIdentifierToken(tokens[i + 1]) && tokens[i + 2].text == "=" &&
tokens[i + 3].text == "subgroupInclusiveAdd" && tokens[i + 4].text == "(" &&
IsIdentifierToken(tokens[i + 5]) && tokens[i + 6].text == ")" && tokens[i + 7].text == ";") {
++scanCount;
scanTokenIndex = i;
}
}
if (scanCount != 1 || sharedDeclarationIndex >= scanTokenIndex) {
return false;
}
TokenCursor cursor(tokens, scanTokenIndex);
String prefixSum;
String importance;
String loopLength;
String loopIndex;
String sum;
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier(prefixSum) || !cursor.Consume("=") ||
!cursor.Consume("subgroupInclusiveAdd") || !cursor.Consume("(") ||
!cursor.ConsumeAnyIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume(";") ||
!cursor.Consume("if") || !cursor.Consume("(") || !cursor.Consume("gl_SubgroupInvocationID") ||
!cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("uint") ||
!cursor.ConsumeAnyIdentifier(loopLength) || !cursor.Consume("=") || !cursor.Consume("uint") ||
!cursor.Consume("(") || !cursor.Consume("findMSB") || !cursor.Consume("(") ||
!cursor.Consume("gl_NumSubgroups") || !cursor.Consume(")") || !cursor.Consume(")") ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("+=") ||
!cursor.Consume("uint") || !cursor.Consume("(") || !cursor.Consume("gl_NumSubgroups") ||
!cursor.Consume("-") || !cursor.Consume("(") || !cursor.Consume("1u") || !cursor.Consume("<<") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(loopLength) || !cursor.Consume("-") ||
!cursor.Consume("1u") || !cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") ||
!cursor.Consume("0u") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("for") ||
!cursor.Consume("(") || !cursor.Consume("uint") || !cursor.ConsumeAnyIdentifier(loopIndex) ||
!cursor.Consume("=") || !cursor.Consume("0") || !cursor.Consume(";") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<") || !cursor.ConsumeIdentifier(loopLength) ||
!cursor.Consume(";") || !cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("++") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume("&") || !cursor.Consume("(") ||
!cursor.Consume("1u") || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume(")") || !cursor.Consume(">") || !cursor.Consume("0u") ||
!cursor.Consume(")") || !cursor.Consume("{") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume("+=") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("(") || !cursor.Consume("gl_SubgroupID") || !cursor.Consume(">>") ||
!cursor.ConsumeIdentifier(loopIndex) || !cursor.Consume("<<") || !cursor.ConsumeIdentifier(loopIndex) ||
!cursor.Consume(")") || !cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume("]") ||
!cursor.Consume(";") || !cursor.Consume("if") || !cursor.Consume("(") ||
!cursor.Consume("gl_SubgroupInvocationID") || !cursor.Consume("==") || !cursor.Consume("gl_SubgroupSize") ||
!cursor.Consume("-") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") || !cursor.Consume("gl_SubgroupID") ||
!cursor.Consume("]") || !cursor.Consume("=") || !cursor.ConsumeIdentifier(prefixSum) ||
!cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("barrier") || !cursor.Consume("(") ||
!cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("}") || !cursor.Consume("if") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("==") || !cursor.Consume("uint") || !cursor.Consume("(") ||
!cursor.Consume("1024") || !cursor.Consume("-") || !cursor.Consume("1") || !cursor.Consume(")") ||
!cursor.Consume(")") || !cursor.ConsumeIdentifier(cacheName) || !cursor.Consume("[") ||
!cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume("=") ||
!cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume(";") || !cursor.Consume("barrier") ||
!cursor.Consume("(") || !cursor.Consume(")") || !cursor.Consume(";") || !cursor.Consume("float") ||
!cursor.ConsumeAnyIdentifier(sum) || !cursor.Consume("=") || !cursor.ConsumeIdentifier(cacheName) ||
!cursor.Consume("[") || !cursor.Consume("0") || !cursor.Consume("]") || !cursor.Consume(";")) {
return false;
}
const SizeT scanEndToken = cursor.Position() - 1;
// Require the scan's immediate consumer as well. This makes the match specific to a
// linear distribution warp, and avoids changing unrelated prefix scans which may rely on
// the implementation's native subgroup partitioning.
if (!cursor.Consume("float") || !cursor.ConsumeAnyIdentifier() || !cursor.Consume("=") ||
!cursor.Consume("(") || !cursor.ConsumeIdentifier(prefixSum) || !cursor.Consume("-") ||
!cursor.ConsumeIdentifier(importance) || !cursor.Consume(")") || !cursor.Consume("/") ||
!cursor.ConsumeIdentifier(sum) || !cursor.Consume("-") || !cursor.Consume("float") ||
!cursor.Consume("(") || !cursor.Consume("gl_LocalInvocationID") || !cursor.Consume(".") ||
!cursor.Consume("x") || !cursor.Consume("+") || !cursor.Consume("1u") || !cursor.Consume(")") ||
!cursor.Consume("/") || !cursor.Consume("float") || !cursor.Consume("(") || !cursor.Consume("1024") ||
!cursor.Consume(")") || !cursor.Consume(";")) {
return false;
}
// No other use may share the scratch array, and no additional subgroup operation or
// builtin may silently retain native-64 semantics after this module becomes virtual-32.
if (CountToken(tokens, cacheName) != 6 || CountToken(tokens, "subgroupInclusiveAdd") != 1 ||
CountToken(tokens, "gl_SubgroupInvocationID") != 2 || CountToken(tokens, "gl_SubgroupSize") != 2 ||
CountToken(tokens, "gl_SubgroupID") != 4 || CountToken(tokens, "gl_NumSubgroups") != 2 ||
CountToken(tokens, "gl_LocalInvocationID") != 2 || CountToken(tokens, "barrier") != 3 ||
CountToken(tokens, "findMSB") != 1 ||
HasIdentifierWithPrefixOutsideAllowed(tokens, "subgroup", {"subgroupInclusiveAdd"}) ||
HasIdentifierWithPrefixOutsideAllowed(
tokens, "gl_Subgroup",
{"gl_SubgroupInvocationID", "gl_SubgroupSize", "gl_SubgroupID", "gl_NumSubgroups"})) {
return false;
}
// The scan must be at the top level of the sole main() body. Its existing barriers already
// require uniform control flow; this check prevents us from introducing extra barriers in
// a nested branch or loop.
SizeT mainOpenBrace = String::npos;
SizeT mainCloseBrace = String::npos;
SizeT mainCount = 0;
for (SizeT i = 0; i + 4 < tokens.size(); ++i) {
if (!MatchTokenSequence(tokens, i, {"void", "main", "(", ")", "{"})) {
continue;
}
++mainCount;
mainOpenBrace = i + 4;
int depth = 1;
for (SizeT j = mainOpenBrace + 1; j < tokens.size(); ++j) {
if (tokens[j].text == "{")
++depth;
else if (tokens[j].text == "}" && --depth == 0) {
mainCloseBrace = j;
break;
}
}
}
if (mainCount != 1 || mainCloseBrace == String::npos || scanTokenIndex <= mainOpenBrace ||
scanEndToken >= mainCloseBrace) {
return false;
}
int depthAtScan = 1;
for (SizeT i = mainOpenBrace + 1; i < scanTokenIndex; ++i) {
if (tokens[i].text == "{")
++depthAtScan;
else if (tokens[i].text == "}")
--depthAtScan;
}
if (depthAtScan != 1) {
return false;
}
constexpr const char* injectedNames[] = {"mglPrefixScanLane", "mglVirtualSubgroupInvocation",
"mglVirtualSubgroup", "mglVirtualSubgroupBase",
"mglPrefixLane", "mglVirtualSubgroupCount"};
for (const char* injectedName : injectedNames) {
if (CountToken(tokens, injectedName) != 0) {
return false;
}
}
match.sharedArraySizeBegin = tokens[sharedDeclarationIndex + 4].begin;
match.sharedArraySizeEnd = tokens[sharedDeclarationIndex + 4].end;
match.scanBegin = tokens[scanTokenIndex].begin;
match.scanEnd = tokens[scanEndToken].end;
match.cache = std::move(cacheName);
match.importance = std::move(importance);
match.prefixSum = std::move(prefixSum);
match.loopLength = std::move(loopLength);
match.loopIndex = std::move(loopIndex);
match.sum = std::move(sum);
return true;
}
String BuildLinearPrefixScanReplacement(const LinearPrefixScanMatch& match) {
String replacement;
replacement.reserve(1800);
replacement += "uint mglPrefixScanLane = gl_LocalInvocationID.x;\n";
replacement += "uint mglVirtualSubgroupInvocation = mglPrefixScanLane & 31u;\n";
replacement += "uint mglVirtualSubgroup = mglPrefixScanLane >> 5u;\n";
replacement += "const uint mglVirtualSubgroupCount = 32u;\n";
replacement += match.cache + "[mglPrefixScanLane] = " + match.importance + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.prefixSum + " = 0.0f;\n";
replacement += "uint mglVirtualSubgroupBase = mglVirtualSubgroup << 5u;\n";
replacement += "for (uint mglPrefixLane = mglVirtualSubgroupBase; "
"mglPrefixLane <= mglPrefixScanLane; ++mglPrefixLane) {\n";
replacement += match.prefixSum + " += " + match.cache + "[mglPrefixLane];\n";
replacement += "}\n";
replacement += "barrier();\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "uint " + match.loopLength + " = uint(findMSB(mglVirtualSubgroupCount));\n";
replacement +=
match.loopLength + " += uint(mglVirtualSubgroupCount - (1u << (" + match.loopLength + " - 1u)) > 0u);\n";
replacement += "for (uint " + match.loopIndex + " = 0u; " + match.loopIndex + " < " + match.loopLength +
"; ++" + match.loopIndex + ") {\n";
replacement += "if ((mglVirtualSubgroup & (1u << " + match.loopIndex + ")) > 0u) {\n";
replacement += match.prefixSum + " += " + match.cache + "[(mglVirtualSubgroup >> " + match.loopIndex + " << " +
match.loopIndex + ") - 1u];\n";
replacement += "if (mglVirtualSubgroupInvocation == 31u) " + match.cache +
"[mglVirtualSubgroup] = " + match.prefixSum + ";\n";
replacement += "}\nbarrier();\n}\n";
replacement += "if (mglPrefixScanLane == 1023u) " + match.cache + "[0] = " + match.prefixSum + ";\n";
replacement += "barrier();\n";
replacement += "float " + match.sum + " = " + match.cache + "[0];";
return replacement;
}
void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) { void SkipDirectiveWhitespace(const MobileGL::String& source, SizeT& pos, SizeT lineEnd) {
while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) { while (pos < lineEnd && std::isspace(static_cast<unsigned char>(source[pos]))) {
pos++; pos++;
@@ -949,36 +576,6 @@ namespace {
namespace MobileGL { namespace MobileGL {
namespace MG_Util { namespace MG_Util {
namespace ShaderTranspiler { namespace ShaderTranspiler {
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize,
String& source) {
constexpr Uint32 capturedSubgroupSize = 32;
if (stage != ShaderStage::Compute || nativeSubgroupSize <= capturedSubgroupSize ||
nativeSubgroupSize % capturedSubgroupSize != 0) {
return false;
}
// Vulkan subgroup widths are powers of two. Keep the workaround restricted to
// wider widths which are a power-of-two multiple of the captured 32-lane model.
const Uint32 subgroupScale = nativeSubgroupSize / capturedSubgroupSize;
if ((subgroupScale & (subgroupScale - 1u)) != 0u) {
return false;
}
const Vector<CodeToken> tokens = TokenizeCode(source);
LinearPrefixScanMatch match;
if (!ParseLinearPrefixScanTemplate(tokens, match)) {
return false;
}
const String replacement = BuildLinearPrefixScanReplacement(match);
source.replace(match.scanBegin, match.scanEnd - match.scanBegin, replacement);
// The declaration occurs before the replaced scan, so its original offsets remain
// valid after the first replacement.
source.replace(match.sharedArraySizeBegin, match.sharedArraySizeEnd - match.sharedArraySizeBegin,
"1024");
return true;
}
void PreprocessShaderSource(ShaderStage stage, String& source) { void PreprocessShaderSource(ShaderStage stage, String& source) {
// Normalize while the inspector's source span still refers to the untouched input. Later passes // Normalize while the inspector's source span still refers to the untouched input. Later passes
// remove comments and directives, so any subsequent insertion re-inspects the current source. // remove comments and directives, so any subsequent insertion re-inspects the current source.
@@ -1034,13 +631,6 @@ namespace MobileGL {
RenameBuiltinShadowingFunction(source, "max3", "mg_max3"); RenameBuiltinShadowingFunction(source, "max3", "mg_max3");
ModernizeLegacyGLSL(stage, source); ModernizeLegacyGLSL(stage, source);
InjectDepthRangeBuiltinShim(stage, source); InjectDepthRangeBuiltinShim(stage, source);
const auto& activeBackend = MG_Backend::pActiveBackendObject;
if (stage == ShaderStage::Compute && activeBackend &&
activeBackend->GetBackendType() == BackendType::DirectVulkan) {
RewriteLinearSubgroupPrefixScanForVulkan(stage, activeBackend->GetDynamicParameters().SubgroupSize,
source);
}
} }
Bool RetargetLegacyVersionDirectiveTo460(String& source) { Bool RetargetLegacyVersionDirectiveTo460(String& source) {
@@ -21,16 +21,6 @@ namespace MobileGL {
namespace ShaderTranspiler { namespace ShaderTranspiler {
void PreprocessShaderSource(ShaderStage stage, String& source); void PreprocessShaderSource(ShaderStage stage, String& source);
// Some desktop-captured compute shaders build a workgroup-wide linear prefix scan
// from subgroupInclusiveAdd plus a shared array of subgroup totals. Qualcomm's
// Vulkan driver miscompiles that exact float InclusiveScan path for native subgroups
// wider than the capture's 32 lanes. For the narrowly recognized, uniform-control-
// flow template, replace the subgroup-local scan with a shared-memory, strict
// left-fold over virtual 32-lane segments. Returns true only when the complete safe
// template was recognized and rewritten. DirectVulkan calls this through
// PreprocessShaderSource; the explicit entry point exists for deterministic tests.
Bool RewriteLinearSubgroupPrefixScanForVulkan(ShaderStage stage, Uint32 nativeSubgroupSize, String& source);
// Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down // Rewrites a "#version 330 core" directive that PreprocessShaderSource normalized down
// from a legacy desktop version back up to "#version 460 core". Returns false (leaving // from a legacy desktop version back up to "#version 460 core". Returns false (leaving
// the source untouched) for anything else: ES, compatibility, or an already-modern // the source untouched) for anything else: ES, compatibility, or an already-modern
@@ -63,17 +63,7 @@ namespace MobileGL {
void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { void TMglGlslIoResolver::reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType(); const glslang::TType& type = ent.symbol->getType();
const glslang::TString& name = ent.symbol->getAccessName(); const glslang::TString& name = ent.symbol->getAccessName();
// OpenGL assigns generic vertex attribute locations only to active inputs. glslang gathers if (currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
// both live and dead declarations before mapping, so allowing the default collector to
// reserve a dead vertex input would make it consume a location that an active input should
// reuse. Other stage interfaces still need the default cross-stage matching behavior.
if (!ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
return;
}
// glBindAttribLocation only affects active inputs in the linked program. Applying an API
// binding to an inactive declaration would reserve its slot in glslang's collector and
// incorrectly push an active, automatically mapped input to a different location.
if (ent.live && currentStage == EShLangVertex && type.getQualifier().isPipeInput()) {
auto it = m_explicitVertexIns.find(name.c_str()); auto it = m_explicitVertexIns.find(name.c_str());
if (it != m_explicitVertexIns.end()) { if (it != m_explicitVertexIns.end()) {
auto& writableType = ent.symbol->getWritableType(); auto& writableType = ent.symbol->getWritableType();
@@ -104,13 +94,6 @@ namespace MobileGL {
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink); TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
} }
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) {
return ent.newLocation = -1;
}
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
}
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) { void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType(); const glslang::TType& type = ent.symbol->getType();
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler && if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
@@ -37,7 +37,6 @@ namespace MobileGL {
opaqueUniformBindings) {} opaqueUniformBindings) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override; void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override; int resolveUniformLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
protected: protected:
+9
View File
@@ -93,6 +93,15 @@ The bundled fixtures cover:
- minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and - minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
iterationT after entering a singleplayer world, with Iris' DSA path disabled. iterationT after entering a singleplayer world, with Iris' DSA path disabled.
![Minecraft 1.21.4 Fabric Iris iterationT no-DSA in-world golden](fixtures/minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world.0000115019.png) ![Minecraft 1.21.4 Fabric Iris iterationT no-DSA in-world golden](fixtures/minecraft-1.21.4-fabric-iris-iterationt-nodsa-in-world.0000115019.png)
- minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
iterationRP after entering a singleplayer world, framing the iterationRP name overlay over a lake with far-shore
tree reflections. iterationRP's temporal auto-exposure makes a single-frame trim overexpose and drop the overlay,
so the fixture is a prefix trace (all calls up to the target frame) that replays the temporal state. The pack also
gates an NVIDIA-only shadow path (`subgroupPartitionNV`, `GL_NV_shader_subgroup_partitioned`) on the GL vendor
string, so the capture reports a masked vendor and the trace carries the portable `subgroupShuffleXor` path that
non-NVIDIA GPUs take.
The trace archive and golden are not committed yet (the repository's Git LFS quota rejects new objects with
`GH009`); the case stays registered and its fixture files are hydrated from the trace fixture mirror.
- minecraft-1.21.4-fabric-iris-photon-v1.1-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and - minecraft-1.21.4-fabric-iris-photon-v1.1-in-world: captured from Minecraft 1.21.4 Fabric with Sodium, Iris, and
Photon v1.1 after entering a singleplayer world. Photon v1.1 after entering a singleplayer world.
![Minecraft 1.21.4 Fabric Iris Photon v1.1 in-world golden](fixtures/minecraft-1.21.4-fabric-iris-photon-v1.1-in-world.0000159866.png) ![Minecraft 1.21.4 Fabric Iris Photon v1.1 in-world golden](fixtures/minecraft-1.21.4-fabric-iris-photon-v1.1-in-world.0000159866.png)
@@ -1,4 +0,0 @@
interface:
display_name: "Capture RenderDoc Trace Frame"
short_description: "Capture exact Android Vulkan/GLES trace frames"
default_prompt: "Use $renderdoc-capture-trace-frame to capture and validate an exact Android retrace frame."
@@ -18,16 +18,17 @@ SUMMARY_HTML = "mobilegl-android-retrace-overview.html"
DEFAULT_ANGLE_VARIANT = "ec889e6ea831" DEFAULT_ANGLE_VARIANT = "ec889e6ea831"
BLISS_ANGLE_VARIANT = "90a62123d794" BLISS_ANGLE_VARIANT = "90a62123d794"
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world" BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
TRACE_APK_DIR = ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "trace" / "debug"
BACKENDS = { BACKENDS = {
"DirectGLES": { "DirectGLES": {
"package": "top.mobilegl.plugin.trace", "apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk",
"use_angle": False, "package": "top.mobilegl.plugin.espryt.trace",
"use_angle": True,
"use_pbuffer": False, "use_pbuffer": False,
}, },
"DirectVulkan": { "DirectVulkan": {
"package": "top.mobilegl.plugin.trace", "apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "magmaTrace" / "debug" / "MobileGL-MagmaTrace-debug.apk",
"package": "top.mobilegl.plugin.magma.trace",
"use_angle": False, "use_angle": False,
"use_pbuffer": False, "use_pbuffer": False,
}, },
@@ -44,11 +45,6 @@ def is_lfs_pointer(path):
return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1") return path.exists() and path.read_bytes()[:80].startswith(b"version https://git-lfs.github.com/spec/v1")
def find_trace_apk():
candidates = list(TRACE_APK_DIR.glob("MobileGL-plugin-trace-release-*.apk"))
return max(candidates, key=lambda path: path.stat().st_mtime) if candidates else None
def bash_path(path): def bash_path(path):
path = Path(path).resolve() path = Path(path).resolve()
drive = path.drive.rstrip(":").lower() drive = path.drive.rstrip(":").lower()
@@ -121,13 +117,9 @@ def render_summary():
def run_case(case, backend): def run_case(case, backend):
backend_info = BACKENDS[backend] backend_info = BACKENDS[backend]
apk = find_trace_apk()
trace_archive = FIXTURES / case["trace_archive"] trace_archive = FIXTURES / case["trace_archive"]
golden = FIXTURES / case["golden"] golden = FIXTURES / case["golden"]
alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None
if apk is None:
mark_skipped(case, backend, f"SKIPPED_MISSING_APK: no trace APK found under {TRACE_APK_DIR}")
return 2
if not trace_archive.exists() or is_lfs_pointer(trace_archive): if not trace_archive.exists() or is_lfs_pointer(trace_archive):
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer") mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
copy_goldens(case, backend) copy_goldens(case, backend)
@@ -142,7 +134,7 @@ def run_case(case, backend):
"C:/Program Files/Git/bin/bash.exe", "C:/Program Files/Git/bin/bash.exe",
"android-plugin/trace-replay-ci.sh", "android-plugin/trace-replay-ci.sh",
"--apk-file", "--apk-file",
bash_path(apk), bash_path(backend_info["apk"]),
"--package", "--package",
backend_info["package"], backend_info["package"],
"--backend", "--backend",
+20
View File
@@ -0,0 +1,20 @@
# MobileGL trace-replay skills
Task-focused skills for capturing, replaying, debugging, and authoring MobileGL
apitrace fixtures. Each skill is a self-contained package:
- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The
directory name equals the frontmatter `name`.
- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`,
`short_description`, `default_prompt`).
- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the
skill has them.
## Skills
| Skill | What it does |
| --- | --- |
| [trace-fixture-authoring-on-android-fcl](trace-fixture-authoring-on-android-fcl/SKILL.md) | Capture an on-device Android apitrace from FCL's MobileGL renderers (DirectGLES / Magma / SimpleFPEWrapper), mark the defect frame, and pull `full.trace`. |
| [renderdoc-debug-on-trace-replay](renderdoc-debug-on-trace-replay/SKILL.md) | Capture and validate an exact frame from a MobileGL retrace on a connected Android device with RenderDoc / rdc-cli. |
| [mismatch-retrace-debugging](mismatch-retrace-debugging/SKILL.md) | Localize the first divergent render pass and draw call when a fixture replays correctly in a golden environment but renders differently on a target backend. |
| [trace-fixture-authoring](trace-fixture-authoring/SKILL.md) | Author a deterministic trace-replay fixture — trim, golden, package under the size budget, register in `trace_cases.json`, and validate on Linux and Android. |
@@ -1,3 +1,8 @@
---
name: mismatch-retrace-debugging
description: Localize the first divergent render pass and draw call when a MobileGL apitrace fixture replays correctly in a golden environment but renders differently under mobilegl_trace_replay, Android trace replay, or another backend. Use to binary-search pass/draw endpoints, diff GL state around the first bad call, and classify the fault as a vertex/VS, fragment/FS, or framebuffer/composition mismatch.
---
# Mismatch retrace debugging # Mismatch retrace debugging
Use this when an apitrace fixture replays correctly on one environment but Use this when an apitrace fixture replays correctly on one environment but
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL Mismatch Retrace Debugging"
short_description: "Localize the first divergent draw in a mismatching MobileGL retrace"
default_prompt: "Use $mismatch-retrace-debugging to find the first divergent render pass and draw call in a MobileGL retrace mismatch."
@@ -1,5 +1,5 @@
--- ---
name: renderdoc-capture-trace-frame name: renderdoc-debug-on-trace-replay
description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures. description: Capture and validate an exact frame from a MobileGL apitrace retrace on a connected Android device with RenderDoc/rdc-cli. Use for DirectVulkan or DirectGLES trace replay, mapping a target API call to an eglSwapBuffers frame, producing an .rdc plus a complete command manifest, checking capture stability, or troubleshooting Android TargetControl timing and replay failures.
--- ---
@@ -19,20 +19,20 @@ adb -s SERIAL shell pm path top.mobilegl.plugin.trace
rdc doctor rdc doctor
``` ```
4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/capture_android_retrace.py`. 4. Pass the unpacked `trace.trace`, its golden PNG, the fixture target call, backend, and output path to `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py`.
## Capture ## Capture
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls: Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
```powershell ```powershell
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectVulkan --output captures/case-vulkan.rdc --serial SERIAL --json
``` ```
Change only the backend and output for GLES: Change only the backend and output for GLES:
```powershell ```powershell
python tools/trace_replay/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json python tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/capture_android_retrace.py --trace .trace-work/case/trace.trace --golden tools/trace_replay/fixtures/case.0002667619.png --target-call 2667619 --backend DirectGLES --output captures/case-gles.rdc --serial SERIAL --json
``` ```
Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately. Use `--target-swap N` when the mapping is already known. Use `--capture-frame N` only to override the backend rule deliberately.
@@ -0,0 +1,4 @@
interface:
display_name: "RenderDoc Debug on Trace Replay"
short_description: "Capture and debug an exact MobileGL trace-replay frame in RenderDoc"
default_prompt: "Use $renderdoc-debug-on-trace-replay to capture and validate an exact Android trace-replay frame in RenderDoc."
@@ -2,7 +2,7 @@
## TargetControl timing ## TargetControl timing
- Start `tools/trace_replay/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects. - Start `tools/trace_replay/skills/renderdoc-debug-on-trace-replay/scripts/queue_android_frame.py` before launching `TraceReplayActivity`. A fast retrace can pass the requested frame before a late client connects.
- Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished. - Keep TargetControl connected until `NewCapture` arrives. A queued request alone is not sufficient evidence that the RDC finished.
- Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost. - Drain the asynchronous `RegisterAPI` and `CapturableWindowCount` messages before calling `QueueCapture`; otherwise `NewCapture` can be lost.
- Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout. - Do not use the daemon-backed `rdc script` path for a capture that may exceed 30 seconds. Its outer RPC times out even when the device later writes a valid RDC. The repository helper imports the RenderDoc module discovered by `rdc` directly and has an independent capture timeout.
@@ -0,0 +1,137 @@
---
name: trace-fixture-authoring-on-android-fcl
description: Capture an on-device Android apitrace from FCL's MobileGL renderers. Use when preparing a reproducible MobileGL DirectGLES, Magma (DirectVulkan), or SimpleFPEWrapper rendering trace, marking the frame of a visual defect, pulling the resulting full.trace, or turning a device capture into a replay fixture.
---
# MobileGL Android trace capture
## Overview
Use FCL's Android `egltrace.so` wrapper, not Perfetto. When enabled before
launch, it records the complete EGL/GL call stream to `full.trace`. The game's
**MobileGL Trace → Capture** control marks the next swap frame in
`capture-result.json`; it does not start or stop recording and does not produce
a one-frame trace by itself.
Run commands from the FoldCraftLauncher repository root:
```sh
export REPO="$PWD"
export CAPTURE="$REPO/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts"
export SERIAL=<adb-device-serial> # omit --serial only if exactly one device is attached
```
The capture scripts are bundled inside this skill under `scripts/`; they
auto-detect the FoldCraftLauncher repository root from their own location, so
`--repo` only needs to be passed for a non-standard checkout layout.
## Prerequisites
- Use an FCL build containing `MobileGLTraceCapture` and the in-game Capture
menu entry.
- Select one of these renderers: MobileGL (DirectGLES), MobileGL Magma
(DirectVulkan), or SimpleFPEWrapper. MobileGlues is not supported by this
capture wrapper.
- Install `adb` and make it available on `PATH`; authorize USB debugging.
- Build the wrapper with Android NDK, CMake, Ninja, Python 3, and the checked
out in-tree `MobileGL/3rdparty/apitrace` submodule.
Confirm the attached device and ABI before building. The wrapper ABI must match
the device process ABI.
```sh
adb devices -l
adb -s "$SERIAL" shell getprop ro.product.cpu.abi
```
Use `arm64-v8a` for the usual `arm64-v8a` result; use the matching NDK ABI for
other devices.
## Build and install the wrapper
Build once per ABI or after changing apitrace/wrapper sources:
```sh
python3 "$CAPTURE/build_android_egltrace.py" --abi arm64-v8a
```
This generates `egltrace.so` under the skill's `scripts/out/` directory. Push
it and write FCL's enable sentinel before launching the game:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" install-wrapper
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" enable
```
The device-side control directory is `/sdcard/FCL/mobilegl-trace`. FCL copies
the shared `egltrace.so` into its private files directory at launch, replaces
the renderer's EGL library with it, and forwards to the real MobileGL library.
## Capture a reproduction
1. Start FCL after the wrapper and enable sentinel are in place. Select the
intended supported MobileGL renderer and launch the game.
2. Trace mode forces the game to `854x480`; account for that when reproducing
and comparing output.
3. Reproduce the issue. Start close to the target scene because tracing starts
when the game launches and trace files can grow rapidly.
4. At the desired visual state, open FCL's right-side game menu and press
**MobileGL Trace → Capture**. Let at least one frame present afterward.
5. Exit the game cleanly, then pull the latest session:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" pull-latest
```
The default local result is:
```text
.trace-work/pulled-mobilegl-captures/capture-YYYYMMDD-HHMMSS-<renderer>/
full.trace
capture-status.json
capture-result.json
```
`capture-result.json` must show `"status": "captured"`. Its `targetFrame`
is the one-based swap count used by `gltrim`; `zeroBasedFrame` is included for
tools that use zero-based indexing.
## Diagnose setup failures
Inspect the active device session directly:
```sh
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/latest-session.txt
adb -s "$SERIAL" shell ls -lh /sdcard/FCL/mobilegl-trace
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-status.json
adb -s "$SERIAL" shell cat /sdcard/FCL/mobilegl-trace/capture-*/capture-result.json
```
If `capture-status.json` reports a missing `egltrace.so`, rebuild/push the
correct ABI and relaunch. If `capture-result.json` is absent, Capture was
pressed without an active trace session, or no subsequent `eglSwapBuffers`
occurred. The menu button itself only writes `capture-once.request`.
Disable tracing when finished; otherwise the next supported MobileGL launch
will trace again:
```sh
python3 "$CAPTURE/adb_capture.py" --serial "$SERIAL" disable
```
## Create a replay fixture (optional)
Keep the raw `full.trace` until replay validation succeeds. To frame-trim and
package the marked frame for MobileGL trace replay, use the existing helper:
```sh
python3 "$CAPTURE/package_capture_fixture.py" \
--serial "$SERIAL" \
--case <case-name> \
--apitrace <path-to-in-tree-apitrace>
```
It pulls the latest capture if necessary, uses `capture-result.json` to select
the frame, runs `apitrace gltrim`, creates a golden image, and enforces the
fixture archive-size limit. Follow `../trace-fixture-authoring/SKILL.md` for
deterministic scene setup, verification, and registry changes.
@@ -0,0 +1,4 @@
interface:
display_name: "Trace Fixture Authoring on Android (FCL)"
short_description: "Capture and package a MobileGL trace fixture on Android FCL"
default_prompt: "Use $trace-fixture-authoring-on-android-fcl to capture a MobileGL trace on my Android device and package it into a replay fixture."
@@ -0,0 +1,3 @@
# Build output produced by build_android_egltrace.py (ABI-specific, regenerated).
out/
__pycache__/
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import argparse
import subprocess
from pathlib import Path
REMOTE_ROOT = "/sdcard/FCL/mobilegl-trace"
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_WRAPPER = SCRIPT_DIR / "out" / "egltrace.so"
def adb(serial, args):
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
cmd += args
print("+", " ".join(cmd), flush=True)
subprocess.run(cmd, check=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--serial")
sub = parser.add_subparsers(dest="command", required=True)
install = sub.add_parser("install-wrapper")
install.add_argument("--wrapper", default=str(DEFAULT_WRAPPER))
sub.add_parser("enable")
sub.add_parser("disable")
sub.add_parser("capture-once")
pull = sub.add_parser("pull-latest")
pull.add_argument("--output", default=".trace-work/pulled-mobilegl-captures")
args = parser.parse_args()
serial = args.serial
if args.command == "install-wrapper":
wrapper = Path(args.wrapper)
if not wrapper.exists():
raise SystemExit(f"missing wrapper: {wrapper}")
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["push", str(wrapper), f"{REMOTE_ROOT}/egltrace.so"])
elif args.command == "enable":
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["shell", f"printf enabled > {REMOTE_ROOT}/enable"])
elif args.command == "disable":
adb(serial, ["shell", "rm", "-f", f"{REMOTE_ROOT}/enable"])
elif args.command == "capture-once":
adb(serial, ["shell", "mkdir", "-p", REMOTE_ROOT])
adb(serial, ["shell", f"date +%s%3N > {REMOTE_ROOT}/capture-once.request"])
elif args.command == "pull-latest":
tmp = subprocess.check_output((["adb"] + (["-s", serial] if serial else []) +
["shell", "cat", f"{REMOTE_ROOT}/latest-session.txt"]),
text=True, encoding="utf-8", errors="replace").strip()
if not tmp:
raise SystemExit("no latest-session.txt on device")
output = Path(args.output)
output.mkdir(parents=True, exist_ok=True)
adb(serial, ["pull", tmp, str(output / Path(tmp).name)])
if __name__ == "__main__":
main()
@@ -0,0 +1,191 @@
cmake_minimum_required(VERSION 3.22.1)
project(mobilegl_android_egltrace)
set(APITRACE_ROOT "" CACHE PATH "Path to apitrace source tree")
set(PATCHED_EGLTRACE_CPP "" CACHE FILEPATH "Generated and patched egltrace.cpp")
set(PATCHED_GLPROC_EGL_CPP "" CACHE FILEPATH "Patched glproc_egl.cpp")
if(NOT EXISTS "${APITRACE_ROOT}/wrappers")
message(FATAL_ERROR "APITRACE_ROOT must point to apitrace")
endif()
if(NOT EXISTS "${PATCHED_EGLTRACE_CPP}")
message(FATAL_ERROR "PATCHED_EGLTRACE_CPP is required")
endif()
if(NOT EXISTS "${PATCHED_GLPROC_EGL_CPP}")
message(FATAL_ERROR "PATCHED_GLPROC_EGL_CPP is required")
endif()
set(APITRACE_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/apitrace")
set(APITRACE_VERSION "mobilegl-capture")
find_package(Python3 REQUIRED)
find_package(Threads REQUIRED)
include("${APITRACE_ROOT}/cmake/ConvenienceLibrary.cmake")
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_STATIC_SNAPPY ON CACHE BOOL "" FORCE)
set(DOC_INSTALL_DIR "doc" CACHE PATH "" FORCE)
set(HAVE_X86 OFF CACHE BOOL "" FORCE)
set(ZLIB_FOUND OFF CACHE BOOL "" FORCE)
set(PNG_FOUND OFF CACHE BOOL "" FORCE)
set(Snappy_FOUND OFF CACHE BOOL "" FORCE)
set(BROTLIDEC_FOUND OFF CACHE BOOL "" FORCE)
set(BROTLIENC_FOUND OFF CACHE BOOL "" FORCE)
set(ZSTD_FOUND OFF CACHE BOOL "" FORCE)
set(CMAKE_EXECUTABLE_FORMAT "MobileGLAndroid" CACHE INTERNAL "" FORCE)
add_custom_target(check)
add_subdirectory("${APITRACE_ROOT}/thirdparty" "${APITRACE_BINARY_DIR}/thirdparty")
set(APITRACE_GENERATED_DIR "${APITRACE_BINARY_DIR}/generated")
file(MAKE_DIRECTORY "${APITRACE_GENERATED_DIR}")
configure_file("${APITRACE_ROOT}/version.h.in" "${APITRACE_GENERATED_DIR}/version.h" @ONLY)
add_custom_command(
OUTPUT
"${APITRACE_GENERATED_DIR}/glproc.hpp"
"${APITRACE_GENERATED_DIR}/glproc.cpp"
COMMAND ${Python3_EXECUTABLE}
"${APITRACE_ROOT}/dispatch/glproc.py"
"${APITRACE_GENERATED_DIR}/glproc.hpp"
"${APITRACE_GENERATED_DIR}/glproc.cpp"
DEPENDS
"${APITRACE_ROOT}/dispatch/glproc.py"
"${APITRACE_ROOT}/dispatch/dispatch.py"
"${APITRACE_ROOT}/specs/wglapi.py"
"${APITRACE_ROOT}/specs/glxapi.py"
"${APITRACE_ROOT}/specs/cglapi.py"
"${APITRACE_ROOT}/specs/eglapi.py"
"${APITRACE_ROOT}/specs/glapi.py"
"${APITRACE_ROOT}/specs/gltypes.py"
"${APITRACE_ROOT}/specs/stdapi.py")
add_library(apitrace_os STATIC
"${APITRACE_ROOT}/lib/os/os_backtrace.cpp"
"${APITRACE_ROOT}/lib/os/os_crtdbg.cpp"
"${APITRACE_ROOT}/lib/os/os_posix.cpp")
target_include_directories(apitrace_os PUBLIC
"${APITRACE_ROOT}/compat"
"${APITRACE_ROOT}/thirdparty"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace")
target_link_libraries(apitrace_os PUBLIC Threads::Threads)
add_library(glproc STATIC
"${APITRACE_GENERATED_DIR}/glproc.cpp"
"${PATCHED_GLPROC_EGL_CPP}")
target_include_directories(glproc PUBLIC
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/wrappers"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(glproc PUBLIC apitrace_os dl)
add_library(highlight STATIC "${APITRACE_ROOT}/lib/highlight/highlight.cpp")
target_include_directories(highlight PUBLIC "${APITRACE_ROOT}/lib/highlight")
add_library(guids STATIC "${APITRACE_ROOT}/lib/guids/guids.cpp")
target_include_directories(guids PUBLIC
"${APITRACE_ROOT}/lib/guids"
"${APITRACE_ROOT}/lib/os")
add_library(common STATIC
"${APITRACE_ROOT}/lib/trace/trace_callset.cpp"
"${APITRACE_ROOT}/lib/trace/trace_dump.cpp"
"${APITRACE_ROOT}/lib/trace/trace_fast_callset.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_read.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zlib.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_brotli.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_snappy.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zstd.cpp"
"${APITRACE_ROOT}/lib/trace/trace_file_zstd_seekable.cpp"
"${APITRACE_ROOT}/lib/trace/trace_model.cpp"
"${APITRACE_ROOT}/lib/trace/trace_option.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_snappy.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_zlib.cpp"
"${APITRACE_ROOT}/lib/trace/trace_ostream_zstd.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser_flags.cpp"
"${APITRACE_ROOT}/lib/trace/trace_parser_loop.cpp"
"${APITRACE_ROOT}/lib/trace/trace_profiler.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer_local.cpp"
"${APITRACE_ROOT}/lib/trace/trace_writer_model.cpp")
target_include_directories(common PUBLIC
"${APITRACE_ROOT}/compat"
"${APITRACE_ROOT}/thirdparty"
"${APITRACE_ROOT}/lib/guids"
"${APITRACE_ROOT}/lib/highlight"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace"
"${APITRACE_ROOT}/lib/ubjson")
target_link_libraries(common PUBLIC
guids
highlight
apitrace_os
Snappy::snappy
ZLIB::ZLIB
PkgConfig::BROTLIDEC
PkgConfig::ZSTD
zstd_seekable)
add_convenience_library(trace
"${APITRACE_ROOT}/wrappers/memtrace.hpp"
"${APITRACE_ROOT}/wrappers/memtrace.cpp")
target_include_directories(trace PUBLIC
"${APITRACE_ROOT}/thirdparty/crc32c")
target_link_libraries(trace
common
guids
crc32c)
add_library(glhelpers STATIC
"${APITRACE_ROOT}/helpers/glfeatures.cpp"
"${APITRACE_ROOT}/helpers/eglsize.cpp")
target_include_directories(glhelpers PUBLIC
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(glhelpers PUBLIC glproc apitrace_os)
add_convenience_library(gltrace_common
"${APITRACE_ROOT}/wrappers/glcaps.cpp"
"${APITRACE_ROOT}/wrappers/config.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_arrays.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_state.cpp"
"${APITRACE_ROOT}/wrappers/glmemshadow.hpp"
"${APITRACE_ROOT}/wrappers/glmemshadow.cpp"
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.hpp"
"${APITRACE_ROOT}/wrappers/gltrace_unpack_compressed.cpp")
add_dependencies(gltrace_common glproc)
target_include_directories(gltrace_common PUBLIC
"${APITRACE_ROOT}/wrappers")
target_link_libraries(gltrace_common
glhelpers
trace)
add_library(egltrace SHARED
"${PATCHED_EGLTRACE_CPP}"
"${APITRACE_ROOT}/wrappers/dlsym.cpp"
"${PATCHED_GLPROC_EGL_CPP}")
add_dependencies(egltrace glproc)
set_target_properties(egltrace PROPERTIES PREFIX "")
target_compile_definitions(egltrace PRIVATE -DEGLTRACE=1)
target_include_directories(egltrace PRIVATE
"${APITRACE_ROOT}/wrappers"
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/lib/trace"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(egltrace
gltrace_common
glproc
Threads::Threads
dl)
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
# This script is bundled inside the trace-fixture-authoring-on-android-fcl skill at
# <FCL>/MobileGL/tools/trace_replay/skills/trace-fixture-authoring-on-android-fcl/scripts/.
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
DEFAULT_OUTPUT = SCRIPT_DIR / "out" / "egltrace.so"
def run(cmd, cwd=None):
print("+", " ".join(str(part) for part in cmd), flush=True)
subprocess.run(cmd, cwd=cwd, check=True)
def find_ndk(repo):
for key in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT"):
value = os.environ.get(key)
if value:
return Path(value)
for key in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
value = os.environ.get(key)
if value:
ndk_root = Path(value) / "ndk"
if ndk_root.exists():
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
if versions:
return versions[-1]
local = repo / "local.properties"
if local.exists():
sdk = None
ndk = None
for line in local.read_text(encoding="utf-8", errors="ignore").splitlines():
if line.startswith("sdk.dir="):
sdk = Path(line.split("=", 1)[1].replace("\\:", ":"))
if line.startswith("ndk.dir="):
ndk = Path(line.split("=", 1)[1].replace("\\:", ":"))
if ndk:
return ndk
if sdk:
ndk_root = sdk / "ndk"
if ndk_root.exists():
versions = sorted([p for p in ndk_root.iterdir() if p.is_dir()])
if versions:
return versions[-1]
raise SystemExit("Android NDK not found; set ANDROID_NDK_HOME or local.properties sdk.dir/ndk.dir")
def generate_and_patch(repo, build_dir):
wrapper_dir = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers"
generated = build_dir / "patched" / "egltrace.cpp"
generated.parent.mkdir(parents=True, exist_ok=True)
with generated.open("w", encoding="utf-8", newline="\n") as out:
subprocess.run([sys.executable, str(wrapper_dir / "egltrace.py")], cwd=wrapper_dir, stdout=out, check=True)
text = generated.read_text(encoding="utf-8")
helper = r'''
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
static unsigned long long mobilegl_capture_swap_count = 0;
static long long mobilegl_capture_time_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (long long) ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
}
static int mobilegl_capture_exists(const char *path) {
return path != NULL && path[0] != '\0' && access(path, F_OK) == 0;
}
static void mobilegl_capture_record_request(void) {
++mobilegl_capture_swap_count;
const char *request = getenv("MOBILEGL_TRACE_CAPTURE_REQUEST_FILE");
const char *output = getenv("MOBILEGL_TRACE_CAPTURE_FRAME_FILE");
if (!mobilegl_capture_exists(request) || output == NULL || output[0] == '\0') {
return;
}
unlink(request);
FILE *file = fopen(output, "w");
if (file == NULL) {
return;
}
const char *trace_file = getenv("TRACE_FILE");
fprintf(file,
"{\n"
" \"status\": \"captured\",\n"
" \"swapCount\": %llu,\n"
" \"targetFrame\": %llu,\n"
" \"zeroBasedFrame\": %llu,\n"
" \"capturedAtMs\": %lld,\n"
" \"traceFile\": \"%s\"\n"
"}\n",
mobilegl_capture_swap_count,
mobilegl_capture_swap_count,
mobilegl_capture_swap_count == 0 ? 0 : mobilegl_capture_swap_count - 1,
mobilegl_capture_time_ms(),
trace_file == NULL ? "" : trace_file);
fclose(file);
}
'''
insert_at = text.find("#include")
if insert_at < 0:
raise SystemExit("generated egltrace.cpp has no include block")
next_block = text.find("\n\n", insert_at)
text = text[:next_block] + "\n" + helper + text[next_block:]
needle = "EGLBoolean EGLAPIENTRY eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)"
start = text.find(needle)
if start < 0:
raise SystemExit("generated egltrace.cpp has no eglSwapBuffers wrapper to patch")
brace = text.find("{", start)
if brace < 0:
raise SystemExit("eglSwapBuffers wrapper has no function body")
text = text[:brace + 1] + "\n mobilegl_capture_record_request();" + text[brace + 1:]
generated.write_text(text, encoding="utf-8", newline="\n")
return generated
def patch_glproc_egl(repo, build_dir):
source = repo / "MobileGL" / "3rdparty" / "apitrace" / "wrappers" / "glproc_egl.cpp"
patched = build_dir / "patched" / "glproc_egl.cpp"
patched.parent.mkdir(parents=True, exist_ok=True)
text = source.read_text(encoding="utf-8")
text = text.replace('#include "dlopen.hpp"\n', '#include "dlopen.hpp"\n#include <stdlib.h>\n')
needle = """void *
_getPublicProcAddress(const char *procName)
{
void *proc;
"""
replacement = """void *
_getPublicProcAddress(const char *procName)
{
void *proc;
static void *traceLibGL = NULL;
static bool triedTraceLibGL = false;
if (!triedTraceLibGL) {
triedTraceLibGL = true;
const char *traceLibGLName = getenv("TRACE_LIBGL");
if (traceLibGLName && traceLibGLName[0]) {
traceLibGL = _dlopen(traceLibGLName, RTLD_GLOBAL | RTLD_LAZY | RTLD_DEEPBIND);
}
}
if (traceLibGL) {
proc = dlsym(traceLibGL, procName);
if (proc) {
return proc;
}
}
"""
if needle not in text:
raise SystemExit("glproc_egl.cpp patch point not found")
text = text.replace(needle, replacement, 1)
patched.write_text(text, encoding="utf-8", newline="\n")
return patched
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
parser.add_argument("--abi", default="arm64-v8a")
parser.add_argument("--android-platform", default="android-23")
parser.add_argument("--build-dir", default=".trace-work/build-android-egltrace")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
args = parser.parse_args()
repo = Path(args.repo).resolve()
ndk = find_ndk(repo)
build_dir = (repo / args.build_dir / args.abi).resolve()
apitrace = repo / "MobileGL" / "3rdparty" / "apitrace"
source_dir = SCRIPT_DIR / "android_egltrace"
toolchain = ndk / "build" / "cmake" / "android.toolchain.cmake"
if not apitrace.exists():
raise SystemExit(f"missing apitrace checkout: {apitrace}")
if not source_dir.exists():
raise SystemExit(f"missing wrapper CMake project: {source_dir}")
if not toolchain.exists():
raise SystemExit(f"missing Android toolchain: {toolchain}")
cache = build_dir / "CMakeCache.txt"
if cache.exists() and "CMAKE_GENERATOR:INTERNAL=Ninja" not in cache.read_text(encoding="utf-8", errors="ignore"):
shutil.rmtree(build_dir)
elif cache.exists() and str(source_dir).replace("\\", "/") not in cache.read_text(encoding="utf-8", errors="ignore").replace("\\", "/"):
shutil.rmtree(build_dir)
ninja = shutil.which("ninja")
if ninja is None:
cmake_ninjas = sorted((Path(os.environ.get("ANDROID_HOME", "")) / "cmake").glob("*/bin/ninja.exe"))
ninja = str(cmake_ninjas[-1]) if cmake_ninjas else None
if ninja is None:
raise SystemExit("ninja not found; install Ninja or Android SDK CMake")
patched_egltrace = generate_and_patch(repo, build_dir)
patched_glproc_egl = patch_glproc_egl(repo, build_dir)
run([
"cmake", "-G", "Ninja", "-S", str(source_dir), "-B", str(build_dir),
"-DCMAKE_BUILD_TYPE=Release",
f"-DCMAKE_TOOLCHAIN_FILE={toolchain}",
f"-DCMAKE_MAKE_PROGRAM={ninja}",
f"-DANDROID_ABI={args.abi}",
f"-DANDROID_PLATFORM={args.android_platform}",
f"-DAPITRACE_ROOT={apitrace}",
f"-DPATCHED_EGLTRACE_CPP={patched_egltrace}",
f"-DPATCHED_GLPROC_EGL_CPP={patched_glproc_egl}",
])
run(["cmake", "--build", str(build_dir), "--target", "egltrace", "--parallel"])
output = Path(args.output)
if not output.is_absolute():
output = repo / output
output = output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
candidates = list(build_dir.rglob("egltrace.so"))
if not candidates:
raise SystemExit("egltrace.so was not produced")
shutil.copy2(candidates[0], output)
print(output)
if __name__ == "__main__":
main()
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
import argparse
import json
import re
import shutil
import subprocess
import tarfile
from pathlib import Path
DEFAULT_MAX_ARCHIVE_BYTES = 20 * 1024 * 1024
# Bundled under the skill at .../skills/trace-fixture-authoring-on-android-fcl/scripts/.
SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_REPO = SCRIPT_DIR.parents[5] # -> FoldCraftLauncher repo root
def run(cmd, cwd=None, capture=False):
print("+", " ".join(str(part) for part in cmd), flush=True)
if capture:
return subprocess.check_output(cmd, cwd=cwd, text=True, encoding="utf-8", errors="replace")
subprocess.run(cmd, cwd=cwd, check=True)
return ""
def adb(args, serial=None):
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
cmd += args
return run(cmd, capture=True)
def pull_latest(serial, dest):
latest = adb(["shell", "cat", "/sdcard/FCL/mobilegl-trace/latest-session.txt"], serial).strip()
if not latest:
raise SystemExit("device has no /sdcard/FCL/mobilegl-trace/latest-session.txt")
dest.mkdir(parents=True, exist_ok=True)
local = dest / Path(latest).name
if local.exists():
shutil.rmtree(local)
adb(["pull", latest, str(local)], serial)
return local
def choose_target_frame(capture_dir, explicit_frame):
if explicit_frame is not None:
return explicit_frame
result = capture_dir / "capture-result.json"
if not result.exists():
raise SystemExit(f"missing {result}; press the FCL capture button or create capture-once.request first")
data = json.loads(result.read_text(encoding="utf-8"))
if "targetFrame" not in data:
raise SystemExit(f"{result} has no targetFrame")
return int(data["targetFrame"])
def choose_snapshot(golden_dir):
pngs = sorted(golden_dir.glob("*.png"))
if not pngs:
raise SystemExit(f"no snapshots produced in {golden_dir}")
def call_no(path):
match = re.search(r"\.(\d+)\.png$", path.name)
return int(match.group(1)) if match else -1
return max(pngs, key=call_no)
def choose_target_call(explicit_call, golden):
if explicit_call is not None:
return explicit_call
if golden is None:
return None
match = re.search(r"\.(\d+)\.png$", golden.name)
return int(match.group(1)) if match else None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default=str(DEFAULT_REPO), help="FoldCraftLauncher repo root")
parser.add_argument("--serial", help="adb serial; when set, pull latest capture from device")
parser.add_argument("--capture-dir", help="local capture directory; defaults to pulled latest")
parser.add_argument("--pull-root", default=".trace-work/pulled-mobilegl-captures")
parser.add_argument("--case", required=True)
parser.add_argument("--target-frame", type=int)
parser.add_argument("--target-call", type=int)
parser.add_argument("--golden", help="existing golden PNG, normally produced by Android replay")
parser.add_argument("--skip-desktop-golden", action="store_true",
help="skip apitrace replay --headless; requires --golden and --target-call")
parser.add_argument("--apitrace", default="apitrace")
parser.add_argument("--fixtures-dir", default="MobileGL/tools/trace_replay/fixtures")
parser.add_argument("--width", type=int, default=854)
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--ssim-threshold", default="0.99")
parser.add_argument("--max-archive-bytes", type=int, default=DEFAULT_MAX_ARCHIVE_BYTES)
args = parser.parse_args()
repo = Path(args.repo).resolve()
if args.capture_dir:
capture_dir = Path(args.capture_dir).resolve()
elif args.serial:
capture_dir = pull_latest(args.serial, repo / args.pull_root)
else:
raise SystemExit("pass --capture-dir or --serial")
full_trace = capture_dir / "full.trace"
if not full_trace.exists():
raise SystemExit(f"missing trace: {full_trace}")
target_frame = choose_target_frame(capture_dir, args.target_frame)
work = capture_dir / "fixture-work"
if work.exists():
shutil.rmtree(work)
work.mkdir(parents=True)
frames_txt = work / "frames.txt"
frames_txt.write_text(run([args.apitrace, "dump", "--calls=frame", str(full_trace)], capture=True), encoding="utf-8")
trimmed = work / "trace.trace"
run([args.apitrace, "gltrim", "-f", str(target_frame), "--output", str(trimmed), str(full_trace)])
supplied_golden = Path(args.golden).resolve() if args.golden else None
target_call = choose_target_call(args.target_call, supplied_golden)
if args.skip_desktop_golden:
if supplied_golden is None or target_call is None:
raise SystemExit("--skip-desktop-golden requires --golden and --target-call")
golden = supplied_golden
else:
golden_dir = work / "golden"
golden_dir.mkdir()
prefix = golden_dir / f"{args.case}."
run([args.apitrace, "replay", "--headless", "--snapshot-prefix", str(prefix), "--call-nos", str(trimmed)])
golden = choose_snapshot(golden_dir)
target_call = choose_target_call(args.target_call, golden)
if target_call is None:
raise SystemExit(f"cannot infer target call from {golden}")
fixtures = (repo / args.fixtures_dir).resolve()
fixtures.mkdir(parents=True, exist_ok=True)
archive_root = work / "archive"
archive_root.mkdir()
shutil.copy2(trimmed, archive_root / "trace.trace")
tgz = fixtures / f"{args.case}.tgz"
with tarfile.open(tgz, "w:gz") as tar:
tar.add(archive_root / "trace.trace", arcname="trace.trace")
archive_size = tgz.stat().st_size
if archive_size > args.max_archive_bytes:
raise SystemExit(
f"{tgz} is {archive_size} bytes, over the {args.max_archive_bytes} byte fixture limit; "
"choose an earlier/smaller frame and re-run gltrim"
)
golden_out = fixtures / f"{args.case}.{target_call:010d}.png"
shutil.copy2(golden, golden_out)
manifest = {
"name": args.case,
"trace_archive": tgz.name,
"trace_file": "trace.trace",
"golden": golden_out.name,
"target_call": target_call,
"width": args.width,
"height": args.height,
"ssim_threshold": float(args.ssim_threshold),
"archive_size": archive_size,
}
manifest_path = capture_dir / f"{args.case}.fixture.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
print(json.dumps(manifest, indent=2))
if __name__ == "__main__":
main()
@@ -1,3 +1,8 @@
---
name: trace-fixture-authoring
description: Author a deterministic MobileGL trace-replay fixture from a captured apitrace - build the in-tree apitrace fork, capture a reproducible scene, frame-trim with gltrim, generate and verify a golden image, package under the archive-size budget, register the case in trace_cases.json, and validate on Linux and Android. Use when adding or re-trimming a trace_replay regression fixture.
---
# Trace fixture authoring # Trace fixture authoring
## Variables ## Variables
@@ -75,10 +80,19 @@ Minecraft specifics that keep the capture deterministic and small:
`doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player `doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player
`Rotation` that frames the intended subject. The camera snaps to the saved `Rotation` that frames the intended subject. The camera snaps to the saved
rotation on world join, so composition is edited in the save, not in-game. rotation on world join, so composition is edited in the save, not in-game.
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), and a - `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), a small
small `renderDistance` (3). Frame rate and render distance are the two main `renderDistance` (3), and the capture resolution pinned to `WIDTH` x `HEIGHT`
levers on trace size; a ~35 s in-world session at 10 fps lands well under (854x480) via `overrideWidth`/`overrideHeight` (or `--width`/`--height`).
the archive budget after repack. These are the main levers on fixture size: a low frame rate keeps the full
trace short, a small render distance keeps per-frame geometry down, and the
854x480 resolution keeps every render target the frame references small (a
trimmed frame's framebuffer/attachment textures scale with resolution
squared). A ~35 s in-world session at 10 fps and 854x480 lands well under the
archive budget after repack.
- `maxFps` has a practical floor: Minecraft ignores values below ~10 and falls
back to unlimited/vsync (a `maxFps:1` capture rendered ~60 fps and ballooned
the trace). 10 is as low as this lever goes, so do not count on a lower frame
rate to shrink the frame count further.
- Enter the world non-interactively with `--quickPlaySingleplayer <world>` so - Enter the world non-interactively with `--quickPlaySingleplayer <world>` so
every capture takes the same path from boot to gameplay. every capture takes the same path from boot to gameplay.
- Keep the game window UNFOCUSED for the whole capture (focus the desktop - Keep the game window UNFOCUSED for the whole capture (focus the desktop
@@ -111,6 +125,61 @@ For Java:
An `@argfile` with the full JVM+game command line keeps the invocation An `@argfile` with the full JVM+game command line keeps the invocation
reproducible across recaptures. reproducible across recaptures.
NEVER put a real credential on the traced command line. apitrace records the
traced process's argv into the trace as a `process.commandLine` property, so
anything passed there - `--accessToken`, session tokens, API keys - is embedded
in the trace and ships inside the committed fixture. Minecraft never validates
`--accessToken` for singleplayer, so pass a placeholder (`--accessToken 0`);
`--username`/`--uuid` are public and may stay real. Before packaging, grep the
UNCOMPRESSED trace for the secret to confirm it is absent:
```sh
"$APITRACE" repack "$WORK/$CASE/trace.trace" /tmp/plain.trace # decompress
grep -ac "<secret-prefix>" /tmp/plain.trace # must be 0
```
If a secret has already been captured, it can be scrubbed in place instead of
recapturing: apitrace's snappy container is `[length][raw snappy]` chunks with
no checksum, and a high-entropy secret is stored as literal bytes, so replacing
those bytes with an EQUAL-LENGTH filler keeps the container valid and leaves the
GL call stream byte-identical. Blank every maximal run of the secret (it splits
across chunks), then verify: frame count unchanged, the decompressed trace no
longer contains the secret, and the replayed target frame still matches the
golden. Treat any already-pushed trace as leaked regardless - rotate the
credential, since a force-push does not purge the LFS object from the remote.
Watch for vendor-gated shader paths. Shader packs branch on the GL vendor that
Iris injects (`MC_GL_VENDOR_NVIDIA` / `_AMD` / ...) and compile a
vendor-exclusive path, so capturing on an NVIDIA card can bake NVIDIA-only GLSL
into the fixture (iterationRP selects `subgroupPartitionNV` /
`GL_NV_shader_subgroup_partitioned` instead of the portable
`subgroupShuffleXor`). Iris resolves the `#ifdef` before `glShaderSource`, so
only the taken branch is in the trace and the fixture cannot replay on the
mobile GPUs MobileGL targets. Rather than hunting for a second GPU (the Windows
per-app GPU preference does NOT change which OpenGL ICD is loaded), mask the
vendor at capture time with apitrace's own config - point `GLTRACE_CONF` at a
file containing:
```
GL_VENDOR = "NoVIDIA (MobileGL spoof)"
GL_RENDERER = "NoVIDIA (MobileGL spoof)"
```
The wrapper then returns that from `glGetString`, so the pack compiles the
portable path while still running on the fast driver. Pick a string that does
NOT contain the real vendor name as a substring (Iris matches by substring, so
"Not NVIDIA ..." would still match) and that is self-describing, so nobody later
mistakes the trace for a capture on different hardware. Afterwards, grep the
decoded trace to confirm the vendor-exclusive symbols are gone:
```sh
"$APITRACE" dump "$WORK/$CASE/full.trace" | grep -c subgroupPartitionNV # must be 0
```
Software rasterisers are not a substitute here: llvmpipe exposes no
`GL_KHR_shader_subgroup` at all, and packs that use subgroup ops unguarded
cannot run on it in any vendor configuration.
Keep `full.trace` until both backends are validated. Keep `full.trace` until both backends are validated.
Persistent-mapped buffers: apps may legally write a `GL_MAP_PERSISTENT_BIT` Persistent-mapped buffers: apps may legally write a `GL_MAP_PERSISTENT_BIT`
@@ -172,12 +241,37 @@ name"-style retrace warnings. If content is missing from the trimmed trace
but present in the full trace, the fix belongs in `3rdparty/apitrace`'s but present in the full trace, the fix belongs in `3rdparty/apitrace`'s
frametrim, not in the fixture. frametrim, not in the fixture.
Temporal shaders (auto-exposure / eye adaptation, TAA, temporal reflections -
e.g. the iterationRP shader pack) break a single-frame `gltrim -f`: the target
frame reads its predecessors' feedback buffers, which the isolated frame no
longer contains, so a mid-sequence frame replays overexposed to white (and any
timed name/version overlay the pack draws in its first seconds silently drops).
The symptom is a trimmed frame that looks blown-out or washed while the same
frame of `full.trace` renders correctly, and it gets worse the later the frame.
When a single-frame trim of such a pack cannot be made to render correctly, keep
the temporal history instead of the dependency slice: select an early in-world
target frame and trim a PREFIX with `apitrace trim --calls=0-<target-swap-call>`
(it preserves call numbers, so `target_call` is just that swap call). The prefix
replays every frame up to the target, so its temporal buffers are correct.
Prefer the earliest frame that already shows the intended subject - fewer lead-in
frames means a smaller archive and a faster CI replay. This deviates from the
single-frame rule deliberately; note it in the README entry.
## Generate golden ## Generate golden
Generate frame snapshots from the trimmed trace, then choose the snapshot that Generate frame snapshots from the trimmed trace, then choose the snapshot that
matches the selected frame. The target call used by replay registration must matches the selected frame. The target call used by replay registration must
come from the trimmed trace, not from a call-filtered full-trace selection. come from the trimmed trace, not from a call-filtered full-trace selection.
Generate the golden with the same GL stack the scene was captured on. A headless
software renderer (llvmpipe) is fine for vanilla and light packs, but heavy
ray-traced shader packs (compute-driven atmosphere LUTs, screen-space tracing -
e.g. iterationRP) render as solid black or blown-out white under llvmpipe. Drive
the golden from a real GPU instead: on Windows a stock `glretrace.exe` (an
upstream apitrace release works for replay even on an in-tree-fork trace) replays
the trace on the discrete GPU and snapshots the target call. Read the resulting
PNG back and confirm the subject actually rendered before trusting it as golden.
```sh ```sh
mkdir -p "$WORK/$CASE/golden" mkdir -p "$WORK/$CASE/golden"
"$APITRACE" replay --headless \ "$APITRACE" replay --headless \
@@ -255,6 +349,15 @@ Check the final archive size. The committed fixture archive should be less than
with a shorter run or a lower frame rate / render distance instead of adding with a shorter run or a lower frame rate / render distance instead of adding
call-based filtering. call-based filtering.
Some packs have an irreducible size floor: a large static lookup table baked
into the pack (iterationRP ships a ~17 MiB half-float atmosphere LUT that the
target frame samples) lands in the trace once and does not compress, so every
variant - single frame, prefix, or full - sits near the same size regardless of
frame count. When the floor alone exceeds the budget, neither a lower frame rate
nor fewer frames helps; confirm the fixture is worth the exception and record the
measured size in the case's README entry rather than chasing an unreachable
target.
```sh ```sh
du -h "$REPO/tools/trace_replay/fixtures/$CASE.tgz" du -h "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
tar -tzf "$REPO/tools/trace_replay/fixtures/$CASE.tgz" tar -tzf "$REPO/tools/trace_replay/fixtures/$CASE.tgz"
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL Trace Fixture Authoring"
short_description: "Author and register a MobileGL trace-replay fixture"
default_prompt: "Use $trace-fixture-authoring to author and register a MobileGL trace replay fixture."
+4 -6
View File
@@ -276,18 +276,16 @@
}, },
{ {
"name": "improved-transparency-minecraft-26.3", "name": "improved-transparency-minecraft-26.3",
"ci": false,
"trace_archive": "improved-transparency-minecraft-26.3.tgz", "trace_archive": "improved-transparency-minecraft-26.3.tgz",
"golden": "improved-transparency-minecraft-26.3.0002667619.png", "golden": "improved-transparency-minecraft-26.3.0002667619.png",
"target_call": 2667619, "target_call": 2667619,
"timeout_seconds": 1800, "timeout_seconds": 1800
"coherent_as_flush": true
}, },
{ {
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world", "name": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world",
"ci": false, "ci": false,
"trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.tgz", "trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.tgz",
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png", "golden": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.0000202020.png",
"target_call": 202020, "target_call": 202020,
"timeout_seconds": 1800 "timeout_seconds": 1800
} }