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

|
||||
- 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
|
||||
Photon v1.1 after entering a singleplayer world.
|
||||

|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
BLISS_ANGLE_VARIANT = "90a62123d794"
|
||||
BLISS_CASE = "minecraft-1.21.4-fabric-iris-bliss-in-world"
|
||||
TRACE_APK_DIR = ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "trace" / "debug"
|
||||
|
||||
BACKENDS = {
|
||||
"DirectGLES": {
|
||||
"package": "top.mobilegl.plugin.trace",
|
||||
"use_angle": False,
|
||||
"apk": ROOT / "android-plugin" / "app" / "build" / "outputs" / "apk" / "esprytTrace" / "debug" / "MobileGL-EsprytTrace-debug.apk",
|
||||
"package": "top.mobilegl.plugin.espryt.trace",
|
||||
"use_angle": True,
|
||||
"use_pbuffer": False,
|
||||
},
|
||||
"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_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")
|
||||
|
||||
|
||||
def find_trace_apk():
|
||||
candidates = list(TRACE_APK_DIR.glob("MobileGL-plugin-trace-release-*.apk"))
|
||||
return max(candidates, key=lambda path: path.stat().st_mtime) if candidates else None
|
||||
|
||||
|
||||
def bash_path(path):
|
||||
path = Path(path).resolve()
|
||||
drive = path.drive.rstrip(":").lower()
|
||||
@@ -121,13 +117,9 @@ def render_summary():
|
||||
|
||||
def run_case(case, backend):
|
||||
backend_info = BACKENDS[backend]
|
||||
apk = find_trace_apk()
|
||||
trace_archive = FIXTURES / case["trace_archive"]
|
||||
golden = FIXTURES / case["golden"]
|
||||
alternate = FIXTURES / case["alternate_golden"] if case.get("alternate_golden") else None
|
||||
if apk is None:
|
||||
mark_skipped(case, backend, f"SKIPPED_MISSING_APK: no trace APK found under {TRACE_APK_DIR}")
|
||||
return 2
|
||||
if not trace_archive.exists() or is_lfs_pointer(trace_archive):
|
||||
mark_skipped(case, backend, "SKIPPED_LFS_POINTER: trace archive is missing or still an LFS pointer")
|
||||
copy_goldens(case, backend)
|
||||
@@ -142,7 +134,7 @@ def run_case(case, backend):
|
||||
"C:/Program Files/Git/bin/bash.exe",
|
||||
"android-plugin/trace-replay-ci.sh",
|
||||
"--apk-file",
|
||||
bash_path(apk),
|
||||
bash_path(backend_info["apk"]),
|
||||
"--package",
|
||||
backend_info["package"],
|
||||
"--backend",
|
||||
|
||||
@@ -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. |
|
||||
+5
@@ -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
|
||||
|
||||
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."
|
||||
+4
-4
@@ -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.
|
||||
---
|
||||
|
||||
@@ -19,20 +19,20 @@ adb -s SERIAL shell pm path top.mobilegl.plugin.trace
|
||||
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
|
||||
|
||||
Let the tool infer the zero-based target swap from `eglSwapBuffers` calls:
|
||||
|
||||
```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:
|
||||
|
||||
```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.
|
||||
@@ -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."
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
@@ -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__/
|
||||
+65
@@ -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()
|
||||
+191
@@ -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)
|
||||
+234
@@ -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()
|
||||
+168
@@ -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()
|
||||
+107
-4
@@ -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
|
||||
|
||||
## Variables
|
||||
@@ -75,10 +80,19 @@ Minecraft specifics that keep the capture deterministic and small:
|
||||
`doMobSpawning`, `randomTickSpeed 0`, a fixed `DayTime`, and the player
|
||||
`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.
|
||||
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), and a
|
||||
small `renderDistance` (3). Frame rate and render distance are the two main
|
||||
levers on trace size; a ~35 s in-world session at 10 fps lands well under
|
||||
the archive budget after repack.
|
||||
- `options.txt`: `pauseOnLostFocus:false`, a low `maxFps` (10 works), a small
|
||||
`renderDistance` (3), and the capture resolution pinned to `WIDTH` x `HEIGHT`
|
||||
(854x480) via `overrideWidth`/`overrideHeight` (or `--width`/`--height`).
|
||||
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
|
||||
every capture takes the same path from boot to gameplay.
|
||||
- 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
|
||||
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.
|
||||
|
||||
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
|
||||
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 frame snapshots from the trimmed trace, then choose the snapshot that
|
||||
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.
|
||||
|
||||
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
|
||||
mkdir -p "$WORK/$CASE/golden"
|
||||
"$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
|
||||
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
|
||||
du -h "$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."
|
||||
@@ -276,18 +276,16 @@
|
||||
},
|
||||
{
|
||||
"name": "improved-transparency-minecraft-26.3",
|
||||
"ci": false,
|
||||
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
|
||||
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
|
||||
"target_call": 2667619,
|
||||
"timeout_seconds": 1800,
|
||||
"coherent_as_flush": true
|
||||
"timeout_seconds": 1800
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world",
|
||||
"name": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world",
|
||||
"ci": false,
|
||||
"trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.tgz",
|
||||
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-in-world.0000202020.png",
|
||||
"trace_archive": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.tgz",
|
||||
"golden": "minecraft-1.21.4-fabric-iris-iterationrp-novidia-in-world.0000202020.png",
|
||||
"target_call": 202020,
|
||||
"timeout_seconds": 1800
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user