mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1734 lines
122 KiB
C++
1734 lines
122 KiB
C++
// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here
|
|
// belong to the applier; no frontend object, lifetime-id lookup or allocator is used.
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of
|
|
// the fifteen P7-marked refusals die (the other four are WireDescriptorFatal's), and until now
|
|
// every one of them raised its own abort: no SessionFault frame for the peer, no
|
|
// SessionFaultCount() for exit gate S8, and nothing tying the hand-typed word to
|
|
// FatalFamilies.def. MGPipeSessionFail hands the line to the hook MG_Remote installs at
|
|
// server-role init, whose arm for UnmigratedVerb is SessionFail(MGFatalFamily::
|
|
// UnmigratedVerb, ...) - and with no hook installed it logs and aborts exactly as this did.
|
|
[[noreturn]] static void MagmaWireFatal(const char* detail) {
|
|
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::UnmigratedVerb,
|
|
"MGPipe: Fatal{UnmigratedVerb, \"Magma:%s\"}", detail);
|
|
}
|
|
|
|
// P7 wave 2-B. THE NAME STAYS FIRST AND THE DRIVER'S ANSWER FOLLOWS IT. Two Iris cases
|
|
// died on the Redmi (Adreno 830v2) with `Fatal{UnmigratedVerb, "Magma:mipmap-shader-
|
|
// format-or-shape"}` and that line was the whole evidence: five disjunctions share the
|
|
// name, so the one thing the device could have told us - WHICH format, at what shape,
|
|
// missing WHICH feature bit - was the one thing it did not. The census, the retrace
|
|
// refusal tally and every log grep key on the leading word, so it is unchanged and
|
|
// unprefixed; everything after the first space is for the reader of the next device run.
|
|
// P8-F: the refusal is a named decline now (WireDeclines.def MipmapShaderFormatOrShape);
|
|
// the line it logs keeps this word and this detail.
|
|
static String MagmaWireShapeDetail(const char* name, VkFormat format, VkFormat storageFormat,
|
|
const VkFormatProperties& props, VkImageAspectFlags aspect,
|
|
VkImageViewType viewType, VkImageUsageFlags usage,
|
|
const VkExtent2D& extent, Uint32 depth, Uint32 levels,
|
|
Uint32 baseLayer, Uint32 layerCount, Uint32 arrayLayers) {
|
|
const auto hex = [](Uint32 value) {
|
|
static const char* digits = "0123456789abcdef";
|
|
String out = "0x";
|
|
Bool leading = true;
|
|
for (Int shift = 28; shift >= 0; shift -= 4) {
|
|
const Uint32 nibble = (value >> shift) & 0xfu;
|
|
if (nibble == 0 && leading && shift) continue;
|
|
leading = false;
|
|
out.push_back(digits[nibble]);
|
|
}
|
|
return out;
|
|
};
|
|
const VkFormatFeatureFlags absent = ~props.optimalTilingFeatures;
|
|
String missing;
|
|
const auto note = [&](VkFormatFeatureFlags bit, const char* word) {
|
|
if (!(absent & bit)) return;
|
|
if (!missing.empty()) missing += "|";
|
|
missing += word;
|
|
};
|
|
note(VK_FORMAT_FEATURE_BLIT_SRC_BIT, "BLIT_SRC");
|
|
note(VK_FORMAT_FEATURE_BLIT_DST_BIT, "BLIT_DST");
|
|
note(VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT, "SAMPLED");
|
|
note(VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT, "SAMPLED_LINEAR");
|
|
note(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT, "COLOR_ATTACHMENT");
|
|
note(VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT, "DEPTH_STENCIL_ATTACHMENT");
|
|
if (missing.empty()) missing = "none";
|
|
String out = name;
|
|
out += " format=" + std::to_string(static_cast<Int>(format));
|
|
// A view that reinterprets its owner's format is the OTHER reason the native blit
|
|
// arm is skipped, and it is invisible in the format number alone.
|
|
if (storageFormat != format) out += " storage=" + std::to_string(static_cast<Int>(storageFormat));
|
|
out += " extent=" + std::to_string(extent.width) + "x" + std::to_string(extent.height) +
|
|
"x" + std::to_string(depth);
|
|
out += " levels=" + std::to_string(levels);
|
|
out += " aspect=" + hex(static_cast<Uint32>(aspect));
|
|
out += " view=" + std::to_string(static_cast<Int>(viewType));
|
|
out += " usage=" + hex(static_cast<Uint32>(usage));
|
|
out += " layers=" + std::to_string(baseLayer) + "+" + std::to_string(layerCount) +
|
|
"/" + std::to_string(arrayLayers);
|
|
out += " missing=" + missing;
|
|
return out;
|
|
}
|
|
|
|
// THE HOST GATE CANNOT REACH THE SHADER MIP ARM ON ITS OWN. lavapipe reports
|
|
// BLIT_SRC|BLIT_DST for every colour format this lane can create, so `nativeBlit` is
|
|
// always true there and both shader shapes below would only ever run on the phone -
|
|
// which is how the Adreno refusal reached a release in the first place. The knob makes
|
|
// them reachable from a host lane and is read exactly once:
|
|
// 1 - drop the native blit (the arm an Adreno takes for a format without BLIT_DST);
|
|
// 2 - also hide the destination's COLOR_ATTACHMENT usage (the arm it takes for a
|
|
// sampled-only texture, which renders through owned scratch and copies back).
|
|
// MGITEST_, not MOBILEGL_: it is a test knob, it is not mirrored into the accepted-env
|
|
// table, and this file is compiled only into the disaggregated build.
|
|
static Uint32 MagmaWireForcedShaderMipmapTier() {
|
|
static const Uint32 tier = [] {
|
|
const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
|
|
if (!value || !*value) return 0u;
|
|
return static_cast<Uint32>(std::strtoul(value, nullptr, 10));
|
|
}();
|
|
return tier;
|
|
}
|
|
|
|
// THE SAME PROBLEM FOR THE ROTATED DEFAULT-BLIT ARM. Every surface this lane can create is
|
|
// VK_SURFACE_TRANSFORM_IDENTITY - headless, pbuffer or X11 - and the Redmi evidence for this
|
|
// cluster is a pbuffer too, so the scratch arm that exists FOR rotation would never execute
|
|
// anywhere a test could watch it. The knob forces it on an identity surface, where it has to
|
|
// produce exactly the pixels the fall-through arm produces; that equality is the assertion.
|
|
// It does NOT fake a rotation: nothing downstream is told the surface turned.
|
|
static Bool MagmaWireForcedDefaultBlitScratch() {
|
|
static const Bool forced = [] {
|
|
const char* value = std::getenv("MGITEST_MAGMA_FORCE_DEFAULT_BLIT_SCRATCH");
|
|
return value && *value && value[0] != '0';
|
|
}();
|
|
return forced;
|
|
}
|
|
|
|
// AND THE SAME PROBLEM AGAIN FOR THE SHADER MULTISAMPLE RESOLVE (P7 wave 2-B2). lavapipe
|
|
// carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the extension arm
|
|
// is taken for every depth/stencil resolve this host can run and the shader arm - the one
|
|
// that exists for the device WITHOUT them, which is where the session used to die at
|
|
// `Magma:multisample-depth-resolve-capability` - would never execute under any lane. The
|
|
// knob drops the extension arm. It does not pretend the extension is missing anywhere else:
|
|
// nothing but the arm choice reads it.
|
|
static Bool MagmaWireForcedShaderDepthResolve() {
|
|
static const Bool forced = [] {
|
|
const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE");
|
|
return value && *value && value[0] != '0';
|
|
}();
|
|
return forced;
|
|
}
|
|
|
|
static Bool WireFormatIsPackedDepthStencil(VkFormat format) {
|
|
return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT ||
|
|
format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
|
}
|
|
|
|
VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage(
|
|
const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface,
|
|
VkImageAspectFlags aspect, Bool isWriteTarget) {
|
|
WireImage out{};
|
|
out.aspect = aspect;
|
|
if (fbo.IsDefault) {
|
|
out.isDefault = true;
|
|
out.extent = m_swapchainObject.GetExtent();
|
|
out.swapchainImageIndex = isWriteTarget ? DefaultFramebufferWriteIndex(true)
|
|
: DefaultFramebufferReadIndex(true);
|
|
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
|
|
out.image = m_swapchainObject.GetImage(out.swapchainImageIndex);
|
|
out.format = m_swapchainObject.GetSurfaceFormat().format;
|
|
out.layout = m_swapchainObject.GetImageLayout(out.swapchainImageIndex);
|
|
} else {
|
|
out.image = m_swapchainObject.GetDepthStencilImage(out.swapchainImageIndex);
|
|
out.format = m_swapchainObject.GetDepthStencilFormat();
|
|
out.layout = m_swapchainObject.GetDepthStencilImageLayout(out.swapchainImageIndex);
|
|
}
|
|
return out;
|
|
}
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return out;
|
|
const Bool renderbuffer = surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer;
|
|
Uint32 level = surface.Level, layer = surface.Layer, availableLayers = 1;
|
|
// FramebufferTexture2D names a cube face in UploadTarget; its Layer is
|
|
// still zero. Resolve that face before applying a texture view's window,
|
|
// just as the monolith attachment resolver does.
|
|
const auto uploadTarget = static_cast<TextureUploadTarget>(surface.UploadTarget);
|
|
if (!renderbuffer && !surface.Layered && uploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
|
uploadTarget <= TextureUploadTarget::CubeMapNegativeZ)
|
|
layer = static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
|
out.storage = renderbuffer ? surface.Res : m_textureManager->ResolveWireTextureStorage(
|
|
surface.Res, level, layer, &viewFormat, &availableLayers);
|
|
if (MG_Pipe::MGPipeHandleIsNull(out.storage)) MagmaWireFatal("framebuffer-view-record");
|
|
auto* resource = m_textureManager->SyncTextureResourceByHandle(out.storage, renderbuffer);
|
|
if (!resource) MagmaWireFatal("framebuffer-resource");
|
|
// Another session writes it: acquired (WireSharedImage.inc) before the layout is read below.
|
|
// An image reached through a framebuffer is one this session renders into (or copies
|
|
// through), which makes it a writer of images from now on.
|
|
if (resource->sharedImageId != 0) {
|
|
m_sharedImageImplicitSync = true;
|
|
AcquireSharedImage(*resource);
|
|
}
|
|
out.image = resource->image;
|
|
out.format = viewFormat != VK_FORMAT_UNDEFINED ? viewFormat : resource->format;
|
|
out.extent = {std::max(resource->extent.width >> level, 1u),
|
|
std::max(resource->extent.height >> level, 1u)};
|
|
out.layout = resource->layout;
|
|
out.trackedLayout = &resource->layout;
|
|
out.samples = resource->sampleCount;
|
|
// P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question
|
|
// about the image and not about the format - VkTextureManager asks for SAMPLED on every
|
|
// attachment it creates, and a device that declined it would make the descriptor write
|
|
// undefined rather than fail. Carried here so the arm can decline instead.
|
|
out.usageFlags = resource->usageFlags;
|
|
out.level = level;
|
|
out.levels = resource->mipLevels;
|
|
out.layer = layer;
|
|
out.is3D = resource->depth > 1;
|
|
out.layers = surface.Layered ? (out.is3D ? std::max(resource->depth >> level, 1u) : availableLayers) : 1;
|
|
const Bool oneDimensional = resource->viewType == VK_IMAGE_VIEW_TYPE_1D ||
|
|
resource->viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY;
|
|
out.viewType = oneDimensional ? (out.layers > 1 ? VK_IMAGE_VIEW_TYPE_1D_ARRAY : VK_IMAGE_VIEW_TYPE_1D) :
|
|
(out.layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D);
|
|
if (out.level >= resource->mipLevels || out.layer + out.layers >
|
|
(out.is3D ? std::max(resource->depth >> level, 1u) : resource->arrayLayers))
|
|
MagmaWireFatal("framebuffer-subresource");
|
|
return out;
|
|
}
|
|
|
|
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) {
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
|
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
|
VkAccessFlags access = 0;
|
|
GetImageTransitionSourceState(image.layout, stage, access);
|
|
// One tracked layout per image: move the complete mip chain, never just the surface.
|
|
VkImageAspectFlags barrierAspect = image.aspect;
|
|
// Every packed format, D16_UNORM_S8_UINT included (g5-msrbo review round): the layout is
|
|
// tracked per image, so a one-aspect barrier would leave the other aspect behind the
|
|
// tracked layout - and CopyWireDepthStencilBlit moves the two aspects in two legs.
|
|
if (WireFormatIsPackedDepthStencil(image.format))
|
|
barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
if (image.layout == layout) {
|
|
// GENERAL->GENERAL is not a layout transition, but the next load,
|
|
// sample or blit still depends on earlier attachment/shader writes.
|
|
// The old per-verb queue idle must not be its only ordering edge.
|
|
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
|
|
}
|
|
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout,
|
|
stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access,
|
|
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, barrierAspect, 0, image.levels))
|
|
MagmaWireFatal("image-transition");
|
|
if (image.trackedLayout) *image.trackedLayout = image.layout;
|
|
if (image.isDefault) {
|
|
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT)
|
|
m_swapchainObject.SetImageLayout(image.swapchainImageIndex, image.layout);
|
|
else m_swapchainObject.SetDepthStencilImageLayout(image.swapchainImageIndex, image.layout);
|
|
}
|
|
}
|
|
|
|
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
|
RetireWireDrawPass();
|
|
// A stream that neither presents nor finishes (an off-screen loop) never submits, so its
|
|
// recording and the per-clear objects retired below would grow until host memory runs
|
|
// out. Past a bound, submit what is recorded: the objects then retire with that work.
|
|
constexpr SizeT kRetiredObjectsBeforeSubmit = 1024;
|
|
if (m_wireRetiredObjects.size() >= kRetiredObjectsBeforeSubmit && m_wirePreparationDepth == 0 &&
|
|
HasPendingRecordedWork())
|
|
(void)FlushPendingCommands();
|
|
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
|
|
const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
|
|
Uint32 colorIndex) {
|
|
if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return;
|
|
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
|
|
const auto mask = MG_Pipe::gPipeInputs.GetColorMaskIndexed(colorIndex);
|
|
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return;
|
|
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
|
|
MGLOG_W_ONCE("DirectVulkan: glClear with a partial color mask is not supported");
|
|
return;
|
|
}
|
|
}
|
|
if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) {
|
|
if (!MG_Pipe::gPipeInputs.GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
}
|
|
if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) {
|
|
const Uint32 mask = MG_Pipe::gPipeInputs.GetStencilState(StencilFace::Front).WriteMask & 255u;
|
|
if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
else if (mask != 255u) {
|
|
MGLOG_W_ONCE("DirectVulkan: glClear with a partial stencil write mask is not supported");
|
|
aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
}
|
|
}
|
|
if (!aspect) return;
|
|
WireImage image = ResolveWireImage(fbo, surface, aspect, /*isWriteTarget=*/true);
|
|
if (!image.image) return;
|
|
m_textureManager->FlushPendingUploads();
|
|
VkRect2D area{{0, 0}, image.extent};
|
|
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
|
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
|
|
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
|
|
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(image.extent.width));
|
|
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(image.extent.height));
|
|
if (x0 >= x1 || y0 >= y1) return;
|
|
area = {{x0, y0}, {static_cast<Uint32>(x1-x0), static_cast<Uint32>(y1-y0)}};
|
|
if (image.isDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1-x0, y1-y0,
|
|
image.extent, m_swapchainObject.GetPreTransform(), &area.offset, &area.extent))
|
|
MagmaWireFatal("clear-surface-transform");
|
|
}
|
|
const Bool color = aspect == VK_IMAGE_ASPECT_COLOR_BIT;
|
|
const auto layout = color ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL :
|
|
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
|
TransitionWireImage(image, layout);
|
|
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
viewInfo.image = image.image;
|
|
viewInfo.viewType = image.viewType;
|
|
viewInfo.format = image.format;
|
|
viewInfo.subresourceRange = {color ? VK_IMAGE_ASPECT_COLOR_BIT :
|
|
(image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT ? VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT : image.aspect), image.level, 1, image.layer, image.layers};
|
|
VkImageView view = VK_NULL_HANDLE;
|
|
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view));
|
|
VkAttachmentDescription attachment{};
|
|
attachment.format = image.format;
|
|
attachment.samples = image.samples;
|
|
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
attachment.initialLayout = layout;
|
|
attachment.finalLayout = layout;
|
|
VkAttachmentReference ref{0, layout};
|
|
VkSubpassDescription subpass{};
|
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
subpass.colorAttachmentCount = color ? 1 : 0;
|
|
subpass.pColorAttachments = color ? &ref : nullptr;
|
|
subpass.pDepthStencilAttachment = color ? nullptr : &ref;
|
|
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
|
passInfo.attachmentCount = 1; passInfo.pAttachments = &attachment;
|
|
passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass;
|
|
VkRenderPass pass = VK_NULL_HANDLE;
|
|
VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &pass));
|
|
VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
fbInfo.renderPass = pass; fbInfo.attachmentCount = 1; fbInfo.pAttachments = &view;
|
|
fbInfo.width = image.extent.width; fbInfo.height = image.extent.height; fbInfo.layers = image.layers;
|
|
VkFramebuffer fb = VK_NULL_HANDLE;
|
|
VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &fb));
|
|
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
begin.renderPass = pass; begin.framebuffer = fb; begin.renderArea = area;
|
|
auto clearPayload = payload;
|
|
if (color) PreCompensateSrgbClearColor(clearPayload, image.format);
|
|
VkClearAttachment clear{};
|
|
clear.aspectMask = aspect;
|
|
clear.clearValue.color = MakeVkClearColorValue(clearPayload, false);
|
|
if (!color) clear.clearValue.depthStencil = {payload.depth, payload.stencil};
|
|
VkClearRect rect{area, 0, image.layers};
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
EndActiveRenderPassOn(frame.commandBuffer);
|
|
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
|
vkCmdClearAttachments(frame.commandBuffer, 1, &clear, 1, &rect);
|
|
vkCmdEndRenderPass(frame.commandBuffer);
|
|
WireRetiredObjects retired{};
|
|
retired.framebuffers.push_back(fb);
|
|
retired.renderPasses.push_back(pass);
|
|
retired.imageViews.push_back(view);
|
|
RetireWireObjects(Move(retired));
|
|
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
|
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
|
|
if (image.isDefault) {
|
|
if (color) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
|
|
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
|
|
}
|
|
};
|
|
if (payload.mask & GL_COLOR_BUFFER_BIT) {
|
|
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
|
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
|
|
const Int32 attachment = fbo.DrawBuffers[i];
|
|
if (attachment >= 0 && attachment < static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments))
|
|
clearSurface(fbo.Color[attachment], VK_IMAGE_ASPECT_COLOR_BIT, i);
|
|
}
|
|
}
|
|
if (payload.mask & GL_DEPTH_BUFFER_BIT) clearSurface(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
|
if (payload.mask & GL_STENCIL_BUFFER_BIT) clearSurface(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT, 0);
|
|
}
|
|
|
|
// P7 wave 2-B (exit gate 3). THE READBACK SAYS WHAT IT IS WAITING FOR INSTEAD OF INFERRING IT.
|
|
//
|
|
// Every other transition in this file derives its barrier's SOURCE scope from the image's
|
|
// tracked layout, through GetImageTransitionSourceState (VulkanRenderer.cpp). That is right
|
|
// for a transition - but two of its arms, VK_IMAGE_LAYOUT_UNDEFINED and
|
|
// VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT with no access
|
|
// bits, which is Vulkan's spelling of AN EMPTY FIRST SCOPE: wait for nothing. For a
|
|
// transition out of UNDEFINED that is correct (the contents are being discarded); for a
|
|
// READBACK it is not, because the pixels being copied were written by the very commands the
|
|
// barrier declines to wait for. A just-presented default framebuffer sits in exactly the
|
|
// second of those layouts.
|
|
//
|
|
// It matters on the wire arm and not on the monolith one because of where the draws are.
|
|
// ReadWirePixels' FlushPendingCommands has already put the frame's draws on the queue under a
|
|
// pooled fence and returned WITHOUT waiting, so the copy recorded below lands in a LATER
|
|
// submission and this barrier is the only thing that could order the two. The monolith
|
|
// readback never flushes first: its draws and its copy are one submission under one fence.
|
|
//
|
|
// Measured on the host (OpenRA, DirectVulkan, inproc): at this point the counters read
|
|
// submit=72 completed=71 - the draws' submission is in flight and unwaited. The host does not
|
|
// diverge because its default framebuffer's tracked layout here is GENERAL, which takes
|
|
// GetImageTransitionSourceState's `default:` arm (ALL_COMMANDS / MEMORY_WRITE) and orders the
|
|
// copy by accident. The device's is not.
|
|
void VulkanRenderer::RecordWireReadbackWriteBarrier() {
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
|
// ALL_COMMANDS as the source scope reaches back across the submission boundary: a
|
|
// barrier's first scope is every command earlier in SUBMISSION order on this queue, not
|
|
// merely earlier in this command buffer.
|
|
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
|
|
}
|
|
|
|
void VulkanRenderer::ReadWirePixels(GLint x, GLint y, GLsizei width, GLsizei height,
|
|
GLenum format, GLenum type, void* pixels) {
|
|
if (width <= 0 || height <= 0) return;
|
|
if (MG_Pipe::MGPipeDebugDeviceLossDue()) NoteDeviceLoss(VK_ERROR_DEVICE_LOST, "the debug knob (read-pixels)");
|
|
if (m_deviceLost && LatchWireDeviceLoss("read-pixels")) return;
|
|
if (HasPendingRecordedWork() && !FlushPendingCommands()) {
|
|
// A lost device: the session is latched and closing, the pixels stay unfilled.
|
|
if (LatchWireDeviceLoss("wire-verb-flush")) return;
|
|
MagmaWireFatal("wire-verb-flush");
|
|
}
|
|
const auto* fbo = MG_Pipe::MGPipeApplier().ReadFramebuffer();
|
|
if (!fbo) MagmaWireFatal("read-framebuffer-record");
|
|
VkImageAspectFlags aspect = format == GL_DEPTH_COMPONENT ? VK_IMAGE_ASPECT_DEPTH_BIT :
|
|
format == GL_STENCIL_INDEX ? VK_IMAGE_ASPECT_STENCIL_BIT :
|
|
VK_IMAGE_ASPECT_COLOR_BIT;
|
|
if (format == GL_DEPTH_STENCIL) aspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
const auto& surface = (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) ? fbo->Depth :
|
|
aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? fbo->Stencil : fbo->ReadSurface;
|
|
WireImage source = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/false);
|
|
if (!source.image) return;
|
|
if (source.isDefault && aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
|
!m_swapchainObject.IsImageContentDefined(source.swapchainImageIndex)) {
|
|
// Named rather than silent: the readback hands back whatever the image holds. The
|
|
// ordinary cause is a readback of the default framebuffer applied after the Present
|
|
// that consumed it, where the app's own colour is still what is expected (see
|
|
// m_defaultFramebufferImageIndex).
|
|
MGLOG_W_ONCE("DirectVulkan::ReadPixels: no write is recorded for swapchain image %u of the "
|
|
"default framebuffer; the readback returns its raw pixels",
|
|
source.swapchainImageIndex);
|
|
}
|
|
if (source.isDefault && aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
|
|
VkOffset2D probeOffset{};
|
|
VkExtent2D probeExtent{};
|
|
if (!MapDefaultFramebufferReadbackRect(x, y, width, height, source.extent,
|
|
m_swapchainObject.GetPreTransform(), &probeOffset, &probeExtent)) {
|
|
// A rectangle reaching past the surface: GL leaves those pixels undefined rather
|
|
// than raising an error, and the usual cause is a client reading back at its
|
|
// window's new size one frame before its surface follows the resize. Nothing is
|
|
// copied, and nothing is recorded.
|
|
MGLOG_E_ONCE("Magma wire: default-framebuffer read %dx%d at (%d,%d) does not fit surface %llx's "
|
|
"%ux%u (transform %u); the read is left unfilled", width, height, x, y,
|
|
static_cast<unsigned long long>(m_activeTargetKey), source.extent.width,
|
|
source.extent.height, static_cast<Uint32>(m_swapchainObject.GetPreTransform()));
|
|
return;
|
|
}
|
|
}
|
|
m_textureManager->FlushPendingUploads();
|
|
RecordWireReadbackWriteBarrier();
|
|
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
if (aspect != VK_IMAGE_ASPECT_COLOR_BIT) {
|
|
ReadDepthStencilImageToClient(source.image,source.format,
|
|
source.trackedLayout ? source.trackedLayout : &source.layout,aspect,
|
|
source.level,source.layer,x,y,width,height,format,type,pixels,source.isDefault);
|
|
if (source.isDefault) m_swapchainObject.SetDepthStencilImageLayout(source.swapchainImageIndex,source.layout);
|
|
return;
|
|
}
|
|
const SizeT texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 : GetReadbackTexelSize(source.format);
|
|
if (!texelSize) MagmaWireFatal("read-format");
|
|
const VkDeviceSize size = static_cast<VkDeviceSize>(width) * height * texelSize;
|
|
VkBufferObject buffer;
|
|
if (!buffer.Create({.allocator=m_allocator, .size=size, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
.memoryUsage=VMA_MEMORY_USAGE_AUTO, .allocationFlags=VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}))
|
|
MagmaWireFatal("read-buffer-allocation");
|
|
VkOffset2D offset{x,y};
|
|
VkExtent2D extent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
|
|
if (source.isDefault && !MapDefaultFramebufferReadbackRect(x,y,width,height,source.extent,
|
|
m_swapchainObject.GetPreTransform(),&offset,&extent)) MagmaWireFatal("read-surface-transform");
|
|
VkBufferImageCopy region{};
|
|
region.imageSubresource = {aspect,source.level,source.is3D ? 0u : source.layer,1};
|
|
region.imageOffset = {offset.x,offset.y,source.is3D ? static_cast<Int32>(source.layer) : 0};
|
|
region.imageExtent = {extent.width,extent.height,1};
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
vkCmdCopyImageToBuffer(frame.commandBuffer,source.image,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
buffer.GetHandle(),1,®ion);
|
|
if (!SubmitReadbackCommandsAndWait(frame)) {
|
|
if (LatchWireDeviceLoss("read-submit")) return;
|
|
MagmaWireFatal("read-submit");
|
|
}
|
|
const auto* bytes = static_cast<const Uint8*>(buffer.Map());
|
|
if (!bytes || !buffer.Invalidate(size)) MagmaWireFatal("read-map");
|
|
Vector<Uint8> remapped;
|
|
if (source.isDefault) {
|
|
remapped.resize(static_cast<SizeT>(size));
|
|
if (!RemapDefaultFramebufferReadback(bytes,width,height,m_swapchainObject.GetPreTransform(),
|
|
texelSize,remapped.data())) MagmaWireFatal("read-remap");
|
|
bytes = remapped.data();
|
|
}
|
|
if (!PackReadbackToClientOrPbo(bytes,source.format,width,height,1,format,type,pixels,false,true))
|
|
MagmaWireFatal("read-conversion");
|
|
}
|
|
|
|
Bool VulkanRenderer::DeclineWireDepthStencilResolveShape(const WireImage& source,
|
|
const WireImage& destination, GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1) {
|
|
const Int64 spanX = static_cast<Int64>(sx1) - sx0;
|
|
const Int64 spanY = static_cast<Int64>(sy1) - sy0;
|
|
const Int64 dstSpanX = static_cast<Int64>(dx1) - dx0;
|
|
const Int64 dstSpanY = static_cast<Int64>(dy1) - dy0;
|
|
const Int64 width = spanX < 0 ? -spanX : spanX;
|
|
const Int64 height = spanY < 0 ? -spanY : spanY;
|
|
const Int64 dstWidth = dstSpanX < 0 ? -dstSpanX : dstSpanX;
|
|
const Int64 dstHeight = dstSpanY < 0 ? -dstSpanY : dstSpanY;
|
|
// A SCALE has no arm here and is not supposed to: 18.3.1 makes a multisample blit whose
|
|
// rectangles differ in size INVALID_OPERATION. Rule I (a), the same three lines the
|
|
// colour arm's multisample-destination decline uses. A ZERO span on one side only is a
|
|
// size mismatch too (review round 2): it used to return silently with the other side
|
|
// non-empty, where GL raises the error. Only when BOTH sides are empty on an axis is
|
|
// there nothing to do and nothing to say - that is the caller's early return.
|
|
if (width != dstWidth || height != dstHeight) {
|
|
MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: a %lldx%lld source rectangle "
|
|
"cannot reach a %lldx%lld destination rectangle - a multisample resolve "
|
|
"carries no scale; declining the blit",
|
|
static_cast<long long>(width), static_cast<long long>(height),
|
|
static_cast<long long>(dstWidth), static_cast<long long>(dstHeight));
|
|
RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation,
|
|
"glBlitFramebuffer from a multisampled read framebuffer requires "
|
|
"identical source and destination rectangle dimensions.");
|
|
return true;
|
|
}
|
|
if (!width || !height) return false;
|
|
// WHICH MIRRORS HAVE AN ARM. The copy-out in ResolveWireDepthStencil moves the resolved
|
|
// aspect through a buffer, and a buffer copy addresses ROWS: one VkBufferImageCopy per
|
|
// row, walked backwards, is a vertical mirror for free. A HORIZONTAL one would need a
|
|
// region per TEXEL, which is not a copy any more - and re-encoding across formats
|
|
// (BlitDepthAcrossFormats, shared with the monolith arm) has no flip either. Both
|
|
// decline by name, each under its OWN once-site (review round 2: one site covered both,
|
|
// so after the first X decline a cross-format Y decline logged nothing); §6 in
|
|
// notes/p7/magma-b2.md carries them. These are declines of a LEGAL blit - GL would
|
|
// raise no error here - so the INVALID_OPERATION is rule I (a)'s named refusal, not
|
|
// GL's answer.
|
|
const Bool mirrorX = (spanX < 0) != (dstSpanX < 0);
|
|
const Bool mirrorY = (spanY < 0) != (dstSpanY < 0);
|
|
if (mirrorX) {
|
|
MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: a multisample resolve mirrored on "
|
|
"X (source format %d, destination format %d) has no arm - the copy-out "
|
|
"reverses whole rows and nothing smaller; declining the blit",
|
|
static_cast<Int>(source.format), static_cast<Int>(destination.format));
|
|
RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation,
|
|
"This device cannot mirror that multisample depth/stencil blit.");
|
|
return true;
|
|
}
|
|
if (mirrorY && source.format != destination.format) {
|
|
MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: a multisample resolve mirrored on "
|
|
"Y across two formats (source format %d, destination format %d) has no "
|
|
"arm - the cross-format re-encode has no flip; declining the blit",
|
|
static_cast<Int>(source.format), static_cast<Int>(destination.format));
|
|
RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation,
|
|
"This device cannot mirror that multisample depth/stencil blit.");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ONE ASPECT OF A DEPTH/STENCIL IMAGE, MOVED THROUGH A BUFFER. The source image must be in
|
|
// TRANSFER_SRC_OPTIMAL and the destination in TRANSFER_DST_OPTIMAL, both of `format`. A
|
|
// buffer copy names exactly one aspect and moves exactly its texels, which an image copy of a
|
|
// packed depth-stencil format does not promise (some drivers move the whole native word, so
|
|
// a depth-only region would replace the destination's stencil). The multisample resolve's
|
|
// copy-out and the single-sample copy arm share this body; the returned buffer is still named
|
|
// by the recorded commands and must be retired with them.
|
|
UniquePtr<VkBufferObject> VulkanRenderer::CopyWireAspectThroughBuffer(
|
|
VkImage srcImage, Uint32 srcLevel, Uint32 srcLayer, GLint sx, GLint sy,
|
|
const WireImage& destination, GLint dx, GLint dy, Uint32 width, Uint32 height,
|
|
VkImageAspectFlags aspect, VkFormat format, Bool mirrorY, const char* fatalDetail) {
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
const VkDeviceSize texelSize = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1 :
|
|
(format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT ? 2 : 4);
|
|
// THE ROW STRIDE IS PADDED TO 4 BYTES. A depth/stencil buffer<->image copy wants
|
|
// every bufferOffset on a multiple of 4 (VUID-vkCmdCopyBufferToImage-dstImage-07978,
|
|
// pRegions-07978 in older layers), and the mirror below starts a region on every
|
|
// row - so a tight row of 1-byte stencil texels whose width is not a multiple of 4,
|
|
// or of 2-byte D16 texels with an odd width, put every row after the first on an
|
|
// illegal offset. The scissor clip in BlitWireFramebuffers produces exactly such
|
|
// widths. Both copies name the padded length as bufferRowLength, so the buffer
|
|
// layout is one layout whichever way it is walked.
|
|
const VkDeviceSize texelsPer4Bytes = 4 / texelSize;
|
|
const VkDeviceSize paddedWidth =
|
|
(static_cast<VkDeviceSize>(width) + texelsPer4Bytes - 1) / texelsPer4Bytes * texelsPer4Bytes;
|
|
const VkDeviceSize bytes = paddedWidth * height * texelSize;
|
|
auto aspectBuffer = MakeUnique<VkBufferObject>();
|
|
if (!aspectBuffer->Create({.allocator = m_allocator, .size = bytes,
|
|
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
.memoryUsage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE}))
|
|
MagmaWireFatal(fatalDetail);
|
|
VkBufferImageCopy copy{};
|
|
copy.bufferRowLength = static_cast<Uint32>(paddedWidth);
|
|
copy.imageSubresource = {aspect, srcLevel, srcLayer, 1};
|
|
copy.imageOffset = {sx, sy, 0};
|
|
copy.imageExtent = {width, height, 1};
|
|
vkCmdCopyImageToBuffer(frame.commandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
aspectBuffer->GetHandle(), 1, ©);
|
|
VkBufferMemoryBarrier barrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.buffer = aspectBuffer->GetHandle();
|
|
barrier.size = bytes;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
0, 0, nullptr, 1, &barrier, 0, nullptr);
|
|
copy.imageSubresource = {aspect, destination.level, destination.layer, 1};
|
|
copy.imageOffset = {dx, dy, 0};
|
|
if (!mirrorY) {
|
|
vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
} else {
|
|
// THE VERTICAL MIRROR, and it costs one region per row rather than a pass. The
|
|
// buffer holds the band bottom row first (the image copy above wrote it from sy
|
|
// upwards, one padded stride per row); writing row r of that buffer to destination
|
|
// row dy + height - 1 - r reverses the band. Each region starts on a padded row,
|
|
// which is what keeps its bufferOffset a multiple of 4.
|
|
Vector<VkBufferImageCopy> rows(static_cast<SizeT>(height), copy);
|
|
const VkDeviceSize rowBytes = paddedWidth * texelSize;
|
|
for (Uint32 row = 0; row < height; ++row) {
|
|
auto& region = rows[static_cast<SizeT>(row)];
|
|
region.bufferOffset = static_cast<VkDeviceSize>(row) * rowBytes;
|
|
region.imageOffset = {dx, static_cast<Int32>(static_cast<Int64>(dy) + height - 1 - row), 0};
|
|
region.imageExtent = {width, 1, 1};
|
|
}
|
|
vkCmdCopyBufferToImage(frame.commandBuffer, aspectBuffer->GetHandle(), destination.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
static_cast<Uint32>(rows.size()), rows.data());
|
|
}
|
|
return aspectBuffer;
|
|
}
|
|
|
|
// P7 gate 5 (g5-msrbo): A SINGLE-SAMPLE DEPTH/STENCIL BLIT THAT IS A PLAIN COPY IS A COPY.
|
|
// Same size, same orientation on both axes, same native format, two distinct non-default,
|
|
// non-3D images, and the rectangle inside both: nothing a blit adds (scale, filter, flip,
|
|
// format conversion) is asked for, so the aspect moves with vkCmdCopyImage - or, for one
|
|
// aspect of a packed depth-stencil format, through CopyWireAspectThroughBuffer so the
|
|
// destination's other aspect survives. That is what the monolith arm does
|
|
// (VulkanRenderer::BlitNamedFramebuffer: vkCmdCopyImage at 1:1, CopyDepthStencilAspectThrough-
|
|
// Buffer for one aspect of a packed format), and neither command needs BLIT_SRC/BLIT_DST,
|
|
// which a depth/stencil format does not have to carry (the Adreno 830 reports BLIT_SRC and no
|
|
// BLIT_DST for every depth format). The wire arm sent every depth/stencil blit to
|
|
// vkCmdBlitImage, and for one aspect of a packed format that moved the whole native word:
|
|
// lavapipe replaced the destination's stencil on a depth-only blit. Returns false when the
|
|
// shape is not a plain copy; the caller then takes the vkCmdBlitImage arm as before.
|
|
Bool VulkanRenderer::CopyWireDepthStencilBlit(WireImage& source, WireImage& destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1,
|
|
VkImageAspectFlags aspect) {
|
|
const Int64 spanX = static_cast<Int64>(sx1) - sx0;
|
|
const Int64 spanY = static_cast<Int64>(sy1) - sy0;
|
|
if (spanX == 0 || spanY == 0 || spanX != static_cast<Int64>(dx1) - dx0 ||
|
|
spanY != static_cast<Int64>(dy1) - dy0)
|
|
return false;
|
|
if (source.isDefault || destination.isDefault || source.is3D || destination.is3D ||
|
|
source.image == destination.image || source.format != destination.format)
|
|
return false;
|
|
const Int64 width = spanX < 0 ? -spanX : spanX;
|
|
const Int64 height = spanY < 0 ? -spanY : spanY;
|
|
const Int64 sxMin = std::min(sx0, sx1), syMin = std::min(sy0, sy1);
|
|
const Int64 dxMin = std::min(dx0, dx1), dyMin = std::min(dy0, dy1);
|
|
if (sxMin < 0 || syMin < 0 || dxMin < 0 || dyMin < 0 ||
|
|
sxMin + width > static_cast<Int64>(source.extent.width) ||
|
|
syMin + height > static_cast<Int64>(source.extent.height) ||
|
|
dxMin + width > static_cast<Int64>(destination.extent.width) ||
|
|
dyMin + height > static_cast<Int64>(destination.extent.height))
|
|
return false;
|
|
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
TransitionWireImage(destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
if (WireFormatIsPackedDepthStencil(source.format)) {
|
|
WireRetiredObjects retired{};
|
|
retired.buffers.push_back(CopyWireAspectThroughBuffer(
|
|
source.image, source.level, source.layer, static_cast<GLint>(sxMin),
|
|
static_cast<GLint>(syMin), destination, static_cast<GLint>(dxMin),
|
|
static_cast<GLint>(dyMin), static_cast<Uint32>(width), static_cast<Uint32>(height),
|
|
aspect, source.format, /*mirrorY=*/false, "depth-stencil-copy-buffer"));
|
|
RetireWireObjects(Move(retired));
|
|
return true;
|
|
}
|
|
VkImageCopy copy{};
|
|
copy.srcSubresource = {aspect, source.level, source.layer, 1};
|
|
copy.srcOffset = {static_cast<Int32>(sxMin), static_cast<Int32>(syMin), 0};
|
|
copy.dstSubresource = {aspect, destination.level, destination.layer, 1};
|
|
copy.dstOffset = {static_cast<Int32>(dxMin), static_cast<Int32>(dyMin), 0};
|
|
copy.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
|
vkCmdCopyImage(m_frameContext.GetCurrent().commandBuffer, source.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
return true;
|
|
}
|
|
|
|
void VulkanRenderer::ResolveWireDepthStencil(WireImage source, WireImage destination,
|
|
GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1) {
|
|
// P7 wave 2-B2 (review round): A REVERSED RECTANGLE IS NOT A MALFORMED ONE. This guard
|
|
// used to require `sx1 > sx0` and `sy1 > sy0` and take the session with a named Fatal
|
|
// otherwise - so `glBlitFramebuffer(0, h, w, 0, 0, 0, w, h, COLOR|DEPTH)` out of a
|
|
// multisample framebuffer blitted its COLOUR correctly (the colour arm has handled a
|
|
// flip since P5f and now handles a scale too) and then died on its DEPTH. GL 4.6 core
|
|
// 18.3.1 asks for identical DIMENSIONS, not identical corners - Mesa compares absolute
|
|
// spans - so a flip is a legal blit that has to produce a mirrored picture.
|
|
//
|
|
// Spans are therefore signed and everything downstream works off the min corner.
|
|
//
|
|
// THE SHAPE DECLINES ARE DECIDED ONCE PER CALL (review round 2). BlitWireFramebuffers
|
|
// asks DeclineWireDepthStencilResolveShape before ANY aspect runs, because the colour
|
|
// lambda runs first: asked only here, a scaled COLOR|DEPTH blit scaled and wrote its
|
|
// colour and THEN recorded INVALID_OPERATION on its depth - a partial effect GL 18.3.1
|
|
// forbids (the whole call errors) - and an X-mirrored one mirrored its colour and left
|
|
// its depth alone. The call below is the same question for a caller that did not ask;
|
|
// on the BlitWireFramebuffers path it has already answered false.
|
|
if (DeclineWireDepthStencilResolveShape(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1))
|
|
return;
|
|
const Int64 spanX = static_cast<Int64>(sx1) - sx0;
|
|
const Int64 spanY = static_cast<Int64>(sy1) - sy0;
|
|
if (!spanX || !spanY) return; // both sides empty on an axis (the helper errored otherwise)
|
|
const Int64 width = spanX < 0 ? -spanX : spanX;
|
|
const Int64 height = spanY < 0 ? -spanY : spanY;
|
|
// Past the helper the only mirror left is a same-format vertical one.
|
|
const Bool mirrorY = (spanY < 0) != (dy1 < dy0);
|
|
const GLint sxMin = std::min(sx0, sx1), syMin = std::min(sy0, sy1);
|
|
const GLint dxMin = std::min(dx0, dx1), dyMin = std::min(dy0, dy1);
|
|
if (source.isDefault || destination.isDefault || source.is3D || destination.is3D ||
|
|
sxMin < 0 || syMin < 0 || dxMin < 0 || dyMin < 0 ||
|
|
static_cast<Uint64>(sxMin + width) > source.extent.width ||
|
|
static_cast<Uint64>(syMin + height) > source.extent.height ||
|
|
static_cast<Uint64>(dxMin + width) > destination.extent.width ||
|
|
static_cast<Uint64>(dyMin + height) > destination.extent.height)
|
|
MagmaWireFatal("multisample-depth-resolve-region");
|
|
const VkImageAspectFlags formatAspects = WireDepthStencilFormatAspects(source.format);
|
|
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): TWO ARMS, and which one
|
|
// runs is a fact about the DEVICE, not about the shape. VK_KHR_depth_stencil_resolve is
|
|
// the direct route and is taken wherever it exists; where it does not - which is where
|
|
// the session used to end at `Magma:multisample-depth-resolve-capability`, on the first
|
|
// glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) out of a multisampled read framebuffer - a
|
|
// baked pass fetches sample ZERO and writes gl_FragDepth (or, with
|
|
// VK_EXT_shader_stencil_export, gl_FragStencilRefARB). Sample zero in both arms, because
|
|
// VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is what the subpass below asks for.
|
|
//
|
|
// THE TWO CAPABILITY TESTS ARE NOT THE SAME TEST, and the difference is structural: the
|
|
// subpass resolves every aspect the FORMAT has, so it needs a mode for each of them,
|
|
// while the shader pass writes only the one aspect this blit asked for and leaves the
|
|
// rest of the scratch undefined - which is safe because the copy below reads exactly
|
|
// that aspect.
|
|
const Bool extensionResolve = !MagmaWireForcedShaderDepthResolve() && m_wireCreateRenderPass2 &&
|
|
(!(formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) ||
|
|
(m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) &&
|
|
(!(formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) ||
|
|
(m_wireStencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT));
|
|
|
|
// vkCmdResolveImage is color-only. A depth/stencil resolve subpass must
|
|
// write at the source coordinates and may require both aspects to use
|
|
// the same mode. Resolve both into owned scratch storage, then copy
|
|
// only the GL-requested aspect, preserving the other destination aspect.
|
|
auto scratch = MakeUnique<VkTextureManager::TextureResource>();
|
|
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
imageInfo.format = source.format;
|
|
imageInfo.extent = {source.extent.width, source.extent.height, 1};
|
|
imageInfo.mipLevels = imageInfo.arrayLayers = 1;
|
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
VmaAllocationCreateInfo allocation{};
|
|
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
allocation.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
if (vmaCreateImage(m_allocator, &imageInfo, &allocation, &scratch->image, &scratch->allocation, nullptr) != VK_SUCCESS)
|
|
MagmaWireFatal("multisample-depth-resolve-allocation");
|
|
scratch->format = source.format;
|
|
scratch->extent = source.extent;
|
|
scratch->aspect = formatAspects;
|
|
WireImage resolved{};
|
|
resolved.image = scratch->image;
|
|
resolved.format = scratch->format;
|
|
resolved.extent = scratch->extent;
|
|
resolved.aspect = formatAspects;
|
|
resolved.trackedLayout = &scratch->layout;
|
|
|
|
VkImageView views[2]{};
|
|
VkRenderPass pass = VK_NULL_HANDLE;
|
|
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
|
// P7 gate 5 (g5-msrbo, g5-msprobe): WHERE THE EXTENSION ARM IS MEASURED NOT TO RESOLVE,
|
|
// THE SHADER ARM GOES FIRST. The extension arm is a render pass with no draw in it, and on
|
|
// the Redmi (Adreno 830, driver 512.800.71) that pass left the resolve target unwritten:
|
|
// KHR-GL46.direct_state_access.renderbuffers_storage_multisample read every resolved depth
|
|
// and stencil value back as 0 on the inproc arm - exactly its samples 2/4/4 iterations,
|
|
// every format - and the same case PASSED on that device with
|
|
// MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=1. The shader arm draws, so nothing about it
|
|
// can be elided. It still declines what it cannot express (a stencil aspect without
|
|
// VK_EXT_shader_stencil_export), and then the extension arm is the fallback rather than
|
|
// a decline. m_wirePreferShaderDepthResolve is the verdict of the resolve probe that ran
|
|
// THIS pass and the shader pass on this device at bring-up (ArmWireDepthResolveOrder,
|
|
// WireDepthResolveProbe.cpp); where the probe finds the pass clean - or cannot tell - the
|
|
// order is the one B2 chose.
|
|
const Bool shaderFirst = m_wirePreferShaderDepthResolve || !extensionResolve;
|
|
const Bool resolvedByShader =
|
|
shaderFirst && ResolveWireDepthStencilWithShader(source, resolved, source.aspect, sxMin, syMin,
|
|
static_cast<Uint32>(width), static_cast<Uint32>(height));
|
|
// WHICH ARM RESOLVED IS SAID, ONCE PER ARM (review round). Lavapipe resolves correctly
|
|
// through both, so the pixels cannot tell a lane which one ran; these two lines can, and
|
|
// the MsResolveBug./MsFlipBug. entries (the probe's defect verdict forced on the host) and the
|
|
// MsResolve1./MsFlip1. ones (the extension arm dropped) read them back from the server
|
|
// log - DepthStencilReadbackMatrixScenario::ExpectTheResolveArmWhereTheLaneAsks.
|
|
// Keep the two phrases in step with that reader.
|
|
//
|
|
// AND A PREFERENCE THAT FELL BACK IS NOT SILENT: where the shader arm goes first it goes
|
|
// first because the probe measured the render pass leaving its target unwritten here,
|
|
// so a shader decline (a stencil aspect without VK_EXT_shader_stencil_export, a format
|
|
// that is not SAMPLED_IMAGE + DEPTH_STENCIL_ATTACHMENT, a failed pipeline create -
|
|
// WireMultisampleResolve.inc) that lands on the render pass is the resolve most likely
|
|
// to read back as zeroes with no GL error. A recorded debt (notes/p7/g5-msrbo.md §4).
|
|
if (resolvedByShader) {
|
|
MGLOG_I_ONCE("DirectVulkan::ResolveWireDepthStencil: resolved by the shader pass "
|
|
"(prefer-shader=%d, render-pass arm available=%d)",
|
|
m_wirePreferShaderDepthResolve ? 1 : 0, extensionResolve ? 1 : 0);
|
|
} else if (extensionResolve) {
|
|
if (m_wirePreferShaderDepthResolve)
|
|
MGLOG_W_ONCE("DirectVulkan::ResolveWireDepthStencil: this device prefers the shader pass, "
|
|
"which did not resolve aspect 0x%x of a %u-sample format %d (stencil export=%d, "
|
|
"source usage 0x%x); falling back to the no-draw VK_KHR_depth_stencil_resolve "
|
|
"render pass, which this device's resolve probe measured leaving its target "
|
|
"unwritten",
|
|
static_cast<Uint32>(source.aspect), static_cast<Uint32>(source.samples),
|
|
static_cast<Int>(source.format), m_wireShaderStencilExport ? 1 : 0,
|
|
static_cast<Uint32>(source.usageFlags));
|
|
MGLOG_I_ONCE("DirectVulkan::ResolveWireDepthStencil: resolved by the "
|
|
"VK_KHR_depth_stencil_resolve render pass (prefer-shader=%d)",
|
|
m_wirePreferShaderDepthResolve ? 1 : 0);
|
|
}
|
|
if (!resolvedByShader && extensionResolve) {
|
|
TransitionWireImage(source, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
|
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
|
|
|
// The view shapes and the pass are WireDepthResolveProbe.cpp's: the resolve probe runs this
|
|
// exact pass on the device at bring-up, so there is one definition of it (g5-msprobe).
|
|
const VkImageViewCreateInfo sourceViewInfo =
|
|
WireDepthStencilViewInfo(source.image, source.format, formatAspects, source.level, source.layer);
|
|
VK_VERIFY(vkCreateImageView(m_device, &sourceViewInfo, nullptr, &views[0]));
|
|
const VkImageViewCreateInfo resolvedViewInfo =
|
|
WireDepthStencilViewInfo(resolved.image, source.format, formatAspects, 0, 0);
|
|
VK_VERIFY(vkCreateImageView(m_device, &resolvedViewInfo, nullptr, &views[1]));
|
|
VK_VERIFY(CreateWireDepthStencilResolvePass(m_wireCreateRenderPass2, m_device, source.format, source.samples,
|
|
&pass));
|
|
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
framebufferInfo.renderPass = pass;
|
|
framebufferInfo.attachmentCount = 2;
|
|
framebufferInfo.pAttachments = views;
|
|
framebufferInfo.width = source.extent.width;
|
|
framebufferInfo.height = source.extent.height;
|
|
framebufferInfo.layers = 1;
|
|
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
|
|
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
begin.renderPass = pass;
|
|
begin.framebuffer = framebuffer;
|
|
begin.renderArea = {{sxMin, syMin}, {static_cast<Uint32>(width), static_cast<Uint32>(height)}};
|
|
EndActiveRenderPassOn(m_frameContext.GetCurrent().commandBuffer);
|
|
vkCmdBeginRenderPass(m_frameContext.GetCurrent().commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
|
vkCmdEndRenderPass(m_frameContext.GetCurrent().commandBuffer);
|
|
} else if (!resolvedByShader) {
|
|
// NEITHER ARM, and rule I (a) says what that is: this device has no
|
|
// VK_KHR_depth_stencil_resolve and the pass cannot stand in for it either - the
|
|
// commonest reason being a stencil-only resolve without VK_EXT_shader_stencil_export,
|
|
// where a fragment shader cannot put a value in the stencil aspect at all. A decline
|
|
// leaves the destination as the application last wrote it, which is what
|
|
// glBlitFramebuffer would do if the driver refused the resolve.
|
|
MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: resolving aspect 0x%x of a %u-sample "
|
|
"format %d needs either VK_KHR_depth_stencil_resolve (renderpass2=%d, "
|
|
"depth modes 0x%x, stencil modes 0x%x) or a shader pass "
|
|
"(stencil export=%d, source usage 0x%x); declining the blit",
|
|
static_cast<Uint32>(source.aspect), static_cast<Uint32>(source.samples),
|
|
static_cast<Int>(source.format), m_wireCreateRenderPass2 ? 1 : 0,
|
|
static_cast<Uint32>(m_wireDepthResolveModes),
|
|
static_cast<Uint32>(m_wireStencilResolveModes),
|
|
m_wireShaderStencilExport ? 1 : 0, static_cast<Uint32>(source.usageFlags));
|
|
RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation,
|
|
"This device cannot resolve that multisample depth/stencil aspect.");
|
|
WireRetiredObjects declined{};
|
|
declined.textures.push_back(Move(scratch));
|
|
RetireWireObjects(Move(declined));
|
|
return;
|
|
}
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
// Depth/stencil resolve writes execute in COLOR_ATTACHMENT_OUTPUT,
|
|
// unlike ordinary depth attachment writes. Include both when exposing
|
|
// the resolved image to a transfer; layout-based inference only covers
|
|
// EARLY/LATE_FRAGMENT_TESTS and misses the resolve's store operation.
|
|
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, resolved.image, scratch->layout,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
|
|
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
|
VK_ACCESS_TRANSFER_READ_BIT, formatAspects))
|
|
MagmaWireFatal("multisample-depth-resolve-transition");
|
|
resolved.layout = scratch->layout;
|
|
TransitionWireImage(destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
UniquePtr<VkBufferObject> aspectBuffer;
|
|
if (source.format == destination.format) {
|
|
// Copy one aspect through its buffer representation. Some packed
|
|
// depth/stencil image-copy paths copy the complete native word even
|
|
// for a single-aspect region. A stencil byte or depth-only word in
|
|
// this GPU buffer cannot replace the destination's other aspect.
|
|
aspectBuffer = CopyWireAspectThroughBuffer(resolved.image, 0, 0, sxMin, syMin, destination,
|
|
dxMin, dyMin, static_cast<Uint32>(width),
|
|
static_cast<Uint32>(height), source.aspect,
|
|
source.format, mirrorY,
|
|
"multisample-depth-resolve-buffer");
|
|
} else {
|
|
// A logical GL depth format may use different native renderbuffer
|
|
// and texture formats. Re-encode only after producing real resolved
|
|
// samples. Keep all scratch objects local across this helper's flush.
|
|
if (!BlitDepthAcrossFormats(frame, resolved.image, resolved.format, &scratch->layout, 0, 0,
|
|
destination.image, destination.format, destination.trackedLayout, destination.level, destination.layer,
|
|
sxMin, syMin, dxMin, dyMin, static_cast<GLint>(width), static_cast<GLint>(height),
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
source.aspect == VK_IMAGE_ASPECT_STENCIL_BIT))
|
|
MagmaWireFatal("multisample-depth-resolve-conversion");
|
|
}
|
|
WireRetiredObjects retired{};
|
|
// The shader arm owns and retires its own pass, framebuffer and views, so these four
|
|
// are null on that path - pushed only when the extension arm made them.
|
|
if (framebuffer != VK_NULL_HANDLE) retired.framebuffers.push_back(framebuffer);
|
|
if (pass != VK_NULL_HANDLE) retired.renderPasses.push_back(pass);
|
|
if (views[0] != VK_NULL_HANDLE) retired.imageViews = {views[0], views[1]};
|
|
retired.textures.push_back(Move(scratch));
|
|
if (aspectBuffer) retired.buffers.push_back(Move(aspectBuffer));
|
|
RetireWireObjects(Move(retired));
|
|
}
|
|
|
|
void VulkanRenderer::BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1,
|
|
GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) {
|
|
RetireWireDrawPass();
|
|
// The scissor test clips blit writes: intersect the destination rectangle with
|
|
// the scissor box and shrink the source proportionally.
|
|
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
|
|
const IntVec4& scissor = MG_Pipe::gPipeInputs.GetScissorBox();
|
|
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
|
|
const Bool dstFlipped = d1 < d0;
|
|
GLint lo = dstFlipped ? d1 : d0;
|
|
GLint hi = dstFlipped ? d0 : d1;
|
|
const GLint newLo = std::max(lo, clipLo);
|
|
const GLint newHi = std::min(hi, clipHi);
|
|
if (newLo >= newHi) {
|
|
return false;
|
|
}
|
|
const double srcSpan = static_cast<double>(s1 - s0);
|
|
const double dstSpan = static_cast<double>(d1 - d0);
|
|
const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0;
|
|
const GLint origD0 = d0;
|
|
const GLint clippedD0 = dstFlipped ? newHi : newLo;
|
|
const GLint clippedD1 = dstFlipped ? newLo : newHi;
|
|
s0 = s0 + static_cast<GLint>(std::lround((clippedD0 - origD0) * scale));
|
|
s1 = s0 + static_cast<GLint>(std::lround((clippedD1 - clippedD0) * scale));
|
|
d0 = clippedD0;
|
|
d1 = clippedD1;
|
|
return true;
|
|
};
|
|
if (!clipAxis(dx0, dx1, sx0, sx1, scissor.x(), scissor.x() + scissor.z()) ||
|
|
!clipAxis(dy0, dy1, sy0, sy1, scissor.y(), scissor.y() + scissor.w())) {
|
|
return; // fully scissored out
|
|
}
|
|
}
|
|
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
const Bool named = !MG_Pipe::MGPipeHandleIsNull(state.VerbBlitReadFbo) ||
|
|
!MG_Pipe::MGPipeHandleIsNull(state.VerbBlitDrawFbo);
|
|
const auto* read = named ? state.FramebufferRecordFor(state.VerbBlitReadFbo) : state.ReadFramebuffer();
|
|
const auto* draw = named ? state.FramebufferRecordFor(state.VerbBlitDrawFbo) : state.DrawFramebuffer();
|
|
if (!read || !draw) MagmaWireFatal("blit-framebuffer-record");
|
|
state.VerbBlitNamedConsumed = named;
|
|
// P7 wave 2-B2 (review round 2): ONE ANSWER FOR THE WHOLE CALL. The aspects below run
|
|
// colour first, so a multisample depth/stencil shape that declines (a scale, an X
|
|
// mirror, a Y mirror across two formats) used to be discovered only after the colour
|
|
// attachment had already been scaled or mirrored and written - the call then recorded
|
|
// INVALID_OPERATION with half its effect in place, which GL 18.3.1 does not allow (an
|
|
// erroring blit writes nothing). The shape is a fact about the rectangles and the two
|
|
// images, so it is asked here, before any aspect runs, for every depth/stencil aspect
|
|
// the mask names that would take the multisample resolve arm.
|
|
//
|
|
// A default READ framebuffer is never multisampled here, so it is left to the lambda. A
|
|
// default DRAW framebuffer is asked like any other (review round 3): round 2 left it out
|
|
// on the belief that a default draw behind a multisample read was ResolveWireDepthStencil's
|
|
// region Fatal whatever the shape, but that arm asks DeclineWireDepthStencilResolveShape
|
|
// BEFORE its region check - so a scaled (or X-mirrored) COLOR|DEPTH blit out of a
|
|
// multisample framebuffer onto the window scaled and wrote its colour through
|
|
// BlitWireColorToDefault and then recorded INVALID_OPERATION on its depth, the very
|
|
// partial effect this block exists to close. Both sides are resolved as READS
|
|
// (isWriteTarget=false): only their format and samples are consulted here, and a write
|
|
// resolve of the default framebuffer would re-point it at the acquired swapchain image
|
|
// before anything had been written to it.
|
|
if ((mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) && !read->IsDefault) {
|
|
const auto declines = [&](const MG_Pipe::MGPSurface& src, const MG_Pipe::MGPSurface& dst,
|
|
VkImageAspectFlags aspect) -> Bool {
|
|
const WireImage source = ResolveWireImage(*read, src, aspect, /*isWriteTarget=*/false);
|
|
if (!source.image || source.samples == VK_SAMPLE_COUNT_1_BIT) return false;
|
|
const WireImage destination = ResolveWireImage(*draw, dst, aspect, /*isWriteTarget=*/false);
|
|
if (!destination.image || destination.samples != VK_SAMPLE_COUNT_1_BIT) return false;
|
|
return DeclineWireDepthStencilResolveShape(source, destination, sx0, sy0, sx1, sy1,
|
|
dx0, dy0, dx1, dy1);
|
|
};
|
|
if (((mask & GL_DEPTH_BUFFER_BIT) && declines(read->Depth, draw->Depth, VK_IMAGE_ASPECT_DEPTH_BIT)) ||
|
|
((mask & GL_STENCIL_BUFFER_BIT) && declines(read->Stencil, draw->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)))
|
|
return;
|
|
}
|
|
const auto blit = [&](const MG_Pipe::MGPSurface& src, const MG_Pipe::MGPSurface& dst,
|
|
VkImageAspectFlags aspect) {
|
|
WireImage source = ResolveWireImage(*read,src,aspect, /*isWriteTarget=*/false);
|
|
WireImage destination = ResolveWireImage(*draw,dst,aspect, /*isWriteTarget=*/true);
|
|
if (!source.image || !destination.image) return;
|
|
m_textureManager->FlushPendingUploads();
|
|
if (aspect != VK_IMAGE_ASPECT_COLOR_BIT && source.samples != VK_SAMPLE_COUNT_1_BIT &&
|
|
destination.samples == VK_SAMPLE_COUNT_1_BIT) {
|
|
ResolveWireDepthStencil(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1);
|
|
if (dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
|
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, 1);
|
|
return;
|
|
}
|
|
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT && source.samples != VK_SAMPLE_COUNT_1_BIT &&
|
|
destination.samples == VK_SAMPLE_COUNT_1_BIT) {
|
|
// A resolve cannot flip axes or convert RGBA to a BGRA/default
|
|
// attachment. Resolve into the source format first, then use
|
|
// the existing single-sample blit for those GL semantics.
|
|
//
|
|
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-shape`): AND IT CANNOT SCALE.
|
|
// vkCmdResolveImage carries ONE extent and one offset per side, so this arm used
|
|
// to refuse every rectangle whose destination was a different size from its
|
|
// source - a shape glBlitFramebuffer names, and one the monolith arm reaches by
|
|
// this very route (VulkanRenderer.cpp's `regionWasTransformed` branch resolves
|
|
// into a scratch and blits THAT). The two legs split the work the same way here:
|
|
// the resolve reads the source band AT RAW OFFSETS into a scratch sized by the
|
|
// SOURCE rect, and only the second leg - an ordinary vkCmdBlitImage on a
|
|
// single-sampled image, which is the one thing it is for - sees the scale and the
|
|
// flip. So the size equality drops out of this guard; what stays is the part
|
|
// neither leg can express (a rotated default source, a 3D source) or that names
|
|
// texels the source does not have.
|
|
const Int64 width = std::abs(static_cast<Int64>(sx1) - sx0);
|
|
const Int64 height = std::abs(static_cast<Int64>(sy1) - sy0);
|
|
if (!width || !height || dx0 == dx1 || dy0 == dy1) return;
|
|
if (source.isDefault || source.is3D ||
|
|
std::min(sx0, sx1) < 0 || std::min(sy0, sy1) < 0 ||
|
|
static_cast<Uint64>(std::max(sx0, sx1)) > source.extent.width ||
|
|
static_cast<Uint64>(std::max(sy0, sy1)) > source.extent.height)
|
|
MagmaWireFatal("multisample-resolve-region");
|
|
auto scratch = MakeUnique<VkTextureManager::TextureResource>();
|
|
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
info.imageType = VK_IMAGE_TYPE_2D;
|
|
info.format = source.format;
|
|
info.extent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
|
info.mipLevels = info.arrayLayers = 1;
|
|
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
VmaAllocationCreateInfo allocation{};
|
|
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
allocation.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
if (vmaCreateImage(m_allocator, &info, &allocation, &scratch->image, &scratch->allocation, nullptr) != VK_SUCCESS)
|
|
MagmaWireFatal("multisample-resolve-allocation");
|
|
scratch->format = source.format;
|
|
scratch->extent = {info.extent.width, info.extent.height};
|
|
scratch->mipLevels = scratch->arrayLayers = scratch->depth = 1;
|
|
scratch->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
WireImage resolved{};
|
|
resolved.image = scratch->image;
|
|
resolved.format = scratch->format;
|
|
resolved.extent = scratch->extent;
|
|
resolved.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
resolved.trackedLayout = &scratch->layout;
|
|
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
VkImageResolve resolve{};
|
|
resolve.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, source.level, source.layer, 1};
|
|
resolve.srcOffset = {std::min(sx0, sx1), std::min(sy0, sy1), 0};
|
|
resolve.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
resolve.extent = info.extent;
|
|
vkCmdResolveImage(m_frameContext.GetCurrent().commandBuffer, source.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, resolved.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &resolve);
|
|
const GLint rx0 = sx1 > sx0 ? 0 : static_cast<GLint>(width);
|
|
const GLint rx1 = sx1 > sx0 ? static_cast<GLint>(width) : 0;
|
|
const GLint ry0 = sy1 > sy0 ? 0 : static_cast<GLint>(height);
|
|
const GLint ry1 = sy1 > sy0 ? static_cast<GLint>(height) : 0;
|
|
if (destination.isDefault) {
|
|
if (!BlitWireColorToDefault(resolved, destination, rx0, ry0, rx1, ry1,
|
|
dx0, dy0, dx1, dy1, filter))
|
|
MagmaWireFatal("multisample-default-blit");
|
|
m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
|
} else {
|
|
TransitionWireImage(resolved, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
TransitionWireImage(destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
VkImageBlit copy{};
|
|
copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
copy.srcOffsets[0] = {rx0, ry0, 0}; copy.srcOffsets[1] = {rx1, ry1, 1};
|
|
copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, destination.level,
|
|
destination.is3D ? 0u : destination.layer, 1};
|
|
const Int32 z = destination.is3D ? static_cast<Int32>(destination.layer) : 0;
|
|
copy.dstOffsets[0] = {dx0, dy0, z}; copy.dstOffsets[1] = {dx1, dy1, z + 1};
|
|
// The filter only started MATTERING with the scale above: at 1:1 LINEAR and
|
|
// NEAREST read the same texel, so nothing here ever asked whether the format
|
|
// could be filtered. VK_FILTER_LINEAR on a source whose optimalTilingFeatures
|
|
// lack SAMPLED_IMAGE_FILTER_LINEAR is undefined behaviour, and GL says filter
|
|
// is ignored for a multisample resolve in the first place - so a format that
|
|
// cannot be filtered takes NEAREST rather than the command being illegal.
|
|
VkFormatProperties scratchProperties{};
|
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &scratchProperties);
|
|
const VkFilter blitFilter =
|
|
filter == GL_LINEAR && (scratchProperties.optimalTilingFeatures &
|
|
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)
|
|
? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
|
|
vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer, resolved.image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
1, ©, blitFilter);
|
|
if (dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
|
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, 1);
|
|
}
|
|
// The shader blit's views were retired first. Keep their backing
|
|
// image until the same submit completes; resizing a shared
|
|
// scratch image here would destroy an older queued resolve.
|
|
WireRetiredObjects retired{};
|
|
retired.textures.push_back(Move(scratch));
|
|
RetireWireObjects(Move(retired));
|
|
return;
|
|
}
|
|
// P7 gate 5 (g5-msrbo): A SINGLE-SAMPLE DEPTH/STENCIL BLIT THAT IS A PLAIN COPY IS A COPY
|
|
// (CopyWireDepthStencilBlit): no blit feature, and one aspect of a packed format moves
|
|
// without touching the other. Anything else - a scale, a flip, the default framebuffer,
|
|
// a format change - still takes vkCmdBlitImage below, exactly as it did and as the
|
|
// monolith arm does. On a format without BLIT_SRC/BLIT_DST that command is outside the
|
|
// spec, and the Adreno 830 (which has no BLIT_DST on any depth format) executes it
|
|
// anyway: declining would have turned a working picture on the target device into a
|
|
// missing one. So it is named once, for the next device reading, and not refused.
|
|
if (aspect != VK_IMAGE_ASPECT_COLOR_BIT && source.samples == VK_SAMPLE_COUNT_1_BIT &&
|
|
destination.samples == VK_SAMPLE_COUNT_1_BIT) {
|
|
if (CopyWireDepthStencilBlit(source, destination, sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1, aspect)) {
|
|
if (dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
|
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level,
|
|
destination.layer, 1);
|
|
return;
|
|
}
|
|
VkFormatProperties sourceProperties{};
|
|
VkFormatProperties destinationProperties{};
|
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
|
|
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format,
|
|
&destinationProperties);
|
|
if (!(sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) ||
|
|
!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) {
|
|
MGLOG_W_ONCE("DirectVulkan::BlitWireFramebuffers: aspect 0x%x from format %d (features 0x%x) "
|
|
"to format %d (features 0x%x) is not a plain copy and takes vkCmdBlitImage "
|
|
"without BLIT_SRC/BLIT_DST - the monolith arm's command, outside the spec; the "
|
|
"driver decides the picture",
|
|
static_cast<Uint32>(aspect), static_cast<Int>(source.format),
|
|
static_cast<Uint32>(sourceProperties.optimalTilingFeatures),
|
|
static_cast<Int>(destination.format),
|
|
static_cast<Uint32>(destinationProperties.optimalTilingFeatures));
|
|
}
|
|
}
|
|
// 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) {
|
|
// P7 wave 2-B, CONTRACT-P7 §3.2: `default-color-blit-shape@P7` RETIRES, in three
|
|
// pieces, because the one Fatal covered three unrelated situations.
|
|
//
|
|
// The shader blit EXISTS for rotation, because an axis-aligned vkCmdBlitImage cannot
|
|
// express a quarter turn. It is still TAKEN FIRST on every transform it can
|
|
// express, identity included. That has been the routing since 38919d453: every
|
|
// host lane (all identity) and the Redmi pbuffer run their default-framebuffer
|
|
// colour blits through it, F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
|
|
// among them. The ordinary vkCmdBlitImage arm below is reached on identity only
|
|
// when this pass declines the source. Because identity blits run here, the pass
|
|
// must copy exactly wherever GL says the blit is a copy: an unscaled blit samples
|
|
// NEAREST whatever `filter` says (WireColorBlitFilter.h, g3-blit; on Adreno 830,
|
|
// linear sampling of a 1:1 GL_LINEAR blit moved create-instancing's edge pixels by
|
|
// 1 LSB, where the monolith's vkCmdBlitImage is exact).
|
|
//
|
|
// The pass can only SAMPLE its source - sampler2D, a float result, one of two 2D
|
|
// view types - so an RGBA8UI attachment, a 3D slice or an array layer makes it
|
|
// return false. That refusal used to fire even on a surface with NO rotation,
|
|
// where the vkCmdBlitImage arm below does the blit correctly and does handle
|
|
// those shapes.
|
|
const auto transform = m_swapchainObject.GetPreTransform();
|
|
const Bool knownRotation = 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;
|
|
if (!knownRotation) {
|
|
// Mirrors, and mirror-rotations, are the shapes neither arm can express: the
|
|
// blit rectangle would have to be reflected, and this package does not have a
|
|
// device that reports one to check the result against. A decline (§3.2's
|
|
// "outside the four rotations"), not a Fatal, and it says which transform.
|
|
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: the surface pre-transform is 0x%x, "
|
|
"which is neither identity nor one of the three rotations either blit "
|
|
"arm can express; declining the blit to the default framebuffer",
|
|
static_cast<Uint32>(transform));
|
|
return;
|
|
}
|
|
if (!MagmaWireForcedDefaultBlitScratch() &&
|
|
BlitWireColorToDefault(source, destination, sx0, sy0, sx1, sy1,
|
|
dx0, dy0, dx1, dy1, filter)) {
|
|
m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
|
return;
|
|
}
|
|
// A source the shader pass cannot sample, on a rotated surface. Move the region
|
|
// into an owned 2D image of a sampled-float format in the same size-compatibility
|
|
// class and blit THAT. The knob forces this arm on an identity surface, where it
|
|
// must produce the same pixels the fall-through below produces - which is what
|
|
// makes it reachable from a host lane at all (every host surface here is
|
|
// identity, and the Redmi evidence is a pbuffer, so nothing else would run it).
|
|
if (transform != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ||
|
|
MagmaWireForcedDefaultBlitScratch()) {
|
|
if (BlitWireColorToDefaultThroughScratch(source, destination, sx0, sy0, sx1, sy1,
|
|
dx0, dy0, dx1, dy1, filter)) {
|
|
m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
|
|
return;
|
|
}
|
|
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: a %ux%u source of format %d "
|
|
"(view type %d) cannot reach the rotated (0x%x) default framebuffer "
|
|
"through either the shader pass or a scratch copy; declining the blit",
|
|
source.extent.width, source.extent.height,
|
|
static_cast<Int>(source.format), static_cast<Int>(source.viewType),
|
|
static_cast<Uint32>(transform));
|
|
return;
|
|
}
|
|
// Identity, and the shader pass above declined this source (an integer format, a
|
|
// 3D slice, a view type it cannot sample): fall through to the ordinary
|
|
// vkCmdBlitImage arm below. It already maps GL's bottom row onto the native
|
|
// image's last row for a default target, and vkCmdBlitImage takes integer
|
|
// formats and array layers natively. Its 3D
|
|
// addressing was wrong and is fixed with this package - see the srcBaseLayer /
|
|
// srcZ note down there; the bug was invisible until now because a 3D source hit
|
|
// the Fatal above and never reached that arm.
|
|
}
|
|
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;
|
|
TransitionWireImage(source,srcLayout);
|
|
if (sameImage) destination.layout = source.layout;
|
|
else TransitionWireImage(destination,dstLayout);
|
|
VkImageBlit region{};
|
|
// A 3D IMAGE CARRIES ITS SLICE ON Z, NOT ON THE LAYER AXIS. Its only subresource is
|
|
// layer 0, and the selected slice is the z of the offsets - which is how the copy
|
|
// endpoint (`CopyImageSliceMapping`) and the multisample resolve arm above already
|
|
// address one. This arm addressed it as `baseArrayLayer = source.layer` with z 0..1,
|
|
// so a blit from slice 1 of a 3D texture read slice 0 out of a layer the image does
|
|
// not have. Nothing caught it, because until this package that shape reached the
|
|
// arm's Fatal first and never got here at all.
|
|
const Uint32 srcBaseLayer = source.is3D ? 0u : source.layer;
|
|
const Uint32 dstBaseLayer = destination.is3D ? 0u : destination.layer;
|
|
const Int32 srcZ = source.is3D ? static_cast<Int32>(source.layer) : 0;
|
|
const Int32 dstZ = destination.is3D ? static_cast<Int32>(destination.layer) : 0;
|
|
region.srcSubresource = {aspect,source.level,srcBaseLayer,1};
|
|
region.dstSubresource = {aspect,destination.level,dstBaseLayer,1};
|
|
region.srcOffsets[0] = {sx0,sy0,srcZ}; region.srcOffsets[1] = {sx1,sy1,srcZ+1};
|
|
region.dstOffsets[0] = {dx0,dy0,dstZ}; region.dstOffsets[1] = {dx1,dy1,dstZ+1};
|
|
// Headless and identity surfaces store GL's bottom row at Vulkan's last row.
|
|
if (source.isDefault) {
|
|
region.srcOffsets[0].y = static_cast<GLint>(source.extent.height)-sy0;
|
|
region.srcOffsets[1].y = static_cast<GLint>(source.extent.height)-sy1;
|
|
}
|
|
if (destination.isDefault) {
|
|
region.dstOffsets[0].y = static_cast<GLint>(destination.extent.height)-dy0;
|
|
region.dstOffsets[1].y = static_cast<GLint>(destination.extent.height)-dy1;
|
|
}
|
|
auto command = m_frameContext.GetCurrent().commandBuffer;
|
|
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT) {
|
|
// P7 wave 2-B2, CONTRACT-P7 §3.2: WHAT IS LEFT HERE IS A MULTISAMPLE DESTINATION.
|
|
// Every blit that resolves a multisample SOURCE into a single-sample destination
|
|
// returned above - the depth/stencil arm at the top of this lambda through
|
|
// ResolveWireDepthStencil, the colour arm below it through its resolve scratch -
|
|
// and both of those now carry their own scale and flip. So the samples of the two
|
|
// sides can only differ here by the DESTINATION being multisampled, which is the
|
|
// shape this arm declines rather than the one it performs.
|
|
const Int32 width = region.srcOffsets[1].x - region.srcOffsets[0].x;
|
|
const Int32 height = region.srcOffsets[1].y - region.srcOffsets[0].y;
|
|
if (width <= 0 || height <= 0 || width != region.dstOffsets[1].x - region.dstOffsets[0].x ||
|
|
height != region.dstOffsets[1].y - region.dstOffsets[0].y || source.format != destination.format) {
|
|
// `multisample-blit-shape@P7` RETIRES: the half that had a shape neither
|
|
// vkCmdResolveImage nor vkCmdBlitImage could express is done above, and this
|
|
// is the half that has none - vkCmdCopyImage is the only command that writes
|
|
// a multisampled image, it takes one extent, and GL 4.6 core 18.3.1 makes a
|
|
// multisample blit whose rectangles differ in size (or whose formats differ)
|
|
// INVALID_OPERATION rather than a picture. Rule I (a): say it once, record
|
|
// the error GL names, leave the destination alone, keep the session.
|
|
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: a %dx%d source rectangle "
|
|
"(format %d, %u sample(s)) cannot reach a %dx%d destination "
|
|
"rectangle (format %d, %u sample(s)) - a blit onto a multisample "
|
|
"attachment carries no scale, no flip and no format conversion; "
|
|
"declining the blit",
|
|
width, height, static_cast<Int>(source.format),
|
|
static_cast<Uint32>(source.samples),
|
|
region.dstOffsets[1].x - region.dstOffsets[0].x,
|
|
region.dstOffsets[1].y - region.dstOffsets[0].y,
|
|
static_cast<Int>(destination.format),
|
|
static_cast<Uint32>(destination.samples));
|
|
RecordTextureCopyError("BlitWireFramebuffers", ErrorCode::InvalidOperation,
|
|
"glBlitFramebuffer onto a multisampled draw framebuffer requires "
|
|
"identical source and destination rectangles and formats.");
|
|
return;
|
|
}
|
|
if (source.samples == destination.samples) {
|
|
VkImageCopy copy{region.srcSubresource, region.srcOffsets[0], region.dstSubresource,
|
|
region.dstOffsets[0], {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}};
|
|
vkCmdCopyImage(command, source.image, srcLayout, destination.image, dstLayout, 1, ©);
|
|
} else {
|
|
// `multisample-blit-aspect@P7` RETIRES. The half with an arm is
|
|
// ResolveWireDepthStencil's: a multisample depth or stencil SOURCE now
|
|
// reaches a single-sample destination on every device, through
|
|
// VK_KHR_depth_stencil_resolve where it exists and through the baked pass in
|
|
// WireMultisampleResolve.inc where it does not. This is the half with none -
|
|
// the sample counts differ and the DESTINATION is the multisampled side, so
|
|
// it is either one sample fanned out to N or N reduced to M.
|
|
//
|
|
// Vulkan has no command for either: vkCmdResolveImage only ever writes a
|
|
// single-sample destination, vkCmdBlitImage refuses both sides, and
|
|
// vkCmdCopyImage requires the counts to match. Reaching it through a draw
|
|
// would need a pipeline with the destination's sample count and a
|
|
// multisample source view, i.e. a second copy of the resolve machinery
|
|
// pointed the other way, for a shape GL 4.6 core 18.3.1 calls
|
|
// INVALID_OPERATION whenever the two rectangles are not identical and
|
|
// describes as sample replication when they are. Rule I (a): name both
|
|
// counts, record the error, leave the destination alone.
|
|
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: aspect 0x%x cannot go from a "
|
|
"%u-sample source to a %u-sample destination - a resolve only ever "
|
|
"writes one sample and a copy needs the counts to match; declining "
|
|
"the blit",
|
|
static_cast<Uint32>(aspect), static_cast<Uint32>(source.samples),
|
|
static_cast<Uint32>(destination.samples));
|
|
RecordTextureCopyError("BlitWireFramebuffers", ErrorCode::InvalidOperation,
|
|
"glBlitFramebuffer cannot change the sample count of an "
|
|
"attachment in that direction.");
|
|
return;
|
|
}
|
|
} else {
|
|
vkCmdBlitImage(command,source.image,srcLayout,destination.image,dstLayout,1,®ion,
|
|
filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
|
}
|
|
if (!destination.isDefault && dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
|
|
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, destination.layers);
|
|
if (destination.isDefault) {
|
|
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex,true);
|
|
else m_swapchainObject.SetDepthStencilContentDefined(destination.swapchainImageIndex,true);
|
|
}
|
|
};
|
|
if (mask & GL_COLOR_BUFFER_BIT) {
|
|
for (Uint32 i=0;i<MG_Pipe::kMGPipeMaxColorAttachments;++i) {
|
|
const Int32 attachment = draw->DrawBuffers[i];
|
|
if (attachment>=0 && attachment<static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments))
|
|
blit(read->ReadSurface,draw->Color[attachment],VK_IMAGE_ASPECT_COLOR_BIT);
|
|
}
|
|
}
|
|
if (mask & GL_DEPTH_BUFFER_BIT) blit(read->Depth,draw->Depth,VK_IMAGE_ASPECT_DEPTH_BIT);
|
|
if (mask & GL_STENCIL_BUFFER_BIT) blit(read->Stencil,draw->Stencil,VK_IMAGE_ASPECT_STENCIL_BIT);
|
|
}
|
|
void VulkanRenderer::CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
|
GLint x, GLint y, GLsizei width, GLsizei height) {
|
|
if (width <= 0 || height <= 0) return;
|
|
if (target != GL_TEXTURE_2D) {
|
|
RecordTextureCopyError(__func__,ErrorCode::InvalidOperation,
|
|
"CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations.");
|
|
return;
|
|
}
|
|
RetireWireDrawPass();
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
const auto* read = state.ReadFramebuffer();
|
|
if (!read) MagmaWireFatal("copy-framebuffer-record");
|
|
const auto* texture = m_textureManager->SyncTextureResourceByHandle(state.VerbCopyTexDst);
|
|
if (!texture) MagmaWireFatal("copy-destination-record");
|
|
const VkImageAspectFlags aspect = texture->aspect;
|
|
MG_Pipe::MGPFramebufferState fake{};
|
|
MG_Pipe::MGPSurface surface{};
|
|
surface.Kind = MG_Pipe::kMGPipeSurfaceKindTexture;
|
|
surface.Res = state.VerbCopyTexDst;
|
|
surface.Level = static_cast<Uint16>(level);
|
|
WireImage source = ResolveWireImage(*read,aspect & VK_IMAGE_ASPECT_DEPTH_BIT ? read->Depth :
|
|
aspect & VK_IMAGE_ASPECT_STENCIL_BIT ? read->Stencil : read->ReadSurface,aspect,
|
|
/*isWriteTarget=*/false);
|
|
WireImage destination = ResolveWireImage(fake,surface,aspect, /*isWriteTarget=*/false);
|
|
if (!source.image || !destination.image) return;
|
|
m_textureManager->FlushPendingUploads();
|
|
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;
|
|
TransitionWireImage(source,srcLayout);
|
|
if (sameImage) destination.layout = source.layout;
|
|
else TransitionWireImage(destination,dstLayout);
|
|
VkImageBlit region{};
|
|
region.srcSubresource = {aspect,source.level,source.layer,1};
|
|
region.dstSubresource = {aspect,destination.level,destination.layer,1};
|
|
region.srcOffsets[0] = {x,y,0}; region.srcOffsets[1] = {x+width,y+height,1};
|
|
region.dstOffsets[0] = {xoffset,yoffset,0}; region.dstOffsets[1] = {xoffset+width,yoffset+height,1};
|
|
if (source.isDefault) {
|
|
region.srcOffsets[0].y = static_cast<Int32>(source.extent.height)-y;
|
|
region.srcOffsets[1].y = static_cast<Int32>(source.extent.height)-y-height;
|
|
}
|
|
vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer,source.image,srcLayout,
|
|
destination.image,dstLayout,1,®ion,VK_FILTER_NEAREST);
|
|
m_textureManager->MarkWireTextureGpuWritten(destination.storage,destination.level,destination.layer);
|
|
}
|
|
|
|
void VulkanRenderer::GenerateWireMipmap() {
|
|
RetireWireDrawPass();
|
|
auto& state = MG_Pipe::MGPipeApplier();
|
|
const auto object = state.VerbMipRes;
|
|
if (MG_Pipe::MGPipeHandleIsNull(object) || object.Slot >= state.TextureResources.size())
|
|
MagmaWireFatal("mipmap-record");
|
|
const auto& objectRecord = state.TextureResources[object.Slot];
|
|
if (!objectRecord.Live || objectRecord.Gen != object.Gen) MagmaWireFatal("mipmap-record-generation");
|
|
// LevelCount is the logical end-exclusive level, not the number of
|
|
// levels after BaseLevel. Clip in the view's coordinates before the
|
|
// resolver adds its root-storage offset.
|
|
const Uint32 logicalBase = state.VerbMipBaseLevel;
|
|
Uint32 logicalEnd = std::min<Uint32>(state.VerbMipLevelCount, objectRecord.Desc.Levels);
|
|
if (objectRecord.ParamsSerial)
|
|
logicalEnd = std::min(logicalEnd, static_cast<Uint32>(objectRecord.Params.MaxLevel) + 1);
|
|
if (!MG_Pipe::MGPipeHandleIsNull(objectRecord.Desc.ViewOf)) {
|
|
const auto viewHandle = objectRecord.ViewCso;
|
|
if (MG_Pipe::MGPipeHandleIsNull(viewHandle) || viewHandle.Slot >= state.SamplerViewCsos.size())
|
|
MagmaWireFatal("mipmap-view-record");
|
|
const auto& view = state.SamplerViewCsos[viewHandle.Slot];
|
|
if (!view.Live || view.Gen != viewHandle.Gen || view.View.Texture != object)
|
|
MagmaWireFatal("mipmap-view-generation");
|
|
logicalEnd = std::min<Uint32>(logicalEnd, view.View.NumLevels);
|
|
}
|
|
if (logicalEnd <= logicalBase + 1) return;
|
|
Uint32 base = logicalBase, baseLayer = 0, layerCount = 1;
|
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
|
const auto handle = m_textureManager->ResolveWireTextureStorage(state.VerbMipRes, base, baseLayer,
|
|
&viewFormat, &layerCount);
|
|
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
|
|
if (!resource) MagmaWireFatal("mipmap-resource");
|
|
// A shared image is one level of a buffer someone else allocated; growing the chain would
|
|
// reallocate it away from the buffer.
|
|
if (resource->sharedImageId != 0) {
|
|
MGLOG_E_ONCE("DirectVulkan::GenerateWireMipmap: the texture's level 0 is shared image %llu, which has "
|
|
"no mip chain; levels %u..%u are left as they are",
|
|
static_cast<unsigned long long>(resource->sharedImageId), logicalBase + 1, logicalEnd - 1);
|
|
return;
|
|
}
|
|
m_textureManager->FlushPendingUploads();
|
|
RetireWireDrawPass();
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
Uint32 fullLevelCount = 1;
|
|
for (Uint32 size = std::max({resource->extent.width, resource->extent.height, resource->depth});
|
|
size > 1; size >>= 1) ++fullLevelCount;
|
|
if (base >= fullLevelCount) MagmaWireFatal("mipmap-base-extent");
|
|
const Uint32 count = std::min(logicalEnd - logicalBase, fullLevelCount - base);
|
|
if (count <= 1) return;
|
|
// P7 wave 2-B, CONTRACT-P7 §3.2: `depth-stencil-mipmap@P7` RETIRES FOR DEPTH AND
|
|
// DECLINES FOR DEPTH|STENCIL - and the decline comes BEFORE the chain is grown, so a
|
|
// level this verb is not going to fill is not created and then recorded as defined.
|
|
//
|
|
// The combined case is a decline rather than a Fatal because the monolith arm does no
|
|
// better: its shader fallback asserts depth-only (VulkanRenderer.cpp's GenerateMipmap,
|
|
// "shader fallback only supports depth-only textures") and an assert is nothing in the
|
|
// release build both arms ship as. Nothing in GL promises a stencil mip either - the
|
|
// pass writes gl_FragDepth and would leave the stencil aspect of the level it produced
|
|
// undefined, which is worse than leaving the whole level alone and saying so.
|
|
//
|
|
// P8-SV: a NAMED decline (WireDeclines.def MipmapDepthStencilAspect), so MGWIRE-DECLINES
|
|
// counts it like every other refusal on this arm; the line keeps its words.
|
|
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) {
|
|
MGL_WIRE_DECLINE_AT(MipmapDepthStencilAspect,
|
|
"DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or "
|
|
"stencil-only texture, format %d) has no mip generation on either arm; "
|
|
"declining and leaving levels %u..%u as they are",
|
|
static_cast<Uint32>(resource->aspect), static_cast<Int>(resource->format),
|
|
base + 1, base + count - 1);
|
|
return;
|
|
}
|
|
// P8-F: WHICH ARM TAKES THIS CHAIN IS DECIDED BEFORE THE CHAIN IS GROWN, for the
|
|
// depth-stencil decline's reason above. Nothing here depends on the growth: the format,
|
|
// its features, the knob tier, the aspect, view type, usage and layer window are the
|
|
// same before and after it (GrowWireTextureMipChain carries every one of them over).
|
|
VkFormatProperties 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 Uint32 forcedShaderTier = MagmaWireForcedShaderMipmapTier();
|
|
const Bool nativeBlit = forcedShaderTier == 0 && 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;
|
|
const Bool layersInRange = baseLayer < resource->arrayLayers &&
|
|
layerCount <= resource->arrayLayers - baseLayer;
|
|
// Both shader arms render a 2D view of one layer of one level, so a 1D, 1D-array or 3D
|
|
// image is a shape neither takes.
|
|
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
|
|
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
|
|
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
|
|
// The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses
|
|
// for an image this function hands it (the view type is passed to it below) - a 1D,
|
|
// 1D-array or 3D view type, and a format that does not both sample and attach as depth.
|
|
// Its other refusals, multisample and a default image, are never built here. P8-SV: the
|
|
// view type half is new; before it a 1D depth chain reached the pass, which made a 2D
|
|
// view of the 1D image.
|
|
const auto depthPassFeatures =
|
|
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
const Bool depthArmTakesShape = planar &&
|
|
layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures;
|
|
// The colour arm's shape (the two shader shapes are explained at the colour arm below).
|
|
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
|
|
// The shader arm samples through a sampler2D and writes a vec4, which is what
|
|
// BlitWireColorImage's own float check enforces; naming it here rather than letting the
|
|
// blit return false is what puts the reason in the decline line.
|
|
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
|
|
const Bool renderableFormat =
|
|
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
|
const Bool attachable = forcedShaderTier < 2 &&
|
|
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
|
|
const Bool viaScratch = !attachable && renderableFormat &&
|
|
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
|
|
const Bool colourArmTakesShape =
|
|
colour && planar && layersInRange && sampledFloat && (attachable || viaScratch);
|
|
// P8-F: `mipmap-shader-format-or-shape` IS A NAMED DECLINE, NOT A FATAL (rule I (a)).
|
|
// The monolith arm has always answered this chain with a decline (VulkanRenderer.cpp's
|
|
// GenerateMipmap: a format without blit support is skipped and the chain left unwritten),
|
|
// and a shape no shader arm takes is a coverage gap in this backend, not a broken record.
|
|
// The leading word stays, so every device-log grep keyed on it still finds the line.
|
|
const Bool shaderArmTakesShape =
|
|
resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? depthArmTakesShape : colourArmTakesShape;
|
|
if (!nativeBlit && !shaderArmTakesShape) {
|
|
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
|
resource->format, props, resource->aspect, resource->viewType, resource->usageFlags,
|
|
resource->extent, resource->depth, resource->mipLevels, baseLayer, layerCount,
|
|
resource->arrayLayers);
|
|
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; no mip arm takes this shape, levels %u..%u left as they are",
|
|
detail.c_str(), base + 1, base + count - 1);
|
|
return;
|
|
}
|
|
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
|
|
MagmaWireFatal("mipmap-growth");
|
|
if (!nativeBlit && resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
|
|
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): THE DEPTH ARM, and it is first because a
|
|
// depth format without BLIT_DST is the ordinary case, not the exotic one -
|
|
// VK_FORMAT_FEATURE_BLIT_DST is optional for depth and plenty of devices decline
|
|
// it. Before this package the whole branch below was colour-only, so such a
|
|
// texture reached `mipmap-shader-format-or-shape` on its very first disjunction
|
|
// while the monolith arm quietly generated the chain with a shader.
|
|
//
|
|
// The per-level pass is WireDepthMipmap.inc's, which is the monolith's fragment
|
|
// shader with the monolith's arithmetic on a baked pipeline that owns no frontend
|
|
// object - §3.2's "observably the monolith's" without the GL program the monolith
|
|
// needs to get there. Its layer window and format were checked before the growth.
|
|
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_DEPTH_BIT;
|
|
// P8-SV: the image's own view type, so the pass can refuse a shape it cannot
|
|
// render (it makes 2D views) instead of receiving the struct's 2D default.
|
|
source.viewType = resource->viewType;
|
|
source.is3D = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
|
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)};
|
|
// The pass reports the SHAPE it cannot take. depthArmTakesShape above asks
|
|
// its questions first, so this is reached only if the two lists drift apart
|
|
// - and then it is the same named decline (P8-F), not a session's end. It
|
|
// can only refuse the first level: the shape does not change down the chain.
|
|
if (!GenerateWireDepthMipLevel(source, destination)) {
|
|
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
|
|
resource->format, props, resource->aspect, resource->viewType,
|
|
resource->usageFlags, resource->extent, resource->depth,
|
|
resource->mipLevels, baseLayer, layerCount, resource->arrayLayers);
|
|
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; the depth pass refused level %u",
|
|
detail.c_str(), level);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else 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.
|
|
//
|
|
// TWO SHADER SHAPES, not one. The arm that was here rendered STRAIGHT INTO the
|
|
// next level, which needs the image to have been created with
|
|
// COLOR_ATTACHMENT usage - and a texture whose format the device refuses as a
|
|
// colour attachment never gets that bit (VkTextureManager.cpp:2156-2160), so
|
|
// every sampled-only texture without BLIT support fell into the refusal rather
|
|
// than into a fallback. When the FORMAT can be rendered and only the IMAGE
|
|
// cannot, an owned scratch attachment of the destination level's size renders
|
|
// the mip and vkCmdCopyImage moves it in: a raw texel move, no conversion, and
|
|
// the texels still come from GPU contents rather than from a staged shadow.
|
|
//
|
|
// Which of the two (`attachable` or `viaScratch`), and whether either takes the
|
|
// shape at all, was decided before the growth (colourArmTakesShape, P8-F).
|
|
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)};
|
|
const GLenum blitFilter = filter == VK_FILTER_LINEAR ? GL_LINEAR : GL_NEAREST;
|
|
if (attachable) {
|
|
if (!BlitWireColorImage(source, destination, 0, 0, source.extent.width,
|
|
source.extent.height, 0, 0, destination.extent.width,
|
|
destination.extent.height, blitFilter, true))
|
|
MagmaWireFatal("mipmap-shader-blit");
|
|
continue;
|
|
}
|
|
// Owned, per level, and retired with the submit that consumes it - the
|
|
// shape the multisample resolve above already uses. A shared scratch
|
|
// resized here would destroy an image an earlier queued pass still reads.
|
|
auto scratch = MakeUnique<VkTextureManager::TextureResource>();
|
|
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
info.imageType = VK_IMAGE_TYPE_2D;
|
|
info.format = format;
|
|
info.extent = {destination.extent.width, destination.extent.height, 1};
|
|
info.mipLevels = info.arrayLayers = 1;
|
|
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
|
VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
VmaAllocationCreateInfo allocation{};
|
|
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
allocation.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
if (vmaCreateImage(m_allocator, &info, &allocation, &scratch->image,
|
|
&scratch->allocation, nullptr) != VK_SUCCESS)
|
|
MagmaWireFatal("mipmap-shader-scratch-allocation");
|
|
scratch->format = format;
|
|
scratch->extent = destination.extent;
|
|
scratch->mipLevels = scratch->arrayLayers = scratch->depth = 1;
|
|
scratch->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
WireImage target{};
|
|
target.image = scratch->image;
|
|
target.format = format;
|
|
target.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
target.extent = destination.extent;
|
|
target.trackedLayout = &scratch->layout;
|
|
if (!BlitWireColorImage(source, target, 0, 0, source.extent.width,
|
|
source.extent.height, 0, 0, target.extent.width, target.extent.height,
|
|
blitFilter, true))
|
|
MagmaWireFatal("mipmap-shader-blit");
|
|
target.layout = scratch->layout;
|
|
TransitionWireImage(target, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
WireImage writeBack = destination;
|
|
writeBack.layout = resource->layout;
|
|
TransitionWireImage(writeBack, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
|
VkImageCopy copy{};
|
|
copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1};
|
|
copy.extent = {destination.extent.width, destination.extent.height, 1};
|
|
vkCmdCopyImage(m_frameContext.GetCurrent().commandBuffer, scratch->image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, resource->image,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
WireRetiredObjects retired{};
|
|
retired.textures.push_back(Move(scratch));
|
|
RetireWireObjects(Move(retired));
|
|
}
|
|
}
|
|
} 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");
|
|
// P8-SV: A 3D IMAGE HAS ONE ARRAY LAYER, AND ITS DEPTH IS THE Z EXTENT OF THE BLIT.
|
|
// ResolveWireTextureStorage answers `layerCount` in slices for Tex3D (Depth >> level,
|
|
// VkTextureManager.cpp) - the addressing attachments, descriptors and readback use - and
|
|
// passing that on as the blit's subresource layerCount names layers the image (created
|
|
// with arrayLayers = 1) does not have (VUID-vkCmdBlitImage-srcImage-00240,
|
|
// -srcSubresource-01707, -dstSubresource-01708). The monolith arm blits layer 0, count 1,
|
|
// with the z offsets below halving the depth level by level; so does this one now.
|
|
const Bool volume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
|
const Uint32 blitBaseLayer = volume ? 0u : baseLayer;
|
|
const Uint32 blitLayerCount = volume ? 1u : layerCount;
|
|
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,blitBaseLayer,blitLayerCount};
|
|
blit.dstSubresource = {resource->aspect,level,blitBaseLayer,blitLayerCount};
|
|
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;
|
|
}
|
|
auto& record = state.TextureResources[handle.Slot];
|
|
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
|
|
resource->syncedWireSerial = ++record.Serial;
|
|
auto& store = MG_Remote::Server::ServerStagedTexture();
|
|
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle);
|
|
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
|
|
// The resource descriptor determines the upload-target set; no texture unit or
|
|
// client upload-target vector participates in defining the generated shadow.
|
|
for (Uint32 face = target == MG_Pipe::MGPipeResourceTarget::TexCube ? baseLayer : 0;
|
|
face < (target == MG_Pipe::MGPipeResourceTarget::TexCube ? baseLayer + layerCount : 1u); ++face) {
|
|
TextureUploadTarget upload = TextureUploadTarget::Texture2D;
|
|
switch (target) {
|
|
case MG_Pipe::MGPipeResourceTarget::Tex1D: upload=TextureUploadTarget::Texture1D; break;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex3D: upload=TextureUploadTarget::Texture3D; break;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: upload=TextureUploadTarget::Texture1DArray; break;
|
|
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: upload=TextureUploadTarget::Texture2DArray; break;
|
|
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: upload=TextureUploadTarget::CubeMapArray; break;
|
|
case MG_Pipe::MGPipeResourceTarget::TexCube:
|
|
upload=static_cast<TextureUploadTarget>(static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)+face); break;
|
|
default: break;
|
|
}
|
|
for (Uint32 level=base+1;level<base+count;++level) {
|
|
const IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(
|
|
record.Desc.Target, record.Desc.Width, record.Desc.Height, record.Desc.Depth, level);
|
|
// PH-4: the record's own target and format, not NoteLevelDefined's Tex2D/RGBA8
|
|
// defaults - a 2D-array or 3D level has depth > 1, which the Tex2D device-limit rule
|
|
// refuses by name (TextureViewScenario.GenerateMipmapThroughAView on all Magma arms).
|
|
store.NoteLevelDefined(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),extent,
|
|
record.Desc.Target,record.Desc.InternalFormat);
|
|
store.MarkLevelGpuDirty(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),true);
|
|
}
|
|
}
|
|
}
|
|
#endif
|