mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7463236b34 | ||
|
|
658e08c918 | ||
|
|
e543d9f7da | ||
|
|
31d7d97e86 | ||
|
|
cf89f394c5 | ||
|
|
2412807afd | ||
|
|
9ed287dbc7 | ||
|
|
836306feee | ||
|
|
126f617428 | ||
|
|
6359b0002b | ||
|
|
c186f5f255 | ||
|
|
3d97f6fa8f | ||
|
|
bb582203d9 | ||
|
|
f39e6eb82d | ||
|
|
cb2ba71feb | ||
|
|
6ea7ccdf64 | ||
|
|
14605723f0 | ||
|
|
a680611c9f | ||
|
|
93224ca406 | ||
|
|
fbed4485b7 | ||
|
|
90ae0f048c | ||
|
|
cff959b2e8 | ||
|
|
a50b2c422b | ||
|
|
9dcda82d71 | ||
|
|
28d0af6f04 | ||
|
|
44ee6b66b3 |
@@ -191,6 +191,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
@@ -315,6 +315,8 @@ namespace MobileGL {
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
Int TextureBufferOffsetAlignment = 1;
|
||||
Int MaxUniformBufferBindings = 24;
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
|
||||
@@ -1112,6 +1112,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
||||
const Int maxSupportedTextureUnits =
|
||||
|
||||
@@ -1621,9 +1621,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& effectiveSampler = textureUnit.GetSamplerObject()
|
||||
? textureUnit.GetSamplerObject()
|
||||
: textureObject->GetSamplerObject();
|
||||
const Bool mipmappedFilter =
|
||||
effectiveSampler && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
if (!MG_State::GLState::IsMipmapCompleteForFilter(textureObject.get(), mipmappedFilter)) {
|
||||
if (MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(),
|
||||
effectiveSampler.get())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -2232,7 +2232,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
|
||||
m_backendTextureId, backendId, buffer->GetSize(),
|
||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
||||
// A texture that names a window of the buffer needs the range form; the
|
||||
// whole-buffer forms report offset 0 and the buffer's current size, which
|
||||
// glTexBuffer expresses more directly (and works where the range entry point
|
||||
// is absent).
|
||||
const SizeT rangeOffset = textureBufferObject->GetBufferRangeOffset();
|
||||
const SizeT rangeSize = textureBufferObject->GetBufferRangeSizeInBytes();
|
||||
if (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
|
||||
g_GLESFuncs.glTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
|
||||
} else if (g_GLESFuncs.glTexBufferRange != nullptr) {
|
||||
g_GLESFuncs.glTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
|
||||
static_cast<GLintptr>(rangeOffset),
|
||||
static_cast<GLsizeiptr>(rangeSize));
|
||||
} else {
|
||||
MGLOG_E("Texture buffer %u names a sub-range but the driver has no "
|
||||
"glTexBufferRange; binding the whole buffer instead",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
g_GLESFuncs.glTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
|
||||
}
|
||||
DebugImpl::ErrorLopper::Loop(
|
||||
[file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) {
|
||||
MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s",
|
||||
@@ -3384,18 +3401,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||
// than approximating one. Rewriting the type to 2D is exact for a lookup that
|
||||
// takes integer texel coordinates and needs the coordinate divided by the
|
||||
// texture size for one that does not - see NormalizeRectSamplerCoordinates
|
||||
// below, which the ESSL the transpiler produces goes through. The pass declines
|
||||
// anything neither step can convert.
|
||||
// than approximating one. The shared pass turns the type into the 2D one and
|
||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||
// size, which is the whole of the difference between the two.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
Bool loweredRectImages = false;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
|
||||
rectLoweredSpirv) &&
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
loweredRectImages = true;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
@@ -3432,26 +3444,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
|
||||
source = EmulateTextureLodBias(source);
|
||||
if (loweredRectImages) {
|
||||
// The image type is 2D now, so the transpiled lookups address [0,1]; the
|
||||
// application wrote them in texels. Only the frontend still knows which
|
||||
// samplers were declared rectangle.
|
||||
Vector<String> rectSamplerNames;
|
||||
const Uint uniformCount = stateProgramObject->GetUniformCount();
|
||||
for (Uint i = 0; i < uniformCount; ++i) {
|
||||
switch (stateProgramObject->GetActiveUniformType(i)) {
|
||||
case GL_SAMPLER_2D_RECT:
|
||||
case GL_SAMPLER_2D_RECT_SHADOW:
|
||||
case GL_INT_SAMPLER_2D_RECT:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
|
||||
rectSamplerNames.push_back(stateProgramObject->GetActiveUniformName(i));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
source = NormalizeRectSamplerCoordinates(source, rectSamplerNames);
|
||||
}
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
|
||||
source = ForceSupporterOutput(source);
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
|
||||
// they are single-level and already clamp, so only the non-normalized coordinates differ.
|
||||
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
|
||||
// ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module
|
||||
// ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module
|
||||
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
|
||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||
switch (target) {
|
||||
|
||||
@@ -597,66 +597,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
String NormalizeRectSamplerCoordinates(const String& glslCode,
|
||||
const Vector<String>& rectSamplerNames) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (rectSamplerNames.empty() || glslCode.find("texture") == String::npos) {
|
||||
return glslCode;
|
||||
}
|
||||
|
||||
// Lookups whose argument 1 is a plain (non-projective) texel-space coordinate on a
|
||||
// rectangle sampler. texelFetch* is absent on purpose: its coordinates are integer
|
||||
// texels on the 2D target too, so it already lands in the right place.
|
||||
static const char* const kRectCoordinateLookups[] = {
|
||||
"textureGatherOffsets", "textureGatherOffset", "textureGather",
|
||||
"textureOffset", "texture",
|
||||
};
|
||||
|
||||
String result = glslCode;
|
||||
// Right to left, so the offsets of the not-yet-rewritten calls stay valid.
|
||||
for (SizeT scan = result.size(); scan-- > 0;) {
|
||||
if (result[scan] != 't') continue;
|
||||
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||
|
||||
SizeT openParen = 0;
|
||||
Bool matched = false;
|
||||
for (const char* name : kRectCoordinateLookups) {
|
||||
const SizeT nameLength = std::strlen(name);
|
||||
if (result.compare(scan, nameLength, name) != 0) continue;
|
||||
const SizeT after = result.find_first_not_of(" \t", scan + nameLength);
|
||||
if (after == String::npos || result[after] != '(') continue;
|
||||
openParen = after;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
if (!matched) continue;
|
||||
|
||||
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||
if (marks.size() < 2) continue; // needs a sampler and a coordinate
|
||||
|
||||
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
|
||||
SizeT firstArgEnd = marks.front();
|
||||
while (firstArgEnd > firstArgStart &&
|
||||
(result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
|
||||
--firstArgEnd;
|
||||
}
|
||||
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
|
||||
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
|
||||
if (std::find(rectSamplerNames.begin(), rectSamplerNames.end(), samplerName) ==
|
||||
rectSamplerNames.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wrap argument 1: (coord) / vec2(textureSize(sampler, 0)).
|
||||
const SizeT coordStart = marks[0] + 1;
|
||||
const SizeT coordEnd = marks[1];
|
||||
result.insert(coordEnd, String(") / vec2(textureSize(") + samplerName + ", 0)))");
|
||||
result.insert(coordStart, "((");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -129,15 +129,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||
// there is nothing to rewrite.
|
||||
String EmulateTextureLodBias(const String& glslCode);
|
||||
// GL_TEXTURE_RECTANGLE is emulated on an ES 2D texture and LowerRectImagesForEssl
|
||||
// rewrites the image type to match, but a rectangle lookup addresses texels
|
||||
// directly while a 2D one addresses [0,1] - so every lookup that takes normalized
|
||||
// coordinates has to divide by the texture's size. `rectSamplerNames` is the set of
|
||||
// samplers the program declared as rectangle; texelFetch is left alone (its
|
||||
// coordinates are unnormalized on both targets) and so is anything projective,
|
||||
// which LowerRectImagesForEssl still declines outright.
|
||||
String NormalizeRectSamplerCoordinates(const String& glslCode,
|
||||
const Vector<String>& rectSamplerNames);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -530,7 +530,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location};
|
||||
E_GL_ARB_explicit_attrib_location,
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -776,6 +780,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits =
|
||||
|
||||
@@ -205,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
@@ -380,6 +381,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ia.topology = payload.topology;
|
||||
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
|
||||
|
||||
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
|
||||
// otherwise is what the spec expects.
|
||||
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
@@ -444,6 +450,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
|
||||
gpi.pViewportState = &vpci;
|
||||
gpi.pRasterizationState = &raster;
|
||||
gpi.pMultisampleState = &ms;
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
|
||||
@@ -1761,6 +1761,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
||||
}
|
||||
|
||||
// The transform feedback capture layout is baked into the modules by
|
||||
// XfbCaptureDecoratePass rather than coming from the SPIR-V, so it has to be part of
|
||||
// the key: two programs can share every shader and still capture differently, which
|
||||
// is exactly what changing the buffer mode does (glTransformFeedbackVaryings with the
|
||||
// same varyings but GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS). Only
|
||||
// hashed for a capturing compile, so nothing else changes key.
|
||||
if (flags & CompileOptionBit::XfbCapture) {
|
||||
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||
}
|
||||
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
|
||||
}
|
||||
}
|
||||
|
||||
HashType hash = XXH64_digest(m_hashState);
|
||||
return hash;
|
||||
}
|
||||
@@ -2388,6 +2407,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||
// stored as - which addresses [0,1] where the application addressed texels.
|
||||
{
|
||||
Vector<Uint> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -266,12 +266,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
||||
// A texture that fails the completeness rules for the filter in effect reads
|
||||
// (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the
|
||||
// same route as a sampler with nothing bound.
|
||||
if (texture != nullptr &&
|
||||
MG_State::GLState::SamplesAsIncompleteTexture(
|
||||
texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||
texture = fallbackHolder.get();
|
||||
MOBILEGL_ASSERT(texture != nullptr,
|
||||
"ResolveSamplerDescriptor: no fallback texture available for binding=%u location=%d unit=%d target=%d",
|
||||
binding, location, unit, static_cast<Int>(preferredTarget));
|
||||
if (texture == nullptr) {
|
||||
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||
"location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
static_cast<Int>(preferredTarget));
|
||||
return false;
|
||||
}
|
||||
MGLOG_W(
|
||||
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||
@@ -592,7 +604,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkDeviceSize texelSize =
|
||||
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
||||
VkDeviceSize viewRange = slice.size;
|
||||
// glTextureBufferRange addresses a window of the buffer, not all of it; the whole-buffer
|
||||
// forms report the buffer's current size here, so both go through the same clamp.
|
||||
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
||||
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
||||
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
||||
if (texelSize > 0) {
|
||||
viewRange = (viewRange / texelSize) * texelSize;
|
||||
}
|
||||
@@ -605,7 +621,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
||||
viewInfo.buffer = slice.buffer;
|
||||
viewInfo.format = vkFormat;
|
||||
viewInfo.offset = slice.offset;
|
||||
viewInfo.offset = slice.offset + rangeOffset;
|
||||
viewInfo.range = viewRange;
|
||||
|
||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||
@@ -760,15 +776,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
||||
MOBILEGL_ASSERT(target == TextureTarget::Texture2D || target == TextureTarget::TextureRectangle,
|
||||
"UniformManager::GetFallbackTexture: unsupported fallback target=%d",
|
||||
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
|
||||
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
|
||||
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
|
||||
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||
// process over an unbound sampler is never the right answer.
|
||||
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||
static_cast<Int>(target));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (m_fallbackTexture2D == nullptr) {
|
||||
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
||||
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||
{.texelSize = {1, 1, 1}, .byteSize = 4});
|
||||
// (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only
|
||||
// sensible answer for a sampler with nothing bound.
|
||||
static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255};
|
||||
fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0,
|
||||
{kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)});
|
||||
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
|
||||
m_fallbackTexture2D = fallbackTexture;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> bindingAttributeLocations;
|
||||
Vector<Bool> bindingUsesClientMemory;
|
||||
Vector<VertexStreamConversion> bindingConversions;
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
Uint32 unsupportedAttribMask = 0;
|
||||
|
||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||
@@ -180,6 +181,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bindingConversions.push_back(conversion);
|
||||
builder.AddBinding(binding, stride, inputRate);
|
||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
|
||||
// else needs the extension to say it.
|
||||
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
|
||||
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
|
||||
}
|
||||
}
|
||||
|
||||
const auto& state = builder.Build();
|
||||
@@ -191,6 +197,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BackendVertexInputState& entry = *slot;
|
||||
entry.hash = hash;
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.bindingDivisors = Move(bindingDivisors);
|
||||
entry.bindings = builder.GetBindings();
|
||||
entry.attributes = builder.GetAttributes();
|
||||
// See the layoutHash declaration: hash only the resolved layout, never
|
||||
@@ -207,6 +214,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||
}
|
||||
for (const auto& divisor : entry.bindingDivisors) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||
entry.layoutHash = XXH64_digest(m_hashState);
|
||||
entry.attributeLocationMask = 0;
|
||||
@@ -224,6 +235,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.state = state;
|
||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
||||
if (!entry.bindingDivisors.empty()) {
|
||||
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
|
||||
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
|
||||
entry.state.pNext = &entry.divisorState;
|
||||
} else {
|
||||
entry.state.pNext = nullptr;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||
// three times per draw, so it is baked once at build time.
|
||||
Uint32 attributeLocationMask = 0;
|
||||
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
||||
// rate advances once per instance and nothing else, so anything else has to be
|
||||
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
||||
// binding uses divisor 1, which is what the plain input rate already means.
|
||||
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
|
||||
};
|
||||
VkPipelineVertexInputStateCreateInfo state{
|
||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||
};
|
||||
|
||||
@@ -561,6 +561,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto resource = GetOrCreateResource(bufferObject);
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
|
||||
// A persistently mapped resource's storage IS the application's copy of the bytes -
|
||||
// the frontend adopted it in place of the shadow and hands out pointers into it, and
|
||||
// a shader can have written bytes the shadow never saw (a transform feedback
|
||||
// capture). Streaming a second copy would feed this draw the stale shadow, and the
|
||||
// downgrade below would release the storage the application still points at,
|
||||
// breaking the "never recreated" promise AcquirePersistentMap makes.
|
||||
if (resource->persistentMapped) {
|
||||
return AcquireResidentSlice(kind, bufferObject, outSlice);
|
||||
}
|
||||
|
||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||
if (size == 0) {
|
||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||
|
||||
@@ -3292,6 +3292,47 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Copies index data, replacing every occurrence of the application's arbitrary restart
|
||||
// index with the fixed all-ones value of the index type - the only one Vulkan restarts
|
||||
// on. An index that already equals the fixed value would then be indistinguishable from
|
||||
// a restart, so it is nudged to the next-lowest value: it can only be a real index (the
|
||||
// application's restart index is a different number), and the vertex it selects is
|
||||
// outside any well-defined draw anyway, whereas leaving it alone would tear the
|
||||
// primitive in two.
|
||||
void RewriteRestartIndices(const void* source, SizeT sizeBytes, VkIndexType indexType,
|
||||
Uint32 applicationRestartIndex, Vector<Uint8>& output) {
|
||||
output.resize(sizeBytes);
|
||||
if (sizeBytes == 0 || source == nullptr) {
|
||||
return;
|
||||
}
|
||||
Memcpy(output.data(), source, sizeBytes);
|
||||
const auto rewrite = [&](auto* indices, auto fixedMax) {
|
||||
const SizeT count = sizeBytes / sizeof(*indices);
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
if (indices[i] == static_cast<decltype(fixedMax)>(applicationRestartIndex)) {
|
||||
indices[i] = fixedMax;
|
||||
} else if (indices[i] == fixedMax) {
|
||||
indices[i] = fixedMax - 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
switch (indexType) {
|
||||
case VK_INDEX_TYPE_UINT8:
|
||||
rewrite(reinterpret_cast<Uint8*>(output.data()), static_cast<Uint8>(0xFFu));
|
||||
break;
|
||||
case VK_INDEX_TYPE_UINT16:
|
||||
rewrite(reinterpret_cast<Uint16*>(output.data()), static_cast<Uint16>(0xFFFFu));
|
||||
break;
|
||||
case VK_INDEX_TYPE_UINT32:
|
||||
rewrite(reinterpret_cast<Uint32*>(output.data()), static_cast<Uint32>(0xFFFFFFFFu));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::VertexArrayObject& vao,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
@@ -3315,8 +3356,10 @@ void main() {
|
||||
|
||||
// GL_PRIMITIVE_RESTART uses an arbitrary restart index (glPrimitiveRestartIndex), but Vulkan
|
||||
// only restarts on the fixed all-ones value of the index type. GL_PRIMITIVE_RESTART_FIXED_INDEX
|
||||
// already matches that, so only the arbitrary form needs checking; hard-fail at this draw with
|
||||
// the reason if the index is not the fixed value (a fallback would silently drop restarts).
|
||||
// already matches that, so only the arbitrary form needs handling: rewrite the indices into a
|
||||
// transient copy where the application's restart index becomes the fixed one.
|
||||
Uint32 substituteRestartIndex = 0;
|
||||
Bool substituteRestart = false;
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) &&
|
||||
!MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex)) {
|
||||
const Uint32 restartIndex = MG_State::pGLContext->GetPrimitiveRestartIndex();
|
||||
@@ -3327,15 +3370,8 @@ void main() {
|
||||
case VK_INDEX_TYPE_UINT32: fixedMax = 0xFFFFFFFFu; break;
|
||||
default: break;
|
||||
}
|
||||
if (restartIndex != fixedMax) {
|
||||
THROW_EXCEPTION("GL_PRIMITIVE_RESTART with an arbitrary restart index (" +
|
||||
std::to_string(restartIndex) +
|
||||
") is not supported by the Vulkan backend, which only restarts on the fixed index "
|
||||
"value (" +
|
||||
std::to_string(fixedMax) +
|
||||
") for this index type; use GL_PRIMITIVE_RESTART_FIXED_INDEX, or set "
|
||||
"glPrimitiveRestartIndex to that value.");
|
||||
}
|
||||
substituteRestart = restartIndex != fixedMax;
|
||||
substituteRestartIndex = restartIndex;
|
||||
}
|
||||
|
||||
const auto* indexBuffer =
|
||||
@@ -3349,9 +3385,16 @@ void main() {
|
||||
MGLOG_E("DrawElements skipped: no element array buffer bound and no client index data");
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> rewrittenIndices;
|
||||
const void* uploadSource = clientIndices;
|
||||
if (substituteRestart) {
|
||||
RewriteRestartIndices(clientIndices, pIndexBufferView->indexByteSize, vkIndexType,
|
||||
substituteRestartIndex, rewrittenIndices);
|
||||
uploadSource = rewrittenIndices.data();
|
||||
}
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
|
||||
clientIndices, pIndexBufferView->indexByteSize, 4, slice)) {
|
||||
uploadSource, pIndexBufferView->indexByteSize, 4, slice)) {
|
||||
MGLOG_E("DrawElements skipped: failed to upload client index data");
|
||||
return false;
|
||||
}
|
||||
@@ -3373,7 +3416,20 @@ void main() {
|
||||
BufferSlice slice{};
|
||||
auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex());
|
||||
MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO");
|
||||
if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) {
|
||||
if (substituteRestart) {
|
||||
// The whole buffer is rewritten, not just this draw's range, so that every element
|
||||
// index keeps its position: an indirect draw's firstIndex lives in GPU memory and
|
||||
// cannot be adjusted from here.
|
||||
indexBufferShared->SyncGpuWrites();
|
||||
Vector<Uint8> rewrittenIndices;
|
||||
RewriteRestartIndices(indexBufferShared->MappedData(), indexBufferShared->GetSize(), vkIndexType,
|
||||
substituteRestartIndex, rewrittenIndices);
|
||||
if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(),
|
||||
rewrittenIndices.data(), rewrittenIndices.size(), 4, slice)) {
|
||||
MGLOG_E("DrawElements skipped: failed to upload restart-substituted index data");
|
||||
return false;
|
||||
}
|
||||
} else if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared)) {
|
||||
MOBILEGL_ASSERT(indexBufferShared->GetSize() != 0, "DrawElements requires non-empty EBO data");
|
||||
if (!m_bufferManager.AcquireStreamedSlice(BufferKind::Index, indexBufferShared, slice)) {
|
||||
MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer");
|
||||
@@ -4153,6 +4209,7 @@ void main() {
|
||||
.subpass = 0,
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
||||
.polygonMode = effectivePolygonMode,
|
||||
.cullMode = cullFaceEnabled
|
||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||
@@ -4692,6 +4749,7 @@ void main() {
|
||||
// Sync each sampled texture at most once across this whole draw: the layout
|
||||
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
|
||||
// otherwise each re-run the full SyncTexture path on the same textures.
|
||||
MakeXfbWritesVisible();
|
||||
VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager);
|
||||
m_textureManager->CollectGarbage();
|
||||
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
|
||||
@@ -6920,6 +6978,7 @@ void main() {
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::FinishPendingGpuWork() {
|
||||
MakeXfbWritesVisible();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) {
|
||||
return true;
|
||||
@@ -8013,6 +8072,18 @@ void main() {
|
||||
}
|
||||
|
||||
resource->layout = finalLayout;
|
||||
|
||||
// The chain above is GPU work recorded into this frame's command buffer, which is not
|
||||
// submitted until the frame ends - but a texture upload goes out on a command buffer of
|
||||
// its own the moment it happens. A glTexSubImage2D into a level this just generated
|
||||
// would therefore reach the GPU FIRST and be overwritten by these blits, which is how
|
||||
// KHR-GL40.texture_gather.base-level lost the texels it wrote into level 1 right after
|
||||
// generating the chain. Submitting here is what orders the two.
|
||||
if (HasPendingRecordedWork() && FlushPendingCommands()) {
|
||||
// Fresh command buffer: the sampled-descriptor-set memo describes bindings that
|
||||
// only existed in the retired one.
|
||||
m_lastSampledSetValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 VulkanRenderer::CurrentXfbCounterSlot() {
|
||||
@@ -8072,8 +8143,11 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
// Host-visible coherent GPU residency: the capture writes land where
|
||||
// MapBuffer/GetBufferSubData read.
|
||||
// MapBuffer/GetBufferSubData read. Coherence makes them visible once they
|
||||
// have happened, so the buffer is also flagged for the wait that a later CPU
|
||||
// read has to perform - the capture is a GPU write like any shader's.
|
||||
bufferObject->EnsureGpuResidentStorage();
|
||||
bufferObject->MarkGpuWritten();
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireResidentSlice(BufferKind::Vertex, bufferObject, slice)) {
|
||||
MGLOG_E("BeginXfbCaptureForDraw: failed to acquire capture buffer %zu", i);
|
||||
@@ -8128,6 +8202,41 @@ void main() {
|
||||
s_vkCmdEndTransformFeedbackEXT(frame.commandBuffer, 0, static_cast<Uint32>(bufferCount), counterBuffers,
|
||||
counterOffsets);
|
||||
m_xfbCountersValid[counterSlot] = true;
|
||||
m_xfbWritesPendingVisibility = true;
|
||||
}
|
||||
|
||||
// GL makes transform feedback results visible to every later command on their own, with no
|
||||
// glMemoryBarrier in between - unlike shader storage writes, which is why the barrier the
|
||||
// Vulkan memory model requires has to be supplied here rather than by the application. It
|
||||
// cannot be recorded where the write happens (inside the capturing draw's render pass, which
|
||||
// declares no self-dependency), so it is emitted at the next point that could read the
|
||||
// captured buffer: the following draw, or a readback.
|
||||
void VulkanRenderer::MakeXfbWritesVisible() {
|
||||
if (!m_xfbWritesPendingVisibility) {
|
||||
return;
|
||||
}
|
||||
m_xfbWritesPendingVisibility = false;
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
VkMemoryBarrier memoryBarrier{};
|
||||
memoryBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
||||
memoryBarrier.srcAccessMask =
|
||||
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT | VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT;
|
||||
// Every way a captured buffer can be read back: replayed as vertex attributes or indices
|
||||
// by glDrawTransformFeedback, sampled through a uniform or storage binding, sourced as an
|
||||
// indirect command, copied out, or mapped.
|
||||
memoryBarrier.dstAccessMask =
|
||||
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT |
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT |
|
||||
VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT |
|
||||
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT;
|
||||
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memoryBarrier, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
void VulkanRenderer::DrawArrays(const DrawCmd& payload) {
|
||||
@@ -9686,6 +9795,7 @@ void main() {
|
||||
? VK_FALSE
|
||||
: supportedDeviceFeatures.robustBufferAccess;
|
||||
deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader;
|
||||
deviceFeatures.tessellationShader = supportedDeviceFeatures.tessellationShader;
|
||||
deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend;
|
||||
m_independentBlendFeatureEnabled = deviceFeatures.independentBlend == VK_TRUE;
|
||||
deviceFeatures.fillModeNonSolid = supportedDeviceFeatures.fillModeNonSolid;
|
||||
@@ -9882,6 +9992,36 @@ void main() {
|
||||
MGLOG_W("VK_EXT_transform_feedback is unavailable; transform feedback capture will not work");
|
||||
}
|
||||
|
||||
// VK_EXT_vertex_attribute_divisor. Vulkan's instance input rate advances an attribute
|
||||
// once per instance and nothing else, so without this every glVertexAttribDivisor value
|
||||
// collapses to 1 and an attribute meant to change every N instances changes every one.
|
||||
m_vertexAttributeDivisorEnabled = false;
|
||||
VkPhysicalDeviceVertexAttributeDivisorFeaturesEXT vertexAttributeDivisorFeatures{};
|
||||
vertexAttributeDivisorFeatures.sType =
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_EXT;
|
||||
if (IsExtensionSupported(availableExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME) &&
|
||||
getPhysicalDeviceFeatures2 != nullptr) {
|
||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
featureQuery.pNext = &vertexAttributeDivisorFeatures;
|
||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
||||
if (vertexAttributeDivisorFeatures.vertexAttributeInstanceRateDivisor == VK_TRUE) {
|
||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
||||
VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME);
|
||||
}
|
||||
vertexAttributeDivisorFeatures.vertexAttributeInstanceRateZeroDivisor = VK_FALSE;
|
||||
vertexAttributeDivisorFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
||||
deviceCreateInfo.pNext = &vertexAttributeDivisorFeatures;
|
||||
m_vertexAttributeDivisorEnabled = true;
|
||||
MGLOG_I("Enabled optional device extension: %s", VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME);
|
||||
}
|
||||
}
|
||||
if (!m_vertexAttributeDivisorEnabled) {
|
||||
MGLOG_W("VK_EXT_vertex_attribute_divisor is unavailable; a glVertexAttribDivisor other "
|
||||
"than 1 will advance its attribute once per instance");
|
||||
}
|
||||
|
||||
// Host query reset lets the occlusion-query ring recycle slots without a
|
||||
// command-buffer round trip.
|
||||
m_hostQueryResetEnabled = false;
|
||||
|
||||
@@ -494,6 +494,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
||||
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
||||
Bool m_vertexAttributeDivisorEnabled = false;
|
||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
@@ -515,6 +518,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// when GL transform feedback is active; binds capture buffers on demand.
|
||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||
// Makes the captured bytes visible to whatever reads them next. Deferred rather than
|
||||
// recorded next to the capture, because the capturing draw runs inside a render pass
|
||||
// that declares no self-dependency.
|
||||
void MakeXfbWritesVisible();
|
||||
Bool m_xfbWritesPendingVisibility = false;
|
||||
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
||||
// query is active. Returns whether a slot was begun (End must mirror it).
|
||||
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
||||
|
||||
@@ -877,6 +877,172 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||
}
|
||||
|
||||
void CreateTransformFeedbacks(GLsizei n, GLuint* ids) {
|
||||
if (n < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (n == 0 || ids == nullptr) return;
|
||||
Vector<Uint> names;
|
||||
MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
|
||||
// Unlike glGenTransformFeedbacks, the names are objects immediately: there is no bind step
|
||||
// to create them from (GL 4.6 core 13.2.1).
|
||||
for (const Uint name : names) {
|
||||
MG_State::pGLContext->CreateTransformFeedbackObject(name);
|
||||
}
|
||||
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Shared front half of the by-name transform feedback entry points: the object has to exist
|
||||
// (INVALID_OPERATION otherwise) before anything else about the call is looked at.
|
||||
Bool ValidateNamedTransformFeedback(GLuint xfb, const char* functionName) {
|
||||
if (!MG_State::pGLContext->IsTransformFeedbackObject(xfb)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
std::to_string(xfb) + " is not a transform feedback object."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateTransformFeedbackBufferIndex(GLuint index, const char* functionName) {
|
||||
if (index >= MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"index exceeds GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A capture binding may not be changed while the object is capturing (GL 4.6 core 13.2.2).
|
||||
Bool ValidateNamedTransformFeedbackNotActive(GLuint xfb, const char* functionName) {
|
||||
if (MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"The transform feedback object is capturing."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SharedPtr<MG_State::GLState::BufferObject> ResolveTransformFeedbackBuffer(GLuint buffer,
|
||||
const char* functionName) {
|
||||
if (buffer == 0) return nullptr;
|
||||
if (!MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
std::to_string(buffer) + " is not a buffer object."));
|
||||
return nullptr;
|
||||
}
|
||||
return MG_State::pGLContext->GetBufferObject(buffer);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer) {
|
||||
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
|
||||
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(buffer) + " is not a buffer object."));
|
||||
return;
|
||||
}
|
||||
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index,
|
||||
ResolveTransformFeedbackBuffer(buffer, __func__), {},
|
||||
false);
|
||||
}
|
||||
|
||||
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
|
||||
if (offset < 0 || size <= 0 || (offset % 4) != 0 || (size % 4) != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"offset and size must be non-negative multiples of 4."));
|
||||
return;
|
||||
}
|
||||
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
std::to_string(buffer) + " is not a buffer object."));
|
||||
return;
|
||||
}
|
||||
auto bufferObject = ResolveTransformFeedbackBuffer(buffer, __func__);
|
||||
const Range1D range{static_cast<SizeT>(offset), static_cast<SizeT>(offset) + static_cast<SizeT>(size)};
|
||||
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, bufferObject, range,
|
||||
bufferObject != nullptr);
|
||||
}
|
||||
|
||||
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param) {
|
||||
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||
if (!param) return;
|
||||
switch (pname) {
|
||||
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||
*param = MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||
*param = MG_State::pGLContext->IsNamedTransformFeedbackPaused(xfb) ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_TRANSFORM_FEEDBACK_ACTIVE or _PAUSED."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param) {
|
||||
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_BINDING) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_BINDING."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||
if (!param) return;
|
||||
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
|
||||
*param = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param) {
|
||||
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_START && pname != GL_TRANSFORM_FEEDBACK_BUFFER_SIZE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_START or _SIZE."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||
if (!param) return;
|
||||
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
|
||||
// glTransformFeedbackBufferBase leaves both at zero; only the range form sets them
|
||||
// (GL 4.6 core table 23.48).
|
||||
if (!binding.Buffer || !binding.HasExplicitRange) {
|
||||
*param = 0;
|
||||
return;
|
||||
}
|
||||
*param = (pname == GL_TRANSFORM_FEEDBACK_BUFFER_START)
|
||||
? static_cast<GLint64>(binding.Range.start)
|
||||
: static_cast<GLint64>(binding.Range.end - binding.Range.start);
|
||||
}
|
||||
|
||||
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) {
|
||||
if (n < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
|
||||
@@ -16,7 +16,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void PauseTransformFeedback(void);
|
||||
void ResumeTransformFeedback(void);
|
||||
void GenTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||
void CreateTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
|
||||
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer);
|
||||
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param);
|
||||
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param);
|
||||
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param);
|
||||
void BindTransformFeedback(GLenum target, GLuint id);
|
||||
GLboolean IsTransformFeedback(GLuint id);
|
||||
void DrawTransformFeedback(GLenum mode, GLuint id);
|
||||
|
||||
@@ -419,7 +419,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLs
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||
@@ -434,7 +434,7 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterIuiv, GLuint sampler, GLenum pnam
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
|
||||
DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c)
|
||||
@@ -996,7 +996,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum ty
|
||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
||||
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, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_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)
|
||||
@@ -1007,12 +1007,12 @@ DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage)
|
||||
@@ -1049,8 +1049,8 @@ DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GL
|
||||
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth)
|
||||
@@ -1090,14 +1090,14 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexArrayVertexBuffers, GLuint vaobj, GLuint fi
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateQueries, target, n, ids)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateQueries, target, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
|
||||
|
||||
@@ -1143,6 +1143,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return;
|
||||
if (!TextureImpl::ValidateTextureName(texture, true)) return;
|
||||
|
||||
if (texture == 0) {
|
||||
@@ -1239,6 +1240,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
|
||||
RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, "FramebufferRenderbuffer_State")) return;
|
||||
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
|
||||
if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return;
|
||||
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
|
||||
@@ -1283,6 +1285,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
FramebufferAttachmentType attachmentType = MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
|
||||
RenderbufferTarget rbTarget = MG_Util::ConvertGLEnumToRenderbufferTarget(renderbuffertarget);
|
||||
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
|
||||
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, "NamedFramebufferRenderbuffer_State"))
|
||||
return;
|
||||
if (!FramebufferImpl::ValidateRenderbufferTarget(rbTarget)) return;
|
||||
|
||||
if (renderbuffer == 0) {
|
||||
@@ -2199,6 +2203,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ReadPixels_Backend(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
|
||||
// Bytes glReadPixels would write for this rectangle under the current GL_PACK_* state
|
||||
// (GL 4.6 core 18.2.8): rows are padded to GL_PACK_ALIGNMENT and laid out GL_PACK_ROW_LENGTH
|
||||
// wide, and the skip parameters offset the first texel. The last row is not padded - nothing
|
||||
// follows it to align - which is what makes a tightly-sized destination legal.
|
||||
static SizeT ComputePackedReadSizeInBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) {
|
||||
const SizeT bytesPerPixel =
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (bytesPerPixel == 0 || width <= 0 || height <= 0) return 0;
|
||||
|
||||
const auto packParam = [](PixelStoreParam param) {
|
||||
return static_cast<SizeT>(std::max(0, MG_State::pGLContext->GetPixelStoreParam(param)));
|
||||
};
|
||||
const SizeT rowLengthInPixels =
|
||||
packParam(PixelStoreParam::PackRowLength) != 0
|
||||
? packParam(PixelStoreParam::PackRowLength)
|
||||
: static_cast<SizeT>(width);
|
||||
const SizeT alignment = std::max<SizeT>(1, packParam(PixelStoreParam::PackAlignment));
|
||||
|
||||
const SizeT unalignedRowBytes = rowLengthInPixels * bytesPerPixel;
|
||||
const SizeT paddedRowBytes = ((unalignedRowBytes + alignment - 1) / alignment) * alignment;
|
||||
const SizeT skipBytes = packParam(PixelStoreParam::PackSkipRows) * paddedRowBytes +
|
||||
packParam(PixelStoreParam::PackSkipPixels) * bytesPerPixel;
|
||||
|
||||
return skipBytes + paddedRowBytes * (static_cast<SizeT>(height) - 1) +
|
||||
static_cast<SizeT>(width) * bytesPerPixel;
|
||||
}
|
||||
|
||||
// glReadnPixels is glReadPixels with a bound on how much it may write (GL 4.6 core 18.2.8,
|
||||
// originally GL_ARB_robustness). It is identical in every other respect, so it validates and
|
||||
// reads through exactly the same path once the destination is known to be big enough.
|
||||
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
|
||||
void* data) {
|
||||
if (bufSize < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ReadPixels_State(x, y, width, height, format, type, data)) return;
|
||||
if (ComputePackedReadSizeInBytes(width, height, format, type) > static_cast<SizeT>(bufSize)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"the data required for this read does not fit in bufSize."));
|
||||
return;
|
||||
}
|
||||
ReadPixels_Backend(x, y, width, height, format, type, data);
|
||||
}
|
||||
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||
if (!ValidateClearBufferfi_State(buffer, drawbuffer)) return;
|
||||
ClearBufferfi_Backend(buffer, drawbuffer, depth, stencil);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
|
||||
void* data);
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "Validators.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/ErrorState/Error.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -60,6 +61,26 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller) {
|
||||
const auto first = static_cast<SizeT>(FramebufferAttachmentType::Color0);
|
||||
const auto index = static_cast<SizeT>(attachment);
|
||||
if (index < first) return true;
|
||||
const auto colorIndex = index - first;
|
||||
const auto limit = static_cast<SizeT>(
|
||||
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||
.MaxColorAttachments
|
||||
: static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS));
|
||||
if (colorIndex >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||
std::format("Colour attachment {} is beyond GL_MAX_COLOR_ATTACHMENTS ({}).", colorIndex, limit)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
|
||||
if (target == RenderbufferTarget::Unknown) {
|
||||
using namespace MG_Util;
|
||||
|
||||
@@ -14,6 +14,10 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
||||
Bool ValidateFramebufferTarget(FramebufferTarget target);
|
||||
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
|
||||
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
|
||||
// GL_COLOR_ATTACHMENTn is a token per n up to 31, but only the first GL_MAX_COLOR_ATTACHMENTS of
|
||||
// them name an attachment point of a framebuffer object; the rest are INVALID_OPERATION for the
|
||||
// attaching entry points (GL 4.6 core 9.2.7). Non-colour attachments pass through unchanged.
|
||||
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller);
|
||||
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
|
||||
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
|
||||
|
||||
@@ -671,6 +671,40 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
case GL_VERTEX_BINDING_DIVISOR:
|
||||
case GL_VERTEX_BINDING_OFFSET:
|
||||
case GL_VERTEX_BINDING_STRIDE: {
|
||||
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Vertex buffer binding index is out of range."));
|
||||
return;
|
||||
}
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
*data = 0;
|
||||
return;
|
||||
}
|
||||
const auto& binding = vao->GetBindingPoint(index);
|
||||
switch (target) {
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
*data = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
|
||||
return;
|
||||
case GL_VERTEX_BINDING_DIVISOR:
|
||||
*data = static_cast<GLint>(binding.Divisor);
|
||||
return;
|
||||
case GL_VERTEX_BINDING_OFFSET:
|
||||
*data = static_cast<GLint>(binding.Offset);
|
||||
return;
|
||||
default:
|
||||
*data = static_cast<GLint>(binding.Stride);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case GL_IMAGE_BINDING_NAME:
|
||||
case GL_IMAGE_BINDING_LEVEL:
|
||||
case GL_IMAGE_BINDING_LAYERED:
|
||||
@@ -1669,7 +1703,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
||||
return;
|
||||
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
|
||||
*params = 0; // texture-buffer range entrypoints are stubbed
|
||||
*params = MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment;
|
||||
return;
|
||||
case GL_TIMESTAMP: {
|
||||
Int64 timestamp = 0;
|
||||
@@ -1738,20 +1772,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
|
||||
return;
|
||||
}
|
||||
// The vertex buffer binding points are per-binding-index state, so the non-indexed getter
|
||||
// has nothing to answer with (GL 4.6 core table 23.4).
|
||||
case GL_VERTEX_BINDING_BUFFER:
|
||||
case GL_VERTEX_BINDING_DIVISOR:
|
||||
*params = 0; // vertex-binding entrypoints are stubbed
|
||||
return;
|
||||
case GL_VERTEX_BINDING_OFFSET:
|
||||
*params = 0; // vertex-binding entrypoints are stubbed
|
||||
return;
|
||||
case GL_VERTEX_BINDING_STRIDE:
|
||||
*params = 0; // vertex-binding entrypoints are stubbed
|
||||
RecordIndexedOnlyGetterError(__func__, pname);
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||
*params = 0; // vertex-binding entrypoints are stubbed
|
||||
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribRelativeOffset());
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATTRIB_BINDINGS:
|
||||
*params = 0; // vertex-binding entrypoints are stubbed
|
||||
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribBindings());
|
||||
return;
|
||||
case GL_MAX_VERTEX_ATTRIB_STRIDE:
|
||||
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribStride());
|
||||
return;
|
||||
case GL_VIEWPORT: {
|
||||
const auto& vp = MG_State::pGLContext->GetViewport();
|
||||
|
||||
@@ -205,6 +205,50 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The GL_UNIFORM interface and glGetActiveUniform(s)iv are the same query in two
|
||||
// spellings, so they answer from the same place - the frontend reflection. The backend
|
||||
// program is not that place: it does not exist at all for a program whose types its
|
||||
// shading language cannot express (a double-precision uniform has no ESSL form), and
|
||||
// the interface queries would then describe a program with no uniforms.
|
||||
//
|
||||
// Writes the GL_UNIFORM value of `prop` for active uniform `index`; false for a prop
|
||||
// the reflection does not model, which the caller forwards to the backend instead.
|
||||
Bool GetUniformResourceProp(const SharedPtr<MG_State::GLState::ProgramObject>& programObject, Uint index,
|
||||
GLenum prop, GLint* out) {
|
||||
switch (prop) {
|
||||
case GL_TYPE:
|
||||
*out = static_cast<GLint>(programObject->GetActiveUniformType(index));
|
||||
return true;
|
||||
case GL_ARRAY_SIZE:
|
||||
*out = programObject->GetActiveUniformArraySize(index);
|
||||
return true;
|
||||
case GL_NAME_LENGTH:
|
||||
*out = static_cast<GLint>(programObject->GetActiveUniformName(index).length() + 1);
|
||||
return true;
|
||||
case GL_BLOCK_INDEX:
|
||||
*out = programObject->GetActiveUniformBlockIndex(index);
|
||||
return true;
|
||||
case GL_OFFSET:
|
||||
*out = programObject->GetActiveUniformOffset(index);
|
||||
return true;
|
||||
case GL_ARRAY_STRIDE:
|
||||
*out = programObject->GetActiveUniformArrayStride(index);
|
||||
return true;
|
||||
case GL_MATRIX_STRIDE:
|
||||
*out = programObject->GetActiveUniformMatrixStride(index);
|
||||
return true;
|
||||
case GL_IS_ROW_MAJOR:
|
||||
*out = programObject->GetActiveUniformIsRowMajor(index);
|
||||
return true;
|
||||
case GL_LOCATION:
|
||||
// A block member has no location; GetUniformLocation already reports -1 for one.
|
||||
*out = programObject->GetUniformLocation(programObject->GetActiveUniformName(index));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyStr(GLsizei bufSize, GLsizei* length, GLchar* dst, const char* src, GLsizei srcLength) {
|
||||
if (bufSize <= 0) {
|
||||
if (length) *length = 0;
|
||||
@@ -2526,6 +2570,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Backend does not support program interface queries."));
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
if (pname == GL_ACTIVE_RESOURCES) {
|
||||
*params = static_cast<GLint>(programObject->GetUniformCount());
|
||||
return;
|
||||
}
|
||||
if (pname == GL_MAX_NAME_LENGTH) {
|
||||
// Stored as the bare length; GL_MAX_NAME_LENGTH counts the terminator.
|
||||
*params = programObject->GetUniformMaxLength() + 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
getProgramInterfaceiv(program, programInterface, pname, params);
|
||||
}
|
||||
|
||||
@@ -2534,6 +2589,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!programObject) return GL_INVALID_INDEX;
|
||||
if (!ValidateNamedProgramResourceInterface(programInterface, __func__)) return GL_INVALID_INDEX;
|
||||
if (!name) return GL_INVALID_INDEX;
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
const Int uniformIndex = programObject->GetActiveUniformIndex(name);
|
||||
return uniformIndex < 0 ? GL_INVALID_INDEX : static_cast<GLuint>(uniformIndex);
|
||||
}
|
||||
auto getProgramResourceIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
||||
if (!getProgramResourceIndex) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -2570,6 +2629,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
// Same index space GetProgramResourceIndex answers in, and the range check above
|
||||
// already used it.
|
||||
const String& uniformName = programObject->GetActiveUniformName(index);
|
||||
CopyStr(bufSize, length, name, uniformName.c_str(), static_cast<GLsizei>(uniformName.length()));
|
||||
return;
|
||||
}
|
||||
auto getProgramResourceName = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceName;
|
||||
if (!getProgramResourceName) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
@@ -2591,6 +2657,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"propCount and bufSize must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (programInterface == GL_UNIFORM) {
|
||||
if (index >= programObject->GetUniformCount()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "index is out of range."));
|
||||
return;
|
||||
}
|
||||
if (props == nullptr || params == nullptr) return;
|
||||
GLsizei written = 0;
|
||||
for (GLsizei i = 0; i < propCount && written < bufSize; ++i) {
|
||||
GLint value = 0;
|
||||
if (!GetUniformResourceProp(programObject, index, props[i], &value)) {
|
||||
// GL_ATOMIC_COUNTER_BUFFER_INDEX and the GL_REFERENCED_BY_* stage props are
|
||||
// not modelled here; ask the backend, which indexes resources by name.
|
||||
auto backendGetIndex = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceIndex;
|
||||
auto backendGetiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
||||
if (backendGetIndex && backendGetiv) {
|
||||
const GLuint backendIndex = backendGetIndex(program, GL_UNIFORM,
|
||||
programObject->GetActiveUniformName(index).c_str());
|
||||
if (backendIndex != GL_INVALID_INDEX) {
|
||||
GLsizei one = 0;
|
||||
backendGetiv(program, GL_UNIFORM, backendIndex, 1, &props[i], 1, &one, &value);
|
||||
}
|
||||
}
|
||||
}
|
||||
params[written++] = value;
|
||||
}
|
||||
if (length) *length = written;
|
||||
return;
|
||||
}
|
||||
auto getProgramResourceiv = MG_Backend::gBackendFunctionsTable.GL.GetProgramResourceiv;
|
||||
if (!getProgramResourceiv) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
struct QueryObject {
|
||||
GLuint id = 0;
|
||||
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
|
||||
// glCreateQueries makes the object outright; glGenQueries only reserves the name,
|
||||
// and the object appears when the name is first used (GL 4.6 core 4.2.1).
|
||||
Bool created = false;
|
||||
MG_Backend::BackendQueryHandle backendHandle = nullptr;
|
||||
Bool active = false;
|
||||
Bool ended = false;
|
||||
@@ -177,6 +180,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glCreateQueries differs from glGenQueries in creating the objects outright, with their
|
||||
// target already fixed and the rest of their state at the defaults (GL 4.6 core 4.2.1).
|
||||
void CreateQueries(GLenum target, GLsizei n, GLuint* ids) {
|
||||
switch (target) {
|
||||
case GL_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED:
|
||||
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
|
||||
case GL_TIME_ELAPSED:
|
||||
case GL_TIMESTAMP:
|
||||
case GL_PRIMITIVES_GENERATED:
|
||||
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
|
||||
break;
|
||||
default:
|
||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not accepted.");
|
||||
return;
|
||||
}
|
||||
if (n < 0) {
|
||||
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||
return;
|
||||
}
|
||||
if (!ids) {
|
||||
return;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
const GLuint id = g_nextQueryId++;
|
||||
auto* queryObject = new QueryObject;
|
||||
queryObject->id = id;
|
||||
queryObject->target = target;
|
||||
queryObject->created = true;
|
||||
g_liveQueryObjects[id] = queryObject;
|
||||
ids[i] = id;
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteQueries(GLsizei n, const GLuint* ids) {
|
||||
if (n < 0) {
|
||||
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||
@@ -228,9 +266,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return GL_FALSE;
|
||||
}
|
||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||
// Gen'd ids count as query objects here: the registry creates live
|
||||
// objects at GenQueries time.
|
||||
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
|
||||
// A name from glGenQueries is not yet a query object: it becomes one when it is first
|
||||
// used with BeginQuery/QueryCounter (which is what a non-zero target records), or
|
||||
// immediately if it came from glCreateQueries.
|
||||
const auto* queryObject = FindQueryObjectLocked(id);
|
||||
return (queryObject != nullptr && (queryObject->created || queryObject->target != 0)) ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
|
||||
void BeginQuery(GLenum target, GLuint id) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GenQueries(GLsizei n, GLuint* ids);
|
||||
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||
void DeleteQueries(GLsizei n, const GLuint* ids);
|
||||
GLboolean IsQuery(GLuint id);
|
||||
void BeginQuery(GLenum target, GLuint id);
|
||||
|
||||
@@ -2035,6 +2035,128 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
|
||||
}
|
||||
|
||||
// The work glTexBuffer[Range] and glTextureBuffer[Range] all share once the texture has been
|
||||
// resolved - by binding for the target forms, by name for the DSA ones. `size` is
|
||||
// kWholeBuffer for the non-Range entry points, which attach the buffer as it grows rather
|
||||
// than freezing the size it happens to have now.
|
||||
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). This is a much
|
||||
// shorter list than the renderable or texturable formats, so it cannot be inferred from either.
|
||||
static Bool IsBufferTextureInternalFormat(GLenum internalformat) {
|
||||
switch (internalformat) {
|
||||
case GL_R8:
|
||||
case GL_R16:
|
||||
case GL_R16F:
|
||||
case GL_R32F:
|
||||
case GL_R8I:
|
||||
case GL_R16I:
|
||||
case GL_R32I:
|
||||
case GL_R8UI:
|
||||
case GL_R16UI:
|
||||
case GL_R32UI:
|
||||
case GL_RG8:
|
||||
case GL_RG16:
|
||||
case GL_RG16F:
|
||||
case GL_RG32F:
|
||||
case GL_RG8I:
|
||||
case GL_RG16I:
|
||||
case GL_RG32I:
|
||||
case GL_RG8UI:
|
||||
case GL_RG16UI:
|
||||
case GL_RG32UI:
|
||||
case GL_RGB32F:
|
||||
case GL_RGB32I:
|
||||
case GL_RGB32UI:
|
||||
case GL_RGBA8:
|
||||
case GL_RGBA16:
|
||||
case GL_RGBA16F:
|
||||
case GL_RGBA32F:
|
||||
case GL_RGBA8I:
|
||||
case GL_RGBA16I:
|
||||
case GL_RGBA32I:
|
||||
case GL_RGBA8UI:
|
||||
case GL_RGBA16UI:
|
||||
case GL_RGBA32UI:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
|
||||
const char* caller) {
|
||||
using MG_State::GLState::TextureObjectBuffer;
|
||||
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||
if (!IsBufferTextureInternalFormat(internalformat)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
|
||||
internalformat)));
|
||||
return;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
|
||||
|
||||
auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||
if (buffer != 0 && !bufferObject) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"`buffer` is not zero and is not the name of an existing buffer object."));
|
||||
return;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
|
||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
||||
// A texture whose target is something else is a wrong object, not a wrong token
|
||||
// (GL 4.6 core 8.9).
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"The effective target of `texture` is not `GL_TEXTURE_BUFFER`."));
|
||||
return;
|
||||
}
|
||||
if (size != TextureObjectBuffer::kWholeBuffer) {
|
||||
// GL 4.6 core 8.9: offset must be non-negative and aligned to
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, and size must be positive.
|
||||
if (offset < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (size == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "size must be greater than zero."));
|
||||
return;
|
||||
}
|
||||
const Int alignment = std::max(
|
||||
1, MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment);
|
||||
if (offset % alignment != 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"offset is not a multiple of GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT."));
|
||||
return;
|
||||
}
|
||||
// The range has to lie inside the buffer that is being attached. Detaching (buffer
|
||||
// zero) carries no range to check.
|
||||
if (bufferObject && static_cast<SizeT>(offset) + size > bufferObject->GetSize()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"offset + size is greater than the buffer object's GL_BUFFER_SIZE."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto* texBufferObject = static_cast<TextureObjectBuffer*>(textureObject.get());
|
||||
texBufferObject->GetBufferBindingSlot().Bind(bufferObject);
|
||||
texBufferObject->SetBufferRange(static_cast<SizeT>(offset < 0 ? 0 : offset), size);
|
||||
texBufferObject->SetInternalFormat(textureInternalFormat);
|
||||
}
|
||||
|
||||
void TexBuffer_State(GLenum target, GLenum internalformat, GLuint buffer) {
|
||||
// ======================= Converting ================================
|
||||
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||
@@ -2081,6 +2203,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& bufferSlot = texBufferObject->GetBufferBindingSlot();
|
||||
bufferSlot.Bind(bufferObject);
|
||||
|
||||
texBufferObject->SetBufferRange(0, MG_State::GLState::TextureObjectBuffer::kWholeBuffer);
|
||||
texBufferObject->SetInternalFormat(textureInternalFormat);
|
||||
}
|
||||
|
||||
@@ -3670,23 +3793,43 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
GetTextureImage(texture, level, format, type, bufSize, pixels);
|
||||
}
|
||||
|
||||
// A buffer texture carries none of the sampler or level state these queries report. Reached by
|
||||
// name there is no target token to blame, so the wrong object is INVALID_OPERATION rather than
|
||||
// the INVALID_ENUM the target forms report for an unaccepted target (GL 4.6 core 8.11).
|
||||
static Bool ValidateNamedTextureHasParameters(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* caller) {
|
||||
if (!textureObject) return false;
|
||||
if (textureObject->GetStorageType() == TextureStorageType::Buffer) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"The effective target of `texture` has no texture parameters."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameteriv_State(target, pname, params); });
|
||||
}
|
||||
|
||||
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterfv_State(target, pname, params); });
|
||||
}
|
||||
|
||||
void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIiv_State(target, pname, params); });
|
||||
}
|
||||
|
||||
void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params) {
|
||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||
if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
|
||||
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIuiv_State(target, pname, params); });
|
||||
}
|
||||
|
||||
@@ -4088,6 +4231,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
TexBuffer_State(target, internalformat, buffer);
|
||||
}
|
||||
|
||||
// The buffer texture bound to `target` on the active unit - what the non-DSA range form
|
||||
// operates on. Kept separate from TexBuffer_State because that one resolves the target
|
||||
// itself and attaches the whole buffer.
|
||||
static const SharedPtr<MG_State::GLState::ITextureObject>& GetBoundBufferTexture(GLenum target,
|
||||
const char* caller) {
|
||||
TextureUploadTarget uploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
if (!TextureImpl::ValidateTextureUploadTarget(uploadTarget)) return nullTextureObject;
|
||||
(void)caller;
|
||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||
return activeUnit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject();
|
||||
}
|
||||
|
||||
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||
AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
|
||||
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
|
||||
}
|
||||
|
||||
void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer) {
|
||||
AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, 0,
|
||||
MG_State::GLState::TextureObjectBuffer::kWholeBuffer, __func__);
|
||||
}
|
||||
|
||||
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||
AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, offset,
|
||||
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
|
||||
}
|
||||
|
||||
GLboolean IsTexture(GLuint texture) {
|
||||
return IsTexture_State(texture);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GenerateTextureMipmap(GLuint texture);
|
||||
void BindTextureUnit(GLuint unit, GLuint texture);
|
||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
||||
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer);
|
||||
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
||||
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
|
||||
|
||||
@@ -105,12 +105,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
||||
}
|
||||
|
||||
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
|
||||
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
|
||||
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
||||
static int EffectiveVertexStride(GLsizei stride, GLint size, GLenum type) {
|
||||
if (stride != 0) return static_cast<int>(stride);
|
||||
switch (type) {
|
||||
case GL_INT_2_10_10_10_REV:
|
||||
case GL_UNSIGNED_INT_2_10_10_10_REV:
|
||||
case GL_UNSIGNED_INT_10F_11F_11F_REV:
|
||||
return 4;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return static_cast<int>(size * MG_Util::GetGLTypeSize(type));
|
||||
}
|
||||
|
||||
// glBindVertexBuffers / glVertexArrayVertexBuffers take a range of binding points, and a
|
||||
// range that runs past the last one is INVALID_OPERATION rather than the INVALID_VALUE a
|
||||
// single out-of-range index gets (GL 4.6 core 10.3.1).
|
||||
static bool ValidateVertexBindingRange(GLuint first, GLsizei count, const char* funcName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) >
|
||||
VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
"first + count exceeds GL_MAX_VERTEX_ATTRIB_BINDINGS."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ValidateVertexBindingIndex(GLuint bindingindex, const char* funcName) {
|
||||
// Bound by the same dynamic limit as attribute indices: the default attribute -> binding
|
||||
// mapping is the identity, so a binding point the backend cannot address as an attribute
|
||||
// would resolve into an attribute the backend must then reject on every draw. Real drivers
|
||||
// likewise report MAX_VERTEX_ATTRIB_BINDINGS == MAX_VERTEX_ATTRIBS.
|
||||
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribs()) {
|
||||
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||
@@ -155,6 +188,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj,
|
||||
const char* caller) {
|
||||
// Name zero is not a vertex array object in a core profile: it names the default vertex
|
||||
// array, which the by-name (direct state access) entry points never accept. MobileGL keeps a
|
||||
// real object at index 0 for the compatibility paths, so the generic name validation below
|
||||
// would otherwise let it through (GL 4.6 core 10.3.1).
|
||||
if (vaobj == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Vertex array name 0 is not a vertex array object."));
|
||||
return nullptr;
|
||||
}
|
||||
if (!VertexArrayImpl::ValidateVertexArrayName(vaobj)) return nullptr;
|
||||
if (!VertexArrayImpl::ValidateVertexArrayObject(vaobj)) return nullptr;
|
||||
return MG_State::pGLContext->GetVertexArrayObject(vaobj);
|
||||
@@ -210,7 +254,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||
// Integer path: never normalized, never BGRA/packed (the validator rejects those).
|
||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, false, stride, true)) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, false, stride, true)) return;
|
||||
|
||||
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) {
|
||||
@@ -227,6 +271,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
|
||||
}
|
||||
|
||||
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
|
||||
@@ -234,7 +279,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||
|
||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, normalized == GL_TRUE, stride, false))
|
||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, normalized == GL_TRUE, stride, false))
|
||||
return;
|
||||
|
||||
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
@@ -256,6 +301,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const int effectiveSize = isBgra ? 4 : size;
|
||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
|
||||
vao->BindAttributeBuffer(index, vbo);
|
||||
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
|
||||
}
|
||||
|
||||
void BindVertexArray_State(GLuint array) {
|
||||
@@ -359,6 +405,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset and stride must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (static_cast<Uint>(stride) > VertexArrayImpl::GetMaxVertexAttribStride()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"stride exceeds GL_MAX_VERTEX_ATTRIB_STRIDE."));
|
||||
return;
|
||||
}
|
||||
auto bufferObject = GetVertexArrayBufferObject_State(buffer, caller);
|
||||
if (buffer != 0 && !bufferObject) return;
|
||||
|
||||
@@ -376,6 +429,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLintptr* offsets, const GLsizei* strides) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingRange(first, count, "VertexArrayVertexBuffers_State")) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!buffers) {
|
||||
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "VertexArrayVertexBuffers_State");
|
||||
@@ -389,12 +443,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
static void VertexAttribFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||
GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||
GLuint relativeoffset, Bool isInteger, const char* caller) {
|
||||
static_cast<void>(caller);
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||
|
||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||
if (!VertexArrayImpl::ValidateVertexAttribPointerParams(attribindex, size, dataType, 0)) return;
|
||||
// The separate-format entry points take the same size/type rules as the pointer ones,
|
||||
// GL_BGRA included, so they need the full format validation rather than the pointer-only
|
||||
// subset - that one reports GL_BGRA as an out-of-range size.
|
||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(attribindex, size, type, dataType, normalized == GL_TRUE, 0,
|
||||
isInteger))
|
||||
return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||
|
||||
vao->SetAttributeFormatSeparate(attribindex, size, dataType, normalized, isInteger, relativeoffset);
|
||||
const Bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
||||
vao->SetAttributeFormatSeparate(attribindex, isBgra ? 4 : size, dataType, normalized, isInteger,
|
||||
relativeoffset, isBgra);
|
||||
}
|
||||
|
||||
// The long (64-bit) attribute format. MobileGL has no 64-bit vertex attributes, so nothing is
|
||||
// recorded; what the entry point owes the application is the parameter validation, which is
|
||||
// observable through glGetError regardless of whether the format could be used in a draw.
|
||||
static void VertexAttribLFormatSeparate_State(GLuint attribindex, GLint size, GLenum type,
|
||||
GLuint relativeoffset) {
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
||||
"64-bit vertex attributes are not supported."));
|
||||
}
|
||||
|
||||
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
||||
@@ -1044,6 +1122,83 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
VertexArrayVertexBuffer_State(vaobj, bindingindex, buffer, offset, stride);
|
||||
}
|
||||
|
||||
// glGetVertexArrayiv reports exactly one thing (GL 4.6 core table 23.4): which buffer the
|
||||
// named vertex array takes its indices from. Everything else about a vertex array is
|
||||
// per-attribute and belongs to the indexed queries below.
|
||||
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||
if (!vao || !param) return;
|
||||
if (pname != GL_ELEMENT_ARRAY_BUFFER_BINDING) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname must be GL_ELEMENT_ARRAY_BUFFER_BINDING."));
|
||||
return;
|
||||
}
|
||||
const auto& indexBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
*param = indexBuffer ? static_cast<GLint>(indexBuffer->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||
if (!vao || !param) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||
const auto& attr = vao->GetAttribute(index);
|
||||
switch (pname) {
|
||||
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||
*param = attr.Enabled ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||
*param = static_cast<GLint>(attr.Size);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||
*param = static_cast<GLint>(attr.Stride);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_NORMALIZED:
|
||||
*param = attr.Normalized ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||
// 64-bit attributes are not supported, so no attribute is ever a long one.
|
||||
*param = GL_FALSE;
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||
*param = static_cast<GLint>(attr.Divisor);
|
||||
return;
|
||||
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||
return;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"pname is not an accepted indexed vertex array query."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only GL_VERTEX_BINDING_OFFSET needs 64 bits. Its `index` names a vertex buffer binding
|
||||
// point directly (GL 4.6 core 10.3.1), not an attribute - unlike every pname the 32-bit
|
||||
// indexed query above accepts, which is why this one does not go through an attribute's
|
||||
// binding index.
|
||||
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||
if (!vao || !param) return;
|
||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||
if (pname != GL_VERTEX_BINDING_OFFSET) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_VERTEX_BINDING_OFFSET."));
|
||||
return;
|
||||
}
|
||||
*param = static_cast<GLint64>(vao->GetBindingPoint(index).Offset);
|
||||
}
|
||||
|
||||
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||
GLuint relativeoffset) {
|
||||
VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset);
|
||||
@@ -1076,6 +1231,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLsizei* strides) {
|
||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
||||
if (!vao) return;
|
||||
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (!buffers) {
|
||||
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers");
|
||||
@@ -1100,6 +1256,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"VertexAttribIFormat");
|
||||
}
|
||||
|
||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
|
||||
}
|
||||
|
||||
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
|
||||
if (!vao) return;
|
||||
VertexAttribLFormatSeparate_State(attribindex, size, type, relativeoffset);
|
||||
}
|
||||
|
||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
||||
if (!vao) return;
|
||||
|
||||
@@ -92,9 +92,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void EnableVertexArrayAttrib(GLuint vaobj, GLuint index);
|
||||
void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
|
||||
void VertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
||||
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param);
|
||||
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param);
|
||||
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param);
|
||||
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||
GLuint relativeoffset);
|
||||
void VertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
void VertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
|
||||
void VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
|
||||
void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers,
|
||||
@@ -104,6 +108,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const GLsizei* strides);
|
||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex);
|
||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor);
|
||||
void VertexAttribDivisor(GLuint index, GLuint divisor);
|
||||
|
||||
@@ -23,6 +23,18 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
return std::min(static_cast<Uint>(backendLimit), capacity);
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribBindings() {
|
||||
return GetMaxVertexAttribs();
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribRelativeOffset() {
|
||||
return 2047;
|
||||
}
|
||||
|
||||
Uint GetMaxVertexAttribStride() {
|
||||
return 2048;
|
||||
}
|
||||
|
||||
Bool ValidateVertexArrayName(Uint index) {
|
||||
Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index);
|
||||
if (!isValid) {
|
||||
@@ -90,9 +102,31 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
|
||||
Bool integerPath) {
|
||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
|
||||
Int stride, Bool integerPath) {
|
||||
constexpr const char* fn = "ValidateVertexAttribFormat";
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV is a three-component float-path-only packing that has no
|
||||
// DataType of its own, so it has to be recognised by name before the conversion below turns
|
||||
// it into Unknown and reports the wrong error (GL 4.6 core 10.3.2).
|
||||
if (glType == GL_UNSIGNED_INT_10F_11F_11F_REV) {
|
||||
if (integerPath) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", fn,
|
||||
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV is not an integer-path type (attribute {}).",
|
||||
index)));
|
||||
return false;
|
||||
}
|
||||
if (sizeRaw != 3) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", fn,
|
||||
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV requires size 3 (attribute {}).", index)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (type == DataType::Unknown) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
@@ -170,4 +204,40 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type) {
|
||||
constexpr const char* fn = "ValidateVertexAttribLFormat";
|
||||
if (size < 1 || size > 4) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", fn,
|
||||
std::format("Invalid size {} for attribute {}. Must be 1-4.", size, index)));
|
||||
return false;
|
||||
}
|
||||
// GL 4.6 core 10.3.2: the long form takes GL_DOUBLE and nothing else.
|
||||
if (type != GL_DOUBLE) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", fn,
|
||||
std::format("Type 0x{:X} is not GL_DOUBLE (attribute {}).", type, index)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset) {
|
||||
const Uint limit = GetMaxVertexAttribRelativeOffset();
|
||||
if (relativeOffset > limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateVertexAttribRelativeOffset",
|
||||
std::format("relativeoffset {} exceeds GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET ({}).", relativeOffset,
|
||||
limit)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
||||
|
||||
@@ -15,6 +15,20 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
// capacity). Falls back to the capacity when no backend is active (unit tests).
|
||||
Uint GetMaxVertexAttribs();
|
||||
|
||||
// GL_MAX_VERTEX_ATTRIB_BINDINGS. The default attribute -> binding mapping is the identity, so a
|
||||
// binding point that cannot also be an attribute index would resolve into an attribute the
|
||||
// backend has to reject on every draw; real drivers report the two limits equal as well.
|
||||
Uint GetMaxVertexAttribBindings();
|
||||
|
||||
// GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET. The relative offset is folded into the resolved
|
||||
// attribute offset in the frontend and never reaches a backend limit, so this is the value the
|
||||
// spec requires an implementation to support at minimum (GL 4.6 core table 23.63).
|
||||
Uint GetMaxVertexAttribRelativeOffset();
|
||||
|
||||
// GL_MAX_VERTEX_ATTRIB_STRIDE. Like the relative offset above, the stride never reaches a
|
||||
// backend limit of its own, so this is the spec minimum (GL 4.6 core table 23.63).
|
||||
Uint GetMaxVertexAttribStride();
|
||||
|
||||
Bool ValidateVertexArrayName(Uint index);
|
||||
Bool ValidateVertexArrayObject(Uint index);
|
||||
Bool ValidateVertexAttributeIndex(Uint index);
|
||||
@@ -22,6 +36,13 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
||||
// Full glVertexAttribPointer / glVertexAttribIPointer format validation, including the packed
|
||||
// 2_10_10_10 types and GL_BGRA size. sizeRaw is the untranslated GL size (possibly GL_BGRA);
|
||||
// integerPath selects the glVertexAttribIPointer rules.
|
||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
|
||||
Bool integerPath);
|
||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
|
||||
Int stride, Bool integerPath);
|
||||
// glVertexAttribLFormat / glVertexArrayAttribLFormat: the only accepted type is GL_DOUBLE and
|
||||
// the size range is 1-4 (GL_BGRA is a float-path size). Separate from the function above
|
||||
// because the long path shares none of its type or size rules.
|
||||
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type);
|
||||
// Shared by every *Format entry point: INVALID_VALUE once relativeoffset leaves the range the
|
||||
// implementation advertises.
|
||||
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset);
|
||||
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
|
||||
// For glBindBufferBase / glBindBufferRange
|
||||
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
|
||||
const BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index) const {
|
||||
return const_cast<BufferState*>(this)->GetBindingPoint(target, index);
|
||||
}
|
||||
constexpr SizeT GetBindingPointCount(const BufferTarget target) const {
|
||||
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
|
||||
auto index = std::distance(BufferBindPointTargets.begin(), it);
|
||||
|
||||
@@ -842,6 +842,64 @@ namespace MobileGL::MG_State {
|
||||
const auto it = m_transformFeedbackObjects.find(index);
|
||||
return it != m_transformFeedbackObjects.end() && it->second.hasCompletedSpan;
|
||||
}
|
||||
|
||||
void GLContext::CreateTransformFeedbackObject(Uint index) {
|
||||
// glCreateTransformFeedbacks has no bind step to infer existence from, so the name it
|
||||
// hands out is already the name of an object (GL 4.6 core 13.2.1).
|
||||
m_transformFeedbackObjects[index] = {};
|
||||
m_transformFeedbackObjects[index].everBound = true;
|
||||
}
|
||||
|
||||
Bool GLContext::IsNamedTransformFeedbackActive(Uint index) const {
|
||||
if (index == m_boundTransformFeedback) return m_transformFeedbackActive;
|
||||
const auto it = m_transformFeedbackObjects.find(index);
|
||||
return it != m_transformFeedbackObjects.end() && it->second.active;
|
||||
}
|
||||
|
||||
Bool GLContext::IsNamedTransformFeedbackPaused(Uint index) const {
|
||||
if (index == m_boundTransformFeedback) return m_transformFeedbackPaused;
|
||||
const auto it = m_transformFeedbackObjects.find(index);
|
||||
return it != m_transformFeedbackObjects.end() && it->second.paused;
|
||||
}
|
||||
|
||||
NamedTransformFeedbackBinding GLContext::GetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex) const {
|
||||
NamedTransformFeedbackBinding result;
|
||||
if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return result;
|
||||
// The bound object's capture bindings live in the context's own binding points, not in
|
||||
// the saved copy - that one is only written when the object is swapped out.
|
||||
if (index == m_boundTransformFeedback) {
|
||||
const auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
|
||||
result.Buffer = point.GetBoundObject();
|
||||
result.Range = point.GetRange();
|
||||
result.HasExplicitRange = point.HasExplicitRange();
|
||||
return result;
|
||||
}
|
||||
const auto it = m_transformFeedbackObjects.find(index);
|
||||
if (it == m_transformFeedbackObjects.end()) return result;
|
||||
const auto& saved = it->second.bindings[bufferIndex];
|
||||
result.Buffer = saved.buffer;
|
||||
result.Range = saved.range;
|
||||
result.HasExplicitRange = saved.hasExplicitRange;
|
||||
return result;
|
||||
}
|
||||
|
||||
void GLContext::SetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex,
|
||||
const SharedPtr<BufferObject>& buffer, Range1D range,
|
||||
Bool hasExplicitRange) {
|
||||
if (bufferIndex >= MAX_TRANSFORM_FEEDBACK_BUFFERS) return;
|
||||
if (index == m_boundTransformFeedback) {
|
||||
auto& point = m_bufferState.GetBindingPoint(BufferTarget::TransformFeedback, bufferIndex);
|
||||
point.Bind(buffer);
|
||||
if (buffer && hasExplicitRange) {
|
||||
point.SetRange(range, true);
|
||||
} else {
|
||||
point.ClearRange();
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto& object = m_transformFeedbackObjects[index];
|
||||
object.bindings[bufferIndex] = {buffer, range, hasExplicitRange};
|
||||
}
|
||||
} // namespace GLState
|
||||
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
|
||||
@@ -45,6 +45,14 @@ namespace MobileGL {
|
||||
// translates the result into its own API call.
|
||||
VertexAttribTypeInfo ClassifyVertexAttribType(GLenum glType);
|
||||
|
||||
// One indexed capture binding of a transform feedback object, as the by-name queries
|
||||
// report it. An empty Buffer means the binding point is unbound.
|
||||
struct NamedTransformFeedbackBinding {
|
||||
SharedPtr<BufferObject> Buffer;
|
||||
Range1D Range{};
|
||||
Bool HasExplicitRange = false;
|
||||
};
|
||||
|
||||
class GLContext {
|
||||
public:
|
||||
GLContext() = default;
|
||||
@@ -299,6 +307,17 @@ namespace MobileGL {
|
||||
// cannot express: an empty completed span is legal and draws nothing.
|
||||
Bool HasTransformFeedbackCompletedSpan(Uint index) const;
|
||||
|
||||
// The by-name (direct state access) view. A named object that happens to be the
|
||||
// bound one is answered from the live copy, since that is where its state actually
|
||||
// is until a bind swaps it out.
|
||||
void CreateTransformFeedbackObject(Uint index);
|
||||
Bool IsNamedTransformFeedbackActive(Uint index) const;
|
||||
Bool IsNamedTransformFeedbackPaused(Uint index) const;
|
||||
NamedTransformFeedbackBinding GetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex) const;
|
||||
void SetNamedTransformFeedbackBinding(Uint index, Uint bufferIndex,
|
||||
const SharedPtr<BufferObject>& buffer, Range1D range,
|
||||
Bool hasExplicitRange);
|
||||
|
||||
// Framebuffer
|
||||
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
|
||||
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
|
||||
|
||||
@@ -348,6 +348,12 @@ namespace MobileGL {
|
||||
|
||||
// TODO: add other texture types as needed
|
||||
|
||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
||||
const Bool mipmapped =
|
||||
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
||||
}
|
||||
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
if (texture == nullptr) return true;
|
||||
if (!texture->IsComplete()) return false;
|
||||
|
||||
@@ -164,6 +164,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
|
||||
|
||||
// The rule above asked as the backends need it: does a lookup on this texture read
|
||||
// (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound
|
||||
// to the unit when there is one, otherwise the texture's own. A backend answers yes by
|
||||
// routing the texture to whatever it already uses for "nothing is bound there".
|
||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler);
|
||||
|
||||
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
|
||||
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
|
||||
? static_cast<const TextureObjectMipmap*>(texture)
|
||||
|
||||
@@ -21,10 +21,31 @@ namespace MobileGL {
|
||||
BindingSlot<BufferObject>& GetBufferBindingSlot(
|
||||
TextureUploadTarget target = TextureUploadTarget::TextureBuffer);
|
||||
|
||||
// The window of the attached buffer the texture addresses. glTexBuffer attaches
|
||||
// the whole buffer, which is expressed here as an offset of 0 and a size of
|
||||
// kWholeBuffer so a later respecify of the buffer keeps being followed - a stored
|
||||
// size would freeze the texture at the size the buffer happened to have.
|
||||
static constexpr SizeT kWholeBuffer = ~static_cast<SizeT>(0);
|
||||
void SetBufferRange(SizeT offset, SizeT size) {
|
||||
m_bufferRangeOffset = offset;
|
||||
m_bufferRangeSize = size;
|
||||
}
|
||||
SizeT GetBufferRangeOffset() const { return m_bufferRangeOffset; }
|
||||
// Resolved against the buffer's current size, so kWholeBuffer tracks it.
|
||||
SizeT GetBufferRangeSizeInBytes() const {
|
||||
const auto& buffer = m_bufferBindingSlot.GetBoundObject();
|
||||
const SizeT bufferSize = buffer != nullptr ? buffer->GetSize() : 0;
|
||||
if (m_bufferRangeSize == kWholeBuffer) return bufferSize;
|
||||
const SizeT available = bufferSize > m_bufferRangeOffset ? bufferSize - m_bufferRangeOffset : 0;
|
||||
return std::min(m_bufferRangeSize, available);
|
||||
}
|
||||
|
||||
protected:
|
||||
Uint GetIndexOfTextureUploadTarget(TextureUploadTarget target) const override;
|
||||
|
||||
BindingSlot<BufferObject> m_bufferBindingSlot = BindingSlot<BufferObject>(BufferTarget::Texture);
|
||||
SizeT m_bufferRangeOffset = 0;
|
||||
SizeT m_bufferRangeSize = kWholeBuffer;
|
||||
const Vector<TextureUploadTarget> m_uploadTargets{TextureUploadTarget::TextureBuffer};
|
||||
};
|
||||
} // namespace GLState
|
||||
|
||||
@@ -77,6 +77,26 @@ namespace MobileGL::MG_State::GLState {
|
||||
BumpAttributeFormatVersion(index);
|
||||
}
|
||||
|
||||
void VertexArrayObject::MirrorPointerIntoBinding(Uint index, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
||||
int effectiveStride) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS || index >= MAX_VERTEX_ATTRIB_BINDINGS) return;
|
||||
|
||||
// glVertexAttribPointer is defined in terms of the binding model (GL 4.6 core 10.3.2): it
|
||||
// also sets binding point `index` to the buffer, the pointer as the offset, and the
|
||||
// *effective* stride, and points the attribute at that binding point with relative offset 0.
|
||||
// The flat attribute view keeps the raw stride, because VERTEX_ATTRIB_ARRAY_STRIDE reports
|
||||
// that argument verbatim, so the binding point is recorded alongside the resolved attribute
|
||||
// rather than being resolved into it.
|
||||
m_attributeBindingIndex[index] = index;
|
||||
m_attributeRelativeOffset[index] = 0;
|
||||
|
||||
auto& binding = m_bindingPoints[index];
|
||||
binding.Buffer = buffer;
|
||||
binding.Offset = offset;
|
||||
binding.Stride = effectiveStride;
|
||||
binding.Divisor = m_attributes[index].Divisor;
|
||||
}
|
||||
|
||||
void VertexArrayObject::BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||
|
||||
@@ -111,6 +131,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void VertexArrayObject::SetAttributeDivisor(Uint index, Uint divisor) {
|
||||
if (index >= MAX_VERTEX_ATTRIBS) return;
|
||||
// glVertexAttribDivisor is VertexBindingDivisor on the attribute's own binding point
|
||||
// (GL 4.6 core 10.3.2), so the binding-point view has to follow the resolved attribute.
|
||||
if (index < MAX_VERTEX_ATTRIB_BINDINGS && m_attributeBindingIndex[index] == index) {
|
||||
m_bindingPoints[index].Divisor = divisor;
|
||||
}
|
||||
if (m_attributes[index].Divisor == divisor) return;
|
||||
m_attributes[index].Divisor = divisor;
|
||||
BumpAttributeFormatVersion(index);
|
||||
@@ -187,18 +212,18 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void VertexArrayObject::SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
|
||||
Bool isInteger, Uint relativeOffset) {
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra) {
|
||||
if (attribIndex >= MAX_VERTEX_ATTRIBS) return;
|
||||
if (size < 1 || size > 4) return;
|
||||
|
||||
auto& attr = m_attributes[attribIndex];
|
||||
if (attr.Size != size || attr.Type != type || attr.Normalized != normalized || attr.IsInteger != isInteger ||
|
||||
attr.IsBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) {
|
||||
attr.IsBgra != isBgra || m_attributeRelativeOffset[attribIndex] != relativeOffset) {
|
||||
attr.Size = size;
|
||||
attr.Type = type;
|
||||
attr.Normalized = normalized;
|
||||
attr.IsInteger = isInteger;
|
||||
attr.IsBgra = false; // the binding-format path (glVertexAttribFormat) does not carry BGRA
|
||||
attr.IsBgra = isBgra;
|
||||
m_attributeRelativeOffset[attribIndex] = relativeOffset;
|
||||
BumpAttributeFormatVersion(attribIndex);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ namespace MobileGL {
|
||||
|
||||
void BindAttributeBuffer(Uint index, const SharedPtr<BufferObject>& buffer);
|
||||
|
||||
// Record what the pointer-style API implies for the binding-point view: attribute
|
||||
// `index` bound to binding point `index` with relative offset 0, and that binding
|
||||
// point carrying the buffer, the pointer offset and the effective stride.
|
||||
void MirrorPointerIntoBinding(Uint index, const SharedPtr<BufferObject>& buffer, SizeT offset,
|
||||
int effectiveStride);
|
||||
|
||||
BindingSlot<BufferObject>& GetIndexBufferBindingSlot();
|
||||
const BindingSlot<BufferObject>& GetIndexBufferBindingSlot() const;
|
||||
|
||||
@@ -82,7 +88,22 @@ namespace MobileGL {
|
||||
void SetBindingDivisor(Uint bindingIndex, Uint divisor);
|
||||
void SetAttributeBinding(Uint attribIndex, Uint bindingIndex);
|
||||
void SetAttributeFormatSeparate(Uint attribIndex, int size, DataType type, Bool normalized,
|
||||
Bool isInteger, Uint relativeOffset);
|
||||
Bool isInteger, Uint relativeOffset, Bool isBgra = false);
|
||||
|
||||
// The binding-point view the attributes were resolved from. Kept queryable
|
||||
// because glGetVertexArrayIndexed[64]iv reports it verbatim, and the resolved
|
||||
// flat attribute cannot always be inverted back into it.
|
||||
Uint GetAttributeRelativeOffset(Uint attribIndex) const {
|
||||
return attribIndex < m_attributeRelativeOffset.size() ? m_attributeRelativeOffset[attribIndex] : 0;
|
||||
}
|
||||
Uint GetAttributeBindingIndex(Uint attribIndex) const {
|
||||
return attribIndex < m_attributeBindingIndex.size() ? m_attributeBindingIndex[attribIndex]
|
||||
: attribIndex;
|
||||
}
|
||||
const VertexBufferBindingPoint& GetBindingPoint(Uint bindingIndex) const {
|
||||
static const VertexBufferBindingPoint kEmpty{};
|
||||
return bindingIndex < m_bindingPoints.size() ? m_bindingPoints[bindingIndex] : kEmpty;
|
||||
}
|
||||
|
||||
const VertexAttributeVersion& GetAttributeVersion(Uint index) const;
|
||||
const Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const;
|
||||
|
||||
@@ -1870,7 +1870,7 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) {
|
||||
// Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and
|
||||
// 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and
|
||||
// rectangle as a plain 2D - it is single-level and already clamps, so only the
|
||||
// non-normalized coordinates differ and LowerRectImagesForEssl handles those.
|
||||
// non-normalized coordinates differ and LowerRectImages handles those.
|
||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D));
|
||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray));
|
||||
EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle));
|
||||
|
||||
@@ -1056,6 +1056,18 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
|
||||
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
|
||||
caps.MaxTextureBufferSize = maxTextureBufferSize;
|
||||
// Through glesFuncs, like every other capability query here: a bare glGetIntegerv resolves
|
||||
// to MobileGL's own exported entry point, which answers this pname from the very
|
||||
// capability table being filled in - so the driver's real alignment never arrived and the
|
||||
// backend reported an unconstrained offset it cannot honour.
|
||||
GLint textureBufferOffsetAlignment = 1;
|
||||
glesFuncs.glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &textureBufferOffsetAlignment);
|
||||
// Core in ES 3.2 and in EXT_texture_buffer; an older context rejects the pname and leaves
|
||||
// the default in place.
|
||||
if (glesFuncs.glGetError) {
|
||||
while (glesFuncs.glGetError() != GL_NO_ERROR) {}
|
||||
}
|
||||
caps.TextureBufferOffsetAlignment = std::max(1, textureBufferOffsetAlignment);
|
||||
caps.MaxUniformBufferBindings = maxUniformBufferBindings;
|
||||
caps.MaxUniformBlockSize = maxUniformBlockSize;
|
||||
caps.MaxImageUnits = maxImageUnits;
|
||||
|
||||
@@ -1117,6 +1117,8 @@ namespace MobileGL {
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
Int TextureBufferOffsetAlignment = 1;
|
||||
Int MaxUniformBufferBindings = 24;
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
|
||||
@@ -177,6 +177,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(p.limits.maxComputeWorkGroupInvocations);
|
||||
caps.MaxShaderStorageBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetStorageBuffers);
|
||||
caps.MaxTextureBufferSize = static_cast<Int>(p.limits.maxTexelBufferElements);
|
||||
caps.TextureBufferOffsetAlignment =
|
||||
static_cast<Int>(std::max<VkDeviceSize>(1, p.limits.minTexelBufferOffsetAlignment));
|
||||
caps.MaxUniformBufferBindings = static_cast<Int>(p.limits.maxDescriptorSetUniformBuffers);
|
||||
caps.MaxUniformBlockSize = static_cast<Int>(p.limits.maxUniformBufferRange);
|
||||
caps.MaxImageUnits = static_cast<Int>(p.limits.maxPerStageDescriptorStorageImages);
|
||||
@@ -268,6 +270,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxComputeWorkGroupInvocations = static_cast<Int>(properties.limits.maxComputeWorkGroupInvocations);
|
||||
caps.MaxShaderStorageBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetStorageBuffers);
|
||||
caps.MaxTextureBufferSize = static_cast<Int>(properties.limits.maxTexelBufferElements);
|
||||
caps.TextureBufferOffsetAlignment =
|
||||
static_cast<Int>(std::max<VkDeviceSize>(1, properties.limits.minTexelBufferOffsetAlignment));
|
||||
caps.MaxUniformBufferBindings = static_cast<Int>(properties.limits.maxDescriptorSetUniformBuffers);
|
||||
caps.MaxUniformBlockSize = static_cast<Int>(properties.limits.maxUniformBufferRange);
|
||||
caps.MaxImageUnits = static_cast<Int>(properties.limits.maxPerStageDescriptorStorageImages);
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace MobileGL {
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
Int TextureBufferOffsetAlignment = 1;
|
||||
Int MaxUniformBufferBindings = 24;
|
||||
Int MaxUniformBlockSize = 16384;
|
||||
Int MaxImageUnits = 8;
|
||||
|
||||
@@ -38,6 +38,10 @@ namespace MobileGL {
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
|
||||
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
|
||||
case GL_PATCHES:
|
||||
// The tessellator decides what a patch becomes; its vertex count is pipeline
|
||||
// state (patchControlPoints), not part of the topology.
|
||||
return VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
|
||||
default:
|
||||
MGLOG_W("Unrecognized primitive topology");
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
|
||||
@@ -298,6 +298,25 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"not supported; no impact: the native indirect path deliberately does not "
|
||||
"rely on it (shader-side emulation handles baseInstance semantics)");
|
||||
}
|
||||
if (glesFuncs.glPatchParameteri != nullptr) {
|
||||
builder.Pass("Tessellation patch parameters",
|
||||
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
|
||||
} else {
|
||||
builder.Warn("Tessellation patch parameters",
|
||||
"glPatchParameteri missing (pre-ES 3.2 without GL_EXT_tessellation_shader); "
|
||||
"GL_PATCH_VERTICES stays at the driver default of 3 and a patch draw of any "
|
||||
"other size renders nothing");
|
||||
}
|
||||
if (glesFuncs.glGenTransformFeedbacks != nullptr && glesFuncs.glBindTransformFeedback != nullptr &&
|
||||
glesFuncs.glPauseTransformFeedback != nullptr && glesFuncs.glResumeTransformFeedback != nullptr) {
|
||||
builder.Pass("Transform feedback objects",
|
||||
"supported (each GL transform feedback object gets one of the driver's, so "
|
||||
"several can hold a paused capture at once)");
|
||||
} else {
|
||||
builder.Warn("Transform feedback objects",
|
||||
"entry points missing; every GL transform feedback object shares the driver's "
|
||||
"default one, so a second object cannot open a capture while the first is paused");
|
||||
}
|
||||
if (caps.SupportsNorm16Texture) {
|
||||
builder.Pass("GL_EXT_texture_norm16", "supported");
|
||||
} else {
|
||||
@@ -1412,6 +1431,39 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"hard-fails at draw");
|
||||
}
|
||||
|
||||
// Core 1.0 features the backend turns GL stages into pipeline stages with.
|
||||
VkPhysicalDeviceFeatures coreFeatures{};
|
||||
vkGetPhysicalDeviceFeatures(physicalDevice, &coreFeatures);
|
||||
if (coreFeatures.tessellationShader == VK_TRUE) {
|
||||
builder.Pass("tessellationShader",
|
||||
"supported (GL_PATCHES draws run the tessellation control/evaluation stages)");
|
||||
} else {
|
||||
builder.Warn("tessellationShader",
|
||||
"unsupported; a program with a tessellation control/evaluation shader cannot build a "
|
||||
"pipeline, so GL_PATCHES draws render nothing");
|
||||
}
|
||||
|
||||
Bool vertexAttributeInstanceRateDivisor = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr &&
|
||||
HasVkExtension(deviceExtensions, VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceVertexAttributeDivisorFeaturesEXT divisorFeatures{};
|
||||
divisorFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_EXT;
|
||||
VkPhysicalDeviceFeatures2 features2{};
|
||||
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
||||
features2.pNext = &divisorFeatures;
|
||||
vkGetPhysicalDeviceFeatures2Fn(physicalDevice, &features2);
|
||||
vertexAttributeInstanceRateDivisor = divisorFeatures.vertexAttributeInstanceRateDivisor == VK_TRUE;
|
||||
}
|
||||
if (vertexAttributeInstanceRateDivisor) {
|
||||
builder.Pass("vertexAttributeInstanceRateDivisor",
|
||||
"supported (glVertexAttribDivisor advances an attribute every N instances)");
|
||||
} else {
|
||||
builder.Warn("vertexAttributeInstanceRateDivisor",
|
||||
"unsupported; Vulkan's instance input rate can only advance once per instance, so "
|
||||
"every non-zero glVertexAttribDivisor behaves as 1 and instanced attributes meant to "
|
||||
"change every N instances change every one");
|
||||
}
|
||||
|
||||
if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
||||
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -363,90 +364,16 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
bool ShaderCompiler::LowerRectImages(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
constexpr SizeT kSpirvHeaderWordCount = 5;
|
||||
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ...
|
||||
constexpr SizeT kTypeImageDimWordIndex = 3;
|
||||
constexpr SizeT kTypeImageMinWordCount = 9;
|
||||
outputBinary.clear();
|
||||
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
|
||||
return false;
|
||||
}
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Vector<SizeT> rectDimWordOffsets;
|
||||
Vector<SizeT> rectCapabilityWordOffsets;
|
||||
Bool hasNormalizedCoordinateLookup = false;
|
||||
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
|
||||
const Uint32 instructionWord = inputBinary[offset];
|
||||
const SizeT wordCount = instructionWord >> 16u;
|
||||
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
|
||||
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
|
||||
return false;
|
||||
}
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass());
|
||||
|
||||
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) {
|
||||
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
|
||||
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
|
||||
}
|
||||
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
|
||||
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
|
||||
if (capability == spv::Capability::SampledRect ||
|
||||
capability == spv::Capability::ImageRect) {
|
||||
rectCapabilityWordOffsets.push_back(offset + 1);
|
||||
}
|
||||
} else {
|
||||
switch (opcode) {
|
||||
// Normalized-coordinate lookups whose ESSL form the backend's
|
||||
// NormalizeRectSamplerCoordinates post-pass cannot repair: the
|
||||
// coordinate is either fused with something else in a single argument
|
||||
// (the Dref sample forms carry the compare value in coord.z) or the
|
||||
// divide would have to happen after a projective divide. Tracing each
|
||||
// one back to its image type would let a module mix a normalized 2D
|
||||
// lookup with a rectangle fetch, but the extra reach is not worth the
|
||||
// risk of getting the trace wrong: decline the whole module instead.
|
||||
//
|
||||
// OpImageSampleImplicitLod, OpImageGather and OpImageDrefGather are
|
||||
// absent because all three become an ESSL call whose argument 1 is the
|
||||
// bare texel-space coordinate, which the post-pass divides by the
|
||||
// texture size.
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSampleProjImplicitLod:
|
||||
case spv::Op::OpImageSampleProjExplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefImplicitLod:
|
||||
case spv::Op::OpImageSampleProjDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleExplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefImplicitLod:
|
||||
case spv::Op::OpImageSparseSampleDrefExplicitLod:
|
||||
case spv::Op::OpImageSparseGather:
|
||||
case spv::Op::OpImageSparseDrefGather:
|
||||
hasNormalizedCoordinateLookup = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outputBinary.assign(inputBinary.begin(), inputBinary.end());
|
||||
for (const SizeT dimWordOffset : rectDimWordOffsets) {
|
||||
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is always
|
||||
// declared by a graphics module, so restating it keeps the word count intact
|
||||
// without leaving a capability SPIRV-Cross would key off.
|
||||
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
|
||||
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
|
||||
}
|
||||
return true;
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
|
||||
@@ -43,20 +43,15 @@ namespace MobileGL {
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
|
||||
// for them at all - it refuses outright ("Rectangle textures are not supported on
|
||||
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
|
||||
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
|
||||
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
|
||||
// lookup would also need its coordinates divided by the texture size, so the pass
|
||||
// declines those modules instead of emitting something subtly wrong. Returns false
|
||||
// when it changed nothing or cannot safely convert. DirectGLES only.
|
||||
static bool LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
// which wrongly includes baseInstance).
|
||||
// GL_TEXTURE_RECTANGLE emulated on a plain 2D texture, for every backend:
|
||||
// divides the coordinate of each normalized-coordinate lookup by the texture
|
||||
// size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for
|
||||
// what it declines and why.
|
||||
static bool LowerRectImages(const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Adds the Invariant decoration to every Position builtin output. GL apps
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "NormalizeRectCoordinatesPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
|
||||
// Image operations whose coordinate operand (in-operand 1) is a plain
|
||||
// normalized coordinate and nothing else. The Dref *sample* forms are absent:
|
||||
// they pack the compare value into the coordinate's last component, so the
|
||||
// divide cannot be applied componentwise. OpImageDrefGather is here because it
|
||||
// carries the compare value in a separate operand.
|
||||
bool TakesPlainNormalizedCoordinate(spv::Op opcode) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpImageSampleImplicitLod:
|
||||
case spv::Op::OpImageSampleExplicitLod:
|
||||
case spv::Op::OpImageGather:
|
||||
case spv::Op::OpImageDrefGather:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The OpTypeImage behind whatever an image operation was handed - a sampled
|
||||
// image, a bare image, or a pointer to either. Returns nullptr when the operand
|
||||
// is not an image at all.
|
||||
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(objectId);
|
||||
if (object == nullptr) return nullptr;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
while (type != nullptr) {
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeImage:
|
||||
return type;
|
||||
case spv::Op::OpTypeSampledImage:
|
||||
case spv::Op::OpTypePointer:
|
||||
// Both name their element type in their last in-operand.
|
||||
type = defUseMgr->GetDef(type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
||||
continue;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsRectImageType(const Instruction* imageType) {
|
||||
// OpTypeImage in-operands: sampled type, Dim, Depth, Arrayed, MS, Sampled, Format.
|
||||
return imageType != nullptr && imageType->NumInOperands() >= 2 &&
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) == spv::Dim::Rect;
|
||||
}
|
||||
|
||||
// The bare image an OpImageQuerySizeLod needs. An operation on a sampled image
|
||||
// has to unwrap it first; one already holding a bare image is used as is.
|
||||
uint32_t GetQueryableImage(IRContext* context, InstructionBuilder& builder, uint32_t imageOperandId,
|
||||
uint32_t imageTypeId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
Instruction* object = defUseMgr->GetDef(imageOperandId);
|
||||
if (object == nullptr) return 0;
|
||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
||||
if (type != nullptr && type->opcode() == spv::Op::OpTypeImage) {
|
||||
return imageOperandId;
|
||||
}
|
||||
Instruction* unwrapped = builder.AddUnaryOp(imageTypeId, spv::Op::OpImage, imageOperandId);
|
||||
return unwrapped != nullptr ? unwrapped->result_id() : 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status NormalizeRectCoordinatesPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constantMgr = irContext->get_constant_mgr();
|
||||
|
||||
// Nothing to do unless the module actually declares a rectangle image.
|
||||
bool hasRectImageType = false;
|
||||
for (const Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
|
||||
hasRectImageType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasRectImageType) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::opt::analysis::Integer signedInt(32, true);
|
||||
spvtools::opt::analysis::Float float32(32);
|
||||
spvtools::opt::analysis::Vector int2(&signedInt, 2);
|
||||
spvtools::opt::analysis::Vector float2(&float32, 2);
|
||||
const uint32_t int2TypeId = typeMgr->GetTypeInstruction(&int2);
|
||||
const uint32_t float2TypeId = typeMgr->GetTypeInstruction(&float2);
|
||||
const uint32_t lodZeroId = constantMgr->GetSIntConstId(0);
|
||||
if (int2TypeId == 0 || float2TypeId == 0 || lodZeroId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
bool rewroteCoordinate = false;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& instruction : block) {
|
||||
if (!TakesPlainNormalizedCoordinate(instruction.opcode()) ||
|
||||
instruction.NumInOperands() < 2) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t imageOperandId = instruction.GetSingleWordInOperand(0);
|
||||
Instruction* imageType = ResolveImageType(irContext, imageOperandId);
|
||||
if (!IsRectImageType(imageType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t coordinateId = instruction.GetSingleWordInOperand(1);
|
||||
InstructionBuilder builder(
|
||||
irContext, &instruction,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
const uint32_t queryableImageId =
|
||||
GetQueryableImage(irContext, builder, imageOperandId, imageType->result_id());
|
||||
if (queryableImageId == 0) {
|
||||
return Status::Failure;
|
||||
}
|
||||
|
||||
// A rectangle image has exactly one level, so the query's level is 0.
|
||||
// The lod form is what the 2D type this becomes accepts.
|
||||
Instruction* size = builder.AddBinaryOp(int2TypeId, spv::Op::OpImageQuerySizeLod,
|
||||
queryableImageId, lodZeroId);
|
||||
Instruction* sizeFloat =
|
||||
builder.AddUnaryOp(float2TypeId, spv::Op::OpConvertSToF, size->result_id());
|
||||
Instruction* normalized = builder.AddBinaryOp(
|
||||
float2TypeId, spv::Op::OpFDiv, coordinateId, sizeFloat->result_id());
|
||||
instruction.SetInOperand(1, {normalized->result_id()});
|
||||
irContext->UpdateDefUse(&instruction);
|
||||
rewroteCoordinate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that no lookup depends on the rectangle semantics any more, the type can
|
||||
// become the 2D one both targets accept. Done unconditionally, because a module
|
||||
// that only ever fetched texels still has to lose the type.
|
||||
for (Instruction& type : irContext->types_values()) {
|
||||
if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) {
|
||||
type.SetInOperand(1, {static_cast<uint32_t>(spv::Dim::Dim2D)});
|
||||
}
|
||||
}
|
||||
// The rectangle capabilities describe types that no longer exist. Shader is
|
||||
// always declared by a graphics module, so restating it keeps the instruction
|
||||
// valid without leaving a capability a consumer would key off.
|
||||
for (Instruction& capability : irContext->capabilities()) {
|
||||
const auto value = static_cast<spv::Capability>(capability.GetSingleWordInOperand(0));
|
||||
if (value == spv::Capability::SampledRect || value == spv::Capability::ImageRect) {
|
||||
capability.SetInOperand(0, {static_cast<uint32_t>(spv::Capability::Shader)});
|
||||
}
|
||||
}
|
||||
if (rewroteCoordinate) {
|
||||
irContext->AddCapability(spv::Capability::ImageQuery);
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<NormalizeRectCoordinatesPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,38 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/pass.h"
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// GL_TEXTURE_RECTANGLE has no counterpart in either target API: ESSL has no
|
||||
// rectangle sampler at all, and Vulkan's SPIR-V environment does not allow
|
||||
// Dim::Rect. Both emulate it on a plain 2D texture, which differs in exactly one
|
||||
// way - a rectangle lookup addresses texels while a 2D one addresses [0,1].
|
||||
//
|
||||
// This pass closes that difference in the module itself, so neither backend has
|
||||
// to reason about it: every lookup that takes normalized coordinates gets its
|
||||
// coordinate divided by the texture's size, and the image type is then rewritten
|
||||
// to 2D. texelFetch is untouched (integer texel coordinates mean the same thing
|
||||
// on both), and so is the Dref *sample* form, whose coordinate carries the
|
||||
// compare value in the last component - the module keeps its rectangle type and
|
||||
// the caller declines it.
|
||||
class NormalizeRectCoordinatesPass final : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "mobilegl-normalize-rect-coordinates"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateNormalizeRectCoordinatesPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
+63
-1
@@ -1,12 +1,16 @@
|
||||
# Running the OpenGL CTS against MobileGL
|
||||
|
||||
This directory contains two supported paths:
|
||||
This directory contains three supported paths:
|
||||
|
||||
- Android arm64 / MobileGL EGL: the KHR-GL33 workflow documented below and in
|
||||
`skills/gl-cts-on-mobilegl/SKILL.md`.
|
||||
- Windows x64 / MobileGL WGL: the GL30-GL46 pipeline in
|
||||
`scripts/wgl_glcts_pipeline.py`, documented by
|
||||
`skills/wgl-gl-cts-on-mobilegl/SKILL.md`.
|
||||
- Desktop Linux x64 / MobileGL EGL: `scripts/run_cts_local.py` against the
|
||||
`mobilegl-desktop` VK-GL-CTS target, documented in "Desktop Linux workflow"
|
||||
below and in `skills/linux-gl-cts-on-mobilegl/SKILL.md`. This is the path that
|
||||
needs no device and no GPU.
|
||||
|
||||
Windows prerequisites are Git, Python 3.9+, CMake, Visual Studio 2022's Desktop
|
||||
C++ workload, and a Vulkan SDK visible to CMake. DirectVulkan also needs a
|
||||
@@ -25,6 +29,64 @@ resumes individual suites after crashes/timeouts, and writes Markdown plus JSON
|
||||
reports below the printed `runs/<first-16-of-run-fingerprint>` directory. Its
|
||||
manifest records provenance and the runner settings used to validate a resume.
|
||||
|
||||
## Desktop Linux workflow
|
||||
|
||||
The `mobilegl-desktop` target builds `glcts` as an ordinary host executable that
|
||||
reaches OpenGL only through `libMobileGL.so`. Both backends run headless with no
|
||||
GPU at all, which makes this the cheapest way to measure a single test group
|
||||
while working on it.
|
||||
|
||||
Apt packages: `mesa-vulkan-drivers` (lavapipe, for DirectVulkan),
|
||||
`libegl1-mesa-dev` and `libgles2-mesa-dev` (the system EGL/ES that DirectGLES
|
||||
drives), `libvulkan-dev`, `ninja-build`.
|
||||
|
||||
```sh
|
||||
cmake -S . -B build-linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
|
||||
cmake --build build-linux --parallel "$(nproc)"
|
||||
|
||||
python tools/cts/scripts/sync_to_cts.py "$CTS"
|
||||
git -C "$CTS" apply <path-to>/tools/cts/patches/0001-fbo-color-texture-attachment.patch
|
||||
cmake -S "$CTS" -B "$CTS/build-cts" -G Ninja -DDEQP_TARGET=mobilegl-desktop \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
ninja -C "$CTS/build-cts" glcts
|
||||
|
||||
python tools/cts/scripts/run_cts_local.py --backend DirectGLES \
|
||||
--glcts "$CTS/build-cts/external/openglcts/modules/glcts" \
|
||||
--lib build-linux/libMobileGL.so \
|
||||
--caselist cases.txt --outdir runs/gles --env EGL_PLATFORM=surfaceless
|
||||
python tools/cts/scripts/qpa_report.py runs/gles --label DirectGLES
|
||||
```
|
||||
|
||||
`EGL_PLATFORM=surfaceless` is not optional for DirectGLES: without a `/dev/dri`
|
||||
node Mesa's EGL fails `eglInitialize` on the default display, and MobileGL
|
||||
reports that as `EGL_BAD_ALLOC` out of `eglCreatePbufferSurface`. DirectVulkan
|
||||
needs nothing extra - lavapipe exposes `VK_EXT_headless_surface`, which is what
|
||||
the desktop platform port's pbuffer path requires.
|
||||
|
||||
Two known differences from a real GPU, both MobileGL's rather than the harness's:
|
||||
DirectVulkan reads back zeros from the **default** framebuffer (a user FBO,
|
||||
renderbuffer- or texture-attached, is correct on both backends), and lavapipe
|
||||
supports renderbuffer formats that Adreno reports as unsupported, so the Android
|
||||
runs see `NotSupported` where these do not.
|
||||
|
||||
### Reference results: KHR-GL45.direct_state_access
|
||||
|
||||
`opengl-cts-4.6.8.1`, the 371 `direct_state_access` cases of the `gl45-main`
|
||||
mustpass list, lavapipe / Mesa 25.2.8, `--deqp-surface-type=fbo`.
|
||||
|
||||
| backend | conformance | strict Pass | Fail | InternalError | Crash |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| DirectGLES | **74.93%** | 67.39% | 85 | 8 | 0 |
|
||||
| DirectVulkan | **73.32%** | 73.05% | 88 | 8 | 3 |
|
||||
|
||||
`textures_storage_multisample_*` is 54 of what remains on either backend and
|
||||
needs a feature rather than a fix: a multisample texture has to be attachable to
|
||||
a framebuffer and then sampleable through `sampler2DMS`, and the array half of
|
||||
the group additionally needs layered attachments, which the framebuffer
|
||||
attachment model does not represent yet. `program_pipelines_*` needs
|
||||
ARB_separate_shader_objects, which is stubbed throughout.
|
||||
|
||||
## Android KHR-GL33 workflow
|
||||
|
||||
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
|
||||
|
||||
@@ -17,3 +17,4 @@ Each skill is a self-contained package, matching the layout used by
|
||||
| --- | --- |
|
||||
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
|
||||
| [wgl-gl-cts-on-mobilegl](wgl-gl-cts-on-mobilegl/SKILL.md) | Build MobileGL's Windows x64 WGL drop-in, run GL30-GL46 core CTS against DirectGLES and DirectVulkan, resume safely, and emit validated reports. |
|
||||
| [linux-gl-cts-on-mobilegl](linux-gl-cts-on-mobilegl/SKILL.md) | Build `glcts` as a desktop Linux host binary against MobileGL, run any CTS group headlessly with no GPU, and report Espryt and Magma separately. The path to reach for while iterating on a fix. |
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: linux-gl-cts-on-mobilegl
|
||||
description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts) against MobileGL on desktop Linux with no GPU and no device, and report a per-backend conformance rate for Espryt (DirectGLES) and Magma (DirectVulkan). Use when measuring or iterating on the conformance of one test group, when a fix needs a before/after number, or when neither an Android device nor a Windows GPU box is available.
|
||||
---
|
||||
|
||||
# OpenGL CTS on MobileGL (desktop Linux)
|
||||
|
||||
## Overview
|
||||
|
||||
`glcts` is built as an ordinary x86-64 host executable that reaches OpenGL only
|
||||
through `libMobileGL.so`, using the `mobilegl-desktop` VK-GL-CTS target and the
|
||||
`tcu::Platform` port in `tools/cts/platform/`. Nothing links `libGL` or `libEGL`,
|
||||
so a result is unambiguously MobileGL's.
|
||||
|
||||
Both backends run headless on software rendering, so this needs no GPU at all:
|
||||
|
||||
- **Espryt** (`DirectGLES`) drives Mesa's OpenGL ES through the system EGL.
|
||||
- **Magma** (`DirectVulkan`) runs on lavapipe, whose `VK_EXT_headless_surface`
|
||||
is what the desktop platform port's pbuffer path requires.
|
||||
|
||||
Always report the two backends **separately**. They are different
|
||||
implementations of the same front end, they fail different cases, and a single
|
||||
combined number hides which one a change moved.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```sh
|
||||
sudo apt-get install -y ninja-build cmake libvulkan-dev \
|
||||
libegl1-mesa-dev libgles2-mesa-dev mesa-vulkan-drivers
|
||||
```
|
||||
|
||||
`mesa-vulkan-drivers` is what installs lavapipe; without it DirectVulkan has no
|
||||
ICD and `eglInitialize` fails inside the backend. A C++23 toolchain is required
|
||||
(GCC 13+, or Clang 20+ — Clang 18 defines `__cpp_concepts` as 201907L, which
|
||||
switches libstdc++'s `<expected>` off and the build fails in
|
||||
`MG_Util/ShaderTranspiler/Types.h`).
|
||||
|
||||
```sh
|
||||
export MG=<path-to-MobileGL-worktree>
|
||||
export CTS=<path-to-VK-GL-CTS-checkout>
|
||||
```
|
||||
|
||||
## Step 1 — build libMobileGL.so
|
||||
|
||||
```sh
|
||||
git -C "$MG" submodule update --init --recursive
|
||||
python3 "$MG/3rdparty/glslang/update_glslang_sources.py" # SPIRV-Tools; ENABLE_OPT is forced on
|
||||
|
||||
cmake -S "$MG" -B "$MG/build-linux" -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
cmake --build "$MG/build-linux" --parallel "$(nproc)"
|
||||
```
|
||||
|
||||
Add `-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=FALSE` when iterating. The
|
||||
Release configuration turns LTO on, which makes every relink cost minutes for no
|
||||
behavioural difference; a conformance number is identical either way.
|
||||
|
||||
## Step 2 — get VK-GL-CTS and build glcts
|
||||
|
||||
Use a release tag so the mustpass list, and therefore the reported rate, is
|
||||
citable.
|
||||
|
||||
```sh
|
||||
git -C "$CTS" checkout opengl-cts-4.6.8.1
|
||||
python3 "$CTS/external/fetch_sources.py"
|
||||
|
||||
python3 "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS"
|
||||
git -C "$CTS" apply "$MG/tools/cts/patches/0001-fbo-color-texture-attachment.patch"
|
||||
|
||||
cmake -S "$CTS" -B "$CTS/build-cts" -G Ninja -DDEQP_TARGET=mobilegl-desktop \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
ninja -C "$CTS/build-cts" glcts
|
||||
```
|
||||
|
||||
Confirm the configure output says `*** Using MobileGL desktop target`. Budget a
|
||||
couple of hours for the `glcts` link on a small machine; it is a one-time cost
|
||||
that ccache makes cheap afterwards.
|
||||
|
||||
If `fetch_sources.py` dies with `HTTP Error 403` it is an egress policy blocking
|
||||
the GitHub archive downloads (zlib, libpng), not a broken checkout: `git clone`
|
||||
still works, so clone the package at the tag the script pins into
|
||||
`external/<pkg>/src` by hand — for libpng also copy
|
||||
`scripts/pnglibconf.h.prebuilt` to `src/pnglibconf.h`, which is what the
|
||||
script's post-extract step does.
|
||||
|
||||
## Step 3 — run, once per backend
|
||||
|
||||
```sh
|
||||
cd "$MG"
|
||||
for BACKEND in DirectGLES DirectVulkan; do
|
||||
python3 tools/cts/scripts/run_cts_local.py --backend "$BACKEND" \
|
||||
--glcts "$CTS/build-cts/external/openglcts/modules/glcts" \
|
||||
--lib build-linux/libMobileGL.so \
|
||||
--caselist cases.txt --outdir "runs/${BACKEND}" \
|
||||
--env EGL_PLATFORM=surfaceless
|
||||
python3 tools/cts/scripts/qpa_report.py "runs/${BACKEND}" --label "$BACKEND"
|
||||
done
|
||||
```
|
||||
|
||||
`cases.txt` is any subset of a mustpass list. For one group, filter the list
|
||||
rather than running the whole suite:
|
||||
|
||||
```sh
|
||||
grep direct_state_access \
|
||||
"$CTS"/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/main/gl45-main.txt \
|
||||
> cases.txt
|
||||
```
|
||||
|
||||
`run_cts_local.py` re-invokes `glcts` with only the cases that have no result
|
||||
yet, so a crash costs one case rather than the run, and it records the case that
|
||||
was open when the process died as `Crash`. `qpa_report.py` scores `Pass`,
|
||||
`NotSupported` and the warning statuses as non-failures, the way Khronos scores
|
||||
a submission, and reports the strict `Pass`-only rate alongside.
|
||||
|
||||
## Required flags, and why
|
||||
|
||||
| Flag | Why it is not optional |
|
||||
| --- | --- |
|
||||
| `--env EGL_PLATFORM=surfaceless` | For DirectGLES. With no `/dev/dri` node Mesa's EGL fails `eglInitialize` on the default display, and MobileGL surfaces that as `EGL_BAD_ALLOC` out of `eglCreatePbufferSurface` — which points at the wrong call entirely. Harmless for DirectVulkan, so pass it to both. |
|
||||
| `--deqp-surface-type=fbo` (the runner's default) | DirectVulkan reads back zeros from the **default** framebuffer. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores Magma near zero for a reason unrelated to conformance. Use it for both backends so the two numbers stay comparable. |
|
||||
| `--deqp-terminate-on-device-lost=disable` (supplied by the runner) | Its default calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from what MobileGL exports, so the pointer is null and the process segfaults on the first case. |
|
||||
|
||||
## What this environment does and does not tell you
|
||||
|
||||
Reproducible here, and MobileGL's own rather than a driver quirk:
|
||||
|
||||
- DirectVulkan's default-framebuffer readback returns zeros; a user FBO,
|
||||
renderbuffer- or texture-attached, is correct on both backends. This is the
|
||||
same defect the Android runs work around, so it can be debugged without a
|
||||
phone.
|
||||
|
||||
Different from a real GPU, so do not read conformance into it:
|
||||
|
||||
- lavapipe supports renderbuffer formats Adreno reports as unsupported, so the
|
||||
Android runs see `NotSupported` where these do not, and vice versa.
|
||||
- Backend limits differ. `GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT` is 16 on llvmpipe
|
||||
and on lavapipe; a device that reports 1 will not exercise the same paths.
|
||||
- Everything is a software rasterizer, so a case that fails only under real
|
||||
timing or real tiling will not fail here.
|
||||
|
||||
## Reference results: KHR-GL45.direct_state_access
|
||||
|
||||
`opengl-cts-4.6.8.1`, the 371 `direct_state_access` cases of the `gl45-main`
|
||||
mustpass list, Mesa 25.2.8, `--deqp-surface-type=fbo`.
|
||||
|
||||
| backend | renderer | conformance | strict Pass | Fail | InternalError | Crash |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| DirectGLES | Espryt | **74.93%** | 67.39% | 85 | 8 | 0 |
|
||||
| DirectVulkan | Magma | **73.32%** | 73.05% | 88 | 8 | 3 |
|
||||
|
||||
## Contents
|
||||
|
||||
platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL
|
||||
targets/mobilegl-desktop.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl-desktop)
|
||||
scripts/sync_to_cts.py inject the port into a CTS checkout
|
||||
scripts/run_cts_local.py crash-resuming local-host runner
|
||||
scripts/qpa_report.py .qpa -> conformance rate
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "OpenGL CTS on MobileGL (desktop Linux)"
|
||||
short_description: "Run VK-GL-CTS glcts against MobileGL headlessly and report Espryt and Magma separately"
|
||||
default_prompt: "Use $linux-gl-cts-on-mobilegl to run the selected OpenGL CTS group against MobileGL on this Linux host and report the conformance rate for DirectGLES (Espryt) and DirectVulkan (Magma) separately."
|
||||
Reference in New Issue
Block a user