[MG_Backend, MG_IntegrationTest] (Disaggregated): rotate native Magma window blits through server-owned shaders

This commit is contained in:
2026-09-20 03:06:26 -04:00
parent 31dc1a7304
commit 38919d453b
10 changed files with 531 additions and 0 deletions
+2
View File
@@ -1454,6 +1454,8 @@ jobs:
"F1WireScenario.UniformBufferRangeAndRebindPixels",
"F1WireScenario.ShortUniformBufferRangePadsMissingBytes",
"F1WireScenario.ComputeWrittenVertexAndIndexBuffersDrawAndReadBack",
"F1WireScenario.ColorBlitToDefaultIgnoresViewportAndPreservesOrientation",
"F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect",
"IndexedDrawFamilyScenario.AnElementBufferDrawElementsOffsetSelectsTheRightQuad",
"IndexedDrawFamilyScenario.DrawElementsBaseVertexMovesTheSameIndicesToTheOtherQuad",
"IndexedDrawFamilyScenario.DrawRangeElementsDrawsTheRangedRun",
@@ -3431,6 +3431,7 @@ void main() {
}
#if MOBILEGL_BUILD_DISAGGREGATED
DestroyWireDrawPass();
DestroyWireColorBlitResources();
#endif
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
@@ -7728,6 +7729,7 @@ void main() {
}
#include "WireFramebuffer.inc"
#include "WireColorBlit.inc"
#include "WireDraw.inc"
void VulkanRenderer::Clear(GLbitfield mask) {
@@ -458,6 +458,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ClearAttachmentPayload& payload, GLint drawbuffer = -1);
void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter);
Bool BlitWireColorToDefault(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter);
struct WireColorBlitResources;
WireColorBlitResources* m_wireColorBlitResources = nullptr;
void DestroyWireColorBlitResources();
void ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, void* pixels);
void GenerateWireMipmap();
@@ -0,0 +1,13 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D sourceImage;
layout(location = 0) in vec2 texCoord;
layout(location = 0) out vec4 outColor;
void main() {
// Source framebuffer clipping preserves the original scale, rather than
// extending the edge texels into pixels outside the source framebuffer.
if (any(lessThan(texCoord, vec2(0.0))) || any(greaterThan(texCoord, vec2(1.0)))) discard;
// Explicit LOD is required on Adreno 650: implicit LOD through a rotated
// single-mip UBWC render target can read outside its allocation.
outColor = textureLod(sourceImage, texCoord, 0.0);
}
@@ -0,0 +1,261 @@
// Included by VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc.
// The native blit owns no frontend object and never invokes the GL shader compiler.
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireColorBlitSpirv.h"
struct VulkanRenderer::WireColorBlitResources {
struct Target {
VkFormat format = VK_FORMAT_UNDEFINED;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
};
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
Vector<Target> targets;
};
void VulkanRenderer::DestroyWireColorBlitResources() {
if (!m_wireColorBlitResources) return;
// Shutdown calls this after vkDeviceWaitIdle; every individual blit also
// waits before recycling its descriptor set or destroying temporary views.
auto& cache = *m_wireColorBlitResources;
for (const auto& target : cache.targets) {
vkDestroyPipeline(m_device, target.pipeline, nullptr);
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
}
vkDestroySampler(m_device, cache.nearest, nullptr);
vkDestroySampler(m_device, cache.linear, nullptr);
vkDestroyDescriptorPool(m_device, cache.descriptorPool, nullptr);
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr);
vkDestroyShaderModule(m_device, cache.vertex, nullptr);
vkDestroyShaderModule(m_device, cache.fragment, nullptr);
delete m_wireColorBlitResources;
m_wireColorBlitResources = nullptr;
}
Bool VulkanRenderer::BlitWireColorToDefault(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
if (!destination.isDefault || source.isDefault || source.image == destination.image ||
source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT ||
source.is3D || (source.viewType != VK_IMAGE_VIEW_TYPE_2D &&
source.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
!vkuFormatIsSampledFloat(source.format)) return false;
if (sx0 == sx1 || sy0 == sy1 || dx0 == dx1 || dy0 == dy1) return true;
if (!source.extent.width || !source.extent.height ||
!destination.extent.width || !destination.extent.height) return false;
const auto transform = m_swapchainObject.GetPreTransform();
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR &&
transform != VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR &&
transform != VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR &&
transform != VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) return false;
VkFormatProperties sourceProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
if ((sourceProperties.optimalTilingFeatures & required) != required) return false;
struct Constants {
float srcRect[4];
float dstRect[4];
Int32 surfaceTransform;
};
static_assert(sizeof(Constants) == 36);
if (!m_wireColorBlitResources) {
m_wireColorBlitResources = new WireColorBlitResources;
auto& cache = *m_wireColorBlitResources;
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = bytes;
info.pCode = code;
VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module));
};
shader(kWireColorBlitVertexSpirv, sizeof(kWireColorBlitVertexSpirv), cache.vertex);
shader(kWireColorBlitFragmentSpirv, sizeof(kWireColorBlitFragmentSpirv), cache.fragment);
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout.bindingCount = 1;
layout.pBindings = &binding;
VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout));
const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(Constants)};
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayout.setLayoutCount = 1;
pipelineLayout.pSetLayouts = &cache.descriptorLayout;
pipelineLayout.pushConstantRangeCount = 1;
pipelineLayout.pPushConstantRanges = &push;
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout));
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.descriptorPool));
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = cache.descriptorPool;
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &cache.descriptorSet));
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
sampler.maxLod = 0.0f;
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest));
sampler.magFilter = sampler.minFilter = VK_FILTER_LINEAR;
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.linear));
}
auto& cache = *m_wireColorBlitResources;
// Honor the same sRGB write policy as wire draw attachments.
destination.format = ResolveSrgbAttachmentWriteFormat(destination.format,
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
WireColorBlitResources::Target* target = nullptr;
for (auto& entry : cache.targets) {
if (entry.format == destination.format) { target = &entry; break; }
}
if (!target) {
cache.targets.emplace_back();
target = &cache.targets.back();
target->format = destination.format;
VkAttachmentDescription attachment{};
attachment.format = destination.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color;
VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
pass.attachmentCount = 1;
pass.pAttachments = &attachment;
pass.subpassCount = 1;
pass.pSubpasses = &subpass;
VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass));
VkPipelineShaderStageCreateInfo stages[2]{};
for (auto& stage : stages) {
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage.pName = "main";
}
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = cache.vertex;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = cache.fragment;
const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewport.viewportCount = viewport.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState colorMask{};
colorMask.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &colorMask;
const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamic.dynamicStateCount = 2;
dynamic.pDynamicStates = dynamicStates;
VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipeline.stageCount = 2;
pipeline.pStages = stages;
pipeline.pVertexInputState = &vertex;
pipeline.pInputAssemblyState = &assembly;
pipeline.pViewportState = &viewport;
pipeline.pRasterizationState = &raster;
pipeline.pMultisampleState = &samples;
pipeline.pColorBlendState = &blend;
pipeline.pDynamicState = &dynamic;
pipeline.layout = cache.pipelineLayout;
pipeline.renderPass = target->renderPass;
VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, &target->pipeline));
}
m_textureManager->FlushPendingUploads();
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
TransitionWireImage(destination, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
const auto makeView = [&](const WireImage& image) {
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view.image = image.image;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = image.format;
// A layered framebuffer blit reads its first layer, as the native path does.
view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, image.level, 1, image.layer, 1};
VkImageView handle = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
return handle;
};
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = target->renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &destinationView;
framebufferInfo.width = destination.extent.width;
framebufferInfo.height = destination.extent.height;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
const VkDescriptorImageInfo sampled{filter == GL_LINEAR ? cache.linear : cache.nearest,
sourceView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = cache.descriptorSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &sampled;
vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr);
const float sw = static_cast<float>(source.extent.width), sh = static_cast<float>(source.extent.height);
const Bool quarterTurn = transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
const float dw = static_cast<float>(quarterTurn ? destination.extent.height : destination.extent.width);
const float dh = static_cast<float>(quarterTurn ? destination.extent.width : destination.extent.height);
const Constants constants{{sx0 / sw, sy0 / sh, (static_cast<float>(sx1) - sx0) / sw,
(static_cast<float>(sy1) - sy0) / sh},
{dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw,
(static_cast<float>(dy1) - dy0) / dh},
static_cast<Int32>(ToBlitSurfaceTransform(transform))};
auto& frame = m_frameContext.GetCurrent();
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = target->renderPass;
begin.framebuffer = framebuffer;
begin.renderArea = {{0, 0}, destination.extent};
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
static_cast<float>(destination.extent.height), 0.0f, 1.0f};
const VkRect2D scissor{{0, 0}, destination.extent};
vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport);
vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor);
vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
cache.pipelineLayout, 0, 1, &cache.descriptorSet, 0, nullptr);
vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
0, sizeof(constants), &constants);
vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0);
vkCmdEndRenderPass(frame.commandBuffer);
ResetDynamicStateShadow();
InvalidatePipelineMemo();
if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("color-blit-submit");
vkDestroyFramebuffer(m_device, framebuffer, nullptr);
vkDestroyImageView(m_device, sourceView, nullptr);
vkDestroyImageView(m_device, destinationView, nullptr);
m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
return true;
}
#endif
@@ -0,0 +1,25 @@
#version 450
// Native Vulkan blit: no GL program, registry entry, or runtime transpilation.
layout(push_constant) uniform BlitRect {
vec4 srcRect;
vec4 dstRect;
int surfaceTransform;
} rect;
layout(location = 0) out vec2 texCoord;
void main() {
// A quad, rather than an oversized triangle, also clips partial blits to
// their destination rectangle. The Vulkan viewport covers the native image.
const vec2 corners[6] = vec2[](
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0),
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0));
vec2 uv = corners[gl_VertexIndex];
vec2 p = (rect.dstRect.xy + uv * rect.dstRect.zw) * 2.0 - 1.0;
// Same GL-bottom-left to native-image mapping as the monolithic blit.
p.y = -p.y;
if (rect.surfaceTransform == 1) p = vec2(-p.y, p.x);
else if (rect.surfaceTransform == 2) p = -p;
else if (rect.surfaceTransform == 3) p = vec2(p.y, -p.x);
gl_Position = vec4(p, 0.0, 1.0);
texCoord = rect.srcRect.xy + uv * rect.srcRect.zw;
}
@@ -0,0 +1,127 @@
// Generated from WireColorBlit.vert / WireColorBlit.frag with:
// glslangValidator -V --target-env vulkan1.0 -o <stage>.spv <source>
// Validated with spirv-val --target-env vulkan1.0. Do not edit the words by hand.
#pragma once
static constexpr Uint32 kWireColorBlitVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000070u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0008000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000016u, 0x0000005du, 0x00000066u,
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00030005u,
0x00000009u, 0x00007675u, 0x00060005u, 0x00000016u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u,
0x00050005u, 0x00000019u, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030005u, 0x0000001cu, 0x00000070u,
0x00050005u, 0x0000001eu, 0x74696c42u, 0x74636552u, 0x00000000u, 0x00050006u, 0x0000001eu, 0x00000000u,
0x52637273u, 0x00746365u, 0x00050006u, 0x0000001eu, 0x00000001u, 0x52747364u, 0x00746365u, 0x00080006u,
0x0000001eu, 0x00000002u, 0x66727573u, 0x54656361u, 0x736e6172u, 0x6d726f66u, 0x00000000u, 0x00040005u,
0x00000020u, 0x74636572u, 0x00000000u, 0x00060005u, 0x0000005bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
0x00000000u, 0x00060006u, 0x0000005bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u,
0x0000005bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000005bu,
0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000005bu, 0x00000003u,
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000005du, 0x00000000u, 0x00050005u,
0x00000066u, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000016u, 0x0000000bu, 0x0000002au,
0x00030047u, 0x0000001eu, 0x00000002u, 0x00050048u, 0x0000001eu, 0x00000000u, 0x00000023u, 0x00000000u,
0x00050048u, 0x0000001eu, 0x00000001u, 0x00000023u, 0x00000010u, 0x00050048u, 0x0000001eu, 0x00000002u,
0x00000023u, 0x00000020u, 0x00030047u, 0x0000005bu, 0x00000002u, 0x00050048u, 0x0000005bu, 0x00000000u,
0x0000000bu, 0x00000000u, 0x00050048u, 0x0000005bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u,
0x0000005bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000005bu, 0x00000003u, 0x0000000bu,
0x00000004u, 0x00040047u, 0x00000066u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u,
0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
0x00000002u, 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x00040015u, 0x0000000au, 0x00000020u,
0x00000000u, 0x0004002bu, 0x0000000au, 0x0000000bu, 0x00000006u, 0x0004001cu, 0x0000000cu, 0x00000007u,
0x0000000bu, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x0005002cu, 0x00000007u, 0x0000000eu,
0x0000000du, 0x0000000du, 0x0004002bu, 0x00000006u, 0x0000000fu, 0x3f800000u, 0x0005002cu, 0x00000007u,
0x00000010u, 0x0000000fu, 0x0000000du, 0x0005002cu, 0x00000007u, 0x00000011u, 0x0000000du, 0x0000000fu,
0x0005002cu, 0x00000007u, 0x00000012u, 0x0000000fu, 0x0000000fu, 0x0009002cu, 0x0000000cu, 0x00000013u,
0x0000000eu, 0x00000010u, 0x00000011u, 0x00000011u, 0x00000010u, 0x00000012u, 0x00040015u, 0x00000014u,
0x00000020u, 0x00000001u, 0x00040020u, 0x00000015u, 0x00000001u, 0x00000014u, 0x0004003bu, 0x00000015u,
0x00000016u, 0x00000001u, 0x00040020u, 0x00000018u, 0x00000007u, 0x0000000cu, 0x00040017u, 0x0000001du,
0x00000006u, 0x00000004u, 0x0005001eu, 0x0000001eu, 0x0000001du, 0x0000001du, 0x00000014u, 0x00040020u,
0x0000001fu, 0x00000009u, 0x0000001eu, 0x0004003bu, 0x0000001fu, 0x00000020u, 0x00000009u, 0x0004002bu,
0x00000014u, 0x00000021u, 0x00000001u, 0x00040020u, 0x00000022u, 0x00000009u, 0x0000001du, 0x0004002bu,
0x00000006u, 0x0000002cu, 0x40000000u, 0x0004002bu, 0x0000000au, 0x00000030u, 0x00000001u, 0x00040020u,
0x00000031u, 0x00000007u, 0x00000006u, 0x0004002bu, 0x00000014u, 0x00000036u, 0x00000002u, 0x00040020u,
0x00000037u, 0x00000009u, 0x00000014u, 0x00020014u, 0x0000003au, 0x0004002bu, 0x0000000au, 0x00000041u,
0x00000000u, 0x0004002bu, 0x00000014u, 0x00000050u, 0x00000003u, 0x0004001cu, 0x0000005au, 0x00000006u,
0x00000030u, 0x0006001eu, 0x0000005bu, 0x0000001du, 0x00000006u, 0x0000005au, 0x0000005au, 0x00040020u,
0x0000005cu, 0x00000003u, 0x0000005bu, 0x0004003bu, 0x0000005cu, 0x0000005du, 0x00000003u, 0x0004002bu,
0x00000014u, 0x0000005eu, 0x00000000u, 0x00040020u, 0x00000063u, 0x00000003u, 0x0000001du, 0x00040020u,
0x00000065u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000065u, 0x00000066u, 0x00000003u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
0x00000009u, 0x00000007u, 0x0004003bu, 0x00000018u, 0x00000019u, 0x00000007u, 0x0004003bu, 0x00000008u,
0x0000001cu, 0x00000007u, 0x0004003du, 0x00000014u, 0x00000017u, 0x00000016u, 0x0003003eu, 0x00000019u,
0x00000013u, 0x00050041u, 0x00000008u, 0x0000001au, 0x00000019u, 0x00000017u, 0x0004003du, 0x00000007u,
0x0000001bu, 0x0000001au, 0x0003003eu, 0x00000009u, 0x0000001bu, 0x00050041u, 0x00000022u, 0x00000023u,
0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du, 0x00000024u, 0x00000023u, 0x0007004fu, 0x00000007u,
0x00000025u, 0x00000024u, 0x00000024u, 0x00000000u, 0x00000001u, 0x0004003du, 0x00000007u, 0x00000026u,
0x00000009u, 0x00050041u, 0x00000022u, 0x00000027u, 0x00000020u, 0x00000021u, 0x0004003du, 0x0000001du,
0x00000028u, 0x00000027u, 0x0007004fu, 0x00000007u, 0x00000029u, 0x00000028u, 0x00000028u, 0x00000002u,
0x00000003u, 0x00050085u, 0x00000007u, 0x0000002au, 0x00000026u, 0x00000029u, 0x00050081u, 0x00000007u,
0x0000002bu, 0x00000025u, 0x0000002au, 0x0005008eu, 0x00000007u, 0x0000002du, 0x0000002bu, 0x0000002cu,
0x00050050u, 0x00000007u, 0x0000002eu, 0x0000000fu, 0x0000000fu, 0x00050083u, 0x00000007u, 0x0000002fu,
0x0000002du, 0x0000002eu, 0x0003003eu, 0x0000001cu, 0x0000002fu, 0x00050041u, 0x00000031u, 0x00000032u,
0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000033u, 0x00000032u, 0x0004007fu, 0x00000006u,
0x00000034u, 0x00000033u, 0x00050041u, 0x00000031u, 0x00000035u, 0x0000001cu, 0x00000030u, 0x0003003eu,
0x00000035u, 0x00000034u, 0x00050041u, 0x00000037u, 0x00000038u, 0x00000020u, 0x00000036u, 0x0004003du,
0x00000014u, 0x00000039u, 0x00000038u, 0x000500aau, 0x0000003au, 0x0000003bu, 0x00000039u, 0x00000021u,
0x000300f7u, 0x0000003du, 0x00000000u, 0x000400fau, 0x0000003bu, 0x0000003cu, 0x00000045u, 0x000200f8u,
0x0000003cu, 0x00050041u, 0x00000031u, 0x0000003eu, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u,
0x0000003fu, 0x0000003eu, 0x0004007fu, 0x00000006u, 0x00000040u, 0x0000003fu, 0x00050041u, 0x00000031u,
0x00000042u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u, 0x00000043u, 0x00000042u, 0x00050050u,
0x00000007u, 0x00000044u, 0x00000040u, 0x00000043u, 0x0003003eu, 0x0000001cu, 0x00000044u, 0x000200f9u,
0x0000003du, 0x000200f8u, 0x00000045u, 0x00050041u, 0x00000037u, 0x00000046u, 0x00000020u, 0x00000036u,
0x0004003du, 0x00000014u, 0x00000047u, 0x00000046u, 0x000500aau, 0x0000003au, 0x00000048u, 0x00000047u,
0x00000036u, 0x000300f7u, 0x0000004au, 0x00000000u, 0x000400fau, 0x00000048u, 0x00000049u, 0x0000004du,
0x000200f8u, 0x00000049u, 0x0004003du, 0x00000007u, 0x0000004bu, 0x0000001cu, 0x0004007fu, 0x00000007u,
0x0000004cu, 0x0000004bu, 0x0003003eu, 0x0000001cu, 0x0000004cu, 0x000200f9u, 0x0000004au, 0x000200f8u,
0x0000004du, 0x00050041u, 0x00000037u, 0x0000004eu, 0x00000020u, 0x00000036u, 0x0004003du, 0x00000014u,
0x0000004fu, 0x0000004eu, 0x000500aau, 0x0000003au, 0x00000051u, 0x0000004fu, 0x00000050u, 0x000300f7u,
0x00000053u, 0x00000000u, 0x000400fau, 0x00000051u, 0x00000052u, 0x00000053u, 0x000200f8u, 0x00000052u,
0x00050041u, 0x00000031u, 0x00000054u, 0x0000001cu, 0x00000030u, 0x0004003du, 0x00000006u, 0x00000055u,
0x00000054u, 0x00050041u, 0x00000031u, 0x00000056u, 0x0000001cu, 0x00000041u, 0x0004003du, 0x00000006u,
0x00000057u, 0x00000056u, 0x0004007fu, 0x00000006u, 0x00000058u, 0x00000057u, 0x00050050u, 0x00000007u,
0x00000059u, 0x00000055u, 0x00000058u, 0x0003003eu, 0x0000001cu, 0x00000059u, 0x000200f9u, 0x00000053u,
0x000200f8u, 0x00000053u, 0x000200f9u, 0x0000004au, 0x000200f8u, 0x0000004au, 0x000200f9u, 0x0000003du,
0x000200f8u, 0x0000003du, 0x0004003du, 0x00000007u, 0x0000005fu, 0x0000001cu, 0x00050051u, 0x00000006u,
0x00000060u, 0x0000005fu, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000061u, 0x0000005fu, 0x00000001u,
0x00070050u, 0x0000001du, 0x00000062u, 0x00000060u, 0x00000061u, 0x0000000du, 0x0000000fu, 0x00050041u,
0x00000063u, 0x00000064u, 0x0000005du, 0x0000005eu, 0x0003003eu, 0x00000064u, 0x00000062u, 0x00050041u,
0x00000022u, 0x00000067u, 0x00000020u, 0x0000005eu, 0x0004003du, 0x0000001du, 0x00000068u, 0x00000067u,
0x0007004fu, 0x00000007u, 0x00000069u, 0x00000068u, 0x00000068u, 0x00000000u, 0x00000001u, 0x0004003du,
0x00000007u, 0x0000006au, 0x00000009u, 0x00050041u, 0x00000022u, 0x0000006bu, 0x00000020u, 0x0000005eu,
0x0004003du, 0x0000001du, 0x0000006cu, 0x0000006bu, 0x0007004fu, 0x00000007u, 0x0000006du, 0x0000006cu,
0x0000006cu, 0x00000002u, 0x00000003u, 0x00050085u, 0x00000007u, 0x0000006eu, 0x0000006au, 0x0000006du,
0x00050081u, 0x00000007u, 0x0000006fu, 0x00000069u, 0x0000006eu, 0x0003003eu, 0x00000066u, 0x0000006fu,
0x000100fdu, 0x00010038u,
};
static constexpr Uint32 kWireColorBlitFragmentSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000027u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000au, 0x0000001fu, 0x00030010u,
0x00000004u, 0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
0x00000000u, 0x00050005u, 0x0000000au, 0x43786574u, 0x64726f6fu, 0x00000000u, 0x00050005u, 0x0000001fu,
0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00050005u, 0x00000023u, 0x72756f73u, 0x6d496563u, 0x00656761u,
0x00040047u, 0x0000000au, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000001fu, 0x0000001eu, 0x00000000u,
0x00040047u, 0x00000023u, 0x00000021u, 0x00000000u, 0x00040047u, 0x00000023u, 0x00000022u, 0x00000000u,
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00020014u, 0x00000006u, 0x00030016u,
0x00000007u, 0x00000020u, 0x00040017u, 0x00000008u, 0x00000007u, 0x00000002u, 0x00040020u, 0x00000009u,
0x00000001u, 0x00000008u, 0x0004003bu, 0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000007u,
0x0000000cu, 0x00000000u, 0x0005002cu, 0x00000008u, 0x0000000du, 0x0000000cu, 0x0000000cu, 0x00040017u,
0x0000000eu, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000007u, 0x00000015u, 0x3f800000u, 0x0005002cu,
0x00000008u, 0x00000016u, 0x00000015u, 0x00000015u, 0x00040017u, 0x0000001du, 0x00000007u, 0x00000004u,
0x00040020u, 0x0000001eu, 0x00000003u, 0x0000001du, 0x0004003bu, 0x0000001eu, 0x0000001fu, 0x00000003u,
0x00090019u, 0x00000020u, 0x00000007u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u,
0x00000000u, 0x0003001bu, 0x00000021u, 0x00000020u, 0x00040020u, 0x00000022u, 0x00000000u, 0x00000021u,
0x0004003bu, 0x00000022u, 0x00000023u, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000008u, 0x0000000bu, 0x0000000au, 0x000500b8u,
0x0000000eu, 0x0000000fu, 0x0000000bu, 0x0000000du, 0x0004009au, 0x00000006u, 0x00000010u, 0x0000000fu,
0x000400a8u, 0x00000006u, 0x00000011u, 0x00000010u, 0x000300f7u, 0x00000013u, 0x00000000u, 0x000400fau,
0x00000011u, 0x00000012u, 0x00000013u, 0x000200f8u, 0x00000012u, 0x0004003du, 0x00000008u, 0x00000014u,
0x0000000au, 0x000500bau, 0x0000000eu, 0x00000017u, 0x00000014u, 0x00000016u, 0x0004009au, 0x00000006u,
0x00000018u, 0x00000017u, 0x000200f9u, 0x00000013u, 0x000200f8u, 0x00000013u, 0x000700f5u, 0x00000006u,
0x00000019u, 0x00000010u, 0x00000005u, 0x00000018u, 0x00000012u, 0x000300f7u, 0x0000001bu, 0x00000000u,
0x000400fau, 0x00000019u, 0x0000001au, 0x0000001bu, 0x000200f8u, 0x0000001au, 0x000100fcu, 0x000200f8u,
0x0000001bu, 0x0004003du, 0x00000021u, 0x00000024u, 0x00000023u, 0x0004003du, 0x00000008u, 0x00000025u,
0x0000000au, 0x00070058u, 0x0000001du, 0x00000026u, 0x00000024u, 0x00000025u, 0x00000002u, 0x0000000cu,
0x0003003eu, 0x0000001fu, 0x00000026u, 0x000100fdu, 0x00010038u,
};
@@ -298,6 +298,19 @@
WireImage destination = ResolveWireImage(*draw,dst,aspect);
if (!source.image || !destination.image) return;
m_textureManager->FlushPendingUploads();
// GL names logical window coordinates. Android's native swapchain may
// be quarter-turned (e.g. logical 2620x1280 over an image 1280x2620).
// An axis-aligned vkCmdBlitImage cannot express that rotation; issuing
// the logical rectangle directly also writes outside the native image.
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT && destination.isDefault && !source.isDefault &&
source.samples == VK_SAMPLE_COUNT_1_BIT && destination.samples == VK_SAMPLE_COUNT_1_BIT &&
source.image != destination.image) {
if (!BlitWireColorToDefault(source, destination, sx0, sy0, sx1, sy1,
dx0, dy0, dx1, dy1, filter))
MagmaWireFatal("default-color-blit-shape@P7");
m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
return;
}
const Bool sameImage = source.image == destination.image;
const VkImageLayout srcLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
const VkImageLayout dstLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
@@ -2432,6 +2432,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
F1WireScenario.UniformBufferRangeAndRebindPixels
F1WireScenario.ShortUniformBufferRangePadsMissingBytes
F1WireScenario.ComputeWrittenVertexAndIndexBuffersDrawAndReadBack
F1WireScenario.ColorBlitToDefaultIgnoresViewportAndPreservesOrientation
F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
IndexedDrawFamilyScenario.AnElementBufferDrawElementsOffsetSelectsTheRightQuad
IndexedDrawFamilyScenario.DrawElementsBaseVertexMovesTheSameIndicesToTheOtherQuad
IndexedDrawFamilyScenario.DrawRangeElementsDrawsTheRangedRun
@@ -95,6 +95,28 @@ protected:
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); }
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)) << "F1.setup.framebuffer";
}
void PaintNonSquareBlitSource() {
// GL coordinates: red/green on the bottom, blue/yellow on the top.
// A non-square source makes accidental extent/axis interchange visible.
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 6, 4);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
constexpr GLfloat colors[4][4] = {{1, 0, 0, 1}, {0, 1, 0, 1}, {0, 0, 1, 1}, {1, 1, 0, 1}};
glEnable(GL_SCISSOR_TEST);
for (int quadrant = 0; quadrant < 4; ++quadrant) {
glScissor((quadrant % 2) * 3, (quadrant / 2) * 2, 3, 2);
glClearColor(colors[quadrant][0], colors[quadrant][1], colors[quadrant][2], colors[quadrant][3]);
glClear(GL_COLOR_BUFFER_BIT);
}
glDisable(GL_SCISSOR_TEST);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "paint non-square source FBO";
}
std::array<GLubyte, 4> ReadOnePixel(int x, int y) {
std::array<GLubyte, 4> pixel{31, 47, 63, 79};
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
return pixel;
}
};
}
@@ -977,4 +999,62 @@ TEST_F(F1WireScenario, NamedBlitDefaultEndpointPixels) {
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 0, 255})) << "default read endpoint";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
}
TEST_F(F1WireScenario, ColorBlitToDefaultIgnoresViewportAndPreservesOrientation) {
if (!Ready()) return;
ASSERT_GE(Gl().Width(), 16);
ASSERT_GE(Gl().Height(), 16);
ASSERT_NO_FATAL_FAILURE(PaintNonSquareBlitSource());
const int width = Gl().Width(), height = Gl().Height();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // The destination really is the default framebuffer.
glClearColor(1, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(1, 2, 3, 5); // glBlitFramebuffer must ignore this unrelated viewport.
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, 6, 4, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
EXPECT_EQ(ReadOnePixel(width / 4, height / 4), (std::array<GLubyte, 4>{255, 0, 0, 255})) << "bottom left";
EXPECT_EQ(ReadOnePixel(3 * width / 4, height / 4), (std::array<GLubyte, 4>{0, 255, 0, 255})) << "bottom right";
EXPECT_EQ(ReadOnePixel(width / 4, 3 * height / 4), (std::array<GLubyte, 4>{0, 0, 255, 255})) << "top left";
EXPECT_EQ(ReadOnePixel(3 * width / 4, 3 * height / 4), (std::array<GLubyte, 4>{255, 255, 0, 255})) << "top right";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
GLint viewport[4]{};
glGetIntegerv(GL_VIEWPORT, viewport);
EXPECT_EQ((std::array<GLint, 4>{viewport[0], viewport[1], viewport[2], viewport[3]}),
(std::array<GLint, 4>{1, 2, 3, 5})) << "blit must not mutate the application's viewport";
Gl().EndFrame();
}
TEST_F(F1WireScenario, ColorBlitToDefaultHonorsScissorAndReversedRect) {
if (!Ready()) return;
ASSERT_GE(Gl().Width(), 16);
ASSERT_GE(Gl().Height(), 16);
ASSERT_NO_FATAL_FAILURE(PaintNonSquareBlitSource());
const int width = Gl().Width(), height = Gl().Height();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glClearColor(1, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(2, 1, 5, 3);
// Copy into the centre half with BOTH destination axes reversed. Scissor
// keeps only its left half, so a plain unclipped vkCmdBlitImage is incorrect.
glScissor(width / 4, height / 4, width / 4, height / 2);
glEnable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, 6, 4, 3 * width / 4, 3 * height / 4,
width / 4, height / 4, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
EXPECT_EQ(ReadOnePixel(3 * width / 8, 3 * height / 8), (std::array<GLubyte, 4>{255, 255, 0, 255}))
<< "reversed bottom-left destination reads the source top-right yellow quadrant";
EXPECT_EQ(ReadOnePixel(3 * width / 8, 5 * height / 8), (std::array<GLubyte, 4>{0, 255, 0, 255}))
<< "reversed top-left destination reads the source bottom-right green quadrant";
const std::array<GLubyte, 4> untouched{255, 0, 255, 255};
EXPECT_EQ(ReadOnePixel(5 * width / 8, 3 * height / 8), untouched) << "inside destination, outside scissor";
EXPECT_EQ(ReadOnePixel(5 * width / 8, 5 * height / 8), untouched) << "upper destination outside scissor";
EXPECT_EQ(ReadOnePixel(width / 8, height / 2), untouched) << "outside destination rectangle";
EXPECT_EQ(ReadOnePixel(3 * width / 8, height / 8), untouched) << "below destination and scissor";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glDisable(GL_SCISSOR_TEST);
Gl().EndFrame();
}
} // namespace MGITest