mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +09:00
[Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else
This commit is contained in:
@@ -3435,90 +3435,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return componentType != 0 ? static_cast<GLenum>(componentType) : GL_UNSIGNED_NORMALIZED;
|
return componentType != 0 ? static_cast<GLenum>(componentType) : GL_UNSIGNED_NORMALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
|
||||||
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
|
||||||
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
|
||||||
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
|
||||||
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
|
||||||
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
|
||||||
// Per the GL addressing rules, slice k row j lands at
|
|
||||||
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
|
||||||
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
|
||||||
static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
|
||||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
|
||||||
void* pixels, Bool applyPackImageParams) {
|
|
||||||
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
|
||||||
if (dstPixelBytes == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ReadbackImpl::PackedReadbackLayout packedLayout{};
|
|
||||||
const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout);
|
|
||||||
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
|
||||||
|
|
||||||
const auto& pixelPackBufferObject =
|
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
|
||||||
|
|
||||||
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
|
||||||
// rows are written so skip regions of the destination stay untouched.
|
|
||||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
|
||||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
|
||||||
const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
|
||||||
const SizeT imageRows =
|
|
||||||
applyPackImageParams && packParams.ImageHeight > 0
|
|
||||||
? static_cast<SizeT>(packParams.ImageHeight)
|
|
||||||
: static_cast<SizeT>(sliceHeight);
|
|
||||||
const SizeT dstImageStride = imageRows * dstRowStride;
|
|
||||||
const SizeT skipImages =
|
|
||||||
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
|
||||||
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
|
||||||
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
|
||||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
|
||||||
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
|
||||||
|
|
||||||
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
|
||||||
if (pixelPackBufferObject) {
|
|
||||||
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
|
||||||
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
|
||||||
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
|
||||||
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
|
||||||
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
|
||||||
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
|
||||||
Vector<Uint8> convertedRow(dstRowBytes);
|
|
||||||
|
|
||||||
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
|
||||||
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
|
||||||
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
|
||||||
static_cast<SizeT>(row);
|
|
||||||
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
|
||||||
ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
|
||||||
mapping, type);
|
|
||||||
|
|
||||||
if (packParams.SwapBytes) {
|
|
||||||
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
|
||||||
if (groupSize > 1) {
|
|
||||||
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
|
||||||
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
|
||||||
static_cast<SizeT>(row) * dstRowStride;
|
|
||||||
if (pixelPackBufferObject) {
|
|
||||||
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
|
||||||
pboBaseOffset + dstOffset);
|
|
||||||
} else {
|
|
||||||
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
|
// Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's
|
||||||
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
|
// (format, type) layout. Returns false when the combination is not convertible (the caller keeps its
|
||||||
@@ -3651,7 +3567,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
|
ExpandNarrowWideRead(wide, static_cast<SizeT>(width) * static_cast<SizeT>(height), readChannels, wideType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels,
|
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels,
|
||||||
honorPackImageParams)) {
|
honorPackImageParams)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3705,7 +3621,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
|
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
|
||||||
if (!StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels,
|
if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels,
|
||||||
applyPackImageParams)) {
|
applyPackImageParams)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -764,5 +764,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
|
||||||
|
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
|
||||||
|
return (rowBytes + align - 1) / align * align;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
|
||||||
|
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
|
||||||
|
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
|
||||||
|
// 4 components x GetReadbackComponentSize(wideType) bytes each.
|
||||||
|
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
|
||||||
|
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
|
||||||
|
// Per the GL addressing rules, slice k row j lands at
|
||||||
|
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
|
||||||
|
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
|
||||||
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
|
void* pixels, Bool applyPackImageParams) {
|
||||||
|
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||||
|
if (dstPixelBytes == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PackedReadbackLayout packedLayout{};
|
||||||
|
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
|
||||||
|
const SizeT dstComponentSize = GetReadbackComponentSize(type);
|
||||||
|
|
||||||
|
const auto& pixelPackBufferObject =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
||||||
|
|
||||||
|
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
|
||||||
|
// rows are written so skip regions of the destination stay untouched.
|
||||||
|
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
||||||
|
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
||||||
|
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
|
||||||
|
const SizeT imageRows =
|
||||||
|
applyPackImageParams && packParams.ImageHeight > 0
|
||||||
|
? static_cast<SizeT>(packParams.ImageHeight)
|
||||||
|
: static_cast<SizeT>(sliceHeight);
|
||||||
|
const SizeT dstImageStride = imageRows * dstRowStride;
|
||||||
|
const SizeT skipImages =
|
||||||
|
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
|
||||||
|
const SizeT dstSkipOffset = skipImages * dstImageStride +
|
||||||
|
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
||||||
|
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
|
||||||
|
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
|
||||||
|
|
||||||
|
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
|
||||||
|
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
|
||||||
|
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
|
||||||
|
if (requiredSize > pixelPackBufferObject->GetSize()) {
|
||||||
|
MGLOG_E("Readback conversion: pixel pack buffer is too small");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
|
||||||
|
const SizeT srcPixelBytes = 4 * srcComponentSize;
|
||||||
|
Vector<Uint8> convertedRow(dstRowBytes);
|
||||||
|
|
||||||
|
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
|
||||||
|
for (GLsizei row = 0; row < sliceHeight; ++row) {
|
||||||
|
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
|
||||||
|
static_cast<SizeT>(row);
|
||||||
|
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
|
||||||
|
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
|
||||||
|
mapping, type);
|
||||||
|
|
||||||
|
if (packParams.SwapBytes) {
|
||||||
|
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
|
||||||
|
if (groupSize > 1) {
|
||||||
|
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
|
||||||
|
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
|
||||||
|
static_cast<SizeT>(row) * dstRowStride;
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
|
||||||
|
pboBaseOffset + dstOffset);
|
||||||
|
} else {
|
||||||
|
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
|
||||||
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
|
||||||
const ReadbackChannelMapping& mapping, GLenum type);
|
const ReadbackChannelMapping& mapping, GLenum type);
|
||||||
|
|
||||||
|
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
|
||||||
|
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
|
||||||
|
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
|
||||||
|
// DirectGLES and DirectVulkan readback conversion paths.
|
||||||
|
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||||
|
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||||
|
void* pixels, Bool applyPackImageParams);
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "VulkanRenderer.h"
|
#include "VulkanRenderer.h"
|
||||||
|
|
||||||
|
#include "MG_Backend/DirectGLES/Utils.h"
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
#include "VertexInputStateBuilder.h"
|
#include "VertexInputStateBuilder.h"
|
||||||
|
|
||||||
@@ -1881,58 +1883,369 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) ---
|
||||||
|
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true;
|
||||||
|
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, 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_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 ?
|
||||||
|
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_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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
||||||
GLsizei height, GLenum format, GLenum type, void* pixels) {
|
GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (type != GL_UNSIGNED_BYTE && type != GL_FLOAT) {
|
|
||||||
MGLOG_E("DirectVulkan readback skipped: unsupported type=0x%x", type);
|
DirectGLES::ReadbackImpl::ReadbackChannelMapping mapping{};
|
||||||
|
if (!DirectGLES::ReadbackImpl::GetReadbackChannelMapping(format, mapping) ||
|
||||||
|
DirectGLES::ReadbackImpl::GetReadbackDstPixelSize(mapping, type) == 0) {
|
||||||
|
MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Int dstChannels = GetReadbackChannelCount(format);
|
Vector<Uint8> wide;
|
||||||
if (dstChannels == 0) {
|
GLenum wideType = GL_FLOAT;
|
||||||
MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x", format);
|
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
|
|
||||||
const SizeT dstComponentBytes = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8);
|
|
||||||
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
|
|
||||||
const SizeT dstRowStride = AlignPixelRow(rowPixels * static_cast<SizeT>(dstChannels) * dstComponentBytes,
|
|
||||||
packParams.Alignment);
|
|
||||||
const SizeT dstOffset = static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
|
|
||||||
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) *
|
|
||||||
static_cast<SizeT>(dstChannels) * dstComponentBytes;
|
|
||||||
const SizeT packedSize = dstOffset +
|
|
||||||
(static_cast<SizeT>(height - 1) * dstRowStride) +
|
|
||||||
(static_cast<SizeT>(width) * static_cast<SizeT>(dstChannels) * dstComponentBytes);
|
|
||||||
Vector<Uint8> packed(packedSize, 0);
|
|
||||||
|
|
||||||
if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type,
|
|
||||||
dstRowStride, packed.data() + dstOffset)) {
|
|
||||||
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
||||||
static_cast<Int>(srcFormat));
|
static_cast<Int>(srcFormat));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& pixelPackBufferObject =
|
const Bool sourceIsInteger = wideType == GL_INT || wideType == GL_UNSIGNED_INT;
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
|
if (sourceIsInteger != mapping.isInteger) {
|
||||||
if (pixelPackBufferObject) {
|
MGLOG_E("DirectVulkan readback skipped: integerness mismatch (format=0x%x source=%d)",
|
||||||
const SizeT pboOffset = reinterpret_cast<SizeT>(pixels);
|
format, static_cast<Int>(srcFormat));
|
||||||
if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) {
|
return false;
|
||||||
MGLOG_E("DirectVulkan readback skipped: pixel pack buffer is too small");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pixels != nullptr && !packed.empty()) {
|
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height,
|
||||||
Memcpy(pixels, packed.data(), packed.size());
|
/*sliceCount=*/1, mapping, type, pixels,
|
||||||
}
|
/*applyPackImageParams=*/false);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user