[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,
GLint sx0, GLint sy0, GLint sx1, GLint sy1,
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;
WireColorBlitResources* m_wireColorBlitResources = nullptr;
void DestroyWireColorBlitResources();
@@ -6,6 +6,7 @@
struct VulkanRenderer::WireColorBlitResources {
struct Target {
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
};
@@ -38,15 +39,25 @@
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 ||
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.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) ||
(destination.viewType != VK_IMAGE_VIEW_TYPE_2D && destination.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();
const auto transform = destination.isDefault ? m_swapchainObject.GetPreTransform() :
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_180_BIT_KHR &&
@@ -56,6 +67,15 @@
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;
// 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 {
float srcRect[4];
@@ -97,17 +117,15 @@
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 (entry.format == destination.format && entry.layout == destinationLayout) { target = &entry; break; }
}
if (!target) {
cache.targets.emplace_back();
target = &cache.targets.back();
target->format = destination.format;
target->layout = destinationLayout;
VkAttachmentDescription attachment{};
attachment.format = destination.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
@@ -115,8 +133,8 @@
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};
attachment.initialLayout = attachment.finalLayout = destinationLayout;
const VkAttachmentReference color{0, destinationLayout};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
@@ -174,8 +192,12 @@
}
m_textureManager->FlushPendingUploads();
TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
TransitionWireImage(destination, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
// Distinct mip/layer views of the same image are legal shader input and
// 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) {
VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view.image = image.image;
@@ -213,7 +235,7 @@
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
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};
write.dstSet = descriptorSet;
write.dstBinding = 0;
@@ -227,6 +249,12 @@
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);
// 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,
(static_cast<float>(sy1) - sy0) / sh},
{dx0 / dw, dy0 / dh, (static_cast<float>(dx1) - dx0) / dw,
@@ -260,7 +288,7 @@
retired.imageViews = {sourceView, destinationView};
retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired));
m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true);
return true;
}
#endif
@@ -661,8 +661,9 @@
RetireWireDrawPass();
auto& state = MG_Pipe::MGPipeApplier();
Uint32 base = state.VerbMipBaseLevel, baseLayer = 0, layerCount = 1;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
const auto handle = m_textureManager->ResolveWireTextureStorage(state.VerbMipRes, base, baseLayer,
nullptr, &layerCount);
&viewFormat, &layerCount);
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
if (!resource) MagmaWireFatal("mipmap-resource");
m_textureManager->FlushPendingUploads();
@@ -676,38 +677,74 @@
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT)
MagmaWireFatal("depth-stencil-mipmap@P7");
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;
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 ?
VK_FILTER_LINEAR : VK_FILTER_NEAREST;
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);
if (!nativeBlit) {
// Packed floating-point color formats may support sampling and
// rendering without BLIT_DST. Generate from actual GPU mip contents
// through the native shader pass; no client or staged texels enter.
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT ||
resource->viewType == VK_IMAGE_VIEW_TYPE_1D || resource->viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
resource->viewType == VK_IMAGE_VIEW_TYPE_3D ||
!(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) ||
baseLayer >= resource->arrayLayers || layerCount > resource->arrayLayers - baseLayer)
MagmaWireFatal("mipmap-shader-format-or-shape");
for (Uint32 level = base + 1; level < base + count; ++level) {
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
WireImage source{};
source.image = resource->image;
source.format = format;
source.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
source.layout = resource->layout;
source.trackedLayout = &resource->layout;
source.levels = resource->mipLevels;
source.level = level - 1;
source.layer = layer;
source.extent = {std::max(resource->extent.width >> (level - 1), 1u),
std::max(resource->extent.height >> (level - 1), 1u)};
WireImage destination = source;
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];
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
resource->syncedWireSerial = ++record.Serial;
@@ -460,6 +460,25 @@ TEST_F(F1WireScenario, GenerateMipmapPackedFloatPixels) {
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
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);
// 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) {