[MG_Backend] (Disaggregated): generate packed float mips with native shader blits

This commit is contained in:
2026-09-20 12:51:19 -04:00
parent d339053ea6
commit 5310ff62bb
4 changed files with 129 additions and 42 deletions
@@ -478,6 +478,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool BlitWireColorToDefault(WireImage source, WireImage destination, Bool BlitWireColorToDefault(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter); GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter);
Bool BlitWireColorImage(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap);
struct WireColorBlitResources; struct WireColorBlitResources;
WireColorBlitResources* m_wireColorBlitResources = nullptr; WireColorBlitResources* m_wireColorBlitResources = nullptr;
void DestroyWireColorBlitResources(); void DestroyWireColorBlitResources();
@@ -6,6 +6,7 @@
struct VulkanRenderer::WireColorBlitResources { struct VulkanRenderer::WireColorBlitResources {
struct Target { struct Target {
VkFormat format = VK_FORMAT_UNDEFINED; VkFormat format = VK_FORMAT_UNDEFINED;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
}; };
@@ -38,15 +39,25 @@
Bool VulkanRenderer::BlitWireColorToDefault(WireImage source, WireImage destination, Bool VulkanRenderer::BlitWireColorToDefault(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) { GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter) {
if (!destination.isDefault || source.isDefault || source.image == destination.image || if (!destination.isDefault) return false;
return BlitWireColorImage(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1, filter, false);
}
Bool VulkanRenderer::BlitWireColorImage(WireImage source, WireImage destination,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter, Bool mipmap) {
const Bool sameImage = source.image == destination.image;
if (source.isDefault || (sameImage && source.level == destination.level && source.layer == destination.layer) ||
source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT || source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT ||
source.is3D || (source.viewType != VK_IMAGE_VIEW_TYPE_2D && source.is3D || destination.is3D || (source.viewType != VK_IMAGE_VIEW_TYPE_2D &&
source.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) || source.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
(destination.viewType != VK_IMAGE_VIEW_TYPE_2D && destination.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) ||
!vkuFormatIsSampledFloat(source.format)) return false; !vkuFormatIsSampledFloat(source.format)) return false;
if (sx0 == sx1 || sy0 == sy1 || dx0 == dx1 || dy0 == dy1) return true; if (sx0 == sx1 || sy0 == sy1 || dx0 == dx1 || dy0 == dy1) return true;
if (!source.extent.width || !source.extent.height || if (!source.extent.width || !source.extent.height ||
!destination.extent.width || !destination.extent.height) return false; !destination.extent.width || !destination.extent.height) return false;
const auto transform = m_swapchainObject.GetPreTransform(); const auto transform = destination.isDefault ? m_swapchainObject.GetPreTransform() :
VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR && if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR &&
transform != VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR && transform != VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR &&
transform != VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR && transform != VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR &&
@@ -56,6 +67,15 @@
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
(filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0); (filter == GL_LINEAR ? VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT : 0);
if ((sourceProperties.optimalTilingFeatures & required) != required) return false; if ((sourceProperties.optimalTilingFeatures & required) != required) return false;
// Mip generation samples and writes the texture's own color space;
// GL_FRAMEBUFFER_SRGB only controls framebuffer operations.
if (!mipmap) destination.format = ResolveSrgbAttachmentWriteFormat(destination.format,
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
VkFormatProperties destinationProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, &destinationProperties);
if (!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) return false;
const VkImageLayout sourceLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
const VkImageLayout destinationLayout = sameImage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
struct Constants { struct Constants {
float srcRect[4]; float srcRect[4];
@@ -97,17 +117,15 @@
VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.linear)); VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.linear));
} }
auto& cache = *m_wireColorBlitResources; 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; WireColorBlitResources::Target* target = nullptr;
for (auto& entry : cache.targets) { for (auto& entry : cache.targets) {
if (entry.format == destination.format) { target = &entry; break; } if (entry.format == destination.format && entry.layout == destinationLayout) { target = &entry; break; }
} }
if (!target) { if (!target) {
cache.targets.emplace_back(); cache.targets.emplace_back();
target = &cache.targets.back(); target = &cache.targets.back();
target->format = destination.format; target->format = destination.format;
target->layout = destinationLayout;
VkAttachmentDescription attachment{}; VkAttachmentDescription attachment{};
attachment.format = destination.format; attachment.format = destination.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT; attachment.samples = VK_SAMPLE_COUNT_1_BIT;
@@ -115,8 +133,8 @@
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; attachment.initialLayout = attachment.finalLayout = destinationLayout;
const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; const VkAttachmentReference color{0, destinationLayout};
VkSubpassDescription subpass{}; VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1; subpass.colorAttachmentCount = 1;
@@ -174,8 +192,12 @@
} }
m_textureManager->FlushPendingUploads(); m_textureManager->FlushPendingUploads();
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); // Distinct mip/layer views of the same image are legal shader input and
TransitionWireImage(destination, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // output. GENERAL keeps the whole-image layout tracker truthful; the
// transition also orders the previous mip's attachment write.
TransitionWireImage(source, sourceLayout);
if (sameImage) destination.layout = source.layout;
else TransitionWireImage(destination, destinationLayout);
const auto makeView = [&](const WireImage& image) { const auto makeView = [&](const WireImage& image) {
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view.image = image.image; view.image = image.image;
@@ -213,7 +235,7 @@
alloc.pSetLayouts = &cache.descriptorLayout; alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
const VkDescriptorImageInfo sampled{filter == GL_LINEAR ? cache.linear : cache.nearest, const VkDescriptorImageInfo sampled{filter == GL_LINEAR ? cache.linear : cache.nearest,
sourceView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; sourceView, sourceLayout};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = descriptorSet; write.dstSet = descriptorSet;
write.dstBinding = 0; write.dstBinding = 0;
@@ -227,6 +249,12 @@
transform == VK_SURFACE_TRANSFORM_ROTATE_270_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 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 float dh = static_cast<float>(quarterTurn ? destination.extent.width : destination.extent.height);
// The shared vertex shader flips window Y. Offscreen images keep the
// source row order, so undo that flip in the destination rectangle.
if (!destination.isDefault) {
dy0 = static_cast<GLint>(destination.extent.height) - dy0;
dy1 = static_cast<GLint>(destination.extent.height) - dy1;
}
const Constants constants{{sx0 / sw, sy0 / sh, (static_cast<float>(sx1) - sx0) / sw, const Constants constants{{sx0 / sw, sy0 / sh, (static_cast<float>(sx1) - sx0) / sw,
(static_cast<float>(sy1) - sy0) / sh}, (static_cast<float>(sy1) - sy0) / sh},
{dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw, {dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw,
@@ -260,7 +288,7 @@
retired.imageViews = {sourceView, destinationView}; retired.imageViews = {sourceView, destinationView};
retired.descriptorPools.push_back(descriptorPool); retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired)); RetireWireObjects(Move(retired));
m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); if (destination.isDefault) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
return true; return true;
} }
#endif #endif
@@ -661,8 +661,9 @@
RetireWireDrawPass(); RetireWireDrawPass();
auto& state = MG_Pipe::MGPipeApplier(); auto& state = MG_Pipe::MGPipeApplier();
Uint32 base = state.VerbMipBaseLevel, baseLayer = 0, layerCount = 1; Uint32 base = state.VerbMipBaseLevel, baseLayer = 0, layerCount = 1;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
const auto handle = m_textureManager->ResolveWireTextureStorage(state.VerbMipRes, base, baseLayer, const auto handle = m_textureManager->ResolveWireTextureStorage(state.VerbMipRes, base, baseLayer,
nullptr, &layerCount); &viewFormat, &layerCount);
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle); auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
if (!resource) MagmaWireFatal("mipmap-resource"); if (!resource) MagmaWireFatal("mipmap-resource");
m_textureManager->FlushPendingUploads(); m_textureManager->FlushPendingUploads();
@@ -676,38 +677,74 @@
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT)
MagmaWireFatal("depth-stencil-mipmap@P7"); MagmaWireFatal("depth-stencil-mipmap@P7");
VkFormatProperties props{}; VkFormatProperties props{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,resource->format,&props); const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,format,&props);
const auto required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; const auto required = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT;
if ((props.optimalTilingFeatures & required) != required) MagmaWireFatal("mipmap-native-format@P7"); const Bool nativeBlit = format == resource->format && (props.optimalTilingFeatures & required) == required;
const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ? const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ?
VK_FILTER_LINEAR : VK_FILTER_NEAREST; VK_FILTER_LINEAR : VK_FILTER_NEAREST;
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; if (!nativeBlit) {
VkAccessFlags access = 0; // Packed floating-point color formats may support sampling and
GetImageTransitionSourceState(resource->layout,stage,access); // rendering without BLIT_DST. Generate from actual GPU mip contents
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout, // through the native shader pass; no client or staged texels enter.
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access, if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT ||
VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition"); resource->viewType == VK_IMAGE_VIEW_TYPE_1D || resource->viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
for (Uint32 level=base+1;level<base+count;++level) { resource->viewType == VK_IMAGE_VIEW_TYPE_3D ||
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; !(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) ||
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous, baseLayer >= resource->arrayLayers || layerCount > resource->arrayLayers - baseLayer)
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, MagmaWireFatal("mipmap-shader-format-or-shape");
VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1); for (Uint32 level = base + 1; level < base + count; ++level) {
VkImageBlit blit{}; for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
blit.srcSubresource = {resource->aspect,level-1,baseLayer,layerCount}; WireImage source{};
blit.dstSubresource = {resource->aspect,level,baseLayer,layerCount}; source.image = resource->image;
blit.srcOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>(level-1),1u)), source.format = format;
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)), source.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))}; source.layout = resource->layout;
blit.dstOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>level,1u)), source.trackedLayout = &resource->layout;
static_cast<Int32>(std::max(resource->extent.height>>level,1u)), source.levels = resource->mipLevels;
static_cast<Int32>(std::max(resource->depth>>level,1u))}; source.level = level - 1;
vkCmdBlitImage(frame.commandBuffer,resource->image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, source.layer = layer;
resource->image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,filter); source.extent = {std::max(resource->extent.width >> (level - 1), 1u),
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous, std::max(resource->extent.height >> (level - 1), 1u)};
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT, WireImage destination = source;
VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,level-1,1); destination.level = level;
destination.extent = {std::max(resource->extent.width >> level, 1u),
std::max(resource->extent.height >> level, 1u)};
if (!BlitWireColorImage(source, destination, 0, 0, source.extent.width, source.extent.height,
0, 0, destination.extent.width, destination.extent.height,
filter == VK_FILTER_LINEAR ? GL_LINEAR : GL_NEAREST, true))
MagmaWireFatal("mipmap-shader-blit");
}
}
} else {
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags access = 0;
GetImageTransitionSourceState(resource->layout,stage,access);
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access,
VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition");
for (Uint32 level=base+1;level<base+count;++level) {
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1);
VkImageBlit blit{};
blit.srcSubresource = {resource->aspect,level-1,baseLayer,layerCount};
blit.dstSubresource = {resource->aspect,level,baseLayer,layerCount};
blit.srcOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>(level-1),1u)),
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)),
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))};
blit.dstOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>level,1u)),
static_cast<Int32>(std::max(resource->extent.height>>level,1u)),
static_cast<Int32>(std::max(resource->depth>>level,1u))};
vkCmdBlitImage(frame.commandBuffer,resource->image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
resource->image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,&blit,filter);
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_READ_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,level-1,1);
}
resource->layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
} }
resource->layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
auto& record = state.TextureResources[handle.Slot]; auto& record = state.TextureResources[handle.Slot];
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count)); record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
resource->syncedWireSerial = ++record.Serial; resource->syncedWireSerial = ++record.Serial;
@@ -460,6 +460,25 @@ TEST_F(F1WireScenario, GenerateMipmapPackedFloatPixels) {
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
const GLfloat expected[4] = {0.25f, 0.5f, 0.75f, 1.0f}; const GLfloat expected[4] = {0.25f, 0.5f, 0.75f, 1.0f};
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 0.01f); for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 0.01f);
// Change the base on the GPU after its client upload. The regenerated mip
// must read those new pixels, and offscreen shader blits must keep row order.
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glDisable(GL_SCISSOR_TEST);
glClearColor(0.875f, 0.375f, 0.125f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 4, 8, 4);
glClearColor(0.125f, 0.75f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
glGenerateMipmap(GL_TEXTURE_2D);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2);
GLfloat rows[8]{};
glReadPixels(1, 0, 1, 2, GL_RGBA, GL_FLOAT, rows);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
const GLfloat generated[8] = {0.875f, 0.375f, 0.125f, 1.0f, 0.125f, 0.75f, 0.5f, 1.0f};
for (int i = 0; i < 8; ++i) EXPECT_NEAR(rows[i], generated[i], 0.01f);
} }
TEST_F(F1WireScenario, GenerateMipmapDepthPixels) { TEST_F(F1WireScenario, GenerateMipmapDepthPixels) {