mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ad62b54c | ||
|
|
f41403e227 | ||
|
|
5fbb17f6b9 | ||
|
|
92d8f7269b | ||
|
|
822e405c77 | ||
|
|
b8233f9c4e | ||
|
|
91475a7b6f | ||
|
|
cee17025a0 | ||
|
|
9642ae4d20 | ||
|
|
595d140036 | ||
|
|
b62d1f2078 | ||
|
|
5e82ff968a |
@@ -1600,7 +1600,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
|
||||
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
|
||||
|
||||
IntVec4 backendViewport = parameters.Viewport;
|
||||
IntVec4 backendViewport = MG_State::pGLContext->GetViewport();
|
||||
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -1614,7 +1614,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_syncedBackendViewport = backendViewport;
|
||||
}
|
||||
|
||||
// All 12 capability bools live after LogicOp in the struct, i.e. in the tail span.
|
||||
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
|
||||
// struct, i.e. in the tail span.
|
||||
if (tailSpanDirty) {
|
||||
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
|
||||
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
|
||||
@@ -1633,11 +1634,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
|
||||
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
|
||||
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
|
||||
SYNC_CAPABILITY(ScissorTest, GL_SCISSOR_TEST);
|
||||
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
|
||||
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
|
||||
|
||||
#undef SYNC_CAPABILITY
|
||||
|
||||
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
|
||||
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
|
||||
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
|
||||
// index every ES draw rasterizes against - can be forwarded; a program that
|
||||
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
|
||||
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
|
||||
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
|
||||
{
|
||||
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
const Bool syncedScissorTest =
|
||||
(g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
if (forceFullPush || scissorTest != syncedScissorTest) {
|
||||
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
||||
@@ -1864,8 +1880,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
|
||||
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
|
||||
}
|
||||
if (forceFullPush || parameters.DepthRange != g_syncedRenderStateParameters.DepthRange) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRange.x(), parameters.DepthRange.y());
|
||||
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
|
||||
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2003,7 +2019,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// everything drawn with GL_SCISSOR_TEST enabled before the app's first glScissor
|
||||
// is clipped away - Minecraft 26.2 keeps only its unscissored sky and hand and
|
||||
// loses the terrain and the whole GUI.
|
||||
IntVec4 backendScissorBox = parameters.ScissorBox;
|
||||
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
|
||||
if (backendScissorBox.z() <= 0 || backendScissorBox.w() <= 0) {
|
||||
Int surfaceWidth = 0;
|
||||
Int surfaceHeight = 0;
|
||||
@@ -4762,7 +4778,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// restores the app state on exit, tracked via the render-state shadow.
|
||||
class ScopedScissorDisable {
|
||||
public:
|
||||
ScopedScissorDisable() : m_wasEnabled(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabled) {
|
||||
ScopedScissorDisable()
|
||||
: m_wasEnabled((RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0) {
|
||||
if (m_wasEnabled) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
~ScopedScissorDisable() {
|
||||
@@ -5563,6 +5580,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glMemoryBarrierByRegion(barriers);
|
||||
}
|
||||
|
||||
// One endpoint of a glCopyImageSubData, expressed the way the ES driver stores it.
|
||||
//
|
||||
// The frontend hands this backend the target the APPLICATION named, and three of the
|
||||
// targets core GL has do not exist in ES at all. They are not missing here either - the
|
||||
// texture managers already store a 1D texture as a height-1 2D one, a 1D array as a
|
||||
// height-1 2D array and a rectangle texture as a plain 2D one (MapToBackendTextureTarget) -
|
||||
// but glCopyImageSubData was the one path that never asked for that translation and passed
|
||||
// 0x84F5 / 0x0DE0 / 0x8C18 straight through. ES rejects the enum, the copy does not happen,
|
||||
// and with the error only asserted on (asserts are compiled out of an INFO build) the
|
||||
// destination silently keeps whatever it held.
|
||||
//
|
||||
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
|
||||
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
|
||||
// target.
|
||||
struct GLESCopyImageEndpoint {
|
||||
GLenum target = GL_TEXTURE_2D;
|
||||
GLint x = 0;
|
||||
GLint y = 0;
|
||||
GLint z = 0;
|
||||
};
|
||||
|
||||
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
|
||||
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||
GLESCopyImageEndpoint endpoint{};
|
||||
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||
endpoint.x = x;
|
||||
endpoint.y = 0;
|
||||
endpoint.z = y;
|
||||
return endpoint;
|
||||
}
|
||||
endpoint.x = x;
|
||||
endpoint.y = y;
|
||||
endpoint.z = z;
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||
// of the copy is one - GL forbids a copy whose two endpoints disagree about how many layers
|
||||
// move, so at most one of the two can be a 1D array only in the degenerate single-layer
|
||||
// case, where the swap is the identity anyway.
|
||||
static void ApplyGLESCopyImageExtent(GLenum appSrcTarget, GLenum appDstTarget, GLsizei& height, GLsizei& depth) {
|
||||
const TextureTarget srcStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appSrcTarget);
|
||||
const TextureTarget dstStateTarget = MG_Util::ConvertGLEnumToTextureTarget(appDstTarget);
|
||||
if (srcStateTarget != TextureTarget::Texture1DArray && dstStateTarget != TextureTarget::Texture1DArray) {
|
||||
return;
|
||||
}
|
||||
std::swap(height, depth);
|
||||
}
|
||||
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
@@ -5592,6 +5659,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
|
||||
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
|
||||
GLsizei copyHeight = srcHeight;
|
||||
GLsizei copyDepth = srcDepth;
|
||||
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
|
||||
|
||||
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
|
||||
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
|
||||
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat());
|
||||
@@ -5599,12 +5672,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
|
||||
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
||||
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dstX, dstY, srcWidth, srcHeight);
|
||||
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5615,29 +5688,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// with the always-live helper so a stale flag cannot misroute a
|
||||
// succeeded native copy into the 2D-only fallback.
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError == GL_NO_ERROR) {
|
||||
return;
|
||||
}
|
||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
|
||||
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
||||
MOBILEGL_ASSERT(srcTarget == GL_TEXTURE_2D && dstTarget == GL_TEXTURE_2D,
|
||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||
MOBILEGL_ASSERT(srcZ == 0 && dstZ == 0 && srcDepth == 1,
|
||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, srcX, srcY, srcWidth, srcHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY);
|
||||
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearGLErrors();
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||
dstBackendTexture->GetBackendTextureId(), dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||
srcWidth, srcHeight, srcDepth);
|
||||
AssertNoGLError("glCopyImageSubData");
|
||||
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||
srcWidth, copyHeight, copyDepth);
|
||||
// Every error condition glCopyImageSubData has was already ruled out by the frontend
|
||||
// validator, so a driver error here is an internal invariant violation, not something
|
||||
// the application can provoke. Say so where an INFO build can still see it, then trap
|
||||
// in the builds that trap - the previous bare assert left a release build with a
|
||||
// destination that silently kept its old contents.
|
||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||
if (copyImageError != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("glCopyImageSubData failed: %s. src target=%s (app %s), dst target=%s (app %s)",
|
||||
MG_Util::ConvertGLEnumToString(copyImageError).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(src.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(srcTarget).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
||||
}
|
||||
}
|
||||
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
|
||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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.viewportCount, sizeof(payload.viewportCount)));
|
||||
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)));
|
||||
@@ -406,8 +407,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
|
||||
@@ -42,6 +42,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
|
||||
@@ -1997,6 +1997,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex);
|
||||
}
|
||||
|
||||
// gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan
|
||||
// (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses
|
||||
// chain touches it, so a plain reflection of the declared output builtins is the whole test.
|
||||
Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
|
||||
return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex);
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
|
||||
for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) {
|
||||
const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex];
|
||||
if (variable != nullptr &&
|
||||
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
|
||||
variable->built_in == builtin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule,
|
||||
SpvBuiltIn builtin) {
|
||||
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
|
||||
@@ -2339,6 +2362,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the
|
||||
// geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation
|
||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||
// pipeline, and a false positive costs only a wider viewportCount.
|
||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
entry.writesViewportIndexBuiltin = false;
|
||||
|
||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||
if (!shaders[moduleIndex]) continue;
|
||||
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||
|
||||
const auto& module = spirv[moduleIndex];
|
||||
if (module.empty()) continue;
|
||||
|
||||
SpvReflectShaderModule reflectModule{};
|
||||
const SpvReflectResult createResult =
|
||||
spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule);
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would
|
||||
// silently collapse every viewport onto 0 (the exact bug this reflection exists
|
||||
// to fix); over-declaring costs one extra viewport slot on a program that never
|
||||
// uses it.
|
||||
MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the "
|
||||
"program writes gl_ViewportIndex",
|
||||
static_cast<Int>(createResult));
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReflectedWritesViewportIndexBuiltin(reflectModule)) {
|
||||
entry.writesViewportIndexBuiltin = true;
|
||||
}
|
||||
spvReflectDestroyShaderModule(&reflectModule);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const {
|
||||
@@ -3189,6 +3252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||
#endif
|
||||
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||
ReflectLayout(program, moduleSpirvs, entry);
|
||||
|
||||
@@ -151,6 +151,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
@@ -400,6 +406,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
@@ -434,6 +446,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
@@ -300,8 +300,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
|
||||
|
||||
VkImageLayout srcTrackedLayout = oldResource.layout;
|
||||
@@ -311,8 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
|
||||
oldResource.arrayLayers);
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
|
||||
|
||||
Vector<VkImageCopy> copyRegions;
|
||||
@@ -344,8 +342,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
|
||||
newResource.arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
|
||||
|
||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
|
||||
@@ -1191,7 +1188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool lowerTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, lowerMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
|
||||
resource.aspect, 0, writtenMipLevel);
|
||||
MOBILEGL_ASSERT(lowerTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1203,8 +1200,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool upperTransitioned = TransitionImageLayout(
|
||||
commandBuffer, resource.image, upperMipLayout, newLayout,
|
||||
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
|
||||
resource.arrayLayers);
|
||||
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel);
|
||||
MOBILEGL_ASSERT(upperTransitioned,
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
|
||||
texture->GetExternalIndex());
|
||||
@@ -1257,8 +1253,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
@@ -1288,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1355,8 +1350,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageLayout& trackedLayout, VkImageLayout newLayout,
|
||||
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
|
||||
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
|
||||
Uint32 layerCount) {
|
||||
VkImageAspectFlags aspectMask, Uint32 baseMipLevel,
|
||||
Uint32 levelCount) {
|
||||
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
|
||||
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
|
||||
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
|
||||
@@ -1381,7 +1376,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
barrier.subresourceRange.baseMipLevel = baseMipLevel;
|
||||
barrier.subresourceRange.levelCount = levelCount;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = layerCount;
|
||||
// Every layer, always - see the declaration for why layout tracking leaves no other
|
||||
// correct answer. VK_REMAINING_ARRAY_LAYERS rather than the image's own `arrayLayers`
|
||||
// because those are not the same number for a 3D image: MobileGL creates 3D images
|
||||
// 2D_ARRAY_COMPATIBLE and their arrayLayers is 1, which today Vulkan reads as "all depth
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -2605,7 +2606,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
uploadSrcAccessMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
|
||||
|
||||
// Array textures keep their GL "depth" in VkImage array layers, so the
|
||||
@@ -2709,7 +2710,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
s_sampledReadStages,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||
outResource.layout = finalLayout;
|
||||
|
||||
|
||||
@@ -388,12 +388,24 @@ public:
|
||||
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
|
||||
|
||||
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
|
||||
//
|
||||
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
|
||||
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
|
||||
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
|
||||
// skipped in the old layout while the tracker claims they moved. Every transfer against a
|
||||
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
|
||||
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
|
||||
//
|
||||
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
|
||||
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
|
||||
// move the complement of the level they wrote so the whole image converges on one layout again.
|
||||
// Nothing does, or can, do that per layer.
|
||||
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
|
||||
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
|
||||
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
|
||||
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1,
|
||||
Uint32 layerCount = 1);
|
||||
Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
|
||||
|
||||
SizeT CollectGarbage();
|
||||
|
||||
|
||||
@@ -335,19 +335,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Complete input inventory of ApplyDynamicDrawStateTail, one line per reader
|
||||
// (each accessor it replaces is a verified plain field read of the same
|
||||
// RenderStateParameters field - RenderState.cpp):
|
||||
// ApplyGLViewportState : Viewport, DepthRange, + extent/isDefaultFbo/preTransform
|
||||
// ApplyGLViewportState : Viewports[0], DepthRanges[0], + extent/isDefaultFbo/preTransform
|
||||
// ApplyBlendConstants : BlendColor
|
||||
// ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor
|
||||
// ApplyLineWidthState : LineWidth (see the caveat below)
|
||||
// ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref}
|
||||
// scissor rect : ScissorTestEnabled, ScissorBox,
|
||||
// scissor rect : ScissorTestEnabledMask bit 0, ScissorBoxes[0],
|
||||
// + extent/isDefaultFbo/preTransform
|
||||
// Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps
|
||||
// to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device
|
||||
// limits queried once at backend init and constant for the renderer's lifetime,
|
||||
// so they are not part of the key (the version gate never covered them either).
|
||||
struct DynamicTailKey {
|
||||
Int viewport[4] = {0, 0, 0, 0};
|
||||
Float viewport[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
Float depthRange[2] = {0.0f, 0.0f};
|
||||
Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
Float polygonOffsetFactor = 0.0f;
|
||||
@@ -437,12 +437,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
|
||||
}
|
||||
|
||||
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
|
||||
const IntVec2& framebufferExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFramebuffer) {
|
||||
const IntVec4& viewportState = MG_State::pGLContext->GetViewport();
|
||||
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRange();
|
||||
// One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of
|
||||
// ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same
|
||||
// arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation
|
||||
// especially, which is the classic way a multi-viewport port comes out upside down for every
|
||||
// index but the one that was tested.
|
||||
static VkViewport ComputeGLViewport(Uint32 index,
|
||||
const IntVec2& framebufferExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFramebuffer) {
|
||||
// Snapped to integers. The viewport is float STATE (glViewportIndexedf may set a
|
||||
// fractional origin, and GetFloati_v hands it back verbatim), but what rasterizes here is
|
||||
// the rounded rectangle - a deliberate, documented infidelity rather than a spec claim:
|
||||
// MobileGL passes the driver's VIEWPORT_SUBPIXEL_BITS through, so it does advertise
|
||||
// subpixel viewport precision it does not deliver. Nothing in KHR-GL43.viewport_array or
|
||||
// in Minecraft sets a fractional viewport (the conformance checks are all on the state
|
||||
// round trip), which is why the honest-but-lossy path was kept over widening every
|
||||
// default-framebuffer Y-flip/pre-transform helper to floats. See the KNOWN INFIDELITY
|
||||
// note in MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp.
|
||||
const FloatVec4& stored = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
const IntVec4 viewportState(static_cast<Int>(std::lround(stored.x())),
|
||||
static_cast<Int>(std::lround(stored.y())),
|
||||
static_cast<Int>(std::lround(stored.z())),
|
||||
static_cast<Int>(std::lround(stored.w())));
|
||||
const FloatVec2& depthRange = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
const IntVec2 logicalExtent = isDefaultFramebuffer
|
||||
? ResolveDefaultFramebufferLogicalExtent(preTransform, framebufferExtent)
|
||||
: framebufferExtent;
|
||||
@@ -477,6 +495,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
viewport.height = static_cast<float>(viewportHeight);
|
||||
viewport.minDepth = depthRange.x();
|
||||
viewport.maxDepth = depthRange.y();
|
||||
return viewport;
|
||||
}
|
||||
|
||||
static void ApplyGLViewportState(VkCommandBuffer commandBuffer,
|
||||
const IntVec2& framebufferExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFramebuffer) {
|
||||
const VkViewport viewport = ComputeGLViewport(0, framebufferExtent, preTransform, isDefaultFramebuffer);
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
if (shadow.viewportValid && shadow.viewport.x == viewport.x && shadow.viewport.y == viewport.y &&
|
||||
shadow.viewport.width == viewport.width && shadow.viewport.height == viewport.height &&
|
||||
@@ -4879,6 +4905,7 @@ void main() {
|
||||
.topology = vkTopology,
|
||||
.primitiveRestartEnable = primitiveRestartEnabled,
|
||||
.patchControlPoints = static_cast<Uint32>(MG_State::pGLContext->GetPatchVertices()),
|
||||
.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin),
|
||||
.polygonMode = effectivePolygonMode,
|
||||
.cullMode = cullFaceEnabled
|
||||
? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise)
|
||||
@@ -5321,9 +5348,71 @@ void main() {
|
||||
}
|
||||
|
||||
|
||||
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
|
||||
Bool isDefaultFbo) {
|
||||
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
||||
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
|
||||
// GL scissor test is disabled gets the whole framebuffer, which is exactly "the test always
|
||||
// passes" (GL 4.6 core 17.3.2).
|
||||
VkRect2D VulkanRenderer::ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFbo) const {
|
||||
const auto& parameters = MG_State::pGLContext->GetRenderStateParameters();
|
||||
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
|
||||
VkRect2D full{};
|
||||
full.offset = {0, 0};
|
||||
full.extent = {static_cast<Uint32>(extent.x()), static_cast<Uint32>(extent.y())};
|
||||
return full;
|
||||
}
|
||||
const IntVec4& scissorBox = parameters.ScissorBoxes[index];
|
||||
return isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform)
|
||||
: MakeClampedScissorRect(scissorBox, extent);
|
||||
}
|
||||
|
||||
// The wide half of ApplyDynamicDrawStateTail: a pipeline built for a gl_ViewportIndex-writing
|
||||
// program declares viewportCount > 1, and Vulkan then requires that many viewports AND that
|
||||
// many scissors to have been set before the draw
|
||||
// (VUID-vkCmdDraw-viewportCount-03417/-03418). Deliberately unmemoized: only conformance
|
||||
// shaders reach it, the single-element dynamic-state shadow cannot describe an array, and
|
||||
// leaving that shadow invalidated is what makes the next ordinary draw re-push its own
|
||||
// single viewport instead of believing the array's element 0 is already bound.
|
||||
void VulkanRenderer::ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount,
|
||||
const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
Bool isDefaultFbo) {
|
||||
MOBILEGL_ASSERT(viewportCount <= RenderStateParameters::MAX_VIEWPORTS,
|
||||
"ApplyMultiViewportDynamicState: viewportCount=%u exceeds the indexed state width",
|
||||
viewportCount);
|
||||
const Uint32 count = std::min<Uint32>(viewportCount, RenderStateParameters::MAX_VIEWPORTS);
|
||||
|
||||
Array<VkViewport, RenderStateParameters::MAX_VIEWPORTS> viewports{};
|
||||
Array<VkRect2D, RenderStateParameters::MAX_VIEWPORTS> scissors{};
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
viewports[i] = ComputeGLViewport(i, extent, preTransform, isDefaultFbo);
|
||||
scissors[i] = ComputeGLScissorRect(i, extent, preTransform, isDefaultFbo);
|
||||
}
|
||||
vkCmdSetViewport(commandBuffer, 0, count, viewports.data());
|
||||
vkCmdSetScissor(commandBuffer, 0, count, scissors.data());
|
||||
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
shadow.viewportValid = false;
|
||||
shadow.scissorValid = false;
|
||||
shadow.dynamicTailValid = false;
|
||||
}
|
||||
|
||||
void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent,
|
||||
Bool isDefaultFbo, Uint32 viewportCount) {
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
if (viewportCount > 1) {
|
||||
// The other five Apply* still run: blend constants, depth bias, line width and the
|
||||
// stencil masks are not per-viewport and a multi-viewport draw needs them just as
|
||||
// much. Only the viewport/scissor pair takes the array shape.
|
||||
ApplyBlendConstants(frame.commandBuffer);
|
||||
ApplyPolygonOffsetState(frame.commandBuffer);
|
||||
ApplyLineWidthState(frame.commandBuffer);
|
||||
ApplyStencilState(frame.commandBuffer);
|
||||
ApplyMultiViewportDynamicState(frame.commandBuffer, viewportCount, extent,
|
||||
m_swapchainObject.GetPreTransform(), isDefaultFbo);
|
||||
return;
|
||||
}
|
||||
// One compare for the whole tail: see the gate's declaration in
|
||||
// DynamicStateShadow for why (version, extent, default-FBO flag) pins every
|
||||
// input the six Apply* below read.
|
||||
@@ -5342,12 +5431,14 @@ void main() {
|
||||
DynamicStateShadow::DynamicTailKey key;
|
||||
{
|
||||
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
|
||||
key.viewport[0] = p.Viewport.x();
|
||||
key.viewport[1] = p.Viewport.y();
|
||||
key.viewport[2] = p.Viewport.z();
|
||||
key.viewport[3] = p.Viewport.w();
|
||||
key.depthRange[0] = p.DepthRange.x();
|
||||
key.depthRange[1] = p.DepthRange.y();
|
||||
// Viewport 0 and its depth range: ApplyGLViewportState reads exactly those two
|
||||
// (per-index state for indices > 0 is keyed separately, see multiViewportKey below).
|
||||
key.viewport[0] = p.Viewports[0].x();
|
||||
key.viewport[1] = p.Viewports[0].y();
|
||||
key.viewport[2] = p.Viewports[0].z();
|
||||
key.viewport[3] = p.Viewports[0].w();
|
||||
key.depthRange[0] = p.DepthRanges[0].x();
|
||||
key.depthRange[1] = p.DepthRanges[0].y();
|
||||
key.blendColor[0] = p.BlendColor.x();
|
||||
key.blendColor[1] = p.BlendColor.y();
|
||||
key.blendColor[2] = p.BlendColor.z();
|
||||
@@ -5362,11 +5453,11 @@ void main() {
|
||||
key.stencilWriteMask[face] = p.StencilStates[face].WriteMask;
|
||||
key.stencilRef[face] = p.StencilStates[face].Ref;
|
||||
}
|
||||
key.scissorEnabled = p.ScissorTestEnabled;
|
||||
key.scissorBox[0] = p.ScissorBox.x();
|
||||
key.scissorBox[1] = p.ScissorBox.y();
|
||||
key.scissorBox[2] = p.ScissorBox.z();
|
||||
key.scissorBox[3] = p.ScissorBox.w();
|
||||
key.scissorEnabled = (p.ScissorTestEnabledMask & 1u) != 0;
|
||||
key.scissorBox[0] = p.ScissorBoxes[0].x();
|
||||
key.scissorBox[1] = p.ScissorBoxes[0].y();
|
||||
key.scissorBox[2] = p.ScissorBoxes[0].z();
|
||||
key.scissorBox[3] = p.ScissorBoxes[0].w();
|
||||
key.extentX = extent.x();
|
||||
key.extentY = extent.y();
|
||||
key.preTransform = static_cast<Uint32>(preTransform);
|
||||
@@ -5759,7 +5850,7 @@ void main() {
|
||||
const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView);
|
||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer");
|
||||
}
|
||||
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault);
|
||||
ApplyDynamicDrawStateTail(frame, snap.renderPassExtent, snap.drawFboIsDefault, snap.viewportCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6199,7 +6290,8 @@ void main() {
|
||||
MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer");
|
||||
}
|
||||
|
||||
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer());
|
||||
ApplyDynamicDrawStateTail(frame, renderPassEntry->extent, drawFbo->IsDefaultFramebuffer(),
|
||||
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
|
||||
|
||||
// Snapshot the fully resolved configuration for the consecutive-draw
|
||||
// fast path (see TrySetupDrawFastPath).
|
||||
@@ -6218,6 +6310,7 @@ void main() {
|
||||
snap.drawFbo = drawFbo.get();
|
||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||
snap.drawFboIsDefault = drawFboIsDefault;
|
||||
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
|
||||
@@ -7113,7 +7206,7 @@ void main() {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok,
|
||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to TRANSFER_DST",
|
||||
texture.GetExternalIndex());
|
||||
@@ -7231,8 +7324,7 @@ void main() {
|
||||
ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, resource->image, clearLayout, sampledLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||
resource->arrayLayers);
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok,
|
||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
|
||||
texture.GetExternalIndex());
|
||||
@@ -7270,7 +7362,7 @@ void main() {
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
resource->aspect, 0, 1, 1);
|
||||
resource->aspect, 0, 1);
|
||||
MOBILEGL_ASSERT(ok,
|
||||
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST",
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -7321,7 +7413,7 @@ void main() {
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_TRANSFER_READ_BIT,
|
||||
resource->aspect, 0, 1, 1);
|
||||
resource->aspect, 0, 1);
|
||||
MOBILEGL_ASSERT(ok,
|
||||
"MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout",
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -7928,6 +8020,9 @@ void main() {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||
// Both blit regions below name `baseArrayLayer` from their binding, and a layered depth
|
||||
// attachment puts that above 0. These barriers carry a mip range only - their layer
|
||||
// range is every layer (see VkTextureManager::TransitionImageLayout).
|
||||
if (readIsDefaultFbo) {
|
||||
VkImageLayout srcTrackedLayout = srcOriginalLayout;
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
@@ -8530,24 +8625,106 @@ void main() {
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// GL hands CopyImageSubData ONE z/depth pair and lets the texture target decide what it
|
||||
// means. Vulkan splits that meaning across two different fields of VkImageCopy, chosen by
|
||||
// the image type:
|
||||
//
|
||||
// VK_IMAGE_TYPE_3D - slices live on the z axis: srcOffset.z/dstOffset.z select them and
|
||||
// extent.depth counts them. The subresource layer range must stay
|
||||
// (0, 1): Vulkan reads a 3D image as a single layer whose depth is
|
||||
// the mip level's depth (VUID-VkImageCopy-apiVersion-07932/-07933).
|
||||
// everything else - slices live in the array dimension: baseArrayLayer selects them and
|
||||
// layerCount counts them, while offset.z stays 0 and (when neither
|
||||
// endpoint is 3D) extent.depth stays 1.
|
||||
//
|
||||
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
|
||||
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
|
||||
// equal the array side's layerCount".
|
||||
struct CopyImageEndpoint {
|
||||
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
|
||||
Bool slicesAreDepth = false;
|
||||
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
|
||||
Uint32 baseSlice = 0;
|
||||
// Slices this endpoint can address at the selected mip level; the copy range check
|
||||
// needs the level's depth for a 3D image (3D mips shrink in z) and the image's array
|
||||
// size for a layered one (array layers do not shrink).
|
||||
Uint32 availableSlices = 1;
|
||||
|
||||
Uint32 BaseArrayLayer() const { return slicesAreDepth ? 0u : baseSlice; }
|
||||
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
|
||||
};
|
||||
|
||||
Bool TryResolveCopyImageEndpoint(TextureTarget target,
|
||||
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
|
||||
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
|
||||
if (glZ < 0 || glDepth <= 0) {
|
||||
return false;
|
||||
}
|
||||
const Uint32 baseSlice = static_cast<Uint32>(glZ);
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::Texture2DMultisample:
|
||||
// Not layered at all: GL still requires the z/depth pair, and it can only name the
|
||||
// one slice these targets have.
|
||||
outEndpoint = {};
|
||||
return baseSlice == 0 && glDepth == 1;
|
||||
case TextureTarget::Texture3D:
|
||||
outEndpoint.slicesAreDepth = true;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
|
||||
return true;
|
||||
case TextureTarget::Texture2DArray:
|
||||
case TextureTarget::Texture2DMultisampleArray:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
|
||||
// numbers its faces on the same z axis an array texture numbers its layers, so both
|
||||
// arrive as a plain layer range.
|
||||
outEndpoint.slicesAreDepth = false;
|
||||
outEndpoint.baseSlice = baseSlice;
|
||||
outEndpoint.availableSlices = resource.arrayLayers;
|
||||
return true;
|
||||
default:
|
||||
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||
// would have to be remapped against a Vulkan extent that also has to stay height 1
|
||||
// for a VK_IMAGE_TYPE_1D image; GL_TEXTURE_BUFFER has no image at all. Declined
|
||||
// rather than mis-addressed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
MOBILEGL_ASSERT(srcWidth > 0 && srcHeight > 0 && srcDepth > 0,
|
||||
"CopyImageSubData requires positive copy dimensions.");
|
||||
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
|
||||
"CopyImageSubData requires valid source and destination textures.");
|
||||
// The frontend already declines a zero or negative extent, so anything else here is a
|
||||
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
|
||||
// zero extent.depth is as invalid as a zero width.
|
||||
if (srcWidth <= 0 || srcHeight <= 0 || srcDepth <= 0) {
|
||||
MGLOG_E_ONCE("%s: non-positive copy extent %dx%dx%d; declining the copy", __func__, srcWidth, srcHeight,
|
||||
srcDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||
MOBILEGL_ASSERT(srcTextureTarget == TextureTarget::Texture2D && dstTextureTarget == TextureTarget::Texture2D,
|
||||
"CopyImageSubData currently only supports GL_TEXTURE_2D sources and destinations.");
|
||||
MOBILEGL_ASSERT(srcDepth == 1 && srcZ == 0 && dstZ == 0,
|
||||
"CopyImageSubData currently only supports single-layer 2D copies.");
|
||||
MOBILEGL_ASSERT(srcTexture.get() != dstTexture.get(),
|
||||
"CopyImageSubData does not support in-place texture copies yet.");
|
||||
// Both endpoints of a same-image copy would have to share one VkImageLayout, so the
|
||||
// TRANSFER_SRC/TRANSFER_DST pair below cannot express it (it needs VK_IMAGE_LAYOUT_GENERAL
|
||||
// and an overlap check). Refused outright, and refused for real rather than through an
|
||||
// assertion the release build drops: recording the pair anyway is a validation error and,
|
||||
// on a tiler, a copy whose source has already been overwritten.
|
||||
if (srcTexture.get() == dstTexture.get()) {
|
||||
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
|
||||
srcTexture->GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!frame.isCommandRecording) {
|
||||
@@ -8616,29 +8793,81 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The supported envelope, replacing the "GL_TEXTURE_2D only" assertion that used to stand
|
||||
// here: every target whose slices this function can address on one of the two Vulkan axes.
|
||||
// A refusal has to be a real decline, not an assertion - the assertion compiled to nothing
|
||||
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
|
||||
CopyImageEndpoint srcEndpoint;
|
||||
CopyImageEndpoint dstEndpoint;
|
||||
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
|
||||
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
|
||||
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
|
||||
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
|
||||
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
|
||||
return;
|
||||
}
|
||||
// The slice half of the region-bounds guard above. A layered endpoint's bound is NOT the
|
||||
// mip-0 2D extent: an array texture is bounded by its layer count (which no mip level
|
||||
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
|
||||
// both come from the endpoint that resolved them.
|
||||
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
|
||||
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
|
||||
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
|
||||
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
|
||||
"declining the copy",
|
||||
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
|
||||
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
|
||||
__func__, srcTexture->GetExternalIndex());
|
||||
// A clear still parked on the destination would otherwise materialize AFTER this copy and
|
||||
// wipe the texels it just wrote.
|
||||
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
|
||||
__func__, dstTexture->GetExternalIndex());
|
||||
|
||||
const VkImageLayout srcOriginalLayout = srcResource->layout;
|
||||
const VkImageLayout dstOriginalLayout = dstResource->layout;
|
||||
MOBILEGL_ASSERT(srcOriginalLayout != VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
"CopyImageSubData source image has undefined layout.");
|
||||
const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED
|
||||
? ((copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
|
||||
// SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are
|
||||
// undefined by the same spec sentence that lets the application ask. Both sides therefore
|
||||
// take the same shape - transition the whole image out of UNDEFINED and settle it on a
|
||||
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
|
||||
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
|
||||
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
return originalLayout;
|
||||
}
|
||||
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||
: dstOriginalLayout;
|
||||
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
};
|
||||
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
|
||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
|
||||
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||
VkImageLayout srcCopyLayout = srcOriginalLayout;
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||
// The barriers below name a MIP range only. Their layer range is not a parameter:
|
||||
// TransitionImageLayout always covers every layer of the image, which is a superset of the
|
||||
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||
srcCopyLayout = srcResource->layout;
|
||||
} else {
|
||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||
}
|
||||
|
||||
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags dstAccessMask = 0;
|
||||
@@ -8649,7 +8878,7 @@ void main() {
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||
dstCopyLayout = dstResource->layout;
|
||||
} else {
|
||||
@@ -8660,18 +8889,31 @@ void main() {
|
||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
||||
}
|
||||
|
||||
// The GL slice count reaches Vulkan on the layer axis of whichever endpoint is NOT 3D, and
|
||||
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
|
||||
// single layer (0, 1) and its slices are counted by the depth of the copy extent. With two
|
||||
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
|
||||
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
|
||||
VkImageCopy copyRegion{};
|
||||
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
||||
copyRegion.srcSubresource.baseArrayLayer = 0;
|
||||
copyRegion.srcSubresource.layerCount = 1;
|
||||
copyRegion.srcOffset = {srcX, srcY, 0};
|
||||
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
|
||||
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
|
||||
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
||||
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
||||
copyRegion.dstSubresource.baseArrayLayer = 0;
|
||||
copyRegion.dstSubresource.layerCount = 1;
|
||||
copyRegion.dstOffset = {dstX, dstY, 0};
|
||||
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight), 1};
|
||||
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
|
||||
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
|
||||
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
|
||||
copyCrossesDepthAxis ? copySliceCount : 1u};
|
||||
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
|
||||
"z=%d) extent=[%d x %d x %u]",
|
||||
MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), srcMipLevel,
|
||||
copyRegion.srcSubresource.baseArrayLayer, copyRegion.srcSubresource.layerCount,
|
||||
copyRegion.srcOffset.z, MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), dstMipLevel,
|
||||
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
|
||||
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
|
||||
vkCmdCopyImage(frame.commandBuffer,
|
||||
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
@@ -8679,12 +8921,21 @@ void main() {
|
||||
|
||||
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcRestoreAccessMask = 0;
|
||||
GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcOriginalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||
srcResource->aspect, 0, srcResource->mipLevels);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||
} else {
|
||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||
}
|
||||
|
||||
VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags dstRestoreAccessMask = 0;
|
||||
@@ -8694,7 +8945,7 @@ void main() {
|
||||
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||
dstResource->aspect, 0, dstResource->mipLevels, dstResource->arrayLayers);
|
||||
dstResource->aspect, 0, dstResource->mipLevels);
|
||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||
} else {
|
||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||
@@ -8859,6 +9110,9 @@ void main() {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask);
|
||||
// The copy below reads `srcBinding.baseArrayLayer`, which for a glFramebufferTextureLayer
|
||||
// attachment is any layer of the array - the barrier covers all of them (see
|
||||
// VkTextureManager::TransitionImageLayout), so the layer being read is one it moved.
|
||||
if (readIsDefaultFbo) {
|
||||
VkImageLayout trackedLayout = srcOriginalLayout;
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
@@ -9661,6 +9915,8 @@ void main() {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags srcAccessMask = 0;
|
||||
GetImageTransitionSourceState(originalLayout, srcStageMask, srcAccessMask);
|
||||
// The copy below reads EVERY layer of the level, which is exactly the range
|
||||
// TransitionImageLayout barriers cover.
|
||||
Bool ok = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
@@ -9793,7 +10049,7 @@ void main() {
|
||||
Bool transitioned = VkTextureManager::TransitionImageLayout(
|
||||
frame.commandBuffer, resource->image, resource->layout, finalLayout,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
0, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
|
||||
0, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(transitioned, "GenerateMipmap: failed to transition uninitialized mip chain");
|
||||
return;
|
||||
}
|
||||
@@ -12051,6 +12307,28 @@ void main() {
|
||||
m_fillModeNonSolidFeatureEnabled = deviceFeatures.fillModeNonSolid == VK_TRUE;
|
||||
deviceFeatures.dualSrcBlend = supportedDeviceFeatures.dualSrcBlend;
|
||||
m_dualSrcBlendFeatureEnabled = deviceFeatures.dualSrcBlend == VK_TRUE;
|
||||
// ARB_viewport_array rasterization. Without multiViewport a pipeline may declare exactly
|
||||
// one viewport (VUID-VkPipelineViewportStateCreateInfo-viewportCount-01216), so a shader's
|
||||
// gl_ViewportIndex can only ever select viewport 0 and the other fifteen rectangles are
|
||||
// state with nowhere to go. The GL state stays 16 wide either way - GL 4.3 core requires
|
||||
// MAX_VIEWPORTS >= 16 and that is a frontend promise, not a device one; this gate decides
|
||||
// only whether a DRAW can rasterize into more than one of them.
|
||||
deviceFeatures.multiViewport = supportedDeviceFeatures.multiViewport;
|
||||
m_multiViewportFeatureEnabled = deviceFeatures.multiViewport == VK_TRUE;
|
||||
m_maxRasterizableViewports =
|
||||
m_multiViewportFeatureEnabled
|
||||
? std::min<Uint32>(RenderStateParameters::MAX_VIEWPORTS,
|
||||
std::max<Uint32>(m_physicalDevice.properties.limits.maxViewports, 1u))
|
||||
: 1u;
|
||||
MGLOG_I("Vulkan: multiViewport %s; rasterizable viewports=%u (device limit %u, GL state width %u)",
|
||||
m_multiViewportFeatureEnabled ? "enabled" : "UNAVAILABLE", m_maxRasterizableViewports,
|
||||
m_physicalDevice.properties.limits.maxViewports,
|
||||
static_cast<Uint32>(RenderStateParameters::MAX_VIEWPORTS));
|
||||
if (!m_multiViewportFeatureEnabled) {
|
||||
MGLOG_W("Vulkan: the device does not support the multiViewport feature; gl_ViewportIndex will always "
|
||||
"select viewport 0 and per-viewport scissor/depth-range state past index 0 cannot be "
|
||||
"rasterized (the state itself is still stored and queryable)");
|
||||
}
|
||||
deviceFeatures.logicOp = supportedDeviceFeatures.logicOp;
|
||||
deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance;
|
||||
deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance;
|
||||
|
||||
@@ -547,6 +547,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
|
||||
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
|
||||
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
|
||||
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
|
||||
Bool m_multiViewportFeatureEnabled = false;
|
||||
Uint32 m_maxRasterizableViewports = 1;
|
||||
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||
@@ -830,6 +836,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
|
||||
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
|
||||
// of which the programLifetimeId/programVersion guards above already pin - carried
|
||||
// here so the fast path does not re-fetch the program object to re-derive it.
|
||||
Uint32 viewportCount = 1;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||
@@ -1120,7 +1131,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||
// viewportCount is the bound pipeline's declared viewport count: 1 for every program that
|
||||
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
|
||||
// rasterizable viewport count, which takes the unmemoized array path.
|
||||
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
|
||||
Uint32 viewportCount = 1);
|
||||
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
|
||||
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
|
||||
// How many viewports a draw with this program rasterizes into: 1 unless the program
|
||||
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
|
||||
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
|
||||
// disagree.
|
||||
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
|
||||
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
|
||||
}
|
||||
|
||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
|
||||
@@ -969,14 +969,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/ErrorCodeConverter.h>
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
@@ -27,6 +28,11 @@
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
// Declared rather than #included from GL_RenderState.h on purpose: that header also declares
|
||||
// a free function named BlendEquation, which would hide the ::MobileGL::BlendEquation enum
|
||||
// this file's blend-state queries name unqualified.
|
||||
GLboolean IsEnabledi(GLenum target, GLuint index);
|
||||
|
||||
namespace {
|
||||
enum class IndexedBufferQueryKind {
|
||||
Binding,
|
||||
@@ -339,26 +345,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||
}
|
||||
|
||||
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||
// scissor box and one depth range, so every in-range index answers with that single
|
||||
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||
// has no case for these, so routing them through it returned zeros.
|
||||
// The ARB_viewport_array indexed rectangles. Each of these is genuinely per-viewport
|
||||
// frontend state (RenderStateParameters::Viewports / ScissorBoxes / DepthRanges), so the
|
||||
// indexed getters must read the indexed storage - the generic path at the bottom of
|
||||
// GetIntegeri_v is a raw backend passthrough that has no case for them and returned
|
||||
// zeros, and routing them to the NON-indexed getter (what this used to do) answered every
|
||||
// index with viewport 0's value, which is what
|
||||
// KHR-GL43.viewport_array.{viewport,scissor,depth_range}_api caught.
|
||||
Bool IsIndexedViewportQuery(GLenum target) {
|
||||
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||
// Component count of an indexed viewport-array query, so every width of getter writes the
|
||||
// caller's whole buffer instead of just element 0 (GL 4.6 core 22.1).
|
||||
GLsizei IndexedViewportQueryComponents(GLenum target) {
|
||||
return target == GL_DEPTH_RANGE ? 2 : 4;
|
||||
}
|
||||
|
||||
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it. The bound is
|
||||
// the frontend's own state width, which is also exactly what GL_MAX_VIEWPORTS reports -
|
||||
// taking it from the backend caps instead would let a device limit of 1 (a Vulkan device
|
||||
// without the multiViewport feature) make index 1 illegal even though the state exists.
|
||||
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||
GLint maxViewports = 0;
|
||||
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The indexed viewport/scissor/depth-range state as floats, which is the widest lossless
|
||||
// shape MobileGL stores (the viewport really is float state; the scissor box is integral
|
||||
// and well inside float's exact range, and every depth range is in [0, 1]). Every indexed
|
||||
// getter width funnels through this so they can never disagree with each other.
|
||||
void ReadIndexedViewportStateFloat(GLenum target, GLuint index, GLfloat* out) {
|
||||
switch (target) {
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(index);
|
||||
out[0] = viewport.x();
|
||||
out[1] = viewport.y();
|
||||
out[2] = viewport.z();
|
||||
out[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_SCISSOR_BOX: {
|
||||
const IntVec4& box = MG_State::pGLContext->GetScissorBoxIndexed(index);
|
||||
out[0] = static_cast<GLfloat>(box.x());
|
||||
out[1] = static_cast<GLfloat>(box.y());
|
||||
out[2] = static_cast<GLfloat>(box.z());
|
||||
out[3] = static_cast<GLfloat>(box.w());
|
||||
return;
|
||||
}
|
||||
case GL_DEPTH_RANGE: {
|
||||
const FloatVec2& range = MG_State::pGLContext->GetDepthRangeIndexed(index);
|
||||
out[0] = range.x();
|
||||
out[1] = range.y();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "ReadIndexedViewportStateFloat: unexpected target 0x%x",
|
||||
static_cast<Uint32>(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||
@@ -629,6 +679,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.ViewportBoundsRangeMax;
|
||||
return;
|
||||
}
|
||||
// Viewport 0's rectangle, verbatim. Falling through to the integer width below would
|
||||
// round the fractional rectangle a glViewportIndexedf(0, ...) is allowed to set, and
|
||||
// glGetFloatv(GL_VIEWPORT) is a lossless query of float state.
|
||||
case GL_VIEWPORT: {
|
||||
const FloatVec4& viewport = MG_State::pGLContext->GetViewportIndexed(0);
|
||||
params[0] = viewport.x();
|
||||
params[1] = viewport.y();
|
||||
params[2] = viewport.z();
|
||||
params[3] = viewport.w();
|
||||
return;
|
||||
}
|
||||
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
|
||||
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
|
||||
@@ -792,15 +853,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// GL 4.6 core 22.1: an indexed query answers EVERY indexed state, and GL_SCISSOR_TEST is
|
||||
// indexed by viewport just like GL_BLEND is by draw buffer. Without this the integer
|
||||
// width fell through to the backend passthrough and answered GL_INVALID_ENUM, which is
|
||||
// the sticky error KHR-GL43.viewport_array.queries trips over at its next error check.
|
||||
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
|
||||
*data = IsEnabledi(target, index);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||
// viewport and one scissor box, so every in-range index reports that one.
|
||||
// (gl4cMultiBindTests and the viewport_array group both do).
|
||||
case GL_VIEWPORT:
|
||||
case GL_SCISSOR_BOX:
|
||||
case GL_DEPTH_RANGE: {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetIntegerv(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
// Round, not truncate: glGetIntegerv on floating-point state rounds to nearest
|
||||
// (GL 4.6 core 22.2), so a 255.875-wide viewport reads back as 256 and not 255.
|
||||
data[i] = static_cast<GLint>(std::lround(values[i]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 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:
|
||||
@@ -927,7 +1005,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetFloatv(target, data);
|
||||
// Verbatim, NOT via the integer width: the viewport is float state and
|
||||
// KHR-GL43.viewport_array.viewport_api compares the read-back with ==, so a
|
||||
// glViewportIndexedf(i, 0.125f, ...) has to come back as 0.125f exactly.
|
||||
ReadIndexedViewportStateFloat(target, index, data);
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -944,7 +1025,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
if (IsIndexedViewportQuery(target)) {
|
||||
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||
GetDoublev(target, data);
|
||||
GLfloat values[4] = {};
|
||||
ReadIndexedViewportStateFloat(target, index, values);
|
||||
const GLsizei components = IndexedViewportQueryComponents(target);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLdouble>(values[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
GLint ints[4] = {};
|
||||
@@ -1020,7 +1106,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// frontend-only value simply is not in the driver's table.
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = static_cast<GLint64>(values[0]);
|
||||
// The viewport-array rectangles are the only multi-component indexed state here; every
|
||||
// other pname is scalar, so widening element 0 alone would silently truncate them.
|
||||
const GLsizei components = IsIndexedViewportQuery(target) ? IndexedViewportQueryComponents(target) : 1;
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = static_cast<GLint64>(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetInteger64v(GLenum pname, GLint64* params) {
|
||||
@@ -2192,7 +2283,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
params[1] = dynamicParameters.MaxViewportHeight;
|
||||
break;
|
||||
case GL_MAX_VIEWPORTS:
|
||||
*params = dynamicParameters.MaxViewports;
|
||||
// The frontend's own state width, not the backend's device limit. GL 4.3 core
|
||||
// requires MAX_VIEWPORTS >= 16 and every indexed viewport entry point validates
|
||||
// against RenderStateParameters::MAX_VIEWPORTS, so reporting anything else would
|
||||
// either advertise viewports the state cannot hold or reject indices it can. A
|
||||
// Vulkan device without the multiViewport feature reports maxViewports == 1, which
|
||||
// limits what can be RASTERIZED to more than one rectangle (see the multiViewport
|
||||
// gate in VulkanRenderer), not what the GL state can hold; caps.MaxViewports keeps
|
||||
// carrying that device number for exactly that decision.
|
||||
*params = static_cast<GLint>(RenderStateParameters::MAX_VIEWPORTS);
|
||||
break;
|
||||
case GL_MINOR_VERSION:
|
||||
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
|
||||
|
||||
@@ -20,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
if (target != GL_BLEND) {
|
||||
// GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
|
||||
// draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
|
||||
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
|
||||
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
|
||||
GLuint limit = 0;
|
||||
const char* indexName = nullptr;
|
||||
switch (target) {
|
||||
case GL_BLEND:
|
||||
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
|
||||
indexName = "Buffer";
|
||||
break;
|
||||
case GL_SCISSOR_TEST:
|
||||
limit = RenderStateParameters::MAX_VIEWPORTS;
|
||||
indexName = "Viewport";
|
||||
break;
|
||||
default:
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Only GL_BLEND is supported for indexed capability state."));
|
||||
"Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
|
||||
"capability state."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
|
||||
if (index >= limit) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", functionName,
|
||||
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " +
|
||||
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + "."));
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
String(indexName) + " index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " + std::to_string(limit - 1) +
|
||||
"."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array parameter validation ------------------
|
||||
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
|
||||
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
|
||||
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
|
||||
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
|
||||
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
|
||||
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Viewport index " + std::to_string(index) +
|
||||
" is out of range. Max supported is " +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
|
||||
if (count < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
|
||||
return false;
|
||||
}
|
||||
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
|
||||
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
|
||||
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
|
||||
if (last > RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"first (" + std::to_string(first) + ") + count (" +
|
||||
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
|
||||
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
|
||||
if (width >= T(0) && height >= T(0)) return true;
|
||||
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
|
||||
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
|
||||
// it passes a full 16-element array with exactly one negative extent and then asserts the
|
||||
// error queue holds exactly one entry.
|
||||
template <typename T>
|
||||
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||
"Width and height must be non-negative (element " + std::to_string(i) +
|
||||
")."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
|
||||
if (v != nullptr) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
|
||||
return false;
|
||||
}
|
||||
|
||||
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
|
||||
::MobileGL::BlendEquation& outEquation) {
|
||||
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
|
||||
@@ -93,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
|
||||
}
|
||||
|
||||
// ------------------ ARB_viewport_array setters ------------------
|
||||
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
|
||||
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
|
||||
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
|
||||
}
|
||||
|
||||
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
|
||||
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
|
||||
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
|
||||
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
|
||||
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
|
||||
}
|
||||
|
||||
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
|
||||
if (count == 0) return;
|
||||
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
|
||||
|
||||
for (GLsizei i = 0; i < count; ++i) {
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
first + static_cast<GLuint>(i),
|
||||
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
|
||||
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
|
||||
}
|
||||
}
|
||||
|
||||
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
|
||||
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetDepthRangeIndexed(
|
||||
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
|
||||
}
|
||||
|
||||
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
Bool applyFront = false;
|
||||
Bool applyBack = false;
|
||||
@@ -175,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
if (width < 0 || height < 0) {
|
||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
|
||||
"Width abd height must be non-negative."));
|
||||
return;
|
||||
}
|
||||
if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
|
||||
|
||||
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
|
||||
}
|
||||
@@ -336,7 +475,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
GLboolean IsEnabledi_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
|
||||
return GL_FALSE;
|
||||
}
|
||||
|
||||
@@ -392,7 +531,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
GLint values[4] = {};
|
||||
GetIntegeri_v(target, index, values);
|
||||
*data = values[0] != 0 ? GL_TRUE : GL_FALSE;
|
||||
// The ARB_viewport_array rectangles are the only multi-component indexed state that
|
||||
// reaches here; writing element 0 alone would leave the caller's other three untouched.
|
||||
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
|
||||
? 4
|
||||
: (target == GL_DEPTH_RANGE ? 2 : 1);
|
||||
for (GLsizei i = 0; i < components; ++i) {
|
||||
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
GLboolean IsEnabled_State(GLenum cap) {
|
||||
@@ -725,7 +871,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Disablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,7 +889,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void Enablei_State(GLenum target, GLuint index) {
|
||||
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) {
|
||||
if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,6 +943,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Viewport_State(x, y, width, height);
|
||||
}
|
||||
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
|
||||
ViewportArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
|
||||
ViewportIndexedf_State(index, x, y, w, h);
|
||||
}
|
||||
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
|
||||
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
|
||||
// must be one GL_INVALID_VALUE, not a null dereference.
|
||||
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
|
||||
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
|
||||
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
|
||||
ScissorArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
|
||||
ScissorIndexed_State(index, left, bottom, width, height);
|
||||
}
|
||||
|
||||
void ScissorIndexedv(GLuint index, const GLint* v) {
|
||||
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
|
||||
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
|
||||
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
|
||||
DepthRangeArrayv_State(first, count, v);
|
||||
}
|
||||
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
|
||||
DepthRangeIndexed_State(index, n, f);
|
||||
}
|
||||
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
|
||||
StencilOpSeparate_State(face, sfail, dpfail, dppass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void Enablei(GLenum target, GLuint index);
|
||||
void BlendFunc(GLenum sfactor, GLenum dfactor);
|
||||
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
|
||||
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
|
||||
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
|
||||
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
|
||||
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
|
||||
void ViewportIndexedfv(GLuint index, const GLfloat* v);
|
||||
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
|
||||
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
|
||||
void ScissorIndexedv(GLuint index, const GLint* v);
|
||||
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
|
||||
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
|
||||
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
|
||||
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
|
||||
void StencilMaskSeparate(GLenum face, GLuint mask);
|
||||
|
||||
@@ -3382,12 +3382,84 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
|
||||
// enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
|
||||
// so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
|
||||
Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
|
||||
switch (target) {
|
||||
case GL_RENDERBUFFER:
|
||||
case GL_TEXTURE_1D:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("{} is not a target glCopyImageSubData accepts as the {}.",
|
||||
MG_Util::ConvertGLEnumToString(target), endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureUploadTarget uploadTarget, GLint level) {
|
||||
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
|
||||
if (!mipmapTexture) return textureObject->GetBaseSize();
|
||||
return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
|
||||
}
|
||||
|
||||
// glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
|
||||
// spells the failure INVALID_VALUE - "if either name does not correspond to a valid
|
||||
// object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
|
||||
// the ~30 entry points that reach it through a BOUND object (where the name was never
|
||||
// in question and the fault is the binding), so this is a local rule rather than a
|
||||
// change to the helper.
|
||||
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
const char* endpointName) {
|
||||
if (textureObject) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} name does not correspond to an existing image object.", endpointName)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
|
||||
// does not match the object INVALID_ENUM, where the shared uniformity helper records
|
||||
// INVALID_OPERATION for the upload paths that share it.
|
||||
Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
TextureTarget target, const char* endpointName) {
|
||||
if (!textureObject || textureObject->GetTarget() == target) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
||||
std::format("The {} target {} does not match the target the object was created with ({}).",
|
||||
endpointName, MG_Util::ConvertTextureTargetToString(target),
|
||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
if (!TextureImpl::ValidateTextureObject(srcTexture) || !TextureImpl::ValidateTextureObject(dstTexture)) {
|
||||
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||
return false;
|
||||
}
|
||||
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||
@@ -3396,8 +3468,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureTargetUniformity(srcTexture, srcTextureTarget) ||
|
||||
!TextureImpl::ValidateTextureTargetUniformity(dstTexture, dstTextureTarget)) {
|
||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||
// 18.3.2 does not accept them here - only the eleven whole-image targets do.
|
||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||
@@ -3425,7 +3502,44 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!TextureImpl::ValidateBaseInternalFormatMatch(srcTexture->GetFormat(), dstTexture->GetFormat())) {
|
||||
// A multisample image can only be copied to one with the same sample count, and a
|
||||
// single-sample image reports zero - so this one comparison is also what rejects
|
||||
// copying between a multisample target and a non-multisample one.
|
||||
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("The two images have different sample counts ({} vs. {}).",
|
||||
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||
return false;
|
||||
}
|
||||
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
||||
// and no defined storage to copy into.
|
||||
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", __func__,
|
||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||
return false;
|
||||
}
|
||||
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||
return false;
|
||||
}
|
||||
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -5600,10 +5714,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||
auto srcTexture = GetTextureObjectByName(srcName, __func__);
|
||||
auto dstTexture = GetTextureObjectByName(dstName, __func__);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, dstTexture, dstTarget, dstLevel,
|
||||
srcWidth, srcHeight, srcDepth)) {
|
||||
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||
// SharedPtr, and let the validator record the error this entry point owes.
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
|
||||
MG_State::pGLContext->GetTextureObject(srcName);
|
||||
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
|
||||
MG_State::pGLContext->GetTextureObject(dstName);
|
||||
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
|
||||
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||
return;
|
||||
}
|
||||
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
Bool ValidateTextureTarget(TextureTarget target) {
|
||||
@@ -515,26 +516,86 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) {
|
||||
const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1);
|
||||
const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2);
|
||||
if (unsizedFormat1 != unsizedFormat2) {
|
||||
// The 3-argument GenericErrorInfo constructor used to be spelled as a single
|
||||
// std::format() call whose format string was the component name, so every
|
||||
// diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then
|
||||
// hand over component/function/message separately.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
|
||||
CopyImageTexelBlock block{};
|
||||
if (compressedFormat != GL_NONE) {
|
||||
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
|
||||
if (info.blockByteSize != 0) {
|
||||
block.byteSize = info.blockByteSize;
|
||||
block.blockWidth = info.blockWidth;
|
||||
block.blockHeight = info.blockHeight;
|
||||
block.compressed = true;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
// The size MobileGL actually stores a texel of this format in, which for every format GL
|
||||
// gives a required size is that required size. The handful of legacy formats GL leaves
|
||||
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
|
||||
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
|
||||
// copy between them would in fact preserve the bytes.
|
||||
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
|
||||
return block;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock) {
|
||||
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
"A copied image has no storage whose texel size is known."));
|
||||
return false;
|
||||
}
|
||||
if (srcBlock.byteSize != dstBlock.byteSize) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch",
|
||||
std::format("The base internal format of the two formats do not match ({} vs. {})",
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1),
|
||||
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2))));
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
|
||||
"formats are not copy-compatible.",
|
||||
srcBlock.byteSize, dstBlock.byteSize)));
|
||||
return false;
|
||||
}
|
||||
// Two compressed images additionally have to agree on the SHAPE of the block, not only
|
||||
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
|
||||
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
|
||||
if (srcBlock.compressed && dstBlock.compressed &&
|
||||
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
|
||||
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
|
||||
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
|
||||
dstBlock.blockHeight)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName) {
|
||||
if (!block.compressed) return true;
|
||||
const Int blockWidth = static_cast<Int>(block.blockWidth);
|
||||
const Int blockHeight = static_cast<Int>(block.blockHeight);
|
||||
if (blockWidth <= 1 && blockHeight <= 1) return true;
|
||||
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
|
||||
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
|
||||
// whole even when the last block is partial.
|
||||
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
|
||||
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
|
||||
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
|
||||
if (originAligned && widthOk && heightOk) return true;
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
|
||||
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
|
||||
"grid of a {} x {} image.",
|
||||
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
|
||||
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
|
||||
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
|
||||
|
||||
@@ -50,8 +50,32 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
||||
TextureTarget target);
|
||||
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||
// Exact base-format equality - what glCopyImageSubData's format compatibility needs.
|
||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||
// The texel block of one glCopyImageSubData endpoint, resolved to the two things the
|
||||
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
|
||||
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
|
||||
// a blocked image" from "uncompressed, and so it is measured in texels".
|
||||
struct CopyImageTexelBlock {
|
||||
SizeT byteSize = 0;
|
||||
Uint blockWidth = 1;
|
||||
Uint blockHeight = 1;
|
||||
Bool compressed = false;
|
||||
};
|
||||
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
|
||||
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
|
||||
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
|
||||
// alone can no longer tell a BPTC image from the RGBA8 backing it.
|
||||
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
|
||||
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
|
||||
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
|
||||
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
|
||||
// pairs with an uncompressed one whose texel is as big as the compressed block.
|
||||
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
|
||||
const CopyImageTexelBlock& dstBlock);
|
||||
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
|
||||
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
|
||||
// the edge of the image.
|
||||
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
|
||||
Int imageWidth, Int imageHeight, const char* endpointName);
|
||||
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
|
||||
// the requested internalformat asks for, but may supply more.
|
||||
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
|
||||
|
||||
@@ -63,6 +63,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/ViewportArrayScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
@@ -79,6 +80,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -199,5 +199,61 @@ namespace MGITest {
|
||||
"derived component limits are computed in";
|
||||
}
|
||||
|
||||
// ARB_viewport_array's own limits. They are advertised from three different places -
|
||||
// GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the
|
||||
// subpixel bits from the backend caps table - and each backend fills that table from a
|
||||
// different source, so all three are checked on both lanes.
|
||||
//
|
||||
// GL_VIEWPORT_BOUNDS_RANGE is the one that shipped wrong: GLES has no such query, the
|
||||
// DirectGLES loader's glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE) therefore raised
|
||||
// GL_INVALID_ENUM and left the probe's zero-initialized array in place, and MobileGL
|
||||
// advertised [0, 0] - a range that admits no viewport origin at all, and the check that
|
||||
// kept KHR-GL43.viewport_array.queries red on Espryt after the indexed-state work.
|
||||
TEST_F(AdvertisedLimitsScenario, ViewportArrayLimitsMeetTheirGL43Floors) {
|
||||
GLint maxViewports = -1;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core table 23.53 sets the MAX_VIEWPORTS minimum at 16";
|
||||
EXPECT_LE(maxViewports, 256) << "one viewport rectangle of indexed state is allocated per advertised "
|
||||
"viewport, and the CTS sizes its arrays off this number";
|
||||
|
||||
GLfloat boundsRange[2] = {1.0f, -1.0f};
|
||||
glGetFloatv(GL_VIEWPORT_BOUNDS_RANGE, boundsRange);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_LE(boundsRange[0], -32768.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
EXPECT_GE(boundsRange[1], 32767.0f)
|
||||
<< "GL 4.6 core table 23.60 sets the VIEWPORT_BOUNDS_RANGE minimum at [-32768, 32767]; got ["
|
||||
<< boundsRange[0] << ", " << boundsRange[1] << "]";
|
||||
|
||||
// KNOWN INFIDELITY, pinned here rather than hidden. MobileGL reports the driver's own
|
||||
// VIEWPORT_SUBPIXEL_BITS (4 on llvmpipe, i.e. 1/16-pixel viewport precision), but the
|
||||
// float viewport rectangle glViewportIndexedf stores is snapped to integers on its
|
||||
// way to both backends (ComputeGLViewport, DirectGLES SyncRenderState). The STATE
|
||||
// round trip is exact - which is all KHR-GL43.viewport_array.viewport_api checks, and
|
||||
// all this cluster set out to fix - so the gap is in rasterization only: a fractional
|
||||
// viewport origin rasterizes as if it had been rounded. Nothing in the suite or in
|
||||
// Minecraft sets one. Only the spec floor is asserted; tightening this to EQ(0) would
|
||||
// mean advertising no subpixel precision at all, which is a separate decision about a
|
||||
// limit MobileGL currently passes through from the driver.
|
||||
GLint subpixelBits = -1;
|
||||
glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &subpixelBits);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
EXPECT_GE(subpixelBits, 0) << "GL 4.6 core table 23.60: VIEWPORT_SUBPIXEL_BITS has a minimum of 0, and "
|
||||
"a negative value is what a sign-flipped uint32 looks like";
|
||||
|
||||
GLint viewportDims[2] = {-1, -1};
|
||||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewportDims);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
GLint maxRenderbufferSize = -1;
|
||||
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
// GL 4.6 core 13.6.1: MAX_VIEWPORT_DIMS must be at least as large as the largest
|
||||
// renderable surface, or a full-size framebuffer could not be fully viewported.
|
||||
EXPECT_GE(viewportDims[0], maxRenderbufferSize);
|
||||
EXPECT_GE(viewportDims[1], maxRenderbufferSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageLayeredScenario.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
|
||||
//
|
||||
// Scenario - glCopyImageSubData MOVES EVERY SLICE IT WAS ASKED FOR, NOT JUST SLICE 0.
|
||||
//
|
||||
// KHR-GL43.copy_image.functional_* copies a whole 12-layer region in one call whenever both
|
||||
// endpoints are layered, i.e. for the four target pairs 2d_array->2d_array, 2d_array->3d,
|
||||
// 3d->2d_array and 3d->3d. DirectVulkan built its VkImageCopy with baseArrayLayer 0, layerCount 1
|
||||
// and srcOffset.z 0 no matter what the call asked for, so slice 0 landed correctly and slices 1..N
|
||||
// were never written - 64 conformance cases (16 compatible format pairs x those 4 pairs) failing
|
||||
// with "first mismatch at [x, y, 1]", the first texel of the first slice the copy skipped.
|
||||
//
|
||||
// The reason one hardcode covered both shapes wrongly is that GL states a layered copy ONE way -
|
||||
// srcZ/dstZ and srcDepth - while Vulkan states it two ways and picks by image type:
|
||||
//
|
||||
// GL_TEXTURE_3D -> VK_IMAGE_TYPE_3D: slices are z, so srcOffset.z/dstOffset.z select them
|
||||
// and extent.depth counts them; the layer range must stay (0, 1).
|
||||
// GL_TEXTURE_2D_ARRAY -> VK_IMAGE_TYPE_2D: slices are array layers, so baseArrayLayer selects
|
||||
// them and layerCount counts them; offset.z stays 0.
|
||||
//
|
||||
// A mixed pair is legal (maintenance1, core in Vulkan 1.1) but only when the counts correspond:
|
||||
// the 3D side's extent.depth has to equal the array side's layerCount. So the four pairs below are
|
||||
// four DIFFERENT VkImageCopy shapes, not one shape with different arguments, which is why one
|
||||
// scenario per pair is the coverage that matters here.
|
||||
//
|
||||
// Every case also asserts the slices OUTSIDE the copied range still hold their fill. A backend
|
||||
// that "fixed" the miss by copying the whole image regardless of srcZ/srcDepth would pass a
|
||||
// slices-landed check and fail this one.
|
||||
//
|
||||
// The verification path is an FBO attachment per slice plus glReadPixels, not glGetTexImage: it is
|
||||
// the readback both backends share, and glFramebufferTextureLayer names an array layer and a 3D
|
||||
// slice through the same call, so the two texture kinds are read back identically.
|
||||
//
|
||||
// DirectGLES is the control - it forwards to the driver's own glCopyImageSubData - so a failure on
|
||||
// both backends means the scenario is wrong, and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 4;
|
||||
constexpr int kHeight = 4;
|
||||
// Six is enough for a copy that starts and ends away from both edges of both endpoints
|
||||
// while still leaving untouched slices on either side to assert against.
|
||||
constexpr int kSlices = 6;
|
||||
|
||||
struct Rgba8 {
|
||||
GLubyte r = 0, g = 0, b = 0, a = 0;
|
||||
|
||||
bool operator==(const Rgba8& other) const {
|
||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||
}
|
||||
};
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-slice constants, uniform within a slice. A uniform fill is deliberate: the defect is
|
||||
// in which SLICE the copy addresses, and a value that also varied within the slice would
|
||||
// make the assertions depend on the framebuffer row order as well.
|
||||
Rgba8 SourceColor(int slice) {
|
||||
return {static_cast<GLubyte>(10 + slice * 20), static_cast<GLubyte>(40 + slice * 10),
|
||||
static_cast<GLubyte>(200 - slice * 15), 255};
|
||||
}
|
||||
|
||||
Rgba8 DestinationFill(int slice) {
|
||||
return {static_cast<GLubyte>(3 + slice), static_cast<GLubyte>(250 - slice * 7),
|
||||
static_cast<GLubyte>(120 + slice * 5), 255};
|
||||
}
|
||||
|
||||
class CopyImageLayeredScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (!CopyImageSubDataUsable()) {
|
||||
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_fbo != 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &m_fbo);
|
||||
m_fbo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A trivial 1x1x1 array-to-array copy: it exercises the entry point without depending
|
||||
// on any of the behaviour under test, so a driver (or a backend function table) that
|
||||
// simply does not have the call skips instead of failing every case below.
|
||||
bool CopyImageSubDataUsable() {
|
||||
GLuint probe[2] = {0, 0};
|
||||
glGenTextures(2, probe);
|
||||
for (const GLuint texture : probe) {
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
0, 1, 1, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(2, probe);
|
||||
return usable;
|
||||
}
|
||||
|
||||
// `target` is GL_TEXTURE_2D_ARRAY or GL_TEXTURE_3D; both take glTexStorage3D and
|
||||
// glTexSubImage3D with the slice on the same axis, which is the whole reason GL can
|
||||
// copy between them. `levels` > 1 puts a real mip chain behind the level the copy
|
||||
// names, so the level's own extent - a 3D level's depth included - has to be resolved
|
||||
// rather than assumed to be the image's.
|
||||
GLuint MakeTexture(GLenum target, int levels, Rgba8 (*colorForSlice)(int)) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(target, texture);
|
||||
glTexStorage3D(target, levels, GL_RGBA8, kWidth << (levels - 1), kHeight << (levels - 1),
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1)) : kSlices);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
|
||||
// Fill every level, so nothing below can pass by reading a level that was never
|
||||
// written and happened to hold the expected bytes.
|
||||
for (int level = 0; level < levels; ++level) {
|
||||
const int levelWidth = kWidth << (levels - 1 - level);
|
||||
const int levelHeight = kHeight << (levels - 1 - level);
|
||||
const int levelSlices =
|
||||
target == GL_TEXTURE_3D ? (kSlices << (levels - 1 - level)) : kSlices;
|
||||
for (int slice = 0; slice < levelSlices; ++slice) {
|
||||
const Rgba8 color = colorForSlice(slice % kSlices);
|
||||
std::vector<Rgba8> texels(static_cast<size_t>(levelWidth) * levelHeight, color);
|
||||
glTexSubImage3D(target, level, 0, 0, slice, levelWidth, levelHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
}
|
||||
glBindTexture(target, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One slice of one level, through an FBO attachment. glFramebufferTextureLayer takes an
|
||||
// array layer and a 3D slice through the same argument, so both targets read back the
|
||||
// same way.
|
||||
Rgba8 ReadSlice(GLuint texture, int level, int slice, int width, int height) {
|
||||
if (m_fbo == 0) {
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, level, slice);
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "slice " << slice << " of level " << level << " is not attachable";
|
||||
std::vector<Rgba8> pixels(static_cast<size_t>(width) * height, Rgba8{});
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// The fill is uniform within a slice, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "slice " << slice << " of level " << level << " is not uniform: texel 0 is "
|
||||
<< Describe(pixels[0]) << ", texel " << i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// The assertion every case ends with: slices inside [dstZ, dstZ + depth) hold the
|
||||
// source slice they were fed, and every slice outside it still holds its own fill.
|
||||
void ExpectCopied(GLuint destination, int level, int width, int height, int sliceCount, int srcZ,
|
||||
int dstZ, int depth, const char* what) {
|
||||
for (int slice = 0; slice < sliceCount; ++slice) {
|
||||
const bool inRange = slice >= dstZ && slice < dstZ + depth;
|
||||
const Rgba8 expected =
|
||||
inRange ? SourceColor(srcZ + (slice - dstZ)) : DestinationFill(slice);
|
||||
const Rgba8 actual = ReadSlice(destination, level, slice, width, height);
|
||||
EXPECT_TRUE(actual == expected)
|
||||
<< what << ": destination slice " << slice << (inRange ? " (copied)" : " (untouched)")
|
||||
<< " is " << Describe(actual) << ", expected " << Describe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
GLuint m_fbo = 0;
|
||||
};
|
||||
|
||||
// 2d_array -> 2d_array. Both endpoints put the slices on the layer axis, so BOTH layer
|
||||
// counts carry the depth and extent.depth must stay 1.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0,
|
||||
kWidth, kHeight, kSlices);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, 0, 0, kSlices, "array->array, all slices");
|
||||
}
|
||||
|
||||
// The same pair with the layer ranges offset differently on the two sides: the shape that
|
||||
// separates "copies more than slice 0" from "copies the RIGHT slices". A backend that read
|
||||
// the source range but wrote from layer 0 (or vice versa) passes the case above.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToArrayHonoursDifferentLayerOffsets) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 3;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 2;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"array->array, offset layer ranges");
|
||||
}
|
||||
|
||||
// 3d -> 3d. Neither endpoint has array layers at all: the depth travels on extent.depth and
|
||||
// the offsets on srcOffset.z/dstOffset.z, with both layer counts pinned to 1.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeHonoursNonZeroZ) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 3;
|
||||
constexpr int kDepth = 3;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->3d, non-zero z");
|
||||
}
|
||||
|
||||
// The same pair one mip level down. A 3D level's DEPTH halves with its width and height, so
|
||||
// this is the only case where the slice count the copy may name is not the image's own -
|
||||
// the bound a layered endpoint is checked against has to come from the level.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToVolumeAtNonZeroMipLevel) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 2, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 2, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kLevel = 1;
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 0;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, kLevel, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, kLevel, 0, 0,
|
||||
kDstZ, kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, kLevel, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth,
|
||||
"3d->3d at mip level 1");
|
||||
}
|
||||
|
||||
// 2d_array -> 3d. The mixed shape: the source counts its slices as layers, the destination
|
||||
// as depth, and Vulkan requires extent.depth to equal the source's layerCount.
|
||||
TEST_F(CopyImageLayeredScenario, ArrayToVolumeCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_3D, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 2;
|
||||
constexpr int kDstZ = 1;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_3D, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "2d_array->3d");
|
||||
}
|
||||
|
||||
// 3d -> 2d_array, the mirror image: the depth now has to reach the DESTINATION's layerCount
|
||||
// while the source states it as extent.depth from a z offset.
|
||||
TEST_F(CopyImageLayeredScenario, VolumeToArrayCopiesEverySlice) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const GLuint source = MakeTexture(GL_TEXTURE_3D, 1, SourceColor);
|
||||
const GLuint destination = MakeTexture(GL_TEXTURE_2D_ARRAY, 1, DestinationFill);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup failed";
|
||||
|
||||
constexpr int kSrcZ = 1;
|
||||
constexpr int kDstZ = 2;
|
||||
constexpr int kDepth = 4;
|
||||
glCopyImageSubData(source, GL_TEXTURE_3D, 0, 0, 0, kSrcZ, destination, GL_TEXTURE_2D_ARRAY, 0, 0, 0, kDstZ,
|
||||
kWidth, kHeight, kDepth);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glCopyImageSubData raised an error";
|
||||
|
||||
ExpectCopied(destination, 0, kWidth, kHeight, kSlices, kSrcZ, kDstZ, kDepth, "3d->2d_array");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,378 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentBarrierScenario.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
|
||||
//
|
||||
// Scenario - A TRANSFER OFF A NON-ZERO ATTACHMENT LAYER READS THE LAYER THE BARRIER MOVED.
|
||||
//
|
||||
// Every transfer DirectVulkan performs against a framebuffer attachment is three commands: a
|
||||
// barrier that puts the image in TRANSFER_SRC/DST, the copy or blit itself, and a barrier that
|
||||
// puts it back. The copy names the attachment's layer - glFramebufferTextureLayer(.., layer) ends
|
||||
// up in `srcSubresource.baseArrayLayer` - but TransitionImageLayout used to emit `layerCount = 1`
|
||||
// from `baseArrayLayer 0`, so for every attachment on a layer above zero the barrier moved layer 0
|
||||
// and the copy read layer N. The layer the transfer touched was never transitioned: it sat in
|
||||
// COLOR_ATTACHMENT_OPTIMAL (or DEPTH_STENCIL_ATTACHMENT_OPTIMAL) while being read as TRANSFER_SRC.
|
||||
//
|
||||
// That is undefined behaviour, not a guaranteed wrong pixel: a layout is a compression/tiling
|
||||
// promise, so a driver that stores both layouts identically returns the right bytes anyway. The
|
||||
// software lanes (lavapipe) are exactly such a driver, which is why this scenario is paired with a
|
||||
// validation-layer run - the layer names the mismatch outright
|
||||
// (VUID-vkCmdCopyImageToBuffer-srcImageLayout-00189, "srcImageLayout ... doesn't match the actual
|
||||
// current layout") where the pixels here cannot. On a tiler that really does re-tile per layout,
|
||||
// these are the reads that come back as garbage.
|
||||
//
|
||||
// The four cases below are the four transfer paths that take an attachment layer from GL:
|
||||
//
|
||||
// glReadPixels (colour) -> VulkanRenderer::ReadPixels
|
||||
// glBlitFramebuffer (colour) -> VulkanRenderer::BlitNamedFramebuffer
|
||||
// glReadPixels (GL_DEPTH_COMPONENT) -> VulkanRenderer::ReadDepthStencilImageToClient
|
||||
// glBlitFramebuffer (GL_DEPTH_BUFFER_BIT) -> VulkanRenderer::BlitNamedFramebuffer, depth leg
|
||||
//
|
||||
// Each one renders or clears INTO the non-zero layer first, so the image is genuinely sitting in
|
||||
// its attachment layout when the transfer starts - a scenario that only uploaded texels would
|
||||
// leave it in a transfer layout already and the mismatched barrier would be a no-op.
|
||||
//
|
||||
// Every case also asserts the layers it did not name still hold their own fill, so a backend that
|
||||
// "fixed" the miss by transferring the whole image passes neither half.
|
||||
//
|
||||
// DirectGLES is the control: it hands the same calls to the driver, so a failure on both backends
|
||||
// means the scenario is wrong and a failure on DirectVulkan alone means Magma is.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kWidth = 8;
|
||||
constexpr int kHeight = 8;
|
||||
// Four layers with the subject at index 2: layers on both sides of it stay untouched, so
|
||||
// "moved the whole image" and "moved layer 0" are both distinguishable from correct.
|
||||
constexpr int kLayers = 4;
|
||||
constexpr int kSubjectLayer = 2;
|
||||
|
||||
// A value no correct read can produce, so "the backend wrote nothing" fails loudly.
|
||||
constexpr float kDepthPoison = 0.2f;
|
||||
|
||||
std::string Describe(const Rgba8& color) {
|
||||
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " + std::to_string(color.b) +
|
||||
", " + std::to_string(color.a) + ")";
|
||||
}
|
||||
|
||||
// Per-layer fill, uniform within a layer: the defect is about WHICH layer is addressed, and
|
||||
// a value that also varied inside the layer would make the assertions depend on row order.
|
||||
Rgba8 LayerFill(int layer) {
|
||||
return {static_cast<GLubyte>(17 + layer * 30), static_cast<GLubyte>(200 - layer * 25),
|
||||
static_cast<GLubyte>(60 + layer * 40), 255};
|
||||
}
|
||||
|
||||
// What the draw paints - matches kFS below, and is deliberately none of the LayerFill
|
||||
// values so "the draw never landed" cannot read as a pass.
|
||||
constexpr Rgba8 kPaintedColor{26, 51, 204, 255};
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.1, 0.2, 0.8, 1.0); }
|
||||
)";
|
||||
|
||||
void DrawFullViewportQuad(unsigned int program) {
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
class LayeredAttachmentBarrierScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(m_program, 0u) << error;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
for (const GLuint fbo : m_fbos) {
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
m_fbos.clear();
|
||||
for (const GLuint texture : m_textures) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
m_textures.clear();
|
||||
if (m_program != 0) {
|
||||
glUseProgram(0);
|
||||
glDeleteProgram(m_program);
|
||||
m_program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// An RGBA8 2D array with a different uniform colour per layer.
|
||||
GLuint MakeColorArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
const std::vector<Rgba8> texels(static_cast<std::size_t>(kWidth) * kHeight, LayerFill(layer));
|
||||
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kWidth, kHeight, 1, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, texels.data());
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A depth 2D array. No initial upload: depth arrays are filled by clearing through an
|
||||
// attachment, which is also the state the transfer paths have to cope with.
|
||||
GLuint MakeDepthArray() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kWidth, kHeight, kLayers);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// One FBO naming `layer` of the given arrays. Depth is optional (0 = colour only).
|
||||
GLuint MakeLayerFbo(GLuint colorArray, GLuint depthArray, int layer) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
m_fbos.push_back(fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorArray, 0, layer);
|
||||
if (depthArray != 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, layer);
|
||||
}
|
||||
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "layer " << layer << " is not attachable";
|
||||
return fbo;
|
||||
}
|
||||
|
||||
// glReadPixels of one whole layer, through an FBO that names it.
|
||||
Rgba8 ReadLayer(GLuint colorArray, int layer) {
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, layer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// The fill is uniform within a layer, so any disagreement between texels is itself
|
||||
// a failure - reported here rather than silently reduced to pixels[0].
|
||||
for (std::size_t i = 1; i < pixels.size(); ++i) {
|
||||
EXPECT_TRUE(pixels[i] == pixels[0])
|
||||
<< "layer " << layer << " is not uniform: texel 0 is " << Describe(pixels[0]) << ", texel "
|
||||
<< i << " is " << Describe(pixels[i]);
|
||||
}
|
||||
return pixels[0];
|
||||
}
|
||||
|
||||
// Every layer but `changed` still holds its own fill.
|
||||
void ExpectOtherLayersUntouched(GLuint colorArray, int changed, const char* what) {
|
||||
for (int layer = 0; layer < kLayers; ++layer) {
|
||||
if (layer == changed) continue;
|
||||
const Rgba8 actual = ReadLayer(colorArray, layer);
|
||||
EXPECT_TRUE(actual == LayerFill(layer))
|
||||
<< what << ": layer " << layer << " should still hold its fill but is " << Describe(actual)
|
||||
<< ", expected " << Describe(LayerFill(layer));
|
||||
}
|
||||
}
|
||||
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_fbos;
|
||||
unsigned int m_program = 0;
|
||||
};
|
||||
|
||||
// glReadPixels straight off a layer that was just rendered to. The image is in
|
||||
// COLOR_ATTACHMENT_OPTIMAL when the readback barrier runs, so the barrier and the copy
|
||||
// disagreeing about the layer is a live layout mismatch, not a bookkeeping detail.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadPixelsOffRenderedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, 0, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
std::vector<Rgba8> pixels(static_cast<std::size_t>(kWidth) * kHeight, Rgba8{});
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
for (std::size_t i = 0; i < pixels.size(); ++i) {
|
||||
ASSERT_NEAR(pixels[i].r, kPaintedColor.r, 2)
|
||||
<< "texel " << i << " of the rendered layer is " << Describe(pixels[i]);
|
||||
ASSERT_NEAR(pixels[i].g, kPaintedColor.g, 2) << "texel " << i;
|
||||
ASSERT_NEAR(pixels[i].b, kPaintedColor.b, 2) << "texel " << i;
|
||||
}
|
||||
|
||||
ExpectOtherLayersUntouched(colorArray, kSubjectLayer, "readback off a rendered layer");
|
||||
}
|
||||
|
||||
// glBlitFramebuffer between two non-zero layers of two different arrays. Both endpoints are
|
||||
// above layer 0, so the source and destination barriers are each wrong on their own side.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitBetweenNonZeroColorLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceArray = MakeColorArray();
|
||||
const GLuint destinationArray = MakeColorArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceArray, 0, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
DrawFullViewportQuad(m_program);
|
||||
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationArray, 0, kDestinationLayer);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
const Rgba8 blitted = ReadLayer(destinationArray, kDestinationLayer);
|
||||
EXPECT_NEAR(blitted.r, kPaintedColor.r, 2) << "blit destination layer is " << Describe(blitted);
|
||||
EXPECT_NEAR(blitted.g, kPaintedColor.g, 2);
|
||||
EXPECT_NEAR(blitted.b, kPaintedColor.b, 2);
|
||||
|
||||
ExpectOtherLayersUntouched(destinationArray, kDestinationLayer, "colour blit destination");
|
||||
// The source layer was rendered, not blitted into, so it is checked separately.
|
||||
const Rgba8 source = ReadLayer(sourceArray, kSourceLayer);
|
||||
EXPECT_NEAR(source.r, kPaintedColor.r, 2) << "blit source layer is " << Describe(source);
|
||||
ExpectOtherLayersUntouched(sourceArray, kSourceLayer, "colour blit source");
|
||||
}
|
||||
|
||||
// The depth aspect of the same readback path: the depth image sits in
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL after the clear, and the copy names the attached layer.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, ReadDepthOffClearedNonZeroLayer) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint colorArray = MakeColorArray();
|
||||
const GLuint depthArray = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
const GLuint fbo = MakeLayerFbo(colorArray, depthArray, kSubjectLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.375);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
const float centre = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(centre, 0.375f, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) off layer " << kSubjectLayer << " returned " << centre
|
||||
<< (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
}
|
||||
|
||||
// The depth leg of the blit path, both endpoints above layer 0. Verified by reading the
|
||||
// destination's depth back, which is the same readback the case above pins - so a failure
|
||||
// here with that one passing is the blit, not the readback.
|
||||
TEST_F(LayeredAttachmentBarrierScenario, BlitDepthBetweenNonZeroLayers) {
|
||||
if (!Ready()) return;
|
||||
|
||||
const GLuint sourceColor = MakeColorArray();
|
||||
const GLuint sourceDepth = MakeDepthArray();
|
||||
const GLuint destinationColor = MakeColorArray();
|
||||
const GLuint destinationDepth = MakeDepthArray();
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "texture setup failed";
|
||||
|
||||
constexpr int kSourceLayer = 3;
|
||||
constexpr int kDestinationLayer = 1;
|
||||
|
||||
const GLuint sourceFbo = MakeLayerFbo(sourceColor, sourceDepth, kSourceLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.625);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// A destination pre-cleared to something the blit must overwrite, so "the blit did
|
||||
// nothing" and "the blit landed" are different answers.
|
||||
const GLuint destinationFbo = MakeLayerFbo(destinationColor, destinationDepth, kDestinationLayer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
glViewport(0, 0, kWidth, kHeight);
|
||||
glDepthMask(GL_TRUE);
|
||||
glClearDepth(0.125);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destinationFbo);
|
||||
glBlitFramebuffer(0, 0, kWidth, kHeight, 0, 0, kWidth, kHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFbo);
|
||||
const float blitted = ReadDepthAt(kWidth / 2, kHeight / 2);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(blitted, 0.625f, 1.0f / 4096.0f)
|
||||
<< "depth blitted onto layer " << kDestinationLayer << " reads back as " << blitted
|
||||
<< (std::fabs(blitted - 0.125f) < 1e-3f ? " - the destination kept its own clear" : "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,524 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.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
|
||||
//
|
||||
// Scenario - gl_ViewportIndex ACTUALLY ROUTES, AND THE PER-INDEX STATE IT SELECTS IS REAL.
|
||||
//
|
||||
// The state half of ARB_viewport_array is asserted in MG_Test/State/RenderStateTest.cpp, which
|
||||
// is a pure set/get exercise and would pass just as green against a backend that stores all 16
|
||||
// rectangles and rasterizes only the first. This file is the other half: every case here routes
|
||||
// primitives to a viewport OTHER than 0 and then looks at where the pixels landed.
|
||||
//
|
||||
// Three claims, one per case:
|
||||
// 1. gl_ViewportIndex selects the viewport RECTANGLE - a 4x4 grid of 32x32 viewports, one
|
||||
// geometry-shader invocation per cell, and every cell must hold its own index.
|
||||
// 2. gl_ViewportIndex selects the DEPTH RANGE - 16 one-pixel-wide viewports whose ranges are
|
||||
// (i/16, 1 - i/16), a quad at each end of clip space, and gl_FragCoord.z read back.
|
||||
// This is the claim that fails loudest against a single-viewport backend, because the
|
||||
// geometry is still in the right place while every depth comes back as viewport 0's.
|
||||
// 3. The per-index SCISSOR TEST ENABLE is honoured. Vulkan has no per-viewport scissor-test
|
||||
// toggle, so a disabled index has to be given the whole framebuffer as its rectangle; the
|
||||
// case draws the same primitive into the same index twice, once with the test off and once
|
||||
// with it on, and requires the two results to differ in the documented direction.
|
||||
//
|
||||
// Case 1 runs a second time against the DEFAULT framebuffer. MobileGL Y-flips (and pre-transform
|
||||
// rotates) the default framebuffer's rectangles and does not touch an FBO's, so a port that
|
||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||
// it matters.
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kViewportCount = 16;
|
||||
constexpr int kGridSide = 4; // 4x4 grid of viewports
|
||||
constexpr int kCellSize = 32; // ... each 32x32
|
||||
constexpr int kSurfaceSide = kGridSide * kCellSize;
|
||||
constexpr GLint kUnwritten = -1;
|
||||
|
||||
// A geometry shader is the only stage GL 4.1 lets write gl_ViewportIndex, and
|
||||
// `invocations` runs it once per viewport off a single input point - the same shape
|
||||
// KHR-GL43.viewport_array.draw_to_single_layer_with_multiple_viewports uses.
|
||||
const char* const kVertexSource = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kGridGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = gl_InvocationID;
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// One invocation, viewport chosen by a uniform: lets a case draw the SAME primitive into
|
||||
// the SAME index twice under two different scissor-enable states.
|
||||
const char* const kSingleGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
uniform int uViewport;
|
||||
flat out int gsIndex;
|
||||
void main() {
|
||||
gsIndex = uViewport;
|
||||
gl_ViewportIndex = uViewport;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kIntFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out int fragColor;
|
||||
void main() { fragColor = gsIndex; }
|
||||
)";
|
||||
|
||||
// Two quads, one at each end of clip space, so the fragment stage can report the depth
|
||||
// the viewport's range mapped them to. gl_FragCoord.z IS the post-range window depth, so
|
||||
// it reads back the per-viewport minDepth/maxDepth directly.
|
||||
const char* const kDepthGeometrySource = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 8) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, -1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
gl_Position = vec4(-1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 0.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 1.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kDepthFragmentSource = R"(#version 410 core
|
||||
layout(location = 0) out float fragColor;
|
||||
void main() { fragColor = gl_FragCoord.z; }
|
||||
)";
|
||||
|
||||
class ViewportArrayScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
|
||||
if (Gl().BackendName() == "DirectGLES") {
|
||||
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||
"rendering half of KHR-GL43.viewport_array.";
|
||||
}
|
||||
|
||||
GLint maxViewports = 0;
|
||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||
|
||||
m_program = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(m_program, 0u) << "grid program failed to build: " << m_buildLog;
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
ResetViewportArrayState();
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
ResetViewportArrayState();
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every case starts from the same slate: this fixture shares its context with every
|
||||
// other scenario in the process, and a leftover per-index scissor enable is exactly
|
||||
// the kind of state that would make a later case pass or fail for the wrong reason.
|
||||
static void ResetViewportArrayState() {
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
||||
}
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
glDepthRange(0.0, 1.0);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// The 4x4 grid: viewport y*4+x covers the cell whose lower-left corner is
|
||||
// (x*cellW, y*cellH), in GL's bottom-left-origin window coordinates. Parameterized on
|
||||
// the cell size because the default framebuffer this scenario also renders into is
|
||||
// deliberately non-square (HeadlessGL is 128x96, so a transposing bug cannot hide).
|
||||
static void SetupGridViewports(int cellW, int cellH) {
|
||||
std::vector<GLfloat> data(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
data[base + 0] = static_cast<GLfloat>(x * cellW);
|
||||
data[base + 1] = static_cast<GLfloat>(y * cellH);
|
||||
data[base + 2] = static_cast<GLfloat>(cellW);
|
||||
data[base + 3] = static_cast<GLfloat>(cellH);
|
||||
}
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, data.data());
|
||||
}
|
||||
|
||||
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
||||
const GLuint vs = CompileStage(GL_VERTEX_SHADER, kVertexSource);
|
||||
if (vs == 0) return 0;
|
||||
const GLuint gs = CompileStage(GL_GEOMETRY_SHADER, geometrySource);
|
||||
if (gs == 0) {
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (fs == 0) {
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, gs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(gs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint CompileStage(GLenum stage, const char* source) {
|
||||
const GLuint shader = glCreateShader(stage);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled) return shader;
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// An R32I colour target, pre-filled with kUnwritten so "nothing was drawn here" is
|
||||
// distinguishable from "index 0 was drawn here".
|
||||
struct IntTarget {
|
||||
GLuint fbo = 0;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
// The "nothing drawn here" value is UPLOADED, not cleared: the CTS fills its R32I
|
||||
// targets the same way (fillTexture), and an upload cannot be confused with a clear
|
||||
// that a backend defers, reorders or drops - which is exactly the ambiguity a case
|
||||
// asserting "this cell must be untouched" cannot afford.
|
||||
static void FillIntTarget(const IntTarget& target, int width, int height) {
|
||||
const std::vector<GLint> unwritten(static_cast<size_t>(width) * height, kUnwritten);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED_INTEGER, GL_INT, unwritten.data());
|
||||
}
|
||||
|
||||
static IntTarget MakeIntTarget(int width, int height) {
|
||||
IntTarget target;
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, width, height, 0, GL_RED_INTEGER, GL_INT, nullptr);
|
||||
glGenFramebuffers(1, &target.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
FillIntTarget(target, width, height);
|
||||
return target;
|
||||
}
|
||||
|
||||
static void DestroyIntTarget(IntTarget& target) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
|
||||
static std::vector<GLint> ReadInts(int width, int height) {
|
||||
std::vector<GLint> pixels(static_cast<size_t>(width) * height, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RED_INTEGER, GL_INT, pixels.data());
|
||||
return pixels;
|
||||
}
|
||||
|
||||
// The centre of grid cell (x, y), in the bottom-left-origin coordinates glReadPixels
|
||||
// returns. Sampling the centre rather than a corner keeps the assertion about WHICH
|
||||
// viewport was selected rather than about edge rounding.
|
||||
static GLint CellCentre(const std::vector<GLint>& pixels, int stride, int x, int y) {
|
||||
const int px = x * kCellSize + kCellSize / 2;
|
||||
const int py = y * kCellSize + kCellSize / 2;
|
||||
return pixels[static_cast<size_t>(py) * stride + px];
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
GLuint m_program = 0;
|
||||
GLuint m_vao = 0;
|
||||
};
|
||||
|
||||
// --- 1. the viewport rectangle -------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexRasterizesIntoItsOwnRectangle) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
SetupGridViewports(kCellSize, kCellSize);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const GLint expected = y * kGridSide + x;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), expected)
|
||||
<< "cell (" << x << ", " << y << ") should hold viewport index " << expected
|
||||
<< "; a single-viewport backend paints the whole image with 15 (the last invocation)";
|
||||
}
|
||||
}
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
// The same claim against the DEFAULT framebuffer, where MobileGL applies its Y-flip and
|
||||
// pre-transform rotation. Index 0 alone getting the mapping is the classic bug.
|
||||
TEST_F(ViewportArrayScenario, TheDefaultFramebufferAppliesTheSameFlipToEveryViewport) {
|
||||
const int surfaceW = Gl().Width();
|
||||
const int surfaceH = Gl().Height();
|
||||
ASSERT_GE(surfaceW, kGridSide);
|
||||
ASSERT_GE(surfaceH, kGridSide);
|
||||
const int cellW = surfaceW / kGridSide;
|
||||
const int cellH = surfaceH / kGridSide;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// Paint a value no viewport index can produce, so an unwritten cell is obvious.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// The default framebuffer is 8-bit RGBA, so the index travels as a colour: cell i is
|
||||
// painted with red = i * 16, which is exact in 8 bits for i in [0, 16).
|
||||
const char* const kColorFragmentSource = R"(#version 410 core
|
||||
flat in int gsIndex;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const GLuint colorProgram = BuildProgram(kGridGeometrySource, kColorFragmentSource);
|
||||
ASSERT_NE(colorProgram, 0u) << "colour program failed to build: " << m_buildLog;
|
||||
|
||||
SetupGridViewports(cellW, cellH);
|
||||
glUseProgram(colorProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<unsigned char> pixels(static_cast<size_t>(surfaceW) * surfaceH * 4, 0);
|
||||
glReadPixels(0, 0, surfaceW, surfaceH, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const int px = x * cellW + cellW / 2;
|
||||
const int py = y * cellH + cellH / 2;
|
||||
const int red = pixels[(static_cast<size_t>(py) * surfaceW + px) * 4];
|
||||
const int expected = (y * kGridSide + x) * 16;
|
||||
// One LSB of slack for an 8-bit round trip; the values are 16 apart, so this
|
||||
// cannot confuse two neighbouring indices.
|
||||
EXPECT_LE(std::abs(red - expected), 1)
|
||||
<< "default-framebuffer cell (" << x << ", " << y << ") holds red=" << red << ", expected "
|
||||
<< expected << ". A vertically mirrored grid means the Y-flip was applied to viewport 0 "
|
||||
<< "only";
|
||||
}
|
||||
}
|
||||
glDeleteProgram(colorProgram);
|
||||
}
|
||||
|
||||
// --- 2. the depth range --------------------------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, EachViewportIndexUsesItsOwnDepthRange) {
|
||||
// 16 columns one pixel wide and two rows tall: row 0 gets the near-plane quad, row 1
|
||||
// the far-plane one, so both ends of viewport i's range land in the same column.
|
||||
constexpr int kWidth = kViewportCount;
|
||||
constexpr int kHeight = 2;
|
||||
|
||||
GLuint texture = 0;
|
||||
GLuint fbo = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kWidth, kHeight, 0, GL_RED, GL_FLOAT, nullptr);
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const GLfloat clearValue[4] = {-1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, clearValue);
|
||||
|
||||
std::vector<GLfloat> viewports(static_cast<size_t>(kViewportCount) * 4);
|
||||
std::vector<GLdouble> ranges(static_cast<size_t>(kViewportCount) * 2);
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
viewports[static_cast<size_t>(i) * 4 + 0] = static_cast<GLfloat>(i);
|
||||
viewports[static_cast<size_t>(i) * 4 + 1] = 0.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 2] = 1.0f;
|
||||
viewports[static_cast<size_t>(i) * 4 + 3] = 2.0f;
|
||||
ranges[static_cast<size_t>(i) * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
ranges[static_cast<size_t>(i) * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
glViewportArrayv(0, kViewportCount, viewports.data());
|
||||
glDepthRangeArrayv(0, kViewportCount, ranges.data());
|
||||
|
||||
const GLuint depthProgram = BuildProgram(kDepthGeometrySource, kDepthFragmentSource);
|
||||
ASSERT_NE(depthProgram, 0u) << "depth program failed to build: " << m_buildLog;
|
||||
glUseProgram(depthProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
|
||||
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||
for (int i = 0; i < kViewportCount; ++i) {
|
||||
const float near = static_cast<float>(i) / 16.0f;
|
||||
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||
// from every other index by at least 1/16.
|
||||
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||
}
|
||||
|
||||
glDeleteProgram(depthProgram);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
// --- 3. the per-index scissor-test enable --------------------------------------------
|
||||
|
||||
TEST_F(ViewportArrayScenario, AnIndexedScissorEnableClipsOnlyThatIndex) {
|
||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
||||
|
||||
// One full-size viewport per index so the scissor rectangle is the ONLY thing that
|
||||
// can shrink the quad - the same separation KHR-GL43.viewport_array.scissor uses.
|
||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
||||
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
const size_t base = static_cast<size_t>(y * kGridSide + x) * 4;
|
||||
boxes[base + 0] = x * kCellSize;
|
||||
boxes[base + 1] = y * kCellSize;
|
||||
boxes[base + 2] = kCellSize;
|
||||
boxes[base + 3] = kCellSize;
|
||||
}
|
||||
}
|
||||
glScissorArrayv(0, kViewportCount, boxes.data());
|
||||
|
||||
const GLuint singleProgram = BuildProgram(kSingleGeometrySource, kIntFragmentSource);
|
||||
ASSERT_NE(singleProgram, 0u) << "single-viewport program failed to build: " << m_buildLog;
|
||||
glUseProgram(singleProgram);
|
||||
glBindVertexArray(m_vao);
|
||||
const GLint uViewport = glGetUniformLocation(singleProgram, "uViewport");
|
||||
ASSERT_NE(uViewport, -1);
|
||||
|
||||
constexpr GLint kProbeIndex = 6; // grid cell (2, 1)
|
||||
constexpr int kProbeX = kProbeIndex % kGridSide;
|
||||
constexpr int kProbeY = kProbeIndex / kGridSide;
|
||||
|
||||
// (a) scissor test ENABLED for this index: the quad is clipped to its 32x32 box.
|
||||
glUniform1i(uViewport, kProbeIndex);
|
||||
glEnablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, kProbeX, kProbeY), kProbeIndex)
|
||||
<< "the scissored index must still paint inside its own box";
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
if (x == kProbeX && y == kProbeY) continue;
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
||||
<< "cell (" << x << ", " << y << ") is outside scissor rectangle " << kProbeIndex
|
||||
<< " and must be untouched";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (b) scissor test DISABLED for the same index, everything else identical: with no
|
||||
// per-viewport toggle in Vulkan this is the case that needs the disabled index to be
|
||||
// given the full framebuffer rectangle, and it is exactly where "leave the last
|
||||
// rectangle bound" would show up as a still-clipped quad.
|
||||
FillIntTarget(target, kSurfaceSide, kSurfaceSide);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||
glDisablei(GL_SCISSOR_TEST, kProbeIndex);
|
||||
glDrawArrays(GL_POINTS, 0, 1);
|
||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
||||
{
|
||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
||||
for (int y = 0; y < kGridSide; ++y) {
|
||||
for (int x = 0; x < kGridSide; ++x) {
|
||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kProbeIndex)
|
||||
<< "with the scissor test off for index " << kProbeIndex
|
||||
<< ", its full-viewport quad must cover cell (" << x << ", " << y << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glDeleteProgram(singleProgram);
|
||||
DestroyIntTarget(target);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -712,10 +712,18 @@ namespace MobileGL::MG_State {
|
||||
m_renderState.SetViewport(viewport);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetViewport() const {
|
||||
IntVec4 GLContext::GetViewport() const {
|
||||
return m_renderState.GetViewport();
|
||||
}
|
||||
|
||||
void GLContext::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
m_renderState.SetViewportIndexed(index, viewport);
|
||||
}
|
||||
|
||||
const FloatVec4& GLContext::GetViewportIndexed(Uint index) const {
|
||||
return m_renderState.GetViewportIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetLineWidth(Float width) {
|
||||
m_renderState.SetLineWidth(width);
|
||||
}
|
||||
@@ -953,6 +961,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetDepthRange();
|
||||
}
|
||||
|
||||
void GLContext::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
m_renderState.SetDepthRangeIndexed(index, range);
|
||||
}
|
||||
|
||||
const FloatVec2& GLContext::GetDepthRangeIndexed(Uint index) const {
|
||||
return m_renderState.GetDepthRangeIndexed(index);
|
||||
}
|
||||
|
||||
void GLContext::SetSampleCoverage(Float value, Bool invert) {
|
||||
m_renderState.SetSampleCoverage(value, invert);
|
||||
}
|
||||
@@ -1017,6 +1033,14 @@ namespace MobileGL::MG_State {
|
||||
return m_renderState.GetScissorBox();
|
||||
}
|
||||
|
||||
void GLContext::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
m_renderState.SetScissorBoxIndexed(index, box);
|
||||
}
|
||||
|
||||
const IntVec4& GLContext::GetScissorBoxIndexed(Uint index) const {
|
||||
return m_renderState.GetScissorBoxIndexed(index);
|
||||
}
|
||||
|
||||
// Framebuffer
|
||||
void GLContext::GenFramebufferNames(Uint number, Vector<Uint>& framebuffers) {
|
||||
m_framebufferState.GenerateNames(number, framebuffers);
|
||||
|
||||
@@ -198,8 +198,10 @@ namespace MobileGL {
|
||||
// Only the pipeline-relevant subset - see RenderState::m_pipelineStateVersion.
|
||||
Uint GetPipelineStateVersion() const;
|
||||
const RenderStateParameters& GetRenderStateParameters() const;
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height; writes ALL viewports
|
||||
IntVec4 GetViewport() const; // x, y, width, height; viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -260,8 +262,10 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
void SetDepthRange(FloatVec2 range); // writes ALL viewports' depth ranges
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -276,8 +280,10 @@ namespace MobileGL {
|
||||
FrontFaceMode GetFrontFaceMode() const;
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height; writes ALL rectangles
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height; rectangle 0
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
// Transform feedback. The fields below are the state of the transform
|
||||
// feedback object currently bound to GL_TRANSFORM_FEEDBACK; see the object
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace MobileGL {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Every viewport's scissor-test bit set, i.e. what glEnable(GL_SCISSOR_TEST) writes.
|
||||
constexpr Uint32 kAllViewportsMask =
|
||||
RenderStateParameters::MAX_VIEWPORTS >= 32
|
||||
? ~0u
|
||||
: (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
|
||||
} // namespace
|
||||
|
||||
RenderState::RenderState() {
|
||||
@@ -32,6 +38,15 @@ namespace MobileGL {
|
||||
for (auto& mask : m_parameters.ColorMasks) {
|
||||
mask = BoolVec4(true, true, true, true);
|
||||
}
|
||||
// Every viewport's depth range starts at (0, 1) - GL 4.6 core table 23.4. The
|
||||
// viewport and scissor rectangles legitimately start all-zero here: their spec
|
||||
// initial value is the size of the window the context is first made current to,
|
||||
// which the frontend does not know yet, so an all-zero rectangle means "never
|
||||
// written" and the backends resolve it against the live surface (see
|
||||
// DirectGLES' SyncRenderState and VulkanRenderer's ApplyGLViewportState).
|
||||
for (auto& range : m_parameters.DepthRanges) {
|
||||
range = FloatVec2(0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
Uint RenderState::GetVersion() const {
|
||||
@@ -47,15 +62,47 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// -------------------- Rasterization --------------------
|
||||
// ARB_viewport_array, "Additions to Chapter 2": Viewport(x, y, w, h) is equivalent to
|
||||
// ViewportIndexedf(i, x, y, w, h) for every i in [0, MAX_VIEWPORTS) - it is not a
|
||||
// synonym for "viewport 0".
|
||||
void RenderState::SetViewport(IntVec4 viewport) {
|
||||
if (m_parameters.Viewport == viewport) return;
|
||||
const FloatVec4 asFloat(static_cast<Float>(viewport.x()), static_cast<Float>(viewport.y()),
|
||||
static_cast<Float>(viewport.z()), static_cast<Float>(viewport.w()));
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.Viewports) {
|
||||
if (stored == asFloat) continue;
|
||||
stored = asFloat;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
m_parameters.Viewport = viewport;
|
||||
IntVec4 RenderState::GetViewport() const {
|
||||
const FloatVec4& viewport = m_parameters.Viewports[0];
|
||||
// Round rather than truncate: glGetIntegerv on floating-point state rounds to
|
||||
// nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport to 63 would
|
||||
// also hand the backends a rectangle one pixel short of what was asked for.
|
||||
return IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
||||
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
||||
}
|
||||
|
||||
void RenderState::SetViewportIndexed(Uint index, FloatVec4 viewport) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.Viewports[index] == viewport) return;
|
||||
|
||||
m_parameters.Viewports[index] = viewport;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetViewport() const {
|
||||
return m_parameters.Viewport;
|
||||
const FloatVec4& RenderState::GetViewportIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
|
||||
return m_parameters.Viewports[0];
|
||||
}
|
||||
return m_parameters.Viewports[index];
|
||||
}
|
||||
|
||||
void RenderState::SetLineWidth(Float width) {
|
||||
@@ -223,7 +270,6 @@ namespace MobileGL {
|
||||
SET_CAPABILITY(SampleAlphaToOne, enabled);
|
||||
SET_CAPABILITY(SampleCoverage, enabled);
|
||||
SET_CAPABILITY(SampleMask, enabled);
|
||||
SET_CAPABILITY(ScissorTest, enabled);
|
||||
SET_CAPABILITY(StencilTest, enabled);
|
||||
SET_CAPABILITY(ProgramPointSize, enabled);
|
||||
case CapabilityInput::Blend: {
|
||||
@@ -236,6 +282,17 @@ namespace MobileGL {
|
||||
if (stateChanged) BumpVersions();
|
||||
break;
|
||||
}
|
||||
// GL 4.6 core 17.3.2: the non-indexed Enable/Disable(SCISSOR_TEST) enables or
|
||||
// disables the test for ALL viewports, exactly like glViewport writes all
|
||||
// viewports. Anything narrower fails KHR-GL43.viewport_array.scissor_test_state_api,
|
||||
// whose "enable all" phase reads every index back through glIsEnabledi.
|
||||
case CapabilityInput::ScissorTest: {
|
||||
const Uint32 updated = enabled ? kAllViewportsMask : 0u;
|
||||
if (m_parameters.ScissorTestEnabledMask == updated) break;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
break;
|
||||
}
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -287,11 +344,14 @@ namespace MobileGL {
|
||||
RETURN_CAPABILITY(SampleAlphaToOne);
|
||||
RETURN_CAPABILITY(SampleCoverage);
|
||||
RETURN_CAPABILITY(SampleMask);
|
||||
RETURN_CAPABILITY(ScissorTest);
|
||||
RETURN_CAPABILITY(StencilTest);
|
||||
RETURN_CAPABILITY(ProgramPointSize);
|
||||
case CapabilityInput::Blend:
|
||||
return m_parameters.BlendStates[0].Enabled;
|
||||
// The non-indexed query of an indexed capability answers for index 0
|
||||
// (GL 4.6 core 22.1), which is also the only bit either backend consumes today.
|
||||
case CapabilityInput::ScissorTest:
|
||||
return (m_parameters.ScissorTestEnabledMask & 1u) != 0;
|
||||
case CapabilityInput::ClipDistance0:
|
||||
case CapabilityInput::ClipDistance1:
|
||||
case CapabilityInput::ClipDistance2:
|
||||
@@ -307,13 +367,29 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) {
|
||||
// Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already
|
||||
// reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this
|
||||
// is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++
|
||||
// exception through the C GL ABI and terminates the process.
|
||||
// GL_BLEND (indexed by draw buffer) and GL_SCISSOR_TEST (indexed by viewport) are
|
||||
// the only indexed capabilities in GL 4.6 core. The GL entry points
|
||||
// (glEnablei/glDisablei) already reject every other target with GL_INVALID_ENUM
|
||||
// and every out-of-range index with GL_INVALID_VALUE before reaching here, so the
|
||||
// guards below are backstops - but they must stay backstops:
|
||||
// THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and
|
||||
// terminates the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
const Uint32 bit = 1u << index;
|
||||
const Uint32 updated = enabled ? (m_parameters.ScissorTestEnabledMask | bit)
|
||||
: (m_parameters.ScissorTestEnabledMask & ~bit);
|
||||
if (updated == m_parameters.ScissorTestEnabledMask) return;
|
||||
m_parameters.ScissorTestEnabledMask = updated;
|
||||
BumpVersions();
|
||||
return;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for "
|
||||
"GL_BLEND (cap=%d, index=%u); ignoring",
|
||||
"GL_BLEND and GL_SCISSOR_TEST (cap=%d, index=%u); ignoring",
|
||||
static_cast<int>(cap), index);
|
||||
return;
|
||||
}
|
||||
@@ -328,9 +404,17 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
|
||||
// Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed:
|
||||
// glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a
|
||||
// query must never be able to terminate the process.
|
||||
// GL_BLEND and GL_SCISSOR_TEST only - same backstop reasoning as
|
||||
// SetCapabilityIndexed: glIsEnabledi has already answered
|
||||
// GL_INVALID_ENUM/GL_INVALID_VALUE for anything else, and a query must never be
|
||||
// able to terminate the process.
|
||||
if (cap == CapabilityInput::ScissorTest) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor test capability index out of range: %u", index);
|
||||
return false;
|
||||
}
|
||||
return (m_parameters.ScissorTestEnabledMask & (1u << index)) != 0;
|
||||
}
|
||||
if (cap != CapabilityInput::Blend) {
|
||||
MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only "
|
||||
"for GL_BLEND (cap=%d, index=%u); reporting disabled",
|
||||
@@ -591,15 +675,39 @@ namespace MobileGL {
|
||||
return m_parameters.BlendColor;
|
||||
}
|
||||
|
||||
// Like Viewport: ARB_viewport_array makes DepthRange(n, f) the same as
|
||||
// DepthRangeIndexed(i, n, f) for every i.
|
||||
void RenderState::SetDepthRange(FloatVec2 range) {
|
||||
if (m_parameters.DepthRange == range) return;
|
||||
|
||||
m_parameters.DepthRange = range;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.DepthRanges) {
|
||||
if (stored == range) continue;
|
||||
stored = range;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRange() const {
|
||||
return m_parameters.DepthRange;
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
|
||||
void RenderState::SetDepthRangeIndexed(Uint index, FloatVec2 range) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.DepthRanges[index] == range) return;
|
||||
|
||||
m_parameters.DepthRanges[index] = range;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const FloatVec2& RenderState::GetDepthRangeIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
|
||||
return m_parameters.DepthRanges[0];
|
||||
}
|
||||
return m_parameters.DepthRanges[index];
|
||||
}
|
||||
|
||||
void RenderState::SetSampleCoverage(Float value, Bool invert) {
|
||||
@@ -726,15 +834,39 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
// --------------------- Scissor ---------------------
|
||||
// Like Viewport: ARB_viewport_array makes Scissor(x, y, w, h) the same as
|
||||
// ScissorIndexed(i, x, y, w, h) for every i.
|
||||
void RenderState::SetScissorBox(IntVec4 box) {
|
||||
if (m_parameters.ScissorBox == box) return;
|
||||
|
||||
m_parameters.ScissorBox = box;
|
||||
++m_version;
|
||||
Bool stateChanged = false;
|
||||
for (auto& stored : m_parameters.ScissorBoxes) {
|
||||
if (stored == box) continue;
|
||||
stored = box;
|
||||
stateChanged = true;
|
||||
}
|
||||
if (stateChanged) ++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBox() const {
|
||||
return m_parameters.ScissorBox;
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
|
||||
void RenderState::SetScissorBoxIndexed(Uint index, IntVec4 box) {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return;
|
||||
}
|
||||
if (m_parameters.ScissorBoxes[index] == box) return;
|
||||
|
||||
m_parameters.ScissorBoxes[index] = box;
|
||||
++m_version;
|
||||
}
|
||||
|
||||
const IntVec4& RenderState::GetScissorBoxIndexed(Uint index) const {
|
||||
if (index >= RenderStateParameters::MAX_VIEWPORTS) {
|
||||
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
|
||||
return m_parameters.ScissorBoxes[0];
|
||||
}
|
||||
return m_parameters.ScissorBoxes[index];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
|
||||
@@ -220,8 +220,22 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct RenderStateParameters {
|
||||
// ARB_viewport_array / GL 4.6 core 13.6.1: the viewport, the scissor rectangle, the depth
|
||||
// range and the scissor-test enable are all arrays indexed by gl_ViewportIndex, and the
|
||||
// spec floor for MAX_VIEWPORTS is 16. MobileGL advertises exactly 16 on both backends, so
|
||||
// this is also what GL_MAX_VIEWPORTS reports (see the backend loaders' caps.MaxViewports).
|
||||
static constexpr Uint MAX_VIEWPORTS = 16;
|
||||
|
||||
// Rasterization
|
||||
IntVec4 Viewport = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// The viewport rectangle is FLOAT state as of GL 4.1 - ViewportIndexedf writes fractional
|
||||
// values and GetFloati_v(GL_VIEWPORT) must hand them back bit-exact
|
||||
// (KHR-GL43.viewport_array.viewport_api compares with ==, no tolerance). glViewport's
|
||||
// integers are simply one way to write it. Index 0 is what a program that never assigns
|
||||
// gl_ViewportIndex rasterizes against, and what the classic glViewport /
|
||||
// glGetIntegerv(GL_VIEWPORT) pair addresses. Both backends rasterize the rectangle
|
||||
// rounded back to integers; the STATE stays exact, which is the half the conformance
|
||||
// suite checks (see the KNOWN INFIDELITY note in AdvertisedLimitsScenario.cpp).
|
||||
Array<FloatVec4, MAX_VIEWPORTS> Viewports{}; // x, y, width, height
|
||||
Float LineWidth = 1.0f;
|
||||
Float PointSize = 1.0f;
|
||||
// GL_PATCH_VERTICES: how many vertices one tessellation patch consumes.
|
||||
@@ -247,7 +261,13 @@ namespace MobileGL {
|
||||
Float ClearDepth = 1.0f;
|
||||
Uint32 ClearStencil = 0;
|
||||
FloatVec4 BlendColor = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
FloatVec2 DepthRange = FloatVec2(0.0f, 1.0f);
|
||||
// Per-viewport depth range (glDepthRangeIndexed / glDepthRangeArrayv). Every entry is
|
||||
// initialized to (0, 1) in RenderState's constructor - a default member initializer would
|
||||
// not survive the Array<> aggregate. Kept float rather than double: DepthRangeArrayv takes
|
||||
// GLdouble, but the value reaches the hardware as VkViewport::minDepth/maxDepth (float) on
|
||||
// Magma and glDepthRangef on Espryt, so a double store would only widen the readback and
|
||||
// then lose it again at the same place.
|
||||
Array<FloatVec2, MAX_VIEWPORTS> DepthRanges{};
|
||||
Float SampleCoverageValue = 1.0f;
|
||||
Bool SampleCoverageInvert = false;
|
||||
Uint32 SampleMaskValue = 0xffffffffu;
|
||||
@@ -299,10 +319,15 @@ namespace MobileGL {
|
||||
Bool SampleAlphaToOneEnabled = false;
|
||||
Bool SampleCoverageEnabled = false;
|
||||
Bool SampleMaskEnabled = false;
|
||||
Bool ScissorTestEnabled = false;
|
||||
Bool StencilTestEnabled = false;
|
||||
Bool ProgramPointSizeEnabled = false;
|
||||
IntVec4 ScissorBox = IntVec4(0, 0, 0, 0); // x, y, width, height
|
||||
// glEnable(GL_SCISSOR_TEST) enables the test for EVERY viewport, glEnablei for one
|
||||
// (GL 4.6 core 17.3.2), so this is 16 bits and not a bool. Bit 0 is what the classic
|
||||
// glIsEnabled(GL_SCISSOR_TEST) reports and what both backends currently consume. Unlike
|
||||
// ClipDistanceEnabledMask below it DOES bump the pipeline version, because DirectGLES
|
||||
// turns it into a real glEnable/glDisable.
|
||||
Uint32 ScissorTestEnabledMask = 0;
|
||||
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
|
||||
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
|
||||
// eight bools because every consumer wants the set, not an individual flag, and because
|
||||
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that
|
||||
@@ -323,8 +348,14 @@ namespace MobileGL {
|
||||
const RenderStateParameters& GetAllParameters() const;
|
||||
|
||||
// Rasterization
|
||||
// ARB_viewport_array defines glViewport as ViewportIndexedf on EVERY index, so the
|
||||
// classic setter broadcasts; GetViewport answers for index 0 (rounded to the
|
||||
// integers glGetIntegerv(GL_VIEWPORT) and both backends want) and is BY VALUE for
|
||||
// that reason. The indexed pair is the verbatim float state.
|
||||
void SetViewport(IntVec4 viewport); // x, y, width, height
|
||||
const IntVec4& GetViewport() const; // x, y, width, height
|
||||
IntVec4 GetViewport() const; // x, y, width, height, viewport 0, rounded
|
||||
void SetViewportIndexed(Uint index, FloatVec4 viewport);
|
||||
const FloatVec4& GetViewportIndexed(Uint index) const;
|
||||
void SetLineWidth(Float width);
|
||||
Float GetLineWidth() const;
|
||||
void SetPointSize(Float size);
|
||||
@@ -400,8 +431,12 @@ namespace MobileGL {
|
||||
Uint32 GetClearStencil() const;
|
||||
void SetBlendColor(FloatVec4 color);
|
||||
const FloatVec4& GetBlendColor() const;
|
||||
// glDepthRange(f) writes every viewport's range (ARB_viewport_array); the indexed
|
||||
// pair is glDepthRangeIndexed / glDepthRangeArrayv. GetDepthRange answers index 0.
|
||||
void SetDepthRange(FloatVec2 range);
|
||||
const FloatVec2& GetDepthRange() const;
|
||||
void SetDepthRangeIndexed(Uint index, FloatVec2 range);
|
||||
const FloatVec2& GetDepthRangeIndexed(Uint index) const;
|
||||
void SetSampleCoverage(Float value, Bool invert);
|
||||
Float GetSampleCoverageValue() const;
|
||||
Bool GetSampleCoverageInvert() const;
|
||||
@@ -421,9 +456,12 @@ namespace MobileGL {
|
||||
void SetProvokingVertexMode(ProvokingVertexMode mode);
|
||||
ProvokingVertexMode GetProvokingVertexMode() const;
|
||||
|
||||
// Scissor
|
||||
// Scissor. glScissor writes every rectangle (ARB_viewport_array); GetScissorBox
|
||||
// answers for index 0.
|
||||
void SetScissorBox(IntVec4 box); // x, y, width, height
|
||||
const IntVec4& GetScissorBox() const; // x, y, width, height
|
||||
void SetScissorBoxIndexed(Uint index, IntVec4 box);
|
||||
const IntVec4& GetScissorBoxIndexed(Uint index) const;
|
||||
|
||||
private:
|
||||
// Bump both: any state change invalidates the draw snapshot, and this one also
|
||||
|
||||
@@ -4,6 +4,7 @@ add_executable(
|
||||
PipelineQuirkTest
|
||||
PipelineQuirkTest.cpp
|
||||
PassthroughTessControlTest.cpp
|
||||
ViewportIndexReflectionTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PipelineQuirkTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// MobileGL - MobileGL/MG_Test/Pipeline/ViewportIndexReflectionTest.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
|
||||
//
|
||||
// ProgramFactory::ReflectedWritesViewportIndexBuiltin is the switch that decides whether a
|
||||
// DirectVulkan pipeline declares one viewport or all sixteen. Getting it wrong is silent in both
|
||||
// directions and neither direction is caught by a state test:
|
||||
//
|
||||
// - a false NEGATIVE collapses every gl_ViewportIndex onto viewport 0, which is precisely the
|
||||
// bug the multi-viewport work exists to fix and which a set/get round trip cannot see;
|
||||
// - a false POSITIVE widens viewportCount for an ordinary Minecraft shader, costing a longer
|
||||
// vkCmdSetViewport per state change and, on a tiler, possibly a hardware fast path.
|
||||
//
|
||||
// So this compiles REAL GLSL through the same glslang path the renderer uses and reflects the
|
||||
// SPIR-V that comes out, rather than asserting against hand-assembled words: what has to hold is
|
||||
// that the detector agrees with what glslang actually emits for a shader that writes the builtin,
|
||||
// including the stage-by-stage question of WHERE it may be written (GL 4.1 allows the geometry
|
||||
// stage; ARB_shader_viewport_layer_array adds vertex and tessellation evaluation).
|
||||
//
|
||||
// The end-to-end claim - that a detected writer really does route pixels to its own viewport -
|
||||
// lives in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
// Owns the reflection module so a failing EXPECT cannot leak it.
|
||||
class ReflectModule {
|
||||
public:
|
||||
explicit ReflectModule(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) return;
|
||||
m_created = spvReflectCreateShaderModule(spirv.size() * sizeof(Uint32), spirv.data(), &m_module) ==
|
||||
SPV_REFLECT_RESULT_SUCCESS;
|
||||
}
|
||||
~ReflectModule() {
|
||||
if (m_created) spvReflectDestroyShaderModule(&m_module);
|
||||
}
|
||||
ReflectModule(const ReflectModule&) = delete;
|
||||
ReflectModule& operator=(const ReflectModule&) = delete;
|
||||
|
||||
Bool Created() const { return m_created; }
|
||||
const SpvReflectShaderModule& Get() const { return m_module; }
|
||||
|
||||
private:
|
||||
SpvReflectShaderModule m_module{};
|
||||
Bool m_created = false;
|
||||
};
|
||||
|
||||
class ViewportIndexReflectionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
|
||||
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Same stage, same shape, writing gl_Layer INSTEAD. Layered rendering and viewport routing
|
||||
// are different features and the detector must not confuse them: a Minecraft-style cubemap
|
||||
// pass writes gl_Layer and must keep the one-viewport pipeline.
|
||||
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
||||
layout(points, invocations = 6) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Layer = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainGeometry = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainVertex = R"(#version 410 core
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
const char* const kPlainFragment = R"(#version 410 core
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
)";
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, TrueForAGeometryShaderThatAssignsViewportIndex) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_TRUE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "a shader that assigns gl_ViewportIndex must get a multi-viewport pipeline; missing it is what "
|
||||
"collapses every index onto viewport 0";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAGeometryShaderThatOnlyAssignsLayer) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()))
|
||||
<< "gl_Layer is layered rendering, not viewport routing; widening viewportCount for it costs the "
|
||||
"single-viewport fast path for nothing";
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAPlainGeometryShader) {
|
||||
const ReflectModule module(CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry));
|
||||
ASSERT_TRUE(module.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(module.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForTheOrdinaryVertexAndFragmentStages) {
|
||||
// The shape every real application ships: neither stage may widen the pipeline.
|
||||
const ReflectModule vertexModule(CompileToSpirv(GL_VERTEX_SHADER, kPlainVertex));
|
||||
ASSERT_TRUE(vertexModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(vertexModule.Get()));
|
||||
|
||||
const ReflectModule fragmentModule(CompileToSpirv(GL_FRAGMENT_SHADER, kPlainFragment));
|
||||
ASSERT_TRUE(fragmentModule.Created());
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(fragmentModule.Get()));
|
||||
}
|
||||
|
||||
TEST_F(ViewportIndexReflectionTest, FalseForAnEmptyModuleWithoutDereferencing) {
|
||||
// A default-constructed module has no entry points. The scan runs on every link, so it
|
||||
// must survive a reflection that never got built rather than walk a null array.
|
||||
SpvReflectShaderModule emptyModule{};
|
||||
EXPECT_FALSE(ProgramFactory::ReflectedWritesViewportIndexBuiltin(emptyModule));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -6,11 +6,20 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this
|
||||
// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and,
|
||||
// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and
|
||||
// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||
// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists for exactly two
|
||||
// capabilities: GL_BLEND, indexed by draw buffer, and GL_SCISSOR_TEST, indexed by viewport
|
||||
// (ARB_viewport_array). Every other capability must come back as GL_INVALID_ENUM per GL 4.6
|
||||
// sec. 17.3.3 - and, far more importantly, must come back at all: RenderState::SetCapabilityIndexed
|
||||
// and IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION,
|
||||
// which unwinds a C++ exception through the C GL ABI and terminates the process.
|
||||
//
|
||||
// The second half of this file is the ARB_viewport_array indexed rectangle state. Every one of
|
||||
// glViewportArrayv/glViewportIndexedf(v)/glScissorArrayv/glScissorIndexed(v)/glDepthRangeArrayv/
|
||||
// glDepthRangeIndexed was a MGLOG_W_ONCE stub that raised no error and stored nothing, and the
|
||||
// indexed getters answered EVERY index with viewport 0's value, so a set/get round trip silently
|
||||
// reported the initial state. The assertions below are deliberately state-shaped rather than
|
||||
// render-shaped: this IS the state machine, and the rendering half (gl_ViewportIndex routing) is
|
||||
// asserted separately in MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
@@ -21,6 +30,7 @@
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/RenderState/RenderState.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -50,10 +60,11 @@ namespace {
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) {
|
||||
TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonIndexedCapabilities) {
|
||||
// GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or
|
||||
// a CTS negative test would hit.
|
||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) {
|
||||
// a CTS negative test would hit. GL_SCISSOR_TEST used to be in this list and is not any more:
|
||||
// ARB_viewport_array makes it the second indexed capability (see the tests below).
|
||||
for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_STENCIL_TEST}) {
|
||||
MG_Impl::GLImpl::Enablei(cap, 0);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
|
||||
@@ -89,3 +100,462 @@ TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// ARB_viewport_array: indexed viewport / scissor / depth-range state
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
constexpr GLuint kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
|
||||
|
||||
Array<Array<GLfloat, 4>, kMaxViewports> ReadAllViewports() {
|
||||
Array<Array<GLfloat, 4>, kMaxViewports> out{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, out[i].data());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Array<Array<GLdouble, 2>, kMaxViewports> ReadAllDepthRanges() {
|
||||
Array<Array<GLdouble, 2>, kMaxViewports> out{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, out[i].data());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(RenderStateTest, ScissorTestIsIndexedByViewport) {
|
||||
// The exact shape of KHR-GL43.viewport_array.scissor_test_state_api's toggle loop: one index
|
||||
// is flipped and EVERY index is read back, so a broadcast masquerading as an indexed write
|
||||
// cannot pass.
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint toggled = 0; toggled < kMaxViewports; ++toggled) {
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, toggled);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "index " << toggled;
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), i == toggled ? GL_TRUE : GL_FALSE)
|
||||
<< "enabled index " << toggled << ", read index " << i;
|
||||
}
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, toggled);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, toggled), GL_FALSE);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, NonIndexedScissorTestEnableWritesEveryViewport) {
|
||||
// GL 4.6 core 17.3.2: Enable/Disable(SCISSOR_TEST) is "for all viewports". Reading only
|
||||
// index 0 back would let a broadcast-less implementation through, so every index is checked.
|
||||
MG_Impl::GLImpl::Enable(GL_SCISSOR_TEST);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_TRUE) << "index " << i;
|
||||
}
|
||||
// ... and the non-indexed query answers for viewport 0 (GL 4.6 core 22.1).
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, i), GL_FALSE) << "index " << i;
|
||||
}
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||
|
||||
// An indexed enable on a NON-zero index must not move the non-indexed answer.
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 3);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_FALSE);
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, 0);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabled(GL_SCISSOR_TEST), GL_TRUE);
|
||||
|
||||
MG_Impl::GLImpl::Disable(GL_SCISSOR_TEST);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ScissorTestEnableRejectsAnOutOfRangeViewportIndex) {
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_SCISSOR_TEST, kMaxViewports), GL_FALSE);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// MAX_VIEWPORTS - 1 is the last LEGAL index and must stay silent.
|
||||
MG_Impl::GLImpl::Enablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::Disablei(GL_SCISSOR_TEST, kMaxViewports - 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, MaxViewportsMatchesTheIndexedStateWidth) {
|
||||
// The advertised limit and the width of the state arrays are the same number by
|
||||
// construction; a divergence would make some index simultaneously legal to the CTS and
|
||||
// out of range to the setters.
|
||||
GLint maxViewports = 0;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
EXPECT_EQ(maxViewports, static_cast<GLint>(kMaxViewports));
|
||||
EXPECT_GE(maxViewports, 16) << "GL 4.3 core requires MAX_VIEWPORTS >= 16";
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportArrayvRoundTripsThroughEveryGetterWidth) {
|
||||
Array<GLfloat, kMaxViewports * 4> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
written[i * 4 + 0] = static_cast<GLfloat>(i) + 0.125f;
|
||||
written[i * 4 + 1] = static_cast<GLfloat>(i) + 0.25f;
|
||||
written[i * 4 + 2] = static_cast<GLfloat>(64 + i);
|
||||
written[i * 4 + 3] = static_cast<GLfloat>(32 + i);
|
||||
}
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLfloat asFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, asFloat);
|
||||
// Bit-exact: the fractional origin is the whole point of float viewport state, and the
|
||||
// CTS compares with == (0.125 and 0.25 are exact binary fractions, so this is fair).
|
||||
EXPECT_EQ(asFloat[0], written[i * 4 + 0]) << "index " << i << " must round-trip verbatim";
|
||||
EXPECT_EQ(asFloat[1], written[i * 4 + 1]) << "index " << i;
|
||||
EXPECT_EQ(asFloat[2], written[i * 4 + 2]) << "index " << i;
|
||||
EXPECT_EQ(asFloat[3], written[i * 4 + 3]) << "index " << i;
|
||||
|
||||
GLdouble asDouble[4] = {};
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_VIEWPORT, i, asDouble);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(asDouble[c], static_cast<GLdouble>(written[i * 4 + c])) << "index " << i << " component " << c;
|
||||
}
|
||||
|
||||
// The integer widths round to nearest rather than truncate; the .5+ case is pinned by
|
||||
// ViewportRoundsRatherThanTruncatesForIntegerQueries below.
|
||||
GLint asInt[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, i, asInt);
|
||||
EXPECT_EQ(asInt[2], static_cast<GLint>(64 + i)) << "index " << i;
|
||||
EXPECT_EQ(asInt[3], static_cast<GLint>(32 + i)) << "index " << i;
|
||||
|
||||
GLint64 asInt64[4] = {};
|
||||
MG_Impl::GLImpl::GetInteger64i_v(GL_VIEWPORT, i, asInt64);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(asInt64[c], static_cast<GLint64>(asInt[c])) << "index " << i << " component " << c;
|
||||
}
|
||||
|
||||
GLboolean asBool[4] = {};
|
||||
MG_Impl::GLImpl::GetBooleani_v(GL_VIEWPORT, i, asBool);
|
||||
EXPECT_EQ(asBool[2], GL_TRUE) << "index " << i << ": a non-zero width is GL_TRUE";
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportRoundsRatherThanTruncatesForIntegerQueries) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(2, 0.0f, 0.0f, 255.875f, 63.5f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
GLint asInt[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 2, asInt);
|
||||
EXPECT_EQ(asInt[2], 256);
|
||||
EXPECT_EQ(asInt[3], 64);
|
||||
|
||||
GLfloat asFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 2, asFloat);
|
||||
EXPECT_EQ(asFloat[2], 255.875f) << "the integer query must not disturb the stored float";
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ViewportIndexedWritesTouchExactlyOneIndex) {
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 8, 8);
|
||||
const auto before = ReadAllViewports();
|
||||
|
||||
for (GLuint target = 0; target < kMaxViewports; ++target) {
|
||||
const GLfloat value[4] = {0.375f, 0.375f, 0.625f, 0.625f};
|
||||
// Alternate the two indexed entry points so both are covered by the isolation claim.
|
||||
if (target % 2 == 0) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(target, value[0], value[1], value[2], value[3]);
|
||||
} else {
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(target, value);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const auto after = ReadAllViewports();
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
if (i == target) {
|
||||
EXPECT_EQ(after[i][0], value[0]) << "index " << i;
|
||||
EXPECT_EQ(after[i][2], value[2]) << "index " << i;
|
||||
} else {
|
||||
EXPECT_EQ(after[i], before[i]) << "write to " << target << " disturbed index " << i;
|
||||
}
|
||||
}
|
||||
MG_Impl::GLImpl::ViewportIndexedf(target, before[target][0], before[target][1], before[target][2],
|
||||
before[target][3]);
|
||||
}
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ClassicViewportWritesEveryIndexAndIsVisibleThroughIndexZero) {
|
||||
// Both directions of the aliasing. ARB_viewport_array defines glViewport as ViewportIndexedf
|
||||
// on every index, and glGetIntegerv(GL_VIEWPORT) as viewport 0.
|
||||
MG_Impl::GLImpl::ViewportIndexedf(5, 1.0f, 2.0f, 3.0f, 4.0f);
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLfloat data[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, i, data);
|
||||
EXPECT_EQ(data[0], 0.0f) << "index " << i;
|
||||
EXPECT_EQ(data[2], 1.0f) << "glViewport must overwrite index " << i;
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::ViewportIndexedf(0, 4.0f, 5.0f, 6.0f, 7.0f);
|
||||
GLint classic[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_VIEWPORT, classic);
|
||||
EXPECT_EQ(classic[0], 4);
|
||||
EXPECT_EQ(classic[2], 6);
|
||||
GLfloat classicFloat[4] = {};
|
||||
MG_Impl::GLImpl::GetFloatv(GL_VIEWPORT, classicFloat);
|
||||
EXPECT_EQ(classicFloat[2], 6.0f);
|
||||
// Index 5 keeps its own value: writing index 0 is not a broadcast.
|
||||
GLfloat other[4] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, 5, other);
|
||||
EXPECT_EQ(other[2], 1.0f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ScissorBoxRoundTripsPerIndexAndAliasesIndexZero) {
|
||||
Array<GLint, kMaxViewports * 4> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
written[i * 4 + 0] = static_cast<GLint>(i);
|
||||
written[i * 4 + 1] = static_cast<GLint>(i * 2);
|
||||
written[i * 4 + 2] = static_cast<GLint>(16 + i);
|
||||
written[i * 4 + 3] = static_cast<GLint>(8 + i);
|
||||
}
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
GLint readBack[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, readBack);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
EXPECT_EQ(readBack[c], written[i * 4 + c]) << "index " << i << " component " << c;
|
||||
}
|
||||
}
|
||||
|
||||
// Indexed writes stay indexed; both spellings.
|
||||
MG_Impl::GLImpl::ScissorIndexed(4, 4, 4, 8, 8);
|
||||
const GLint indexedV[4] = {9, 9, 12, 12};
|
||||
MG_Impl::GLImpl::ScissorIndexedv(7, indexedV);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
GLint probe[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 4, probe);
|
||||
EXPECT_EQ(probe[2], 8);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 7, probe);
|
||||
EXPECT_EQ(probe[2], 12);
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 5, probe);
|
||||
EXPECT_EQ(probe[2], static_cast<GLint>(16 + 5)) << "index 5 must be untouched";
|
||||
|
||||
// glScissor writes every rectangle, and glGetIntegerv(GL_SCISSOR_BOX) reports rectangle 0.
|
||||
MG_Impl::GLImpl::Scissor(2, 3, 5, 6);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, i, probe);
|
||||
EXPECT_EQ(probe[0], 2) << "index " << i;
|
||||
EXPECT_EQ(probe[2], 5) << "index " << i;
|
||||
}
|
||||
GLint classic[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_SCISSOR_BOX, classic);
|
||||
EXPECT_EQ(classic[2], 5);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, DepthRangeRoundTripsPerIndexAndAliasesIndexZero) {
|
||||
Array<GLdouble, kMaxViewports * 2> written{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
// Exact binary fractions, like the CTS uses: a float-backed store round-trips them.
|
||||
written[i * 2 + 0] = static_cast<GLdouble>(i) / 16.0;
|
||||
written[i * 2 + 1] = 1.0 - static_cast<GLdouble>(i) / 16.0;
|
||||
}
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(0, kMaxViewports, written.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
const auto readBack = ReadAllDepthRanges();
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
EXPECT_EQ(readBack[i][0], written[i * 2 + 0]) << "index " << i;
|
||||
EXPECT_EQ(readBack[i][1], written[i * 2 + 1]) << "index " << i;
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(9, 0.25, 0.75);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
GLdouble probe[2] = {};
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 9, probe);
|
||||
EXPECT_EQ(probe[0], 0.25);
|
||||
EXPECT_EQ(probe[1], 0.75);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, 8, probe);
|
||||
EXPECT_EQ(probe[0], 8.0 / 16.0) << "index 8 must be untouched";
|
||||
|
||||
GLfloat asFloat[2] = {};
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_DEPTH_RANGE, 9, asFloat);
|
||||
EXPECT_EQ(asFloat[0], 0.25f);
|
||||
EXPECT_EQ(asFloat[1], 0.75f);
|
||||
|
||||
// glDepthRange writes every range; glGetDoublev(GL_DEPTH_RANGE) reports range 0.
|
||||
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, i, probe);
|
||||
EXPECT_EQ(probe[0], 0.0) << "index " << i;
|
||||
EXPECT_EQ(probe[1], 1.0) << "index " << i;
|
||||
}
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(0, 0.125, 0.875);
|
||||
GLdouble classic[2] = {};
|
||||
MG_Impl::GLImpl::GetDoublev(GL_DEPTH_RANGE, classic);
|
||||
EXPECT_EQ(classic[0], 0.125);
|
||||
EXPECT_EQ(classic[1], 0.875);
|
||||
MG_Impl::GLImpl::DepthRange(0.0, 1.0);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, IndexedRectangleSettersRejectAnOutOfRangeIndex) {
|
||||
const GLfloat viewport[4] = {0.0f, 0.0f, 1.0f, 1.0f};
|
||||
const GLint scissor[4] = {0, 0, 1, 1};
|
||||
|
||||
for (const GLuint index : {kMaxViewports, kMaxViewports + 1}) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(index, viewport);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorIndexedv(index, scissor);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(index, 0.0, 1.0);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// The last legal index must stay silent - api_errors checks both sides of the boundary.
|
||||
MG_Impl::GLImpl::ViewportIndexedf(kMaxViewports - 1, 0.0f, 0.0f, 1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ScissorIndexed(kMaxViewports - 1, 0, 0, 1, 1);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::DepthRangeIndexed(kMaxViewports - 1, 0.0, 1.0);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, ArraySettersRejectAnOutOfRangeRangeButAcceptAnExactlyFullOne) {
|
||||
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||
Array<GLint, kMaxViewports * 4> scissors{};
|
||||
Array<GLdouble, kMaxViewports * 2> depths{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
viewports[i * 4 + 2] = 1.0f;
|
||||
viewports[i * 4 + 3] = 1.0f;
|
||||
scissors[i * 4 + 2] = 1;
|
||||
scissors[i * 4 + 3] = 1;
|
||||
depths[i * 2 + 1] = 1.0;
|
||||
}
|
||||
|
||||
// first == MAX_VIEWPORTS, and first + count > MAX_VIEWPORTS.
|
||||
MG_Impl::GLImpl::ViewportArrayv(kMaxViewports, 1, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(kMaxViewports, 1, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(kMaxViewports, 1, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// first + count == MAX_VIEWPORTS is LEGAL - the off-by-one an ">=" bound would get wrong,
|
||||
// and one KHR-GL43.viewport_array.api_errors asserts explicitly.
|
||||
MG_Impl::GLImpl::ViewportArrayv(1, kMaxViewports - 1, viewports.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::ScissorArrayv(1, kMaxViewports - 1, scissors.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(1, kMaxViewports - 1, depths.data());
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
// A negative count is GL_INVALID_VALUE and must not be read as a huge unsigned length.
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, -1, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, -1, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::DepthRangeArrayv(0, -1, depths.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, NegativeExtentsAreRejectedWithoutDisturbingState) {
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 4, 4);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, 4, 4);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::Viewport(0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Viewport(0, 0, 1, -1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::Scissor(0, 0, 1, -1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
for (GLuint index = 0; index < kMaxViewports; ++index) {
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, -1.0f, 1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ViewportIndexedf(index, 0.0f, 0.0f, 1.0f, -1.0f);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
const GLfloat badW[4] = {0.0f, 0.0f, -1.0f, 1.0f};
|
||||
MG_Impl::GLImpl::ViewportIndexedfv(index, badW);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::ScissorIndexed(index, 0, 0, -1, 1);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
const GLint badH[4] = {0, 0, 1, -1};
|
||||
MG_Impl::GLImpl::ScissorIndexedv(index, badH);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
// The array form must reject the WHOLE call for one bad element, exactly once, and
|
||||
// leave every rectangle alone - api_errors submits a full 16-element array with a
|
||||
// single negative extent and then requires the error queue to hold one entry.
|
||||
Array<GLfloat, kMaxViewports * 4> viewports{};
|
||||
Array<GLint, kMaxViewports * 4> scissors{};
|
||||
for (GLuint i = 0; i < kMaxViewports; ++i) {
|
||||
viewports[i * 4 + 2] = 1.0f;
|
||||
viewports[i * 4 + 3] = 1.0f;
|
||||
scissors[i * 4 + 2] = 1;
|
||||
scissors[i * 4 + 3] = 1;
|
||||
}
|
||||
viewports[index * 4 + 2] = -1.0f;
|
||||
scissors[index * 4 + 3] = -1;
|
||||
MG_Impl::GLImpl::ViewportArrayv(0, kMaxViewports, viewports.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::ScissorArrayv(0, kMaxViewports, scissors.data());
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// Nothing above may have landed.
|
||||
GLint viewport[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_VIEWPORT, 0, viewport);
|
||||
EXPECT_EQ(viewport[2], 4);
|
||||
EXPECT_EQ(viewport[3], 4);
|
||||
GLint scissor[4] = {};
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, 0, scissor);
|
||||
EXPECT_EQ(scissor[2], 4);
|
||||
EXPECT_EQ(scissor[3], 4);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) {
|
||||
GLint ints[4] = {};
|
||||
GLfloat floats[4] = {};
|
||||
GLdouble doubles[4] = {};
|
||||
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports, ints);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports, floats);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports, doubles);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
|
||||
MG_Impl::GLImpl::GetIntegeri_v(GL_SCISSOR_BOX, kMaxViewports - 1, ints);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::GetFloati_v(GL_VIEWPORT, kMaxViewports - 1, floats);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles);
|
||||
ExpectSingleGlError(GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -4033,3 +4033,306 @@ TEST_F(TextureTest, TexStorage2DLeavesAGenericCompressedFormatUncompressed) {
|
||||
EXPECT_EQ(compressed, GL_FALSE);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ===================== glCopyImageSubData validation (KHR-GL43.copy_image) =====================
|
||||
//
|
||||
// Every case below is a mechanism the conformance group caught in the field, and each one is
|
||||
// pinned here because the backend cannot: a wrongly ACCEPTED copy shows up only as wrong pixels
|
||||
// on a device, and a wrongly REJECTED one shows up only as a conformance failure.
|
||||
|
||||
namespace {
|
||||
struct CopyImageSubDataCall {
|
||||
Bool Called = false;
|
||||
GLenum SrcTarget = GL_NONE;
|
||||
GLenum DstTarget = GL_NONE;
|
||||
GLint SrcZ = -1;
|
||||
GLint DstZ = -1;
|
||||
GLsizei Depth = -1;
|
||||
} g_copyImageSubDataCall;
|
||||
|
||||
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
|
||||
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
|
||||
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
|
||||
GLsizei srcHeight, GLsizei srcDepth) {
|
||||
(void)srcTexture;
|
||||
(void)srcLevel;
|
||||
(void)srcX;
|
||||
(void)srcY;
|
||||
(void)dstTexture;
|
||||
(void)dstLevel;
|
||||
(void)dstX;
|
||||
(void)dstY;
|
||||
(void)srcWidth;
|
||||
(void)srcHeight;
|
||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
|
||||
}
|
||||
|
||||
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
||||
// call in every respect except the one the test is about.
|
||||
void MakeCopyImagePair(GLenum srcFormat, GLenum dstFormat, GLuint& srcTexture, GLuint& dstTexture,
|
||||
GLsizei levels = 1, GLsizei extent = 8) {
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, levels, srcFormat, extent, extent);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, levels, dstFormat, extent, extent);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// GL 4.6 core 18.3.2 compatibility is texel-block SIZE, not base internal format. RGB10_A2 and
|
||||
// R11F_G11F_B10F are both 32-bit and their bases differ (RGBA vs RGB); the old exact-base-format
|
||||
// predicate rejected the pair, which is what took down the whole cross-format half of the
|
||||
// conformance matrix on both backends.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsEqualTexelSizeAcrossDifferentBaseFormats) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGB10_A2, GL_R11F_G11F_B10F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the same rule: equal base format is not sufficient either. RGBA8 and RGBA32F
|
||||
// are both RGBA and 32 vs 128 bits, so the copy is illegal.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsDifferentTexelSizesWithTheSameBaseFormat) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA32F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// ...and the pairing that is legal purely because the sizes agree, across integer-ness too.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsIntegerAndFloatOfTheSameTexelSize) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA32UI, GL_RGBA32F, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// 18.3.2 spells a name that is not an object INVALID_VALUE. The shared texture-object validator
|
||||
// says INVALID_OPERATION, which is right for the entry points that reach an object through a
|
||||
// BINDING - hence a rule local to this entry point rather than a change to the helper.
|
||||
TEST_F(TextureTest, CopyImageSubDataNonExistentNameIsInvalidValue) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(4242, GL_TEXTURE_2D, 0, 0, 0, 0, 4243, GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// A target that disagrees with the object it names is INVALID_ENUM, not the INVALID_OPERATION the
|
||||
// shared target-uniformity validator records for the upload paths.
|
||||
TEST_F(TextureTest, CopyImageSubDataTargetNotMatchingTheObjectIsInvalidEnum) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D_ARRAY, 0, 0,
|
||||
0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
// The eleven whole-image targets only: a cube FACE converts to a target the frontend knows, so the
|
||||
// generic target validator lets it through, but 18.3.2 does not accept it here.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsTargetsOutsideTheSpecList) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, 0, dstTexture,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_ENUM);
|
||||
}
|
||||
|
||||
// A level the image does not have is INVALID_VALUE; a single-level texture asked for level 1 used
|
||||
// to reach the backend with whatever the storage layer answered for that level.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsLevelTheImageDoesNotHave) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MakeCopyImagePair(GL_RGBA8, GL_RGBA8, srcTexture, dstTexture);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 1, 0, 0, 0,
|
||||
1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
||||
}
|
||||
|
||||
// Sample counts must match. A single-sample image reports zero, so this same comparison is also
|
||||
// what refuses a copy between a multisample target and a non-multisample one.
|
||||
TEST_F(TextureTest, CopyImageSubDataRejectsSampleCountMismatch) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
// Two DIFFERENT counts are the whole point, so the case needs a context that can actually
|
||||
// create multisample storage - which this unit-test binary, with no backend behind the
|
||||
// renderable-format and sample-count queries, may not be able to. The precondition is
|
||||
// checked on the state objects rather than assumed, so this can only ever skip or test the
|
||||
// real rule; it can never pass vacuously.
|
||||
GLint maxSamples = 1;
|
||||
MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &maxSamples);
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(srcTexture, 1, GL_RGBA8, 8, 8, GL_FALSE);
|
||||
MG_Impl::GLImpl::TextureStorage2DMultisample(dstTexture, std::max(maxSamples, 2), GL_RGBA8, 8, 8, GL_FALSE);
|
||||
DrainPendingGlErrors();
|
||||
|
||||
const Int srcSamples = MG_State::pGLContext->GetTextureObject(srcTexture)->GetSamples();
|
||||
const Int dstSamples = MG_State::pGLContext->GetTextureObject(dstTexture)->GetSamples();
|
||||
if (srcSamples == dstSamples) {
|
||||
GTEST_SKIP() << "this context could not give the two textures different sample counts (both " << srcSamples
|
||||
<< "); nothing for the rule to reject";
|
||||
}
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstTexture,
|
||||
GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 1, 1, 1);
|
||||
EXPECT_FALSE(g_copyImageSubDataCall.Called);
|
||||
ExpectSingleGlError(GL_INVALID_OPERATION);
|
||||
}
|
||||
|
||||
// The layer range has to survive the frontend intact. Both backends used to drop it - DirectVulkan
|
||||
// pinned baseArrayLayer/layerCount at 0/1 - so a 12-layer copy moved one layer and said nothing;
|
||||
// this pins the frontend half of that contract.
|
||||
TEST_F(TextureTest, CopyImageSubDataForwardsTheWholeLayerRangeToTheBackend) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage3D(srcTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
MG_Impl::GLImpl::TextureStorage3D(dstTexture, 1, GL_RGBA8, 8, 8, 12);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 2, dstTexture, GL_TEXTURE_2D_ARRAY,
|
||||
0, 0, 0, 5, 4, 4, 7);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.SrcZ, 2);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstZ, 5);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.Depth, 7);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The shape KHR-GL43.copy_image.invalid_object ends on once the invalid-name cases are answered
|
||||
// correctly: two ordinary glTexImage2D textures, no storage object, one texel copied from the
|
||||
// origin. Nothing about it is exotic, which is exactly why it is worth a case of its own - every
|
||||
// rule added to this validator is a new way to reject it.
|
||||
TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &srcTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, srcTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
MG_Impl::GLImpl::GenTextures(1, &dstTexture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, dstTexture);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_2D, 0, 0, 0, 0, dstTexture, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||
1, 1, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
// ...and again after the names have been through a delete/regenerate cycle, which is what the
|
||||
// conformance case does between its sub-cases: it deletes an object to make it invalid, then
|
||||
// builds the next pair from names the allocator hands straight back.
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &srcTexture);
|
||||
MG_Impl::GLImpl::DeleteTextures(1, &dstTexture);
|
||||
DrainPendingGlErrors();
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint reusedSrc = 0;
|
||||
GLuint reusedDst = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::GenTextures(1, &reusedDst);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
|
||||
1, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A rectangle target reaches the backend as itself. The translation to the GL_TEXTURE_2D the ES
|
||||
// driver actually stores it in belongs to DirectGLES, not here - and putting it here would break
|
||||
// DirectVulkan, which needs the real target to tell an array copy from a flat one.
|
||||
TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) {
|
||||
const ScopedTextureBackendFunctionsOverride backendGuard;
|
||||
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
|
||||
g_copyImageSubDataCall = {};
|
||||
|
||||
GLuint srcTexture = 0;
|
||||
GLuint dstTexture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcTexture);
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstTexture);
|
||||
MG_Impl::GLImpl::TextureStorage2D(srcTexture, 1, GL_RGBA8, 8, 8);
|
||||
MG_Impl::GLImpl::TextureStorage2D(dstTexture, 1, GL_RGBA8, 8, 8);
|
||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
|
||||
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstTexture, GL_TEXTURE_RECTANGLE,
|
||||
0, 0, 0, 0, 4, 4, 1);
|
||||
EXPECT_TRUE(g_copyImageSubDataCall.Called);
|
||||
EXPECT_EQ(g_copyImageSubDataCall.SrcTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
@@ -1000,7 +1000,11 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
GLfloat smoothLineWidthRange[2] = {1.0f, 1.0f};
|
||||
GLfloat smoothLineWidthGranularity = 1.0f;
|
||||
GLfloat aliasedPointSizeRange[2] = {1.0f, 1.0f};
|
||||
GLfloat viewportBoundsRange[2] = {0.0f, 0.0f};
|
||||
// GL 4.6 core table 23.60 sets the MINIMUM VIEWPORT_BOUNDS_RANGE at [-32768, 32767], and
|
||||
// KHR-GL43.viewport_array.queries asserts exactly that floor. GLES has no such query, so
|
||||
// the glGetFloatv below raises GL_INVALID_ENUM and leaves this untouched - starting it at
|
||||
// {0, 0} advertised a range that admits no viewport origin at all.
|
||||
GLfloat viewportBoundsRange[2] = {-32768.0f, 32767.0f};
|
||||
GLint maxViewportDims[2] = {16384, 16384};
|
||||
GLint viewportSubpixelBits = 0;
|
||||
GLint max3DTextureSize = 16384;
|
||||
@@ -1289,8 +1293,12 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
caps.MaxViewports = maxViewports;
|
||||
caps.MaxViewportWidth = maxViewportDims[0];
|
||||
caps.MaxViewportHeight = maxViewportDims[1];
|
||||
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
|
||||
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
|
||||
// Only ever WIDER than the core minimum: a driver that answered the query is allowed to
|
||||
// exceed the floor but never to sit inside it, and a driver that rejected the query left
|
||||
// the floor in place. Written as a clamp rather than a plain assignment so a partial
|
||||
// write (one component answered, the other not) cannot narrow the range either.
|
||||
caps.ViewportBoundsRangeMin = std::min(viewportBoundsRange[0], -32768.0f);
|
||||
caps.ViewportBoundsRangeMax = std::max(viewportBoundsRange[1], 32767.0f);
|
||||
caps.ViewportSubpixelBits = viewportSubpixelBits;
|
||||
caps.MinFragmentInterpolationOffset =
|
||||
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
|
||||
|
||||
Reference in New Issue
Block a user