mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
2978 lines
188 KiB
C++
2978 lines
188 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#include "VulkanRendererInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
// The wire arm's framebuffer verbs and texture readback. All inputs here belong to the
|
|
// applier; no frontend object, lifetime-id lookup or allocator is used.
|
|
static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) {
|
|
MG_Pipe::gPipeInputs->RecordError(code, MakeUnique<GenericErrorInfo>("DirectVulkan", func, message));
|
|
}
|
|
|
|
namespace {
|
|
// Generic VkFormat texel decode into the wide RGBA row layouts the shared readback
|
|
// store expects: GL_FLOAT rows for normalized/float sources, GL_INT / GL_UNSIGNED_INT
|
|
// rows for integer sources. Missing channels take GL defaults (0,0,0,1).
|
|
enum class ReadbackSourceClass : Uint8 {
|
|
Unsupported,
|
|
Float,
|
|
SignedInt,
|
|
UnsignedInt
|
|
};
|
|
|
|
struct ReadbackSourceDesc {
|
|
ReadbackSourceClass sourceClass = ReadbackSourceClass::Unsupported;
|
|
Int channels = 0; // component count stored per texel
|
|
Int componentBits = 0; // per-component bits for regular formats; 0 for special packed
|
|
Bool isSnorm = false;
|
|
Bool isSrgb = false;
|
|
Bool bgraSwizzle = false;
|
|
VkFormat special = VK_FORMAT_UNDEFINED; // set for packed/special formats
|
|
};
|
|
|
|
static Bool GetReadbackSourceDesc(VkFormat format, ReadbackSourceDesc& out) {
|
|
out = ReadbackSourceDesc{};
|
|
switch (format) {
|
|
// --- regular UNORM ---
|
|
case VK_FORMAT_R8_UNORM:
|
|
out = {ReadbackSourceClass::Float, 1, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8_UNORM:
|
|
out = {ReadbackSourceClass::Float, 2, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8B8A8_UNORM:
|
|
out = {ReadbackSourceClass::Float, 4, 8};
|
|
return true;
|
|
case VK_FORMAT_B8G8R8A8_UNORM:
|
|
out = {ReadbackSourceClass::Float, 4, 8, false, false, true};
|
|
return true;
|
|
case VK_FORMAT_R16_UNORM:
|
|
out = {ReadbackSourceClass::Float, 1, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16_UNORM:
|
|
out = {ReadbackSourceClass::Float, 2, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16B16A16_UNORM:
|
|
out = {ReadbackSourceClass::Float, 4, 16};
|
|
return true;
|
|
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
|
|
// GL GetTexImage/ReadPixels of sRGB textures return the raw sRGB-encoded
|
|
// bytes (GL 3.3 has no FRAMEBUFFER_SRGB read decode) - do NOT linearize.
|
|
case VK_FORMAT_R8G8B8A8_SRGB:
|
|
out = {ReadbackSourceClass::Float, 4, 8};
|
|
return true;
|
|
case VK_FORMAT_B8G8R8A8_SRGB:
|
|
out = {ReadbackSourceClass::Float, 4, 8, false, false, true};
|
|
return true;
|
|
// --- SNORM ---
|
|
case VK_FORMAT_R8_SNORM:
|
|
out = {ReadbackSourceClass::Float, 1, 8, true};
|
|
return true;
|
|
case VK_FORMAT_R8G8_SNORM:
|
|
out = {ReadbackSourceClass::Float, 2, 8, true};
|
|
return true;
|
|
case VK_FORMAT_R8G8B8A8_SNORM:
|
|
out = {ReadbackSourceClass::Float, 4, 8, true};
|
|
return true;
|
|
case VK_FORMAT_R16_SNORM:
|
|
out = {ReadbackSourceClass::Float, 1, 16, true};
|
|
return true;
|
|
case VK_FORMAT_R16G16_SNORM:
|
|
out = {ReadbackSourceClass::Float, 2, 16, true};
|
|
return true;
|
|
case VK_FORMAT_R16G16B16A16_SNORM:
|
|
out = {ReadbackSourceClass::Float, 4, 16, true};
|
|
return true;
|
|
// --- SFLOAT ---
|
|
case VK_FORMAT_R16_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 1, 16};
|
|
out.special = format;
|
|
return true;
|
|
case VK_FORMAT_R16G16_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 2, 16};
|
|
out.special = format;
|
|
return true;
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 4, 16};
|
|
out.special = format;
|
|
return true;
|
|
case VK_FORMAT_R32_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 1, 32};
|
|
out.special = format;
|
|
return true;
|
|
case VK_FORMAT_R32G32_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 2, 32};
|
|
out.special = format;
|
|
return true;
|
|
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
|
out = {ReadbackSourceClass::Float, 4, 32};
|
|
out.special = format;
|
|
return true;
|
|
// --- UINT ---
|
|
case VK_FORMAT_R8_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 1, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 2, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8B8A8_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 4, 8};
|
|
return true;
|
|
case VK_FORMAT_R16_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 1, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 2, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16B16A16_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 4, 16};
|
|
return true;
|
|
case VK_FORMAT_R32_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 1, 32};
|
|
return true;
|
|
case VK_FORMAT_R32G32_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 2, 32};
|
|
return true;
|
|
case VK_FORMAT_R32G32B32A32_UINT:
|
|
out = {ReadbackSourceClass::UnsignedInt, 4, 32};
|
|
return true;
|
|
// --- SINT ---
|
|
case VK_FORMAT_R8_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 1, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 2, 8};
|
|
return true;
|
|
case VK_FORMAT_R8G8B8A8_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 4, 8};
|
|
return true;
|
|
case VK_FORMAT_R16_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 1, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 2, 16};
|
|
return true;
|
|
case VK_FORMAT_R16G16B16A16_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 4, 16};
|
|
return true;
|
|
case VK_FORMAT_R32_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 1, 32};
|
|
return true;
|
|
case VK_FORMAT_R32G32_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 2, 32};
|
|
return true;
|
|
case VK_FORMAT_R32G32B32A32_SINT:
|
|
out = {ReadbackSourceClass::SignedInt, 4, 32};
|
|
return true;
|
|
// --- packed / special ---
|
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
|
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
|
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
|
|
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
|
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
|
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
|
case VK_FORMAT_B5G6R5_UNORM_PACK16:
|
|
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
|
|
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
|
|
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
|
|
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
|
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
|
out.sourceClass =
|
|
(format == VK_FORMAT_A2B10G10R10_UINT_PACK32 || format == VK_FORMAT_A2R10G10B10_UINT_PACK32)
|
|
? ReadbackSourceClass::UnsignedInt
|
|
: ReadbackSourceClass::Float;
|
|
out.special = format;
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static Float SrgbToLinear(Float value) {
|
|
if (value <= 0.04045f) {
|
|
return value / 12.92f;
|
|
}
|
|
return std::pow((value + 0.055f) / 1.055f, 2.4f);
|
|
}
|
|
|
|
static Float DecodeUnsignedF11(Uint32 bits) {
|
|
const Uint32 exponent = (bits >> 6) & 0x1F;
|
|
const Uint32 mantissa = bits & 0x3F;
|
|
if (exponent == 0) {
|
|
return static_cast<Float>(mantissa) / 64.0f * std::pow(2.0f, -14.0f);
|
|
}
|
|
if (exponent == 31) {
|
|
return mantissa == 0 ? std::numeric_limits<Float>::infinity() : std::numeric_limits<Float>::quiet_NaN();
|
|
}
|
|
return (1.0f + static_cast<Float>(mantissa) / 64.0f) *
|
|
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
|
|
}
|
|
|
|
static Float DecodeUnsignedF10(Uint32 bits) {
|
|
const Uint32 exponent = (bits >> 5) & 0x1F;
|
|
const Uint32 mantissa = bits & 0x1F;
|
|
if (exponent == 0) {
|
|
return static_cast<Float>(mantissa) / 32.0f * std::pow(2.0f, -14.0f);
|
|
}
|
|
if (exponent == 31) {
|
|
return mantissa == 0 ? std::numeric_limits<Float>::infinity() : std::numeric_limits<Float>::quiet_NaN();
|
|
}
|
|
return (1.0f + static_cast<Float>(mantissa) / 32.0f) *
|
|
std::pow(2.0f, static_cast<Float>(static_cast<Int>(exponent)) - 15.0f);
|
|
}
|
|
|
|
static void DecodeReadbackTexelSpecialFloat(const Uint8* source, VkFormat format, Float* rgba) {
|
|
rgba[0] = 0.0f;
|
|
rgba[1] = 0.0f;
|
|
rgba[2] = 0.0f;
|
|
rgba[3] = 1.0f;
|
|
switch (format) {
|
|
case VK_FORMAT_R16_SFLOAT:
|
|
case VK_FORMAT_R16G16_SFLOAT:
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT: {
|
|
const Int channels = format == VK_FORMAT_R16_SFLOAT ? 1 : (format == VK_FORMAT_R16G16_SFLOAT ? 2 : 4);
|
|
for (Int c = 0; c < channels; ++c) {
|
|
Uint16 bits = 0;
|
|
Memcpy(&bits, source + static_cast<SizeT>(c) * sizeof(bits), sizeof(bits));
|
|
rgba[c] = MG_Util::DecodeHalfBitsToFloat(bits);
|
|
}
|
|
return;
|
|
}
|
|
case VK_FORMAT_R32_SFLOAT:
|
|
case VK_FORMAT_R32G32_SFLOAT:
|
|
case VK_FORMAT_R32G32B32A32_SFLOAT: {
|
|
const Int channels = format == VK_FORMAT_R32_SFLOAT ? 1 : (format == VK_FORMAT_R32G32_SFLOAT ? 2 : 4);
|
|
Memcpy(rgba, source, static_cast<SizeT>(channels) * sizeof(Float));
|
|
return;
|
|
}
|
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[0] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
|
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
|
rgba[2] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
|
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
|
return;
|
|
}
|
|
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[2] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
|
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
|
rgba[0] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
|
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
|
return;
|
|
}
|
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[0] = DecodeUnsignedF11(word & 0x7FFu);
|
|
rgba[1] = DecodeUnsignedF11((word >> 11) & 0x7FFu);
|
|
rgba[2] = DecodeUnsignedF10((word >> 22) & 0x3FFu);
|
|
return;
|
|
}
|
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
const Int exponent = static_cast<Int>((word >> 27) & 0x1Fu) - 15 - 9;
|
|
const Float scale = std::pow(2.0f, static_cast<Float>(exponent));
|
|
rgba[0] = static_cast<Float>(word & 0x1FFu) * scale;
|
|
rgba[1] = static_cast<Float>((word >> 9) & 0x1FFu) * scale;
|
|
rgba[2] = static_cast<Float>((word >> 18) & 0x1FFu) * scale;
|
|
return;
|
|
}
|
|
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
|
case VK_FORMAT_B5G6R5_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
const Float c0 = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
|
|
const Float c1 = static_cast<Float>((word >> 5) & 0x3Fu) / 63.0f;
|
|
const Float c2 = static_cast<Float>(word & 0x1Fu) / 31.0f;
|
|
const Bool bgr = format == VK_FORMAT_B5G6R5_UNORM_PACK16;
|
|
rgba[0] = bgr ? c2 : c0;
|
|
rgba[1] = c1;
|
|
rgba[2] = bgr ? c0 : c2;
|
|
return;
|
|
}
|
|
case VK_FORMAT_A1R5G5B5_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[3] = static_cast<Float>((word >> 15) & 0x1u);
|
|
rgba[0] = static_cast<Float>((word >> 10) & 0x1Fu) / 31.0f;
|
|
rgba[1] = static_cast<Float>((word >> 5) & 0x1Fu) / 31.0f;
|
|
rgba[2] = static_cast<Float>(word & 0x1Fu) / 31.0f;
|
|
return;
|
|
}
|
|
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[0] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
|
|
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
|
|
rgba[2] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
|
|
rgba[3] = static_cast<Float>(word & 0x1u);
|
|
return;
|
|
}
|
|
case VK_FORMAT_B5G5R5A1_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[2] = static_cast<Float>((word >> 11) & 0x1Fu) / 31.0f;
|
|
rgba[1] = static_cast<Float>((word >> 6) & 0x1Fu) / 31.0f;
|
|
rgba[0] = static_cast<Float>((word >> 1) & 0x1Fu) / 31.0f;
|
|
rgba[3] = static_cast<Float>(word & 0x1u);
|
|
return;
|
|
}
|
|
case VK_FORMAT_R4G4B4A4_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[0] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
|
|
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
|
|
rgba[2] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
|
|
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
|
|
return;
|
|
}
|
|
case VK_FORMAT_B4G4R4A4_UNORM_PACK16: {
|
|
Uint16 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[2] = static_cast<Float>((word >> 12) & 0xFu) / 15.0f;
|
|
rgba[1] = static_cast<Float>((word >> 8) & 0xFu) / 15.0f;
|
|
rgba[0] = static_cast<Float>((word >> 4) & 0xFu) / 15.0f;
|
|
rgba[3] = static_cast<Float>(word & 0xFu) / 15.0f;
|
|
return;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
static Bool DecodeReadbackRowsToWide(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width, GLsizei height,
|
|
Vector<Uint8>& outWide, GLenum& outWideType) {
|
|
ReadbackSourceDesc desc{};
|
|
if (!GetReadbackSourceDesc(srcFormat, desc)) {
|
|
return false;
|
|
}
|
|
const SizeT texelSize = VulkanRenderer::GetReadbackTexelSize(srcFormat);
|
|
if (texelSize == 0) {
|
|
return false;
|
|
}
|
|
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
|
outWide.assign(pixelCount * 4 * sizeof(Uint32), 0);
|
|
|
|
if (desc.sourceClass == ReadbackSourceClass::Float) {
|
|
outWideType = GL_FLOAT;
|
|
Float* wide = reinterpret_cast<Float*>(outWide.data());
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
const Uint8* source = srcPixels + i * texelSize;
|
|
Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
if (desc.special != VK_FORMAT_UNDEFINED) {
|
|
DecodeReadbackTexelSpecialFloat(source, desc.special, rgba);
|
|
} else {
|
|
for (Int c = 0; c < desc.channels; ++c) {
|
|
Float value = 0.0f;
|
|
if (desc.componentBits == 8) {
|
|
if (desc.isSnorm) {
|
|
Int8 raw = 0;
|
|
Memcpy(&raw, source + c, sizeof(raw));
|
|
value = std::max(static_cast<Float>(raw) / 127.0f, -1.0f);
|
|
} else {
|
|
value = static_cast<Float>(source[c]) / 255.0f;
|
|
}
|
|
} else { // 16
|
|
if (desc.isSnorm) {
|
|
Int16 raw = 0;
|
|
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
|
|
value = std::max(static_cast<Float>(raw) / 32767.0f, -1.0f);
|
|
} else {
|
|
Uint16 raw = 0;
|
|
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
|
|
value = static_cast<Float>(raw) / 65535.0f;
|
|
}
|
|
}
|
|
if (desc.isSrgb && c < 3) {
|
|
value = SrgbToLinear(value);
|
|
}
|
|
rgba[c] = value;
|
|
}
|
|
if (desc.bgraSwizzle) {
|
|
std::swap(rgba[0], rgba[2]);
|
|
}
|
|
}
|
|
Memcpy(wide + i * 4, rgba, sizeof(rgba));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Integer classes: decode to 4 x (U)Int32 per texel; missing alpha reads 1.
|
|
outWideType = desc.sourceClass == ReadbackSourceClass::SignedInt ? GL_INT : GL_UNSIGNED_INT;
|
|
Uint32* wide = reinterpret_cast<Uint32*>(outWide.data());
|
|
for (SizeT i = 0; i < pixelCount; ++i) {
|
|
const Uint8* source = srcPixels + i * texelSize;
|
|
Uint32 rgba[4] = {0, 0, 0, 1};
|
|
if (srcFormat == VK_FORMAT_A2B10G10R10_UINT_PACK32) {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[0] = word & 0x3FFu;
|
|
rgba[1] = (word >> 10) & 0x3FFu;
|
|
rgba[2] = (word >> 20) & 0x3FFu;
|
|
rgba[3] = (word >> 30) & 0x3u;
|
|
} else if (srcFormat == VK_FORMAT_A2R10G10B10_UINT_PACK32) {
|
|
Uint32 word = 0;
|
|
Memcpy(&word, source, sizeof(word));
|
|
rgba[2] = word & 0x3FFu;
|
|
rgba[1] = (word >> 10) & 0x3FFu;
|
|
rgba[0] = (word >> 20) & 0x3FFu;
|
|
rgba[3] = (word >> 30) & 0x3u;
|
|
} else {
|
|
for (Int c = 0; c < desc.channels; ++c) {
|
|
if (desc.componentBits == 8) {
|
|
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
|
|
Int8 raw = 0;
|
|
Memcpy(&raw, source + c, sizeof(raw));
|
|
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
|
|
} else {
|
|
rgba[c] = source[c];
|
|
}
|
|
} else if (desc.componentBits == 16) {
|
|
if (desc.sourceClass == ReadbackSourceClass::SignedInt) {
|
|
Int16 raw = 0;
|
|
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
|
|
rgba[c] = static_cast<Uint32>(static_cast<Int32>(raw));
|
|
} else {
|
|
Uint16 raw = 0;
|
|
Memcpy(&raw, source + static_cast<SizeT>(c) * 2, sizeof(raw));
|
|
rgba[c] = raw;
|
|
}
|
|
} else {
|
|
Memcpy(&rgba[c], source + static_cast<SizeT>(c) * 4, sizeof(Uint32));
|
|
}
|
|
}
|
|
}
|
|
Memcpy(wide + i * 4, rgba, sizeof(rgba));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// True floating-point color formats are exempt from GL_FIXED_ONLY read clamping.
|
|
static Bool IsFloatingPointReadbackFormat(VkFormat format) {
|
|
switch (format) {
|
|
case VK_FORMAT_R16_SFLOAT:
|
|
case VK_FORMAT_R16G16_SFLOAT:
|
|
case VK_FORMAT_R16G16B16_SFLOAT:
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
|
case VK_FORMAT_R32_SFLOAT:
|
|
case VK_FORMAT_R32G32_SFLOAT:
|
|
case VK_FORMAT_R32G32B32_SFLOAT:
|
|
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The GL internal format a packed VkFormat stores, for the raw-word readback test below.
|
|
// Only the packed 32-bit layouts MobileGL keeps natively need an entry; anything else takes
|
|
// the wide decode path.
|
|
static TextureInternalFormat GetPackedReadbackInternalFormat(VkFormat format) {
|
|
switch (format) {
|
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
|
return TextureInternalFormat::RGB9E5;
|
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
|
return TextureInternalFormat::R11FG11FB10F;
|
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
|
return TextureInternalFormat::RGB10A2;
|
|
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
|
return TextureInternalFormat::RGB10A2UI;
|
|
default:
|
|
return TextureInternalFormat::Unknown;
|
|
}
|
|
}
|
|
|
|
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
|
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
|
|
void* pixels, Bool applyPackImageParams,
|
|
Bool applyReadColorClamp = false) {
|
|
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
|
|
return true;
|
|
}
|
|
|
|
ClientReadback::ReadbackChannelMapping mapping{};
|
|
if (!ClientReadback::GetReadbackChannelMapping(format, mapping) ||
|
|
ClientReadback::GetReadbackDstPixelSize(mapping, type) == 0) {
|
|
MGLOG_E_ONCE("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type);
|
|
return false;
|
|
}
|
|
|
|
// A packed image read with the matching client type hands back its own words: the
|
|
// decode-to-float / re-encode round trip is lossy in the bits (it canonicalizes an
|
|
// RGB9_E5 shared exponent), which glGetTexImage must not do. Left to the wide path when
|
|
// GL_CLAMP_READ_COLOR may still have to act, i.e. for glReadPixels.
|
|
if (!applyReadColorClamp &&
|
|
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
|
GetPackedReadbackInternalFormat(srcFormat), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
|
|
return ClientReadback::StorePackedWordsToClient(srcPixels, width, sliceHeight, sliceCount, type, pixels,
|
|
applyPackImageParams);
|
|
}
|
|
|
|
Vector<Uint8> wide;
|
|
GLenum wideType = GL_FLOAT;
|
|
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, sliceHeight * sliceCount, wide, wideType)) {
|
|
MGLOG_E_ONCE("DirectVulkan readback skipped: unsupported source format=%d",
|
|
static_cast<Int>(srcFormat));
|
|
return false;
|
|
}
|
|
|
|
// glReadPixels final conversion: GL_CLAMP_READ_COLOR defaults to GL_FIXED_ONLY,
|
|
// clamping fixed-point (normalized) buffers to [0,1] - visible for SNORM reads.
|
|
if (applyReadColorClamp && wideType == GL_FLOAT) {
|
|
const GLenum clampMode = MG_Pipe::gPipeInputs->GetClampReadColor();
|
|
const Bool clamp =
|
|
clampMode == GL_TRUE || (clampMode == GL_FIXED_ONLY && !IsFloatingPointReadbackFormat(srcFormat));
|
|
if (clamp) {
|
|
Float* values = reinterpret_cast<Float*>(wide.data());
|
|
const SizeT count = wide.size() / sizeof(Float);
|
|
for (SizeT i = 0; i < count; ++i) {
|
|
values[i] = std::min(std::max(values[i], 0.0f), 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
const Bool sourceIsInteger = wideType == GL_INT || wideType == GL_UNSIGNED_INT;
|
|
if (sourceIsInteger != mapping.isInteger) {
|
|
MGLOG_E_ONCE("DirectVulkan readback skipped: integerness mismatch (format=0x%x source=%d)", format,
|
|
static_cast<Int>(srcFormat));
|
|
return false;
|
|
}
|
|
|
|
return ClientReadback::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping,
|
|
type, pixels, applyPackImageParams);
|
|
}
|
|
} // namespace
|
|
|
|
// 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 color 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;
|
|
}
|
|
|
|
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) {
|
|
// Every use of the acquired image is reached through here, ahead of its commands:
|
|
// the work recorded before it goes out without the acquire wait.
|
|
if (out.swapchainImageIndex == m_imageIndexAcquired) SubmitAheadOfAcquiredImage();
|
|
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.cpp) 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.resource = resource;
|
|
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, Bool coalesceWithLastFullBarrier) {
|
|
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);
|
|
// A swapchain image leaves PRESENT_SRC_KHR (or UNDEFINED, new) only after it was acquired:
|
|
// the transition is ordered after the presentation engine's release through the stages
|
|
// the acquire wait blocks, which an empty (TOP_OF_PIPE) source scope would not chain to.
|
|
if (image.isDefault && image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
|
(image.layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR || image.layout == VK_IMAGE_LAYOUT_UNDEFINED))
|
|
stage |= FrameContext::kAcquireWaitStages;
|
|
// 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;
|
|
// P15: A DRAW PASS'S ATTACHMENTS SHARE ONE FULL BARRIER, and the sampled-image
|
|
// barrier shares it too. The memory barrier below is global (ALL_COMMANDS, every write
|
|
// made visible), the same one ResolveWireImageDescriptor records, so a second one with no
|
|
// image written since the last (WireImageWriteEpoch unchanged) orders nothing. Only the
|
|
// new-pass caller (PrepareWireDrawPass) opts in: its attachments are written inside the
|
|
// pass it is about to begin, and that pass's end moves the epoch. A transfer caller
|
|
// writes right after this call with the epoch already moved, so it must neither skip nor
|
|
// note - noting there would let a later barrier be skipped past its write.
|
|
// FCL Magma: 27.5 barriers and 12.6 render passes per frame against dev's 3.4 and 4.1,
|
|
// most of them a second barrier per attachment and a pass closed and reopened at the
|
|
// first sampled texture after every pass begin.
|
|
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.
|
|
const Bool covered = coalesceWithLastFullBarrier && m_uniformManager &&
|
|
WireImageWriteEpoch() == m_uniformManager->WireImageBarrierEpoch();
|
|
if (!covered) {
|
|
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 (coalesceWithLastFullBarrier && m_uniformManager) m_uniformManager->NoteWireImageBarrier();
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ---- P15: WIRE DEFERRED CLEARS -------------------------------------------------------------
|
|
//
|
|
// Minecraft (1.21.5) clears its targets through framebuffers of their own - a depth-only one
|
|
// for the shared depth texture, a color+depth one per target - and then draws through the
|
|
// main framebuffer. A clear that begins a LOAD_OP_CLEAR pass of its own made every such clear a
|
|
// pass that did nothing but clear, closed by the very next draw: 11.5 passes a frame against
|
|
// dev's 4.1 (whose clear manager folded the same clears into the next pass's load op), each
|
|
// one an extra full load/store of the attachments on a tiler.
|
|
//
|
|
// So a clear of an image the open pass holds is recorded inside that pass, and a whole-image
|
|
// clear of any other image WAITS ON THE IMAGE (WirePendingClear, kept on the image's own
|
|
// resource with its level and layers - never keyed by a GL name). The next draw pass that has
|
|
// it as an attachment begins with LOAD_OP_CLEAR for it (PrepareWireDrawPass /
|
|
// BeginWireDrawPassFolding); any other use reaches the image through
|
|
// SyncTextureResourceByHandle, which runs it first (MaterializeWirePendingClears). The default
|
|
// framebuffer never defers: its in-pass clear is a known Adreno problem (see
|
|
// ClearWireFramebufferInPass), and it is presented, not sampled.
|
|
|
|
// Whether `image` can carry a deferred clear: a plain single-sample 2D(-array) image of this
|
|
// session with a float color or a depth/stencil format, clearable by a transfer command.
|
|
Bool VulkanRenderer::WireImageCanDeferClear(const WireImage& image) {
|
|
const auto* resource = image.resource;
|
|
if (resource == nullptr || image.isDefault || image.is3D || image.samples != VK_SAMPLE_COUNT_1_BIT)
|
|
return false;
|
|
if (resource->sharedImageId != 0 || resource->yuvImageId != 0) return false;
|
|
if ((resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0) return false;
|
|
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
|
|
(vkuFormatIsSRGB(image.format) || vkuFormatIsSINT(image.format) || vkuFormatIsUINT(image.format) ||
|
|
vkuFormatIsCompressed(image.format)))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void VulkanRenderer::MaterializeWirePendingClears(VkTextureManager::TextureResource& resource) {
|
|
if (resource.pendingClears.empty()) return;
|
|
if (resource.image == VK_NULL_HANDLE) {
|
|
resource.pendingClears.clear();
|
|
return;
|
|
}
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
|
|
EndActiveRenderPassOn(frame.commandBuffer);
|
|
// After every earlier access to the image (the clears were issued after them).
|
|
if (resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
|
if (!VkTextureManager::TransitionImageLayout(
|
|
frame.commandBuffer, resource.image, resource.layout, VK_IMAGE_LAYOUT_GENERAL,
|
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
resource.layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_WRITE_BIT,
|
|
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource.aspect, 0, resource.mipLevels))
|
|
MagmaWireFatal("pending-clear-transition");
|
|
} else {
|
|
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);
|
|
}
|
|
for (const auto& pending : resource.pendingClears) {
|
|
const VkImageSubresourceRange range{pending.aspects, pending.level, 1, pending.layer, pending.layers};
|
|
if (pending.aspects & VK_IMAGE_ASPECT_COLOR_BIT)
|
|
vkCmdClearColorImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL, &pending.value.color,
|
|
1, &range);
|
|
else
|
|
vkCmdClearDepthStencilImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
|
|
&pending.value.depthStencil, 1, &range);
|
|
}
|
|
resource.pendingClears.clear();
|
|
// Recorded writes: the next sampled-image or attachment barrier must cover them.
|
|
BumpWireImageWriteEpoch();
|
|
}
|
|
|
|
// Records `aspects` of `image` cleared to `value` as pending, replacing whatever an earlier
|
|
// pending clear of the same subresource said for those aspects.
|
|
void VulkanRenderer::NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects,
|
|
const VkClearValue& value) {
|
|
auto& pending = image.resource->pendingClears;
|
|
for (SizeT i = 0; i < pending.size();) {
|
|
auto& entry = pending[i];
|
|
if (entry.level == image.level && entry.layer == image.layer && entry.layers == image.layers) {
|
|
entry.aspects &= ~aspects;
|
|
if (entry.aspects == 0) {
|
|
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
|
|
continue;
|
|
}
|
|
}
|
|
++i;
|
|
}
|
|
VkTextureManager::WirePendingClear entry{};
|
|
entry.level = image.level;
|
|
entry.layer = image.layer;
|
|
entry.layers = image.layers;
|
|
entry.aspects = aspects;
|
|
entry.value = value;
|
|
pending.push_back(entry);
|
|
}
|
|
|
|
// The open wire draw pass's attachment that IS `image` (same image, level and layers), or
|
|
// VK_ATTACHMENT_UNUSED.
|
|
Uint32 VulkanRenderer::OpenPassAttachmentOf(const WireImage& image,
|
|
const Vector<WireDrawAttachmentKey>& attachments) {
|
|
for (Uint32 a = 0; a < attachments.size(); ++a) {
|
|
const auto& key = attachments[a];
|
|
if (key.image == image.image && key.level == image.level && key.layer == image.layer &&
|
|
key.layers == image.layers)
|
|
return a;
|
|
}
|
|
return VK_ATTACHMENT_UNUSED;
|
|
}
|
|
|
|
// A clear whose images the OPEN draw pass holds - through another framebuffer than the
|
|
// pass's own (the depth-only clear framebuffer) - is recorded inside that pass. Returns the
|
|
// buffers it did not take.
|
|
GLbitfield VulkanRenderer::ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
|
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
if (!remaining || fbo.IsDefault || m_wirePreparationDepth != 0 || !m_wireDrawPass) return remaining;
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
|
if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash)
|
|
return remaining;
|
|
const auto& key = m_wireDrawPassKey;
|
|
const RenderPassEntry* pass = m_wireDrawPass.get();
|
|
const VkCommandBuffer passBuffer = active->commandBuffer;
|
|
const Uint64 passHash = active->hash;
|
|
VkRect2D area{{0, 0}, {key.width, key.height}};
|
|
if (GLScissorTestClips()) {
|
|
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>(key.width));
|
|
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(key.height));
|
|
if (x0 >= x1 || y0 >= y1) return remaining; // the standalone path answers an empty box
|
|
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
|
|
}
|
|
// The image must be exactly the pass's extent, or the pass's render area is not its area.
|
|
const auto fits = [&](const WireImage& image) {
|
|
return image.extent.width == key.width && image.extent.height == key.height;
|
|
};
|
|
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
|
|
Uint32 count = 0;
|
|
GLbitfield handled = 0;
|
|
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
|
if (remaining & GL_COLOR_BUFFER_BIT) {
|
|
Bool all = true;
|
|
Uint32 colorCount = 0;
|
|
VkClearAttachment colorClears[MG_Pipe::kMGPipeMaxColorAttachments]{};
|
|
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
|
|
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
|
|
const Int32 slot = fbo.DrawBuffers[i];
|
|
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
|
if (fbo.Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
|
|
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
|
|
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
|
|
if (!mask.r() || !mask.g() || !mask.b() || !mask.a() ||
|
|
payload.colorEncoding != ClearColorEncoding::Float) {
|
|
all = false;
|
|
break;
|
|
}
|
|
const WireImage image = ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT, true);
|
|
const Uint32 a = image.image ? OpenPassAttachmentOf(image, key.attachments) : VK_ATTACHMENT_UNUSED;
|
|
Uint32 reference = VK_ATTACHMENT_UNUSED;
|
|
for (Uint32 c = 0; a != VK_ATTACHMENT_UNUSED && c < key.colorReferences.size(); ++c)
|
|
if (key.colorReferences[c] == a) reference = c;
|
|
if (reference == VK_ATTACHMENT_UNUSED || !fits(image) || vkuFormatIsSRGB(image.format) ||
|
|
vkuFormatIsSRGB(key.attachments[a].format) || vkuFormatIsSINT(image.format) ||
|
|
vkuFormatIsUINT(image.format)) {
|
|
all = false;
|
|
break;
|
|
}
|
|
auto& clear = colorClears[colorCount++];
|
|
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
clear.colorAttachment = reference;
|
|
clear.clearValue.color = MakeVkClearColorValue(payload, false);
|
|
}
|
|
if (all) {
|
|
for (Uint32 c = 0; c < colorCount; ++c)
|
|
clears[count++] = colorClears[c];
|
|
handled |= GL_COLOR_BUFFER_BIT;
|
|
}
|
|
}
|
|
VkImageAspectFlags depthStencilAspects = 0;
|
|
const auto depthStencilHeld = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) {
|
|
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
|
return image.image && fits(image) && key.depthReference != VK_ATTACHMENT_UNUSED &&
|
|
OpenPassAttachmentOf(image, key.attachments) == key.depthReference &&
|
|
(key.attachments[key.depthReference].aspect & aspect) != 0;
|
|
};
|
|
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
|
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
|
handled |= GL_DEPTH_BUFFER_BIT;
|
|
} else if (depthStencilHeld(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT)) {
|
|
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
handled |= GL_DEPTH_BUFFER_BIT;
|
|
}
|
|
}
|
|
if (remaining & GL_STENCIL_BUFFER_BIT) {
|
|
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
|
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
|
|
handled |= GL_STENCIL_BUFFER_BIT;
|
|
} else if (mask == 255u && depthStencilHeld(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
|
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
|
|
handled |= GL_STENCIL_BUFFER_BIT;
|
|
}
|
|
}
|
|
if (depthStencilAspects) {
|
|
auto& clear = clears[count++];
|
|
clear.aspectMask = depthStencilAspects;
|
|
clear.clearValue.depthStencil = {payload.depth, payload.stencil};
|
|
}
|
|
// A resolve above may have submitted (a lazy upload) or run another image's deferred clear,
|
|
// closing the pass: then nothing is recorded and nothing is taken.
|
|
active = VkRenderPassManager::GetActiveRenderPass();
|
|
if (active == nullptr || active->commandBuffer != passBuffer || active->hash != passHash ||
|
|
m_wireDrawPass.get() != pass)
|
|
return remaining;
|
|
if (count > 0) {
|
|
const VkClearRect rect{area, 0, key.layers};
|
|
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
|
|
}
|
|
return remaining & ~handled;
|
|
}
|
|
|
|
// A whole-image clear of images the open pass does not hold waits on the images (see the
|
|
// section comment). Returns the buffers it did not take.
|
|
GLbitfield VulkanRenderer::DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& payload, GLint drawbuffer) {
|
|
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
if (!remaining || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining;
|
|
const Bool scissor = GLScissorTestClips();
|
|
const auto wholeImage = [&](const WireImage& image) {
|
|
if (!scissor) return true;
|
|
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
|
|
return box.x() <= 0 && box.y() <= 0 && box.x() + box.z() >= static_cast<Int32>(image.extent.width) &&
|
|
box.y() + box.w() >= static_cast<Int32>(image.extent.height);
|
|
};
|
|
const Bool passOpen = VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass;
|
|
const auto heldByOpenPass = [&](const WireImage& image) {
|
|
return passOpen && OpenPassAttachmentOf(image, m_wireDrawPassKey.attachments) != VK_ATTACHMENT_UNUSED;
|
|
};
|
|
struct Target {
|
|
WireImage image;
|
|
VkImageAspectFlags aspects;
|
|
VkClearValue value;
|
|
Bool texture;
|
|
};
|
|
Target targets[MG_Pipe::kMGPipeMaxColorAttachments + 2]{};
|
|
Uint32 targetCount = 0;
|
|
GLbitfield handled = 0;
|
|
VkTextureManager::PendingClearHold hold(*m_textureManager);
|
|
if ((remaining & GL_COLOR_BUFFER_BIT) && payload.colorEncoding == ClearColorEncoding::Float) {
|
|
Bool all = true;
|
|
const Uint32 first = targetCount;
|
|
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
|
|
const Int32 slot = fbo.DrawBuffers[i];
|
|
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
|
|
const auto& surface = fbo.Color[slot];
|
|
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
|
|
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
|
|
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
|
|
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
|
|
all = false;
|
|
break;
|
|
}
|
|
const WireImage image = ResolveWireImage(fbo, surface, VK_IMAGE_ASPECT_COLOR_BIT, true);
|
|
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) {
|
|
all = false;
|
|
break;
|
|
}
|
|
auto& target = targets[targetCount++];
|
|
target.image = image;
|
|
target.aspects = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
target.value.color = MakeVkClearColorValue(payload, false);
|
|
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
|
}
|
|
if (all)
|
|
handled |= GL_COLOR_BUFFER_BIT;
|
|
else
|
|
targetCount = first;
|
|
}
|
|
const auto depthStencil = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Bool {
|
|
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
|
|
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image))
|
|
return false;
|
|
for (Uint32 t = 0; t < targetCount; ++t) {
|
|
auto& target = targets[t];
|
|
if (target.image.image == image.image && target.image.level == image.level &&
|
|
target.image.layer == image.layer && target.image.layers == image.layers &&
|
|
!(target.aspects & VK_IMAGE_ASPECT_COLOR_BIT)) {
|
|
target.aspects |= aspect; // a combined depth/stencil image: one entry, both aspects
|
|
return true;
|
|
}
|
|
}
|
|
auto& target = targets[targetCount++];
|
|
target.image = image;
|
|
target.aspects = aspect;
|
|
target.value.depthStencil = {payload.depth, payload.stencil};
|
|
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
|
|
return true;
|
|
};
|
|
if (remaining & GL_DEPTH_BUFFER_BIT) {
|
|
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
|
handled |= GL_DEPTH_BUFFER_BIT;
|
|
else if (depthStencil(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT))
|
|
handled |= GL_DEPTH_BUFFER_BIT;
|
|
}
|
|
if (remaining & GL_STENCIL_BUFFER_BIT) {
|
|
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
|
|
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
|
|
handled |= GL_STENCIL_BUFFER_BIT;
|
|
else if (mask == 255u && depthStencil(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT))
|
|
handled |= GL_STENCIL_BUFFER_BIT;
|
|
}
|
|
m_textureManager->FlushPendingUploads();
|
|
for (Uint32 t = 0; t < targetCount; ++t) {
|
|
const auto& target = targets[t];
|
|
NoteWirePendingClear(target.image, target.aspects, target.value);
|
|
// A GPU write as far as the CPU shadow is concerned, as a recorded clear is.
|
|
if (target.texture)
|
|
m_textureManager->MarkWireTextureGpuWritten(target.image.storage, target.image.level,
|
|
target.image.layer, target.image.layers);
|
|
}
|
|
return remaining & ~handled;
|
|
}
|
|
|
|
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
|
|
const ClearAttachmentPayload& clearRequest, GLint drawbuffer) {
|
|
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
|
|
// A clear of the window: the frame's work so far goes out ahead of the acquire wait, before
|
|
// any of the clear's paths (in-pass, deferred, standalone) records or resolves anything.
|
|
if (fbo.IsDefault) SubmitAheadOfAcquiredImage();
|
|
// P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass,
|
|
// WireDraw.cpp); only the buffers it hands back take the standalone pass below.
|
|
// P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds
|
|
// its images (another framebuffer's), deferred on the images when it covers them whole,
|
|
// and only then a pass of its own (the in-pass clear again, which begins one) or the
|
|
// standalone path.
|
|
ClearAttachmentPayload payload = clearRequest;
|
|
if (m_wirePreparationDepth == 0 && WirePassMemoHolds(fbo, m_frameContext.GetCurrent()))
|
|
payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
|
if (payload.mask) payload.mask = ClearWireAttachmentsOfOpenPass(fbo, payload, drawbuffer);
|
|
if (payload.mask) payload.mask = DeferWireClear(fbo, payload, drawbuffer);
|
|
if (payload.mask) payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
|
|
if (!payload.mask) return;
|
|
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 (GLScissorTestClips()) {
|
|
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 color 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
|
|
// color 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
|
|
// the phase notes 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 COLOR correctly (the color 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 - a conforming check 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 color
|
|
// lambda runs first: asked only here, a scaled COLOR|DEPTH blit scaled and wrote its
|
|
// color 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 color 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.cpp) that lands on the render pass is the resolve most likely
|
|
// to read back as zeroes with no GL error. A recorded debt (§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 (GLScissorTestClips()) {
|
|
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
|
|
// color 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 color
|
|
// 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 color 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 behavior, 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
|
|
// color 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 color 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.cpp 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.cpp'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 color arm's shape (the two shader shapes are explained at the color 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 color-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.cpp'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
|
|
// color 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_Record::ServerStagedTexture();
|
|
const Uint64 key = MG_Record::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_Record::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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Server-owned texture readback. Image layout tracking and frame retirement are the same as
|
|
// every other wire verb's.
|
|
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
|
|
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
|
|
Vector<Uint8>& result) {
|
|
using namespace ClientReadback;
|
|
ReadbackChannelMapping mapping{};
|
|
if (!GetReadbackChannelMapping(info.Format, mapping)) return false;
|
|
const SizeT sourcePixelSize = VulkanRenderer::GetReadbackTexelSize(format);
|
|
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, info.Type);
|
|
const Uint64 rows64 = Uint64(info.Box.H) * info.Box.D;
|
|
if (!info.Box.W || !rows64 || rows64 > std::numeric_limits<GLsizei>::max() ||
|
|
rows64 > std::numeric_limits<SizeT>::max() / 16 / info.Box.W)
|
|
return false;
|
|
const SizeT pixels = static_cast<SizeT>(info.Box.W) * static_cast<SizeT>(rows64);
|
|
if (!sourcePixelSize || !pixelBytes || sourceBytes != pixels * sourcePixelSize ||
|
|
info.DstSize != pixels * pixelBytes)
|
|
return false;
|
|
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
|
GetPackedReadbackInternalFormat(format), MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
|
if (pixelBytes != sourcePixelSize) return false;
|
|
result.assign(source, source + sourceBytes);
|
|
return true;
|
|
}
|
|
Vector<Uint8> wide;
|
|
GLenum wideType = GL_NONE;
|
|
const GLsizei rows = static_cast<GLsizei>(Uint64(info.Box.H) * info.Box.D);
|
|
if (!DecodeReadbackRowsToWide(source, format, info.Box.W, rows, wide, wideType) ||
|
|
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT))
|
|
return false;
|
|
if (ResolveTextureFormatInfo(logicalFormat).expandRgbToRgba) {
|
|
const Float floatOne = 1.0f;
|
|
const Uint32 integerOne = 1;
|
|
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
|
for (SizeT i = 0; i < pixels; ++i)
|
|
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
|
}
|
|
result.resize(info.DstSize);
|
|
for (GLsizei row = 0; row < rows; ++row)
|
|
ConvertWideReadbackRow(wide.data() + row * static_cast<SizeT>(info.Box.W) * 16,
|
|
result.data() + row * static_cast<SizeT>(info.Box.W) * pixelBytes, info.Box.W,
|
|
wideType, mapping, info.Type);
|
|
return true;
|
|
}
|
|
|
|
Bool VulkanRenderer::ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& ownedBytes) {
|
|
ownedBytes.clear();
|
|
const auto& box = info.Box;
|
|
if (box.X || box.Y || box.Z || !box.W || !box.H || !box.D ||
|
|
box.W > static_cast<Uint32>(std::numeric_limits<GLsizei>::max()) ||
|
|
Uint64(box.H) * box.D > static_cast<Uint64>(std::numeric_limits<GLsizei>::max()))
|
|
return false;
|
|
const auto& state = MG_Pipe::MGPipeApplier();
|
|
if (MG_Pipe::MGPipeHandleIsNull(info.Res) || info.Res.Slot >= state.TextureResources.size()) return false;
|
|
const auto& record = state.TextureResources[info.Res.Slot];
|
|
if (!record.Live || record.Gen != info.Res.Gen) return false;
|
|
|
|
using Target = MG_Pipe::MGPipeResourceTarget;
|
|
const auto target = static_cast<Target>(record.Desc.Target);
|
|
const Bool oneDArray = target == Target::Tex1DArray;
|
|
const Bool volume = target == Target::Tex3D;
|
|
Uint32 level = info.Level, layer = 0, layerCount = 1, availableLayers = 1;
|
|
VkExtent3D extent{box.W, box.H, 1};
|
|
if (oneDArray) {
|
|
if (info.Target != GL_TEXTURE_1D_ARRAY || box.D != 1) return false;
|
|
layerCount = box.H;
|
|
extent.height = 1;
|
|
} else if (volume) {
|
|
if (info.Target != GL_TEXTURE_3D) return false;
|
|
extent.depth = box.D;
|
|
} else if (target == Target::Tex2DArray || target == Target::TexCubeArray) {
|
|
if (info.Target != (target == Target::Tex2DArray ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_CUBE_MAP_ARRAY))
|
|
return false;
|
|
layerCount = box.D;
|
|
} else if (target == Target::TexCube) {
|
|
if (info.Target == GL_TEXTURE_CUBE_MAP) {
|
|
if (box.D != 6) return false;
|
|
layerCount = 6;
|
|
} else if (info.Target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && info.Target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) {
|
|
if (box.D != 1) return false;
|
|
layer = info.Target - GL_TEXTURE_CUBE_MAP_POSITIVE_X;
|
|
} else
|
|
return false;
|
|
} else {
|
|
if (box.D != 1) return false;
|
|
const GLenum expected = target == Target::Tex1D ? GL_TEXTURE_1D
|
|
: target == Target::Tex2D ? GL_TEXTURE_2D
|
|
: target == Target::TexRect ? GL_TEXTURE_RECTANGLE
|
|
: GL_NONE;
|
|
if (!expected || info.Target != expected || (target == Target::Tex1D && box.H != 1)) return false;
|
|
}
|
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
|
const auto storage =
|
|
m_textureManager->ResolveWireTextureStorage(info.Res, level, layer, &viewFormat, &availableLayers);
|
|
if (MG_Pipe::MGPipeHandleIsNull(storage)) return false;
|
|
const Uint64 pixels64 = Uint64(box.W) * box.H * box.D;
|
|
if (pixels64 > std::numeric_limits<SizeT>::max() / 16) return false;
|
|
const auto readUnbacked = [&]() {
|
|
// Only a level the VkImage cannot represent may use its owned,
|
|
// non-GPU-dirty staging bytes. A failed GPU read never falls back.
|
|
if (storage != info.Res || info.Target == GL_TEXTURE_CUBE_MAP) return false;
|
|
Vector<Uint8> raw;
|
|
VkFormat nativeFormat = VK_FORMAT_UNDEFINED;
|
|
if (!m_textureManager->ReadUnbackedWireLevel(
|
|
storage, MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
|
|
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)}, nativeFormat,
|
|
raw))
|
|
return false;
|
|
// A depth-only level's shadow holds the format's own depth words, which the
|
|
// depth packer reads exactly as it reads them off a Vulkan copy.
|
|
if (nativeFormat == VK_FORMAT_D16_UNORM || nativeFormat == VK_FORMAT_D32_SFLOAT ||
|
|
nativeFormat == VK_FORMAT_X8_D24_UNORM_PACK32) {
|
|
if (info.Format != GL_DEPTH_COMPONENT) return false;
|
|
const SizeT texels = static_cast<SizeT>(Uint64(box.W) * box.H * box.D);
|
|
const SizeT wordBytes = nativeFormat == VK_FORMAT_D16_UNORM ? 2 : 4;
|
|
if (raw.size() < texels * wordBytes) return false;
|
|
Vector<Uint8> packed;
|
|
if (!PackDepthStencilTight(raw.data(), nullptr, nativeFormat, texels, info.Format, info.Type, packed) ||
|
|
packed.size() != info.DstSize)
|
|
return false;
|
|
ownedBytes = Move(packed);
|
|
return true;
|
|
}
|
|
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
|
|
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
|
|
};
|
|
RetireWireDrawPass();
|
|
MakeXfbWritesVisible();
|
|
auto* resource = m_textureManager->SyncTextureResourceByHandle(storage);
|
|
if (!resource || !resource->image || level >= resource->mipLevels) return readUnbacked();
|
|
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) return false;
|
|
m_textureManager->FlushPendingUploads();
|
|
const Bool nativeVolume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
|
|
const Uint32 nativeDepth = nativeVolume ? std::max(resource->depth >> level, 1u) : resource->arrayLayers;
|
|
if (extent.width != std::max(resource->extent.width >> level, 1u) ||
|
|
extent.height != std::max(resource->extent.height >> level, 1u) || layer >= nativeDepth ||
|
|
(nativeVolume ? extent.depth : layerCount) > nativeDepth - layer ||
|
|
(!nativeVolume && layerCount > availableLayers))
|
|
return readUnbacked();
|
|
// A view can reinterpret the same native words. Decode using the outer
|
|
// view format, while Vulkan copies the storage allocation's bytes.
|
|
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
|
|
const Bool depth = info.Format == GL_DEPTH_COMPONENT || info.Format == GL_DEPTH_STENCIL;
|
|
const Bool stencil = info.Format == GL_STENCIL_INDEX || info.Format == GL_DEPTH_STENCIL;
|
|
const Bool depthStencil = depth || stencil;
|
|
const VkImageAspectFlags aspects =
|
|
depthStencil ? ((depth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0) | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0))
|
|
: VK_IMAGE_ASPECT_COLOR_BIT;
|
|
if ((resource->aspect & aspects) != aspects) return false;
|
|
const SizeT pixels = static_cast<SizeT>(pixels64);
|
|
const SizeT destinationPixelSize =
|
|
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type));
|
|
if (!destinationPixelSize || pixels64 * destinationPixelSize != info.DstSize) return false;
|
|
SizeT sourcePixelSize = GetReadbackTexelSize(resource->format);
|
|
if (depthStencil) {
|
|
switch (resource->format) {
|
|
case VK_FORMAT_D16_UNORM:
|
|
sourcePixelSize = 2;
|
|
break;
|
|
case VK_FORMAT_D24_UNORM_S8_UINT:
|
|
case VK_FORMAT_X8_D24_UNORM_PACK32:
|
|
case VK_FORMAT_D32_SFLOAT:
|
|
case VK_FORMAT_D32_SFLOAT_S8_UINT:
|
|
sourcePixelSize = 4;
|
|
break;
|
|
case VK_FORMAT_S8_UINT:
|
|
sourcePixelSize = 0;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
} else if (!sourcePixelSize || sourcePixelSize != GetReadbackTexelSize(format))
|
|
return false;
|
|
const VkDeviceSize firstBytes = pixels * (depthStencil ? (depth ? sourcePixelSize : 0) : sourcePixelSize);
|
|
const VkDeviceSize stencilOffset = (firstBytes + 3) & ~VkDeviceSize{3};
|
|
const VkDeviceSize copyBytes = depthStencil ? stencilOffset + (stencil ? pixels : 0) : firstBytes;
|
|
if (!copyBytes) return false;
|
|
VkBufferObject readback;
|
|
if (!readback.Create({.allocator = m_allocator,
|
|
.size = copyBytes,
|
|
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
|
|
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}))
|
|
return false;
|
|
WireImage image{};
|
|
image.image = resource->image;
|
|
image.format = resource->format;
|
|
image.aspect = resource->aspect;
|
|
image.layout = resource->layout;
|
|
image.trackedLayout = &resource->layout;
|
|
image.levels = resource->mipLevels;
|
|
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
|
|
auto& frame = m_frameContext.GetCurrent();
|
|
VkBufferImageCopy regions[2]{};
|
|
Uint32 regionCount = 0;
|
|
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
|
|
auto& copy = regions[regionCount++];
|
|
copy.bufferOffset = offset;
|
|
copy.imageSubresource = {aspect, level, nativeVolume ? 0u : layer, nativeVolume ? 1u : layerCount};
|
|
copy.imageOffset = {0, 0, nativeVolume ? static_cast<Int32>(layer) : 0};
|
|
copy.imageExtent = extent;
|
|
};
|
|
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
|
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
|
|
// E5B9G9R9 on a device whose transfer reads canonicalize it: the words leave through a
|
|
// shader into a twin (WireE5RawCopy.h), which is what the buffer copy then reads.
|
|
VkImage copySource = image.image;
|
|
if (!depthStencil && m_textureManager->WireE5RawReadApplies(*resource)) {
|
|
TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
|
|
WireE5RawCopyRegion region;
|
|
region.source = image.image;
|
|
region.volume = nativeVolume;
|
|
region.level = level;
|
|
region.baseLayer = nativeVolume ? 0u : layer;
|
|
region.layerCount = nativeVolume ? 1u : layerCount;
|
|
region.offset = regions[0].imageOffset;
|
|
region.extent = extent;
|
|
auto twin = MakeUnique<VkTextureManager::TextureResource>();
|
|
WireE5RawCopyObjects objects;
|
|
if (m_textureManager->RecordWireE5RawTwin(frame.commandBuffer, region, VK_IMAGE_LAYOUT_GENERAL, *twin,
|
|
objects)) {
|
|
copySource = twin->image;
|
|
regions[0].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, region.layerCount};
|
|
regions[0].imageOffset = {0, 0, 0};
|
|
WireRetiredObjects retired{};
|
|
retired.textures.push_back(Move(twin));
|
|
retired.imageViews = {objects.sourceView, objects.twinView};
|
|
retired.descriptorPools = {objects.descriptorPool};
|
|
RetireWireObjects(Move(retired));
|
|
}
|
|
}
|
|
if (copySource == image.image) TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
|
vkCmdCopyImageToBuffer(frame.commandBuffer, copySource, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
readback.GetHandle(), regionCount, regions);
|
|
TransitionWireImage(image, restore);
|
|
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
|
host.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
|
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1,
|
|
&host, 0, nullptr, 0, nullptr);
|
|
if (!SubmitReadbackCommandsAndWait(frame)) return false;
|
|
const auto* mapped = static_cast<const Uint8*>(readback.Map());
|
|
if (!mapped || !readback.Invalidate(copyBytes)) return false;
|
|
Vector<Uint8> result;
|
|
if (depthStencil) {
|
|
if (!PackDepthStencilTight(depth ? mapped : nullptr, stencil ? mapped + stencilOffset : nullptr,
|
|
resource->format, pixels, info.Format, info.Type, result))
|
|
return false;
|
|
} else {
|
|
using namespace ClientReadback;
|
|
ReadbackChannelMapping mapping{};
|
|
if (!GetReadbackChannelMapping(info.Format, mapping) ||
|
|
GetReadbackDstPixelSize(mapping, info.Type) != destinationPixelSize)
|
|
return false;
|
|
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
|
GetPackedReadbackInternalFormat(format), MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
|
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type))) {
|
|
if (destinationPixelSize != sourcePixelSize) return false;
|
|
result.assign(mapped, mapped + firstBytes);
|
|
} else {
|
|
Vector<Uint8> wide;
|
|
GLenum wideType = GL_NONE;
|
|
const GLsizei rows = static_cast<GLsizei>(Uint64(box.H) * box.D);
|
|
if (!DecodeReadbackRowsToWide(mapped, format, box.W, rows, wide, wideType) ||
|
|
mapping.isInteger != (wideType == GL_INT || wideType == GL_UNSIGNED_INT))
|
|
return false;
|
|
if (ResolveTextureFormatInfo(static_cast<TextureInternalFormat>(record.Desc.InternalFormat))
|
|
.expandRgbToRgba) {
|
|
const Float floatOne = 1.0f;
|
|
const Uint32 integerOne = 1;
|
|
const void* alpha = wideType == GL_FLOAT ? static_cast<const void*>(&floatOne) : &integerOne;
|
|
for (SizeT i = 0; i < pixels; ++i)
|
|
Memcpy(wide.data() + (i * 4 + 3) * sizeof(Uint32), alpha, sizeof(Uint32));
|
|
}
|
|
result.resize(pixels * destinationPixelSize);
|
|
const SizeT srcRow = static_cast<SizeT>(box.W) * 4 * sizeof(Uint32);
|
|
const SizeT dstRow = static_cast<SizeT>(box.W) * destinationPixelSize;
|
|
for (GLsizei row = 0; row < rows; ++row)
|
|
ConvertWideReadbackRow(wide.data() + row * srcRow, result.data() + row * dstRow, box.W, wideType,
|
|
mapping, info.Type);
|
|
}
|
|
}
|
|
if (result.size() != info.DstSize) return false;
|
|
ownedBytes = Move(result);
|
|
return true;
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|