feat(graphics): emulate ETC2/EAC textures on Vulkan

Hosts without native ETC2 now report ETC2/EAC support. Uploads into emulated images are decoded to RGBA8/R16 staging buffers at submit. MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns it off.
This commit is contained in:
vii committed 2026-09-13 11:45:28 -05:00
1 parent ad76f68224
commit eae9fa7030
9 files changed
+2188 -4

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
#include <cstddef>
#include <cstdint>
namespace mocktail::graphics {
// ETC2 and EAC block encodings from the OpenGL ES 3.0 / Vulkan core formats.
// sRGB variants share the unorm encoding; the colour space belongs to the
// destination format.
enum class EtcFormat {
kEtc2Rgb8,
kEtc2Rgb8A1,
kEtc2Rgba8,
kEacR11,
kEacR11Signed,
kEacRg11,
kEacRg11Signed,
};
// Compressed bytes per 4x4 block.
std::size_t EtcBlockBytes(EtcFormat format);
// Decoded bytes per texel: 4 (RGBA8) for ETC2 formats, 2 per channel
// (16-bit, little-endian) for EAC formats.
std::size_t EtcDecodedTexelBytes(EtcFormat format);
// Decodes a width x height image stored as ceil(width/4) x ceil(height/4)
// blocks into row-major texels. Returns false, writing nothing, when a buffer
// is smaller than the image requires.
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
std::size_t source_bytes, std::uint32_t width,
std::uint32_t height, std::uint8_t* destination,
std::size_t destination_bytes);
} // namespace mocktail::graphics
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
@@ -0,0 +1,107 @@
#ifndef MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
#define MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
#include <vulkan/vulkan.h>
#include <cstdint>
#include "mocktail/graphics/etc2_decoder.h"
namespace mocktail::graphics {
// Host format that stores a decoded ETC2/EAC image, with its block encoding.
struct Etc2EmulatedFormat {
VkFormat host_format = VK_FORMAT_UNDEFINED;
EtcFormat etc_format = EtcFormat::kEtc2Rgb8;
};
// Maps an ETC2/EAC Vulkan format to its decoded host format. Returns false for
// every other format.
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out);
// Format features reported for an emulated ETC2/EAC format: the host format's
// sampling and transfer features only, as for a native compressed format.
VkFormatFeatureFlags EmulatedEtc2FormatFeatures(
VkFormatFeatureFlags host_features);
// Staging bytes consumed and decoded bytes produced by one 2D upload region.
// Returns false for an empty extent, a depth other than 1, or no layers.
bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
std::uint32_t layer_count, VkDeviceSize* compressed,
VkDeviceSize* decoded);
// Reports ETC2/EAC support on hosts without native ETC2 (desktop GPUs) and
// stores those textures decoded. Images are created with the host format;
// each staged upload into one is redirected to a host-visible staging buffer
// that is filled by decoding the application's staging bytes when the
// command buffer is submitted. Every submit decodes again, as a native copy
// re-reads its source on each execution.
// Staging buffers live until their command buffer is begun, reset or freed.
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
class VulkanEtc2Emulation final {
public:
VulkanEtc2Emulation();
~VulkanEtc2Emulation();
VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete;
VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete;
// True when this physical device lacks native ETC2 and emulation is on.
bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device,
PFN_vkGetPhysicalDeviceFeatures host);
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
bool emulated,
const VkPhysicalDeviceMemoryProperties& memory,
PFN_vkGetDeviceProcAddr get_device_proc_addr);
void DestroyDevice(VkDevice device);
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
const VkAllocationCallbacks* allocator, VkImage* image);
void DestroyImage(VkDevice device, VkImage image,
const VkAllocationCallbacks* allocator);
VkResult CreateImageView(VkDevice device,
const VkImageViewCreateInfo* create_info,
const VkAllocationCallbacks* allocator,
VkImageView* view);
VkResult BindBufferMemory(VkDevice device, VkBuffer buffer,
VkDeviceMemory memory, VkDeviceSize offset);
VkResult BindBufferMemory2(VkDevice device, std::uint32_t count,
const VkBindBufferMemoryInfo* infos);
VkResult MapMemory(VkDevice device, VkDeviceMemory memory,
VkDeviceSize offset, VkDeviceSize size,
VkMemoryMapFlags flags, void** data);
VkResult MapMemory2(VkDevice device, const VkMemoryMapInfo* info,
void** data);
void UnmapMemory(VkDevice device, VkDeviceMemory memory);
VkResult UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo* info);
void FreeMemory(VkDevice device, VkDeviceMemory memory,
const VkAllocationCallbacks* allocator);
void DestroyBuffer(VkDevice device, VkBuffer buffer,
const VkAllocationCallbacks* allocator);
void CmdCopyBufferToImage(VkDevice device, VkCommandBuffer command_buffer,
VkBuffer source, VkImage destination,
VkImageLayout layout, std::uint32_t region_count,
const VkBufferImageCopy* regions);
void CmdCopyBufferToImage2(VkDevice device, VkCommandBuffer command_buffer,
const VkCopyBufferToImageInfo2* info);
void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer,
std::uint32_t count,
const VkCommandBuffer* secondaries);
// Decodes every upload recorded into these command buffers and the
// secondary command buffers they execute.
void PrepareSubmit(const VkCommandBuffer* command_buffers,
std::uint32_t count);
// Destroys the staging buffers owned by a command buffer that is no longer
// pending.
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
struct State;
private:
State* state_;
};
} // namespace mocktail::graphics
#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
+414 -2
View File
@@ -17,6 +17,7 @@
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
#include "mocktail/graphics/present_mode_policy.h"
#include "mocktail/graphics/vulkan_etc2_emulation.h"
#include "mocktail/graphics/vulkan_text_overlay_compositor.h"
#include "mocktail/platform/platform_runtime.h"
@@ -188,6 +189,7 @@ struct AdapterState {
VkInstance latest_instance = VK_NULL_HANDLE;
size_t active_instance_count = 0;
mocktail::graphics::VulkanTextOverlayCompositor text_overlay;
mocktail::graphics::VulkanEtc2Emulation etc2;
std::atomic<NotePresentFn> note_present{nullptr};
std::atomic<NoteHostPresentBeginFn> note_host_present_begin{nullptr};
std::atomic<NoteHostPresentEndFn> note_host_present_end{nullptr};
@@ -727,12 +729,87 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
MOCKTAIL_VK_PROC(vkQueueWaitIdle)
MOCKTAIL_VK_PROC(vkDeviceWaitIdle)
MOCKTAIL_VK_PROC(vkQueuePresentKHR)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures2)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties2)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties)
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties2)
MOCKTAIL_VK_PROC(vkCreateImage)
MOCKTAIL_VK_PROC(vkDestroyImage)
MOCKTAIL_VK_PROC(vkCreateImageView)
MOCKTAIL_VK_PROC(vkBindBufferMemory)
MOCKTAIL_VK_PROC(vkBindBufferMemory2)
MOCKTAIL_VK_PROC(vkMapMemory)
MOCKTAIL_VK_PROC(vkMapMemory2)
MOCKTAIL_VK_PROC(vkUnmapMemory)
MOCKTAIL_VK_PROC(vkUnmapMemory2)
MOCKTAIL_VK_PROC(vkFreeMemory)
MOCKTAIL_VK_PROC(vkDestroyBuffer)
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage)
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage2)
MOCKTAIL_VK_PROC(vkCmdExecuteCommands)
#undef MOCKTAIL_VK_PROC
#define MOCKTAIL_VK_ALIAS(alias, function) \
if (std::strcmp(name, #alias) == 0) { \
return reinterpret_cast<PFN_vkVoidFunction>(function); \
}
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFeatures2KHR,
vkGetPhysicalDeviceFeatures2)
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFormatProperties2KHR,
vkGetPhysicalDeviceFormatProperties2)
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceImageFormatProperties2KHR,
vkGetPhysicalDeviceImageFormatProperties2)
MOCKTAIL_VK_ALIAS(vkBindBufferMemory2KHR, vkBindBufferMemory2)
MOCKTAIL_VK_ALIAS(vkMapMemory2KHR, vkMapMemory2)
MOCKTAIL_VK_ALIAS(vkUnmapMemory2KHR, vkUnmapMemory2)
MOCKTAIL_VK_ALIAS(vkCmdCopyBufferToImage2KHR, vkCmdCopyBufferToImage2)
#undef MOCKTAIL_VK_ALIAS
return nullptr;
}
bool NameInList(const char* name, const char* const* names,
std::size_t count) {
return name != nullptr &&
std::any_of(names, names + count, [name](const char* candidate) {
return std::strcmp(name, candidate) == 0;
});
}
// Device commands wrapped for ETC2/EAC emulation.
bool IsEtc2DeviceProc(const char* name) {
static constexpr const char* kNames[] = {
"vkCreateImage", "vkDestroyImage",
"vkCreateImageView", "vkBindBufferMemory",
"vkBindBufferMemory2", "vkBindBufferMemory2KHR",
"vkMapMemory", "vkMapMemory2",
"vkMapMemory2KHR", "vkUnmapMemory",
"vkUnmapMemory2", "vkUnmapMemory2KHR",
"vkFreeMemory", "vkDestroyBuffer",
"vkCmdCopyBufferToImage", "vkCmdCopyBufferToImage2",
"vkCmdCopyBufferToImage2KHR", "vkCmdExecuteCommands",
};
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
}
// Physical-device queries that report emulated ETC2/EAC support.
bool IsEtc2PhysicalDeviceProc(const char* name) {
static constexpr const char* kNames[] = {
"vkGetPhysicalDeviceFeatures",
"vkGetPhysicalDeviceFeatures2",
"vkGetPhysicalDeviceFeatures2KHR",
"vkGetPhysicalDeviceFormatProperties",
"vkGetPhysicalDeviceFormatProperties2",
"vkGetPhysicalDeviceFormatProperties2KHR",
"vkGetPhysicalDeviceImageFormatProperties",
"vkGetPhysicalDeviceImageFormatProperties2",
"vkGetPhysicalDeviceImageFormatProperties2KHR",
};
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
}
bool IsDeviceAdapterProc(const char* name) {
return name != nullptr && (std::strcmp(name, "vkDestroyDevice") == 0 ||
return name != nullptr && (IsEtc2DeviceProc(name) || std::strcmp(name, "vkDestroyDevice") == 0 ||
std::strcmp(name, "vkGetDeviceQueue") == 0 ||
std::strcmp(name, "vkGetDeviceQueue2") == 0 ||
std::strcmp(name, "vkCreateSwapchainKHR") == 0 ||
@@ -1042,6 +1119,58 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
return result;
}
template <typename Function>
Function HostPhysicalDeviceProc(const char* name, const char* alias) {
VkInstance instance = VK_NULL_HANDLE;
{
AdapterState& state = State();
std::lock_guard<std::mutex> lock(state.mutex);
instance = state.latest_instance;
}
PFN_vkVoidFunction proc = HostInstanceProc(instance, name);
if (proc == nullptr && alias != nullptr) {
proc = HostInstanceProc(instance, alias);
}
return reinterpret_cast<Function>(proc);
}
bool Etc2EmulatedPhysicalDevice(VkPhysicalDevice physical_device) {
return State().etc2.PhysicalDeviceNeedsEmulation(
physical_device, HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
"vkGetPhysicalDeviceFeatures", nullptr));
}
void ReleaseEtc2PoolCommandBuffers(VkDevice device,
VkCommandPool command_pool) {
std::vector<VkCommandBuffer> command_buffers;
{
AdapterState& state = State();
std::lock_guard<std::mutex> lock(state.mutex);
for (const auto& binding : state.command_buffer_bindings) {
if (binding.device == device && binding.command_pool == command_pool) {
command_buffers.push_back(binding.command_buffer);
}
}
}
for (VkCommandBuffer command_buffer : command_buffers) {
State().etc2.ReleaseCommandBuffer(command_buffer);
}
}
void PrepareEtc2Submit2(std::uint32_t submit_count,
const VkSubmitInfo2* submits) {
if (submits == nullptr) {
return;
}
for (std::uint32_t index = 0; index < submit_count; ++index) {
for (std::uint32_t info = 0; info < submits[index].commandBufferInfoCount;
++info) {
State().etc2.PrepareSubmit(
&submits[index].pCommandBufferInfos[info].commandBuffer, 1);
}
}
}
} // namespace
extern "C" {
@@ -1247,7 +1376,7 @@ vkGetInstanceProcAddr(VkInstance instance, const char* name) {
}
if (const PFN_vkVoidFunction adapter = AdapterProc(name);
adapter != nullptr) {
if (IsDeviceAdapterProc(name) &&
if ((IsDeviceAdapterProc(name) || IsEtc2PhysicalDeviceProc(name)) &&
HostInstanceProc(instance, name) == nullptr) {
return nullptr;
}
@@ -1306,6 +1435,31 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
VkDeviceCreateInfo host_info = *create_info;
EnabledDeviceFeatures enabled = InspectEnabledFeatures(*create_info);
const bool etc2_emulated = State().etc2.PhysicalDeviceNeedsEmulation(
physical_device, reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(
HostInstanceProc(instance,
"vkGetPhysicalDeviceFeatures")));
VkPhysicalDeviceFeatures host_enabled_features{};
VkPhysicalDeviceFeatures2* requested_features2 = nullptr;
VkBool32 requested_features2_etc2 = VK_FALSE;
if (etc2_emulated) {
enabled.root.features.textureCompressionETC2 = VK_FALSE;
if (create_info->pEnabledFeatures != nullptr) {
host_enabled_features = *create_info->pEnabledFeatures;
host_enabled_features.textureCompressionETC2 = VK_FALSE;
host_info.pEnabledFeatures = &host_enabled_features;
}
// The feature chain is caller-owned; clear the emulated bit only for the
// host call and restore it afterwards.
requested_features2 = const_cast<VkPhysicalDeviceFeatures2*>(
reinterpret_cast<const VkPhysicalDeviceFeatures2*>(FindFeature(
create_info->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)));
if (requested_features2 != nullptr) {
requested_features2_etc2 =
requested_features2->features.textureCompressionETC2;
requested_features2->features.textureCompressionETC2 = VK_FALSE;
}
}
enabled.root.pNext = &enabled.vulkan11;
enabled.vulkan11.pNext = &enabled.vulkan12;
enabled.vulkan12.pNext = nullptr;
@@ -1341,11 +1495,25 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
const VkResult result =
host_create(physical_device, &host_info, allocator, device);
if (requested_features2 != nullptr) {
requested_features2->features.textureCompressionETC2 =
requested_features2_etc2;
}
if (result != VK_SUCCESS) {
return result;
}
RegisterHostDeviceDispatch(*device, physical_device,
host_get_device_proc_addr);
VkPhysicalDeviceMemoryProperties memory_properties{};
if (const auto host_memory_properties =
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
HostInstanceProc(instance,
"vkGetPhysicalDeviceMemoryProperties"));
host_memory_properties != nullptr) {
host_memory_properties(physical_device, &memory_properties);
}
State().etc2.RegisterDevice(*device, physical_device, etc2_emulated,
memory_properties, host_get_device_proc_addr);
std::uint32_t queue_family_count = 0;
const auto host_get_queue_families =
@@ -1403,6 +1571,7 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) {
const auto host_destroy = reinterpret_cast<PFN_vkDestroyDevice>(
HostDeviceProc(device, "vkDestroyDevice"));
State().text_overlay.DestroyDevice(device);
State().etc2.DestroyDevice(device);
if (host_destroy != nullptr) {
host_destroy(device, allocator);
}
@@ -1650,6 +1819,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
ReleaseEtc2PoolCommandBuffers(device, command_pool);
const VkResult result = host_reset(device, command_pool, flags);
observation.SetResult(result);
return result;
@@ -1706,6 +1876,11 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return;
}
if (command_buffers != nullptr) {
for (std::uint32_t index = 0; index < command_buffer_count; ++index) {
State().etc2.ReleaseCommandBuffer(command_buffers[index]);
}
}
host_free(device, command_pool, command_buffer_count, command_buffers);
RemoveHostCommandBuffers(device, command_pool, command_buffer_count,
command_buffers);
@@ -1722,6 +1897,7 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return;
}
ReleaseEtc2PoolCommandBuffers(device, command_pool);
host_destroy(device, command_pool, allocator);
RemoveHostCommandPoolBindings(device, command_pool);
observation.SetResult(VK_SUCCESS);
@@ -1737,6 +1913,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
State().etc2.ReleaseCommandBuffer(command_buffer);
const VkResult result = host_begin(command_buffer, begin_info);
observation.SetResult(result);
return result;
@@ -1765,6 +1942,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
State().etc2.ReleaseCommandBuffer(command_buffer);
const VkResult result = host_reset(command_buffer, flags);
observation.SetResult(result);
return result;
@@ -1801,6 +1979,12 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
if (submits != nullptr) {
for (std::uint32_t index = 0; index < submit_count; ++index) {
State().etc2.PrepareSubmit(submits[index].pCommandBuffers,
submits[index].commandBufferCount);
}
}
const VkResult result = State().text_overlay.QueueSubmit(
queue, submit_count, submits, fence, host_submit);
observation.SetResult(result);
@@ -1817,6 +2001,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
PrepareEtc2Submit2(submit_count, submits);
const VkResult result = State().text_overlay.QueueSubmit2(
queue, submit_count, submits, fence, host_submit);
observation.SetResult(result);
@@ -1833,6 +2018,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
return VK_ERROR_INITIALIZATION_FAILED;
}
PrepareEtc2Submit2(submit_count, submits);
const VkResult result = State().text_overlay.QueueSubmit2(
queue, submit_count, submits, fence,
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
@@ -2025,4 +2211,230 @@ vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* present_info) {
return normalized_result;
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures(
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures* features) {
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
"vkGetPhysicalDeviceFeatures", nullptr);
if (host == nullptr || features == nullptr) {
return;
}
host(physical_device, features);
if (Etc2EmulatedPhysicalDevice(physical_device)) {
features->textureCompressionETC2 = VK_TRUE;
}
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2(
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures2* features) {
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures2>(
"vkGetPhysicalDeviceFeatures2", "vkGetPhysicalDeviceFeatures2KHR");
if (host == nullptr || features == nullptr) {
return;
}
host(physical_device, features);
if (Etc2EmulatedPhysicalDevice(physical_device)) {
features->features.textureCompressionETC2 = VK_TRUE;
}
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties(
VkPhysicalDevice physical_device, VkFormat format,
VkFormatProperties* properties) {
const auto host =
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties>(
"vkGetPhysicalDeviceFormatProperties", nullptr);
if (host == nullptr || properties == nullptr) {
return;
}
mocktail::graphics::Etc2EmulatedFormat mapped;
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
!Etc2EmulatedPhysicalDevice(physical_device)) {
host(physical_device, format, properties);
return;
}
host(physical_device, mapped.host_format, properties);
properties->linearTilingFeatures = 0;
properties->optimalTilingFeatures =
mocktail::graphics::EmulatedEtc2FormatFeatures(
properties->optimalTilingFeatures);
properties->bufferFeatures = 0;
}
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2(
VkPhysicalDevice physical_device, VkFormat format,
VkFormatProperties2* properties) {
const auto host =
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties2>(
"vkGetPhysicalDeviceFormatProperties2",
"vkGetPhysicalDeviceFormatProperties2KHR");
if (host == nullptr || properties == nullptr) {
return;
}
mocktail::graphics::Etc2EmulatedFormat mapped;
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
!Etc2EmulatedPhysicalDevice(physical_device)) {
host(physical_device, format, properties);
return;
}
host(physical_device, mapped.host_format, properties);
VkFormatProperties& base = properties->formatProperties;
base.linearTilingFeatures = 0;
base.optimalTilingFeatures =
mocktail::graphics::EmulatedEtc2FormatFeatures(base.optimalTilingFeatures);
base.bufferFeatures = 0;
if (auto* extended = const_cast<VkFormatProperties3*>(
reinterpret_cast<const VkFormatProperties3*>(FindFeature(
properties->pNext, VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3)));
extended != nullptr) {
extended->linearTilingFeatures = 0;
extended->optimalTilingFeatures =
mocktail::graphics::EmulatedEtc2FormatFeatures(
static_cast<VkFormatFeatureFlags>(
extended->optimalTilingFeatures));
extended->bufferFeatures = 0;
}
}
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties(
VkPhysicalDevice physical_device, VkFormat format, VkImageType type,
VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags,
VkImageFormatProperties* properties) {
const auto host =
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties>(
"vkGetPhysicalDeviceImageFormatProperties", nullptr);
if (host == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
mocktail::graphics::Etc2EmulatedFormat mapped;
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
!Etc2EmulatedPhysicalDevice(physical_device)) {
return host(physical_device, format, type, tiling, usage, flags,
properties);
}
if (tiling != VK_IMAGE_TILING_OPTIMAL || type != VK_IMAGE_TYPE_2D) {
return VK_ERROR_FORMAT_NOT_SUPPORTED;
}
return host(physical_device, mapped.host_format, type, tiling, usage,
flags & ~static_cast<VkImageCreateFlags>(
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT),
properties);
}
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceImageFormatInfo2* info,
VkImageFormatProperties2* properties) {
const auto host =
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties2>(
"vkGetPhysicalDeviceImageFormatProperties2",
"vkGetPhysicalDeviceImageFormatProperties2KHR");
if (host == nullptr || info == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
mocktail::graphics::Etc2EmulatedFormat mapped;
if (!mocktail::graphics::LookupEmulatedEtc2Format(info->format, &mapped) ||
!Etc2EmulatedPhysicalDevice(physical_device)) {
return host(physical_device, info, properties);
}
if (info->tiling != VK_IMAGE_TILING_OPTIMAL ||
info->type != VK_IMAGE_TYPE_2D) {
return VK_ERROR_FORMAT_NOT_SUPPORTED;
}
VkPhysicalDeviceImageFormatInfo2 host_info = *info;
host_info.format = mapped.host_format;
host_info.flags &= ~static_cast<VkImageCreateFlags>(
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT);
return host(physical_device, &host_info, properties);
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
VkDevice device, const VkImageCreateInfo* create_info,
const VkAllocationCallbacks* allocator, VkImage* image) {
return State().etc2.CreateImage(device, create_info, allocator, image);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyImage(
VkDevice device, VkImage image, const VkAllocationCallbacks* allocator) {
State().etc2.DestroyImage(device, image, allocator);
}
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
VkDevice device, const VkImageViewCreateInfo* create_info,
const VkAllocationCallbacks* allocator, VkImageView* view) {
return State().etc2.CreateImageView(device, create_info, allocator, view);
}
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device,
VkBuffer buffer,
VkDeviceMemory memory,
VkDeviceSize offset) {
return State().etc2.BindBufferMemory(device, buffer, memory, offset);
}
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2(
VkDevice device, std::uint32_t count,
const VkBindBufferMemoryInfo* infos) {
return State().etc2.BindBufferMemory2(device, count, infos);
}
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device,
VkDeviceMemory memory,
VkDeviceSize offset,
VkDeviceSize size,
VkMemoryMapFlags flags,
void** data) {
return State().etc2.MapMemory(device, memory, offset, size, flags, data);
}
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2(VkDevice device,
const VkMemoryMapInfo* info,
void** data) {
return State().etc2.MapMemory2(device, info, data);
}
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device,
VkDeviceMemory memory) {
State().etc2.UnmapMemory(device, memory);
}
VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2(VkDevice device,
const VkMemoryUnmapInfo* info) {
return State().etc2.UnmapMemory2(device, info);
}
VKAPI_ATTR void VKAPI_CALL vkFreeMemory(
VkDevice device, VkDeviceMemory memory,
const VkAllocationCallbacks* allocator) {
State().etc2.FreeMemory(device, memory, allocator);
}
VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(
VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) {
State().etc2.DestroyBuffer(device, buffer, allocator);
}
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(
VkCommandBuffer command_buffer, VkBuffer source, VkImage destination,
VkImageLayout layout, std::uint32_t region_count,
const VkBufferImageCopy* regions) {
State().etc2.CmdCopyBufferToImage(
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
source, destination, layout, region_count, regions);
}
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2(
VkCommandBuffer command_buffer, const VkCopyBufferToImageInfo2* info) {
State().etc2.CmdCopyBufferToImage2(
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
info);
}
VKAPI_ATTR void VKAPI_CALL vkCmdExecuteCommands(
VkCommandBuffer command_buffer, std::uint32_t count,
const VkCommandBuffer* secondaries) {
State().etc2.CmdExecuteCommands(
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
count, secondaries);
}
} // extern "C"
+278
View File
@@ -0,0 +1,278 @@
#include "mocktail/graphics/etc2_decoder.h"
#include <algorithm>
#include <array>
#include <limits>
namespace mocktail::graphics {
namespace {
constexpr std::array<int, 8> kDistance = {3, 6, 11, 16, 23, 32, 41, 64};
// Indexed by (msb << 1) | lsb.
constexpr int kEtc1Modifiers[8][4] = {
{2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29},
{13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80},
{33, 106, -33, -106}, {47, 183, -47, -183},
};
constexpr int kEacModifiers[16][8] = {
{-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12},
{-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12},
{-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10},
{-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10},
{-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9},
{-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9},
{-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9},
{-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8},
};
std::uint8_t Clamp255(int value) {
return static_cast<std::uint8_t>(std::clamp(value, 0, 255));
}
int Expand4(int value) { return (value << 4) | value; }
int Expand5(int value) { return (value << 3) | (value >> 2); }
int Expand6(int value) { return (value << 2) | (value >> 4); }
int Expand7(int value) { return (value << 1) | (value >> 6); }
// Decodes one ETC2 RGB block into 16 column-major RGBA texels (x * 4 + y).
void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
std::array<std::uint8_t, 64>* out) {
static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1};
const bool diff_or_opaque = (b[3] & 2) != 0;
const bool opaque = !punchthrough || diff_or_opaque;
const int msb = (b[4] << 8) | b[5];
const int lsb = (b[6] << 8) | b[7];
auto index_of = [msb, lsb](int texel) {
return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1);
};
auto write = [out](int texel, int r, int g, int bl, int a) {
const std::size_t o = static_cast<std::size_t>(texel) * 4;
(*out)[o] = Clamp255(r);
(*out)[o + 1] = Clamp255(g);
(*out)[o + 2] = Clamp255(bl);
(*out)[o + 3] = static_cast<std::uint8_t>(a);
};
const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7];
const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7];
const int b_sum = (b[2] >> 3) + kDelta[b[2] & 7];
const bool individual = !punchthrough && !diff_or_opaque;
if (!individual && (r_sum < 0 || r_sum > 31 || g_sum < 0 || g_sum > 31)) {
std::array<std::array<int, 3>, 4> paint{};
if (r_sum < 0 || r_sum > 31) {
const std::array<int, 3> c1 = {
Expand4((((b[0] >> 3) & 3) << 2) | (b[0] & 3)),
Expand4(b[1] >> 4), Expand4(b[1] & 15)};
const std::array<int, 3> c2 = {Expand4(b[2] >> 4), Expand4(b[2] & 15),
Expand4(b[3] >> 4)};
const int d = kDistance[(((b[3] >> 2) & 3) << 1) | (b[3] & 1)];
for (int c = 0; c < 3; ++c) {
paint[0][c] = c1[c];
paint[1][c] = c2[c] + d;
paint[2][c] = c2[c];
paint[3][c] = c2[c] - d;
}
} else {
const std::array<int, 3> c1 = {
Expand4((b[0] >> 3) & 15),
Expand4(((b[0] & 7) << 1) | ((b[1] >> 4) & 1)),
Expand4((b[1] & 8) | ((b[1] & 3) << 1) | (b[2] >> 7))};
const std::array<int, 3> c2 = {
Expand4((b[2] >> 3) & 15), Expand4(((b[2] & 7) << 1) | (b[3] >> 7)),
Expand4((b[3] >> 3) & 15)};
const int v1 = (c1[0] << 16) | (c1[1] << 8) | c1[2];
const int v2 = (c2[0] << 16) | (c2[1] << 8) | c2[2];
const int d =
kDistance[(b[3] & 4) | ((b[3] & 1) << 1) | (v1 >= v2 ? 1 : 0)];
for (int c = 0; c < 3; ++c) {
paint[0][c] = c1[c] + d;
paint[1][c] = c1[c] - d;
paint[2][c] = c2[c] + d;
paint[3][c] = c2[c] - d;
}
}
for (int texel = 0; texel < 16; ++texel) {
const int index = index_of(texel);
if (!opaque && index == 2) {
write(texel, 0, 0, 0, 0);
} else {
write(texel, paint[index][0], paint[index][1], paint[index][2], 255);
}
}
return;
}
if (!individual && (b_sum < 0 || b_sum > 31)) {
const int ro = Expand6((b[0] >> 1) & 63);
const int go = Expand7(((b[0] & 1) << 6) | ((b[1] >> 1) & 63));
const int bo = Expand6(((b[1] & 1) << 5) | (b[2] & 0x18) |
((b[2] & 3) << 1) | (b[3] >> 7));
const int rh = Expand6(((b[3] & 0x7c) >> 1) | (b[3] & 1));
const int gh = Expand7(b[4] >> 1);
const int bh = Expand6(((b[4] & 1) << 5) | (b[5] >> 3));
const int rv = Expand6(((b[5] & 7) << 3) | (b[6] >> 5));
const int gv = Expand7(((b[6] & 31) << 2) | (b[7] >> 6));
const int bv = Expand6(b[7] & 63);
for (int x = 0; x < 4; ++x) {
for (int y = 0; y < 4; ++y) {
write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2,
(x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2,
(x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255);
}
}
return;
}
std::array<std::array<int, 3>, 2> base{};
for (int c = 0; c < 3; ++c) {
if (individual) {
base[0][c] = Expand4(b[c] >> 4);
base[1][c] = Expand4(b[c] & 15);
} else {
base[0][c] = Expand5(b[c] >> 3);
base[1][c] = Expand5((b[c] >> 3) + kDelta[b[c] & 7]);
}
}
const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7};
const bool flipped = (b[3] & 1) != 0;
for (int x = 0; x < 4; ++x) {
for (int y = 0; y < 4; ++y) {
const int texel = x * 4 + y;
const int index = index_of(texel);
if (!opaque && index == 2) {
write(texel, 0, 0, 0, 0);
continue;
}
const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0);
const int modifier =
(!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index];
write(texel, base[sub][0] + modifier, base[sub][1] + modifier,
base[sub][2] + modifier, 255);
}
}
}
// EAC 3-bit index for column-major texel (x * 4 + y).
int EacIndex(const std::uint8_t* b, int texel) {
std::uint64_t bits = 0;
for (int i = 2; i < 8; ++i) {
bits = (bits << 8) | b[i];
}
return static_cast<int>((bits >> (45 - texel * 3)) & 7);
}
int EacModifier(const std::uint8_t* b, int texel) {
return kEacModifiers[b[1] & 15][EacIndex(b, texel)];
}
std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) {
const int multiplier = b[1] >> 4;
const int modifier = EacModifier(b, texel);
const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier;
if (!is_signed) {
const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047);
return static_cast<std::uint16_t>((value << 5) | (value >> 6));
}
const int base = std::max(-127, static_cast<int>(static_cast<std::int8_t>(b[0])));
const int value = std::clamp(base * 8 + scaled, -1023, 1023);
const int magnitude = value < 0 ? -value : value;
const int expanded = (magnitude << 5) | (magnitude >> 5);
return static_cast<std::uint16_t>(
static_cast<std::int16_t>(value < 0 ? -expanded : expanded));
}
} // namespace
std::size_t EtcBlockBytes(EtcFormat format) {
switch (format) {
case EtcFormat::kEtc2Rgba8:
case EtcFormat::kEacRg11:
case EtcFormat::kEacRg11Signed:
return 16;
default:
return 8;
}
}
std::size_t EtcDecodedTexelBytes(EtcFormat format) {
switch (format) {
case EtcFormat::kEacR11:
case EtcFormat::kEacR11Signed:
return 2;
default:
return 4;
}
}
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
std::size_t source_bytes, std::uint32_t width,
std::uint32_t height, std::uint8_t* destination,
std::size_t destination_bytes) {
if (source == nullptr || destination == nullptr || width == 0 ||
height == 0) {
return false;
}
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
const std::uint64_t blocks_high =
(static_cast<std::uint64_t>(height) + 3) / 4;
const std::size_t texel_bytes = EtcDecodedTexelBytes(format);
const std::size_t block_bytes = EtcBlockBytes(format);
if (blocks_wide * blocks_high * block_bytes > source_bytes ||
static_cast<std::uint64_t>(width) * height * texel_bytes >
destination_bytes) {
return false;
}
std::array<std::uint8_t, 64> rgba{};
for (std::uint64_t by = 0; by < blocks_high; ++by) {
for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) {
const std::uint8_t* block =
source + (by * blocks_wide + bx) * block_bytes;
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba);
} else if (format == EtcFormat::kEtc2Rgba8) {
DecodeRgbBlock(block + 8, false, &rgba);
}
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
const int texel = static_cast<int>(x * 4 + y);
std::uint8_t* out =
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
switch (format) {
case EtcFormat::kEtc2Rgb8:
case EtcFormat::kEtc2Rgb8A1:
case EtcFormat::kEtc2Rgba8:
std::copy_n(rgba.data() + texel * 4, 4, out);
if (format == EtcFormat::kEtc2Rgba8) {
const int alpha =
block[0] + EacModifier(block, texel) * (block[1] >> 4);
out[3] = Clamp255(alpha);
}
break;
case EtcFormat::kEacR11:
case EtcFormat::kEacR11Signed:
case EtcFormat::kEacRg11:
case EtcFormat::kEacRg11Signed: {
const bool is_signed = format == EtcFormat::kEacR11Signed ||
format == EtcFormat::kEacRg11Signed;
const int channels = texel_bytes / 2;
for (int channel = 0; channel < channels; ++channel) {
const std::uint16_t value =
DecodeR11(block + channel * 8, texel, is_signed);
out[channel * 2] = static_cast<std::uint8_t>(value);
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
}
break;
}
}
}
}
}
}
return true;
}
} // namespace mocktail::graphics
+848
View File
@@ -0,0 +1,848 @@
#include "mocktail/graphics/vulkan_etc2_emulation.h"
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <vector>
namespace mocktail::graphics {
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out) {
Etc2EmulatedFormat mapped;
switch (format) {
case VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8};
break;
case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8};
break;
case VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgb8A1};
break;
case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgb8A1};
break;
case VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_UNORM, EtcFormat::kEtc2Rgba8};
break;
case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
mapped = {VK_FORMAT_R8G8B8A8_SRGB, EtcFormat::kEtc2Rgba8};
break;
case VK_FORMAT_EAC_R11_UNORM_BLOCK:
mapped = {VK_FORMAT_R16_UNORM, EtcFormat::kEacR11};
break;
case VK_FORMAT_EAC_R11_SNORM_BLOCK:
mapped = {VK_FORMAT_R16_SNORM, EtcFormat::kEacR11Signed};
break;
case VK_FORMAT_EAC_R11G11_UNORM_BLOCK:
mapped = {VK_FORMAT_R16G16_UNORM, EtcFormat::kEacRg11};
break;
case VK_FORMAT_EAC_R11G11_SNORM_BLOCK:
mapped = {VK_FORMAT_R16G16_SNORM, EtcFormat::kEacRg11Signed};
break;
default:
return false;
}
if (out != nullptr) {
*out = mapped;
}
return true;
}
VkFormatFeatureFlags EmulatedEtc2FormatFeatures(
VkFormatFeatureFlags host_features) {
constexpr VkFormatFeatureFlags kCompressedFeatures =
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT |
VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
return host_features & kCompressedFeatures;
}
bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
std::uint32_t layer_count, VkDeviceSize* compressed,
VkDeviceSize* decoded) {
if (extent.width == 0 || extent.height == 0 || extent.depth != 1 ||
layer_count == 0 || compressed == nullptr || decoded == nullptr) {
return false;
}
const VkDeviceSize blocks =
((static_cast<VkDeviceSize>(extent.width) + 3) / 4) *
((static_cast<VkDeviceSize>(extent.height) + 3) / 4);
*compressed = blocks * EtcBlockBytes(format) * layer_count;
*decoded = static_cast<VkDeviceSize>(extent.width) * extent.height *
EtcDecodedTexelBytes(format) * layer_count;
return true;
}
namespace {
bool EmulationDisabledByEnvironment() {
static const bool disabled = [] {
const char* value = std::getenv("MOCKTAIL_DISABLE_ETC2_EMULATION");
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}();
return disabled;
}
bool ShouldLog(std::atomic<unsigned>* counter, unsigned limit) {
return counter->fetch_add(1, std::memory_order_relaxed) < limit;
}
struct HostDevice {
VkDevice device = VK_NULL_HANDLE;
bool emulated = false;
VkPhysicalDeviceMemoryProperties memory{};
PFN_vkCreateImage create_image = nullptr;
PFN_vkDestroyImage destroy_image = nullptr;
PFN_vkCreateImageView create_image_view = nullptr;
PFN_vkBindBufferMemory bind_buffer_memory = nullptr;
PFN_vkBindBufferMemory2 bind_buffer_memory2 = nullptr;
PFN_vkMapMemory map_memory = nullptr;
PFN_vkMapMemory2 map_memory2 = nullptr;
PFN_vkUnmapMemory unmap_memory = nullptr;
PFN_vkUnmapMemory2 unmap_memory2 = nullptr;
PFN_vkFreeMemory free_memory = nullptr;
PFN_vkDestroyBuffer destroy_buffer = nullptr;
PFN_vkCmdCopyBufferToImage copy_buffer_to_image = nullptr;
PFN_vkCmdCopyBufferToImage2 copy_buffer_to_image2 = nullptr;
PFN_vkCmdExecuteCommands execute_commands = nullptr;
PFN_vkCreateBuffer create_buffer = nullptr;
PFN_vkAllocateMemory allocate_memory = nullptr;
PFN_vkGetBufferMemoryRequirements get_buffer_memory_requirements = nullptr;
};
template <typename Function>
Function DeviceProc(PFN_vkGetDeviceProcAddr get, VkDevice device,
const char* name, const char* alias = nullptr) {
PFN_vkVoidFunction proc = get(device, name);
if (proc == nullptr && alias != nullptr) {
proc = get(device, alias);
}
return reinterpret_cast<Function>(proc);
}
struct BufferBinding {
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
};
struct Mapping {
void* data = nullptr;
VkDeviceSize offset = 0;
VkDeviceSize size = VK_WHOLE_SIZE;
};
struct ImageRecord {
VkDevice device = VK_NULL_HANDLE;
Etc2EmulatedFormat format;
};
struct Staging {
VkDevice device = VK_NULL_HANDLE;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
};
struct PendingUpload {
VkDevice device = VK_NULL_HANDLE;
VkBuffer source = VK_NULL_HANDLE;
VkDeviceSize source_offset = 0;
VkDeviceSize compressed = 0;
VkDeviceSize decoded = 0;
VkDeviceSize target_offset = 0;
EtcFormat format = EtcFormat::kEtc2Rgb8;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t layers = 0;
std::uint8_t* target = nullptr;
};
struct CommandRecord {
std::vector<PendingUpload> uploads;
std::vector<Staging> staging;
std::vector<VkCommandBuffer> secondaries;
};
std::atomic<unsigned> g_image_logs{0};
std::atomic<unsigned> g_decode_logs{0};
std::atomic<unsigned> g_failure_logs{0};
void LogFailure(const char* message) {
if (ShouldLog(&g_failure_logs, 8)) {
std::fprintf(stderr, " [vulkan] ETC2 emulation: %s\n", message);
}
}
bool CreateStaging(const HostDevice& dev, VkDeviceSize size, Staging* staging,
std::uint8_t** mapped) {
if (dev.create_buffer == nullptr || dev.allocate_memory == nullptr ||
dev.get_buffer_memory_requirements == nullptr ||
dev.bind_buffer_memory == nullptr || dev.map_memory == nullptr ||
dev.destroy_buffer == nullptr || dev.free_memory == nullptr ||
size == 0) {
return false;
}
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = size;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer buffer = VK_NULL_HANDLE;
if (dev.create_buffer(dev.device, &buffer_info, nullptr, &buffer) !=
VK_SUCCESS) {
return false;
}
VkMemoryRequirements requirements{};
dev.get_buffer_memory_requirements(dev.device, buffer, &requirements);
constexpr VkMemoryPropertyFlags kRequired =
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
std::uint32_t type = UINT32_MAX;
for (std::uint32_t index = 0; index < dev.memory.memoryTypeCount; ++index) {
if ((requirements.memoryTypeBits & (1U << index)) != 0 &&
(dev.memory.memoryTypes[index].propertyFlags & kRequired) ==
kRequired) {
type = index;
break;
}
}
VkDeviceMemory memory = VK_NULL_HANDLE;
VkMemoryAllocateInfo allocate_info{};
allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate_info.allocationSize = requirements.size;
allocate_info.memoryTypeIndex = type;
void* data = nullptr;
if (type == UINT32_MAX ||
dev.allocate_memory(dev.device, &allocate_info, nullptr, &memory) !=
VK_SUCCESS) {
dev.destroy_buffer(dev.device, buffer, nullptr);
return false;
}
if (dev.bind_buffer_memory(dev.device, buffer, memory, 0) != VK_SUCCESS ||
dev.map_memory(dev.device, memory, 0, VK_WHOLE_SIZE, 0, &data) !=
VK_SUCCESS) {
dev.destroy_buffer(dev.device, buffer, nullptr);
dev.free_memory(dev.device, memory, nullptr);
return false;
}
*staging = {dev.device, buffer, memory};
*mapped = static_cast<std::uint8_t*>(data);
return true;
}
// Freeing mapped memory unmaps it implicitly.
void DestroyStaging(const HostDevice& dev, const Staging& staging) {
if (dev.destroy_buffer != nullptr) {
dev.destroy_buffer(dev.device, staging.buffer, nullptr);
}
if (dev.free_memory != nullptr) {
dev.free_memory(dev.device, staging.memory, nullptr);
}
}
template <typename Region>
bool PlanRegions(const Etc2EmulatedFormat& format, VkDevice device,
VkBuffer source, std::uint32_t count, const Region* regions,
std::vector<Region>* rewritten,
std::vector<PendingUpload>* uploads, VkDeviceSize* total) {
*total = 0;
if (regions == nullptr || count == 0) {
return false;
}
for (std::uint32_t index = 0; index < count; ++index) {
const Region& region = regions[index];
PendingUpload upload;
if (!Etc2UploadByteCounts(format.etc_format, region.imageExtent,
region.imageSubresource.layerCount,
&upload.compressed, &upload.decoded)) {
return false;
}
upload.device = device;
upload.source = source;
upload.source_offset = region.bufferOffset;
upload.target_offset = *total;
upload.format = format.etc_format;
upload.width = region.imageExtent.width;
upload.height = region.imageExtent.height;
upload.layers = region.imageSubresource.layerCount;
Region copy = region;
copy.bufferOffset = *total;
rewritten->push_back(copy);
uploads->push_back(upload);
*total += upload.decoded;
}
return true;
}
} // namespace
struct VulkanEtc2Emulation::State {
std::mutex mutex;
std::mutex devices_mutex;
std::unordered_map<VkPhysicalDevice, bool> physical_devices;
std::vector<std::unique_ptr<HostDevice>> devices;
std::unordered_map<VkImage, ImageRecord> images;
std::unordered_map<VkBuffer, BufferBinding> buffers;
std::unordered_map<VkDeviceMemory, Mapping> mappings;
std::unordered_map<VkCommandBuffer, CommandRecord> commands;
std::atomic<bool> has_commands{false};
const HostDevice* Find(VkDevice device) {
std::lock_guard<std::mutex> lock(devices_mutex);
for (const auto& candidate : devices) {
if (candidate->device == device) {
return candidate.get();
}
}
return nullptr;
}
bool LookupImage(const HostDevice& dev, VkImage image,
Etc2EmulatedFormat* format) {
if (!dev.emulated) {
return false;
}
std::lock_guard<std::mutex> lock(mutex);
const auto found = images.find(image);
if (found == images.end()) {
return false;
}
*format = found->second.format;
return true;
}
void Record(VkCommandBuffer command_buffer,
std::vector<PendingUpload> uploads, const Staging& staging) {
std::lock_guard<std::mutex> lock(mutex);
CommandRecord& record = commands[command_buffer];
record.uploads.insert(record.uploads.end(), uploads.begin(),
uploads.end());
record.staging.push_back(staging);
has_commands.store(true, std::memory_order_release);
}
};
VulkanEtc2Emulation::VulkanEtc2Emulation() : state_(new State) {}
VulkanEtc2Emulation::~VulkanEtc2Emulation() { delete state_; }
bool VulkanEtc2Emulation::PhysicalDeviceNeedsEmulation(
VkPhysicalDevice physical_device, PFN_vkGetPhysicalDeviceFeatures host) {
if (EmulationDisabledByEnvironment() || host == nullptr ||
physical_device == VK_NULL_HANDLE) {
return false;
}
{
std::lock_guard<std::mutex> lock(state_->mutex);
const auto found = state_->physical_devices.find(physical_device);
if (found != state_->physical_devices.end()) {
return found->second;
}
}
VkPhysicalDeviceFeatures features{};
host(physical_device, &features);
const bool needed = features.textureCompressionETC2 != VK_TRUE;
std::lock_guard<std::mutex> lock(state_->mutex);
state_->physical_devices[physical_device] = needed;
return needed;
}
void VulkanEtc2Emulation::RegisterDevice(
VkDevice device, VkPhysicalDevice physical_device, bool emulated,
const VkPhysicalDeviceMemoryProperties& memory,
PFN_vkGetDeviceProcAddr get_device_proc_addr) {
static_cast<void>(physical_device);
if (device == VK_NULL_HANDLE || get_device_proc_addr == nullptr) {
return;
}
auto dev = std::make_unique<HostDevice>();
const auto get = get_device_proc_addr;
dev->device = device;
dev->emulated = emulated;
dev->memory = memory;
dev->create_image = DeviceProc<PFN_vkCreateImage>(get, device, "vkCreateImage");
dev->destroy_image =
DeviceProc<PFN_vkDestroyImage>(get, device, "vkDestroyImage");
dev->create_image_view =
DeviceProc<PFN_vkCreateImageView>(get, device, "vkCreateImageView");
dev->bind_buffer_memory =
DeviceProc<PFN_vkBindBufferMemory>(get, device, "vkBindBufferMemory");
dev->bind_buffer_memory2 = DeviceProc<PFN_vkBindBufferMemory2>(
get, device, "vkBindBufferMemory2", "vkBindBufferMemory2KHR");
dev->map_memory = DeviceProc<PFN_vkMapMemory>(get, device, "vkMapMemory");
dev->map_memory2 = DeviceProc<PFN_vkMapMemory2>(get, device, "vkMapMemory2",
"vkMapMemory2KHR");
dev->unmap_memory =
DeviceProc<PFN_vkUnmapMemory>(get, device, "vkUnmapMemory");
dev->unmap_memory2 = DeviceProc<PFN_vkUnmapMemory2>(
get, device, "vkUnmapMemory2", "vkUnmapMemory2KHR");
dev->free_memory = DeviceProc<PFN_vkFreeMemory>(get, device, "vkFreeMemory");
dev->destroy_buffer =
DeviceProc<PFN_vkDestroyBuffer>(get, device, "vkDestroyBuffer");
dev->copy_buffer_to_image = DeviceProc<PFN_vkCmdCopyBufferToImage>(
get, device, "vkCmdCopyBufferToImage");
dev->copy_buffer_to_image2 = DeviceProc<PFN_vkCmdCopyBufferToImage2>(
get, device, "vkCmdCopyBufferToImage2", "vkCmdCopyBufferToImage2KHR");
dev->execute_commands =
DeviceProc<PFN_vkCmdExecuteCommands>(get, device, "vkCmdExecuteCommands");
dev->create_buffer =
DeviceProc<PFN_vkCreateBuffer>(get, device, "vkCreateBuffer");
dev->allocate_memory =
DeviceProc<PFN_vkAllocateMemory>(get, device, "vkAllocateMemory");
dev->get_buffer_memory_requirements =
DeviceProc<PFN_vkGetBufferMemoryRequirements>(
get, device, "vkGetBufferMemoryRequirements");
if (emulated) {
std::fprintf(stderr,
" [vulkan] ETC2/EAC emulation enabled: compressed uploads "
"decode to RGBA8/R16 host images\n");
}
std::lock_guard<std::mutex> lock(state_->devices_mutex);
state_->devices.erase(
std::remove_if(state_->devices.begin(), state_->devices.end(),
[device](const std::unique_ptr<HostDevice>& candidate) {
return candidate->device == device;
}),
state_->devices.end());
state_->devices.push_back(std::move(dev));
}
void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
std::vector<Staging> doomed;
{
std::lock_guard<std::mutex> lock(state_->mutex);
for (auto it = state_->commands.begin(); it != state_->commands.end();) {
const bool owned = std::any_of(
it->second.staging.begin(), it->second.staging.end(),
[device](const Staging& staging) { return staging.device == device; });
if (owned) {
doomed.insert(doomed.end(), it->second.staging.begin(),
it->second.staging.end());
it = state_->commands.erase(it);
} else {
++it;
}
}
for (auto it = state_->images.begin(); it != state_->images.end();) {
it = it->second.device == device ? state_->images.erase(it) : std::next(it);
}
state_->has_commands.store(!state_->commands.empty(),
std::memory_order_release);
}
if (const HostDevice* dev = state_->Find(device); dev != nullptr) {
for (const Staging& staging : doomed) {
DestroyStaging(*dev, staging);
}
}
std::lock_guard<std::mutex> lock(state_->devices_mutex);
state_->devices.erase(
std::remove_if(state_->devices.begin(), state_->devices.end(),
[device](const std::unique_ptr<HostDevice>& candidate) {
return candidate->device == device;
}),
state_->devices.end());
}
VkResult VulkanEtc2Emulation::CreateImage(VkDevice device,
const VkImageCreateInfo* create_info,
const VkAllocationCallbacks* allocator,
VkImage* image) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->create_image == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
Etc2EmulatedFormat format;
if (!dev->emulated || create_info == nullptr ||
!LookupEmulatedEtc2Format(create_info->format, &format)) {
return dev->create_image(device, create_info, allocator, image);
}
VkImageCreateInfo host_info = *create_info;
host_info.format = format.host_format;
host_info.flags &= ~static_cast<VkImageCreateFlags>(
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT);
const VkResult result = dev->create_image(device, &host_info, allocator, image);
if (result == VK_SUCCESS && image != nullptr) {
{
std::lock_guard<std::mutex> lock(state_->mutex);
state_->images[*image] = {device, format};
}
if (ShouldLog(&g_image_logs, 4)) {
std::fprintf(stderr,
" [vulkan] ETC2 image %ux%u mips=%u format=%d -> host "
"format=%d\n",
create_info->extent.width, create_info->extent.height,
create_info->mipLevels,
static_cast<int>(create_info->format),
static_cast<int>(format.host_format));
}
}
return result;
}
void VulkanEtc2Emulation::DestroyImage(VkDevice device, VkImage image,
const VkAllocationCallbacks* allocator) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->destroy_image == nullptr) {
return;
}
if (dev->emulated) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->images.erase(image);
}
dev->destroy_image(device, image, allocator);
}
VkResult VulkanEtc2Emulation::CreateImageView(
VkDevice device, const VkImageViewCreateInfo* create_info,
const VkAllocationCallbacks* allocator, VkImageView* view) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->create_image_view == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
Etc2EmulatedFormat format;
if (!dev->emulated || create_info == nullptr ||
!LookupEmulatedEtc2Format(create_info->format, &format)) {
return dev->create_image_view(device, create_info, allocator, view);
}
VkImageViewCreateInfo host_info = *create_info;
host_info.format = format.host_format;
return dev->create_image_view(device, &host_info, allocator, view);
}
VkResult VulkanEtc2Emulation::BindBufferMemory(VkDevice device, VkBuffer buffer,
VkDeviceMemory memory,
VkDeviceSize offset) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->bind_buffer_memory == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
const VkResult result =
dev->bind_buffer_memory(device, buffer, memory, offset);
if (result == VK_SUCCESS && dev->emulated) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->buffers[buffer] = {memory, offset};
}
return result;
}
VkResult VulkanEtc2Emulation::BindBufferMemory2(
VkDevice device, std::uint32_t count, const VkBindBufferMemoryInfo* infos) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->bind_buffer_memory2 == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
const VkResult result = dev->bind_buffer_memory2(device, count, infos);
if (result == VK_SUCCESS && dev->emulated && infos != nullptr) {
std::lock_guard<std::mutex> lock(state_->mutex);
for (std::uint32_t index = 0; index < count; ++index) {
state_->buffers[infos[index].buffer] = {infos[index].memory,
infos[index].memoryOffset};
}
}
return result;
}
VkResult VulkanEtc2Emulation::MapMemory(VkDevice device, VkDeviceMemory memory,
VkDeviceSize offset, VkDeviceSize size,
VkMemoryMapFlags flags, void** data) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->map_memory == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
const VkResult result =
dev->map_memory(device, memory, offset, size, flags, data);
if (result == VK_SUCCESS && dev->emulated && data != nullptr) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->mappings[memory] = {*data, offset, size};
}
return result;
}
VkResult VulkanEtc2Emulation::MapMemory2(VkDevice device,
const VkMemoryMapInfo* info,
void** data) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->map_memory2 == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
const VkResult result = dev->map_memory2(device, info, data);
if (result == VK_SUCCESS && dev->emulated && info != nullptr &&
data != nullptr) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->mappings[info->memory] = {*data, info->offset, info->size};
}
return result;
}
void VulkanEtc2Emulation::UnmapMemory(VkDevice device, VkDeviceMemory memory) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->unmap_memory == nullptr) {
return;
}
if (dev->emulated) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->mappings.erase(memory);
}
dev->unmap_memory(device, memory);
}
VkResult VulkanEtc2Emulation::UnmapMemory2(VkDevice device,
const VkMemoryUnmapInfo* info) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->unmap_memory2 == nullptr) {
return VK_ERROR_INITIALIZATION_FAILED;
}
if (dev->emulated && info != nullptr) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->mappings.erase(info->memory);
}
return dev->unmap_memory2(device, info);
}
void VulkanEtc2Emulation::FreeMemory(VkDevice device, VkDeviceMemory memory,
const VkAllocationCallbacks* allocator) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->free_memory == nullptr) {
return;
}
if (dev->emulated) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->mappings.erase(memory);
}
dev->free_memory(device, memory, allocator);
}
void VulkanEtc2Emulation::DestroyBuffer(VkDevice device, VkBuffer buffer,
const VkAllocationCallbacks* allocator) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->destroy_buffer == nullptr) {
return;
}
if (dev->emulated) {
std::lock_guard<std::mutex> lock(state_->mutex);
state_->buffers.erase(buffer);
}
dev->destroy_buffer(device, buffer, allocator);
}
void VulkanEtc2Emulation::CmdCopyBufferToImage(
VkDevice device, VkCommandBuffer command_buffer, VkBuffer source,
VkImage destination, VkImageLayout layout, std::uint32_t region_count,
const VkBufferImageCopy* regions) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->copy_buffer_to_image == nullptr) {
return;
}
Etc2EmulatedFormat format;
if (!state_->LookupImage(*dev, destination, &format)) {
dev->copy_buffer_to_image(command_buffer, source, destination, layout,
region_count, regions);
return;
}
std::vector<VkBufferImageCopy> rewritten;
std::vector<PendingUpload> uploads;
VkDeviceSize total = 0;
Staging staging;
std::uint8_t* mapped = nullptr;
if (!PlanRegions(format, device, source, region_count, regions, &rewritten,
&uploads, &total)) {
LogFailure("skipped an upload with an unsupported region");
return;
}
if (!CreateStaging(*dev, total, &staging, &mapped)) {
LogFailure("could not allocate a host-visible staging buffer");
return;
}
for (PendingUpload& upload : uploads) {
upload.target = mapped + upload.target_offset;
}
state_->Record(command_buffer, std::move(uploads), staging);
dev->copy_buffer_to_image(command_buffer, staging.buffer, destination,
layout, region_count, rewritten.data());
}
void VulkanEtc2Emulation::CmdCopyBufferToImage2(
VkDevice device, VkCommandBuffer command_buffer,
const VkCopyBufferToImageInfo2* info) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->copy_buffer_to_image2 == nullptr ||
info == nullptr) {
return;
}
Etc2EmulatedFormat format;
if (!state_->LookupImage(*dev, info->dstImage, &format)) {
dev->copy_buffer_to_image2(command_buffer, info);
return;
}
std::vector<VkBufferImageCopy2> rewritten;
std::vector<PendingUpload> uploads;
VkDeviceSize total = 0;
Staging staging;
std::uint8_t* mapped = nullptr;
if (!PlanRegions(format, device, info->srcBuffer, info->regionCount,
info->pRegions, &rewritten, &uploads, &total)) {
LogFailure("skipped an upload with an unsupported region");
return;
}
if (!CreateStaging(*dev, total, &staging, &mapped)) {
LogFailure("could not allocate a host-visible staging buffer");
return;
}
for (PendingUpload& upload : uploads) {
upload.target = mapped + upload.target_offset;
}
state_->Record(command_buffer, std::move(uploads), staging);
VkCopyBufferToImageInfo2 host_info = *info;
host_info.srcBuffer = staging.buffer;
host_info.pRegions = rewritten.data();
dev->copy_buffer_to_image2(command_buffer, &host_info);
}
void VulkanEtc2Emulation::CmdExecuteCommands(
VkDevice device, VkCommandBuffer command_buffer, std::uint32_t count,
const VkCommandBuffer* secondaries) {
const HostDevice* dev = state_->Find(device);
if (dev == nullptr || dev->execute_commands == nullptr) {
return;
}
if (dev->emulated && secondaries != nullptr &&
state_->has_commands.load(std::memory_order_acquire)) {
std::lock_guard<std::mutex> lock(state_->mutex);
for (std::uint32_t index = 0; index < count; ++index) {
if (state_->commands.count(secondaries[index]) != 0) {
state_->commands[command_buffer].secondaries.push_back(
secondaries[index]);
}
}
}
dev->execute_commands(command_buffer, count, secondaries);
}
void VulkanEtc2Emulation::PrepareSubmit(const VkCommandBuffer* command_buffers,
std::uint32_t count) {
if (command_buffers == nullptr || count == 0 ||
!state_->has_commands.load(std::memory_order_acquire)) {
return;
}
struct Work {
PendingUpload upload;
std::uint8_t* source = nullptr;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize memory_offset = 0;
};
std::vector<Work> work;
{
std::lock_guard<std::mutex> lock(state_->mutex);
auto collect = [this, &work](VkCommandBuffer command_buffer) {
const auto record = state_->commands.find(command_buffer);
if (record == state_->commands.end()) {
return;
}
for (const PendingUpload& upload : record->second.uploads) {
const auto binding = state_->buffers.find(upload.source);
if (binding == state_->buffers.end()) {
LogFailure("upload source buffer has no tracked memory binding");
continue;
}
Work item;
item.upload = upload;
const VkDeviceSize absolute =
binding->second.offset + upload.source_offset;
const auto mapping = state_->mappings.find(binding->second.memory);
if (mapping == state_->mappings.end()) {
item.memory = binding->second.memory;
item.memory_offset = absolute;
} else if (absolute >= mapping->second.offset &&
(mapping->second.size == VK_WHOLE_SIZE ||
(absolute - mapping->second.offset <=
mapping->second.size &&
upload.compressed <= mapping->second.size -
(absolute -
mapping->second.offset)))) {
item.source = static_cast<std::uint8_t*>(mapping->second.data) +
(absolute - mapping->second.offset);
} else {
LogFailure("upload source lies outside the mapped range");
continue;
}
work.push_back(item);
}
};
for (std::uint32_t index = 0; index < count; ++index) {
collect(command_buffers[index]);
const auto record = state_->commands.find(command_buffers[index]);
if (record != state_->commands.end()) {
const std::vector<VkCommandBuffer> secondaries =
record->second.secondaries;
for (VkCommandBuffer secondary : secondaries) {
collect(secondary);
}
}
}
}
for (const Work& item : work) {
const PendingUpload& upload = item.upload;
const HostDevice* dev = state_->Find(upload.device);
std::uint8_t* source = item.source;
if (source == nullptr) {
void* data = nullptr;
if (dev == nullptr || dev->map_memory == nullptr ||
dev->map_memory(dev->device, item.memory, 0, VK_WHOLE_SIZE, 0,
&data) != VK_SUCCESS) {
LogFailure("could not map an unmapped upload source");
continue;
}
source = static_cast<std::uint8_t*>(data) + item.memory_offset;
}
const VkDeviceSize compressed_layer = upload.compressed / upload.layers;
const VkDeviceSize decoded_layer = upload.decoded / upload.layers;
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
if (!DecodeEtcImage(upload.format, source + layer * compressed_layer,
compressed_layer, upload.width, upload.height,
upload.target + layer * decoded_layer,
decoded_layer)) {
LogFailure("could not decode an upload layer");
}
}
if (item.source == nullptr && dev != nullptr &&
dev->unmap_memory != nullptr) {
dev->unmap_memory(dev->device, item.memory);
}
if (ShouldLog(&g_decode_logs, 4)) {
std::fprintf(stderr,
" [vulkan] ETC2 upload decoded %ux%u layers=%u\n",
upload.width, upload.height, upload.layers);
}
}
}
void VulkanEtc2Emulation::ReleaseCommandBuffer(VkCommandBuffer command_buffer) {
if (!state_->has_commands.load(std::memory_order_acquire)) {
return;
}
std::vector<Staging> doomed;
{
std::lock_guard<std::mutex> lock(state_->mutex);
const auto record = state_->commands.find(command_buffer);
if (record == state_->commands.end()) {
return;
}
doomed = std::move(record->second.staging);
state_->commands.erase(record);
state_->has_commands.store(!state_->commands.empty(),
std::memory_order_release);
}
for (const Staging& staging : doomed) {
if (const HostDevice* dev = state_->Find(staging.device); dev != nullptr) {
DestroyStaging(*dev, staging);
}
}
}
} // namespace mocktail::graphics
+2
View File
@@ -106,6 +106,8 @@ target_link_libraries(stub_glesv2 PRIVATE dl)
add_library(stub_vulkan SHARED
${CMAKE_SOURCE_DIR}/src/graphics/bionic_vulkan_loader_adapter.cc
${CMAKE_SOURCE_DIR}/src/graphics/vulkan_text_overlay_compositor.cc
${CMAKE_SOURCE_DIR}/src/graphics/vulkan_etc2_emulation.cc
${CMAKE_SOURCE_DIR}/src/graphics/etc2_decoder.cc
)
configure_stub(stub_vulkan "libvulkan.so")
target_include_directories(stub_vulkan PRIVATE ${CMAKE_SOURCE_DIR}/include)
+25 -2
View File
@@ -470,6 +470,27 @@ target_link_libraries(vulkan_text_overlay_compositor_test PRIVATE
)
mocktail_apply_compile_options(vulkan_text_overlay_compositor_test)
add_executable(etc2_decoder_test
etc2_decoder_test.cc
${PROJECT_SOURCE_DIR}/src/graphics/etc2_decoder.cc
)
target_include_directories(etc2_decoder_test PRIVATE
${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(etc2_decoder_test PRIVATE GTest::gtest_main)
mocktail_apply_compile_options(etc2_decoder_test)
add_executable(vulkan_etc2_emulation_test
vulkan_etc2_emulation_test.cc
${PROJECT_SOURCE_DIR}/src/graphics/vulkan_etc2_emulation.cc
${PROJECT_SOURCE_DIR}/src/graphics/etc2_decoder.cc
)
target_include_directories(vulkan_etc2_emulation_test PRIVATE
${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(vulkan_etc2_emulation_test PRIVATE GTest::gtest_main)
mocktail_apply_compile_options(vulkan_etc2_emulation_test)
add_executable(video_driver_policy_test
video_driver_policy_test.cc
)
@@ -885,6 +906,8 @@ gtest_discover_tests(failure_dialog_test)
gtest_discover_tests(frame_rate_policy_test)
gtest_discover_tests(performance_policy_test)
gtest_discover_tests(texture_memory_policy_test)
gtest_discover_tests(device_memory_profile_test)
gtest_discover_tests(display_size_test)
gtest_discover_tests(http_client_policy_test)
gtest_discover_tests(http_client_spin_guard_test)
gtest_discover_tests(crash_report_policy_test)
@@ -906,8 +929,6 @@ gtest_discover_tests(external_launch_broker_test)
gtest_discover_tests(webview_helper_launcher_test)
gtest_discover_tests(webview_helper_policy_test)
gtest_discover_tests(webview_roblox_cookie_test)
gtest_discover_tests(device_memory_profile_test)
gtest_discover_tests(display_size_test)
gtest_discover_tests(startup_state_machine_test)
gtest_discover_tests(game_session_coordinator_test)
gtest_discover_tests(owned_pthread_test)
@@ -929,6 +950,8 @@ gtest_discover_tests(input_pump_pacer_test)
gtest_discover_tests(vulkan_surface_recovery_gate_test)
gtest_discover_tests(vulkan_present_progress_gate_test)
gtest_discover_tests(vulkan_text_overlay_compositor_test)
gtest_discover_tests(etc2_decoder_test)
gtest_discover_tests(vulkan_etc2_emulation_test)
gtest_discover_tests(video_driver_policy_test)
gtest_discover_tests(window_surface_lifecycle_test)
gtest_discover_tests(window_creation_policy_test)
+196
View File
@@ -0,0 +1,196 @@
#include "mocktail/graphics/etc2_decoder.h"
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <vector>
namespace mocktail::graphics {
namespace {
using Block8 = std::array<std::uint8_t, 8>;
struct Rgba {
int r;
int g;
int b;
int a;
};
std::vector<std::uint8_t> Decode(EtcFormat format,
const std::vector<std::uint8_t>& source,
std::uint32_t width, std::uint32_t height) {
std::vector<std::uint8_t> output(
static_cast<std::size_t>(width) * height * EtcDecodedTexelBytes(format),
0xAB);
EXPECT_TRUE(DecodeEtcImage(format, source.data(), source.size(), width,
height, output.data(), output.size()));
return output;
}
std::vector<std::uint8_t> Bytes(const Block8& block) {
return {block.begin(), block.end()};
}
void ExpectRgba(const std::vector<std::uint8_t>& texels, std::uint32_t width,
std::uint32_t x, std::uint32_t y, Rgba expected) {
const std::size_t offset = (static_cast<std::size_t>(y) * width + x) * 4;
ASSERT_LT(offset + 3, texels.size());
EXPECT_EQ(texels[offset], expected.r) << "x=" << x << " y=" << y;
EXPECT_EQ(texels[offset + 1], expected.g) << "x=" << x << " y=" << y;
EXPECT_EQ(texels[offset + 2], expected.b) << "x=" << x << " y=" << y;
EXPECT_EQ(texels[offset + 3], expected.a) << "x=" << x << " y=" << y;
}
std::uint16_t Unsigned16(const std::vector<std::uint8_t>& texels,
std::size_t index) {
return static_cast<std::uint16_t>(texels[index * 2] |
(texels[index * 2 + 1] << 8));
}
std::int16_t Signed16(const std::vector<std::uint8_t>& texels,
std::size_t index) {
return static_cast<std::int16_t>(Unsigned16(texels, index));
}
TEST(Etc2DecoderTest, ReportsBlockAndTexelSizes) {
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8), 8U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8A1), 8U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgba8), 16U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11), 8U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11Signed), 8U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11), 16U);
EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11Signed), 16U);
EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8), 4U);
EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8A1), 4U);
EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgba8), 4U);
EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacR11), 2U);
EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacRg11Signed), 4U);
}
TEST(Etc2DecoderTest, DecodesIndividualMode) {
// Base colours 8 (left sub-block) and 0 (right), codeword 0 (2/8).
// Pixel (0,0) index 1 (+8), pixels in column 1 index 2 (-2).
const Block8 block = {0x80, 0x80, 0x80, 0x00, 0x00, 0xF0, 0x00, 0x01};
const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {144, 144, 144, 255});
ExpectRgba(texels, 4, 0, 1, {138, 138, 138, 255});
ExpectRgba(texels, 4, 1, 0, {134, 134, 134, 255});
ExpectRgba(texels, 4, 2, 0, {2, 2, 2, 255});
}
TEST(Etc2DecoderTest, DecodesDifferentialMode) {
// R/G/B = 16 with delta +1: base colours 132 and 140, modifier +2.
const Block8 block = {0x81, 0x81, 0x81, 0x02, 0x00, 0x00, 0x00, 0x00};
const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {134, 134, 134, 255});
ExpectRgba(texels, 4, 2, 3, {142, 142, 142, 255});
}
TEST(Etc2DecoderTest, DecodesTMode) {
// Red overflows: colour 1 (0,255,0), colour 2 (0,255,136), distance 11.
const Block8 block = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03};
const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {11, 255, 147, 255});
ExpectRgba(texels, 4, 0, 1, {0, 244, 125, 255});
ExpectRgba(texels, 4, 1, 0, {0, 255, 136, 255});
ExpectRgba(texels, 4, 1, 1, {0, 255, 0, 255});
}
TEST(Etc2DecoderTest, DecodesHMode) {
// Green overflows: colour 1 (0,0,0), colour 2 (255,17,255), distance 3.
const Block8 block = {0x00, 0x04, 0x78, 0xFA, 0x80, 0x01, 0x80, 0x00};
const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {255, 20, 255, 255});
ExpectRgba(texels, 4, 3, 3, {252, 14, 252, 255});
ExpectRgba(texels, 4, 1, 0, {3, 3, 3, 255});
}
TEST(Etc2DecoderTest, DecodesPlanarMode) {
// Blue overflows: O=(130,64,0), H=(0,255,0), V=(255,0,255).
const Block8 block = {0x40, 0x40, 0x04, 0x02, 0xFE, 0x07, 0xE0, 0x3F};
const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {130, 64, 0, 255});
ExpectRgba(texels, 4, 3, 0, {33, 207, 0, 255});
ExpectRgba(texels, 4, 0, 3, {224, 16, 191, 255});
ExpectRgba(texels, 4, 3, 3, {126, 159, 191, 255});
}
TEST(Etc2DecoderTest, DecodesPunchthroughAlpha) {
// Opaque bit clear: index 2 is transparent black, index 0 has no modifier.
const Block8 block = {0x81, 0x81, 0x81, 0x00, 0x00, 0x01, 0x00, 0x02};
const auto texels = Decode(EtcFormat::kEtc2Rgb8A1, Bytes(block), 4, 4);
ExpectRgba(texels, 4, 0, 0, {0, 0, 0, 0});
ExpectRgba(texels, 4, 0, 1, {140, 140, 140, 255});
ExpectRgba(texels, 4, 1, 0, {132, 132, 132, 255});
ExpectRgba(texels, 4, 2, 0, {140, 140, 140, 255});
}
TEST(Etc2DecoderTest, DecodesRgba8WithAlphaBlockFirst) {
// Alpha: base 100, multiplier 2, table 0; pixel (0,0) index 7 (+14).
const std::vector<std::uint8_t> block = {
100, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const auto texels = Decode(EtcFormat::kEtc2Rgba8, block, 4, 4);
ExpectRgba(texels, 4, 0, 0, {138, 138, 138, 128});
ExpectRgba(texels, 4, 1, 0, {138, 138, 138, 94});
}
TEST(Etc2DecoderTest, DecodesUnsignedR11) {
const Block8 block = {128, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00};
const auto texels = Decode(EtcFormat::kEacR11, Bytes(block), 4, 4);
EXPECT_EQ(Unsigned16(texels, 0), 36497);
EXPECT_EQ(Unsigned16(texels, 1), 32143);
const Block8 zero_multiplier = {0, 0x00, 0, 0, 0, 0, 0, 0};
const auto flat = Decode(EtcFormat::kEacR11, Bytes(zero_multiplier), 1, 1);
EXPECT_EQ(Unsigned16(flat, 0), 32);
}
TEST(Etc2DecoderTest, DecodesSignedR11) {
const Block8 block = {0x80, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00};
const auto texels = Decode(EtcFormat::kEacR11Signed, Bytes(block), 4, 4);
EXPECT_EQ(Signed16(texels, 0), -28956);
EXPECT_EQ(Signed16(texels, 1), -32767);
}
TEST(Etc2DecoderTest, DecodesRg11AsTwoChannels) {
const std::vector<std::uint8_t> block = {
128, 0x10, 0xE0, 0, 0, 0, 0, 0,
0, 0x00, 0x00, 0, 0, 0, 0, 0,
};
const auto texels = Decode(EtcFormat::kEacRg11, block, 1, 1);
EXPECT_EQ(Unsigned16(texels, 0), 36497);
EXPECT_EQ(Unsigned16(texels, 1), 32);
}
TEST(Etc2DecoderTest, DecodesPartialAndMultipleBlocks) {
const Block8 t_mode = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03};
const Block8 differential = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0};
std::vector<std::uint8_t> source = Bytes(t_mode);
source.insert(source.end(), differential.begin(), differential.end());
const auto texels = Decode(EtcFormat::kEtc2Rgb8, source, 5, 1);
ExpectRgba(texels, 5, 0, 0, {11, 255, 147, 255});
ExpectRgba(texels, 5, 1, 0, {0, 255, 136, 255});
ExpectRgba(texels, 5, 4, 0, {134, 134, 134, 255});
}
TEST(Etc2DecoderTest, RejectsShortBuffers) {
const std::vector<std::uint8_t> source(8, 0);
std::vector<std::uint8_t> output(4 * 4 * 4, 0);
EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), 7, 4, 4,
output.data(), output.size()));
EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(),
source.size(), 4, 4, output.data(),
output.size() - 1));
EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(),
source.size(), 5, 4, output.data(),
output.size()));
EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, nullptr, 0, 0, 0,
output.data(), output.size()));
}
} // namespace
} // namespace mocktail::graphics
+279
View File
@@ -0,0 +1,279 @@
#include "mocktail/graphics/vulkan_etc2_emulation.h"
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
namespace mocktail::graphics {
namespace {
void ExpectMapping(VkFormat format, VkFormat host, EtcFormat etc) {
Etc2EmulatedFormat mapped;
ASSERT_TRUE(LookupEmulatedEtc2Format(format, &mapped)) << format;
EXPECT_EQ(mapped.host_format, host) << format;
EXPECT_EQ(mapped.etc_format, etc) << format;
}
TEST(VulkanEtc2EmulationTest, MapsEveryEtc2AndEacFormat) {
ExpectMapping(VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM,
EtcFormat::kEtc2Rgb8);
ExpectMapping(VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB,
EtcFormat::kEtc2Rgb8);
ExpectMapping(VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM,
EtcFormat::kEtc2Rgb8A1);
ExpectMapping(VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB,
EtcFormat::kEtc2Rgb8A1);
ExpectMapping(VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM,
EtcFormat::kEtc2Rgba8);
ExpectMapping(VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB,
EtcFormat::kEtc2Rgba8);
ExpectMapping(VK_FORMAT_EAC_R11_UNORM_BLOCK, VK_FORMAT_R16_UNORM,
EtcFormat::kEacR11);
ExpectMapping(VK_FORMAT_EAC_R11_SNORM_BLOCK, VK_FORMAT_R16_SNORM,
EtcFormat::kEacR11Signed);
ExpectMapping(VK_FORMAT_EAC_R11G11_UNORM_BLOCK, VK_FORMAT_R16G16_UNORM,
EtcFormat::kEacRg11);
ExpectMapping(VK_FORMAT_EAC_R11G11_SNORM_BLOCK, VK_FORMAT_R16G16_SNORM,
EtcFormat::kEacRg11Signed);
}
TEST(VulkanEtc2EmulationTest, IgnoresOtherFormats) {
Etc2EmulatedFormat mapped;
EXPECT_FALSE(LookupEmulatedEtc2Format(VK_FORMAT_R8G8B8A8_UNORM, &mapped));
EXPECT_FALSE(LookupEmulatedEtc2Format(VK_FORMAT_BC7_UNORM_BLOCK, &mapped));
EXPECT_FALSE(
LookupEmulatedEtc2Format(VK_FORMAT_ASTC_4x4_UNORM_BLOCK, &mapped));
}
TEST(VulkanEtc2EmulationTest, KeepsOnlySamplingAndTransferFeatures) {
const VkFormatFeatureFlags host =
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT |
VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT |
VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT |
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT |
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
EXPECT_EQ(EmulatedEtc2FormatFeatures(host),
static_cast<VkFormatFeatureFlags>(
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT |
VK_FORMAT_FEATURE_BLIT_SRC_BIT |
VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
VK_FORMAT_FEATURE_TRANSFER_DST_BIT));
EXPECT_EQ(EmulatedEtc2FormatFeatures(VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT),
0U);
}
TEST(VulkanEtc2EmulationTest, CountsUploadBytesPerRegion) {
VkDeviceSize compressed = 0;
VkDeviceSize decoded = 0;
ASSERT_TRUE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgba8, {5, 3, 1}, 2,
&compressed, &decoded));
EXPECT_EQ(compressed, 64U);
EXPECT_EQ(decoded, 120U);
ASSERT_TRUE(Etc2UploadByteCounts(EtcFormat::kEacR11, {1, 1, 1}, 1,
&compressed, &decoded));
EXPECT_EQ(compressed, 8U);
EXPECT_EQ(decoded, 2U);
}
TEST(VulkanEtc2EmulationTest, RejectsUnsupportedRegions) {
VkDeviceSize compressed = 0;
VkDeviceSize decoded = 0;
EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {4, 4, 2}, 1,
&compressed, &decoded));
EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {0, 4, 1}, 1,
&compressed, &decoded));
EXPECT_FALSE(Etc2UploadByteCounts(EtcFormat::kEtc2Rgb8, {4, 4, 1}, 0,
&compressed, &decoded));
}
template <typename Handle>
Handle FakeHandle(std::uintptr_t value) {
return reinterpret_cast<Handle>(value);
}
constexpr std::uintptr_t kSourceMemory = 0x300;
constexpr std::uintptr_t kStagingBuffer = 0x500;
constexpr std::uintptr_t kStagingMemory = 0x600;
std::array<std::uint8_t, 64> g_source{};
std::array<std::uint8_t, 64> g_staging{};
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateImage(VkDevice,
const VkImageCreateInfo*,
const VkAllocationCallbacks*,
VkImage* image) {
*image = FakeHandle<VkImage>(0x100);
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateBuffer(VkDevice,
const VkBufferCreateInfo*,
const VkAllocationCallbacks*,
VkBuffer* buffer) {
*buffer = FakeHandle<VkBuffer>(kStagingBuffer);
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeBufferRequirements(
VkDevice, VkBuffer, VkMemoryRequirements* requirements) {
requirements->size = g_staging.size();
requirements->memoryTypeBits = 1;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeAllocateMemory(
VkDevice, const VkMemoryAllocateInfo*, const VkAllocationCallbacks*,
VkDeviceMemory* memory) {
*memory = FakeHandle<VkDeviceMemory>(kStagingMemory);
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeBindBufferMemory(VkDevice, VkBuffer,
VkDeviceMemory,
VkDeviceSize) {
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL FakeMapMemory(VkDevice, VkDeviceMemory memory,
VkDeviceSize offset, VkDeviceSize,
VkMemoryMapFlags, void** data) {
*data = memory == FakeHandle<VkDeviceMemory>(kSourceMemory)
? g_source.data() + offset
: g_staging.data() + offset;
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL FakeUnmapMemory(VkDevice, VkDeviceMemory) {}
VKAPI_ATTR void VKAPI_CALL FakeDestroyBuffer(VkDevice, VkBuffer,
const VkAllocationCallbacks*) {}
VKAPI_ATTR void VKAPI_CALL FakeFreeMemory(VkDevice, VkDeviceMemory,
const VkAllocationCallbacks*) {}
VKAPI_ATTR void VKAPI_CALL FakeCopyBufferToImage(VkCommandBuffer, VkBuffer,
VkImage, VkImageLayout,
std::uint32_t,
const VkBufferImageCopy*) {}
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
VkDevice, const char* name) {
const auto is = [name](const char* candidate) {
return std::strcmp(name, candidate) == 0;
};
if (is("vkCreateImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateImage);
if (is("vkCreateBuffer")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateBuffer);
if (is("vkGetBufferMemoryRequirements"))
return reinterpret_cast<PFN_vkVoidFunction>(FakeBufferRequirements);
if (is("vkAllocateMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeAllocateMemory);
if (is("vkBindBufferMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeBindBufferMemory);
if (is("vkMapMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeMapMemory);
if (is("vkUnmapMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeUnmapMemory);
if (is("vkDestroyBuffer")) return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroyBuffer);
if (is("vkFreeMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeFreeMemory);
if (is("vkCmdCopyBufferToImage"))
return reinterpret_cast<PFN_vkVoidFunction>(FakeCopyBufferToImage);
return nullptr;
}
TEST(VulkanEtc2EmulationTest, SkipsUploadOutsideTheMappedWindow) {
const VkDevice device = FakeHandle<VkDevice>(0x10);
const VkBuffer source = FakeHandle<VkBuffer>(0x200);
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x700);
g_source.fill(0);
g_staging.fill(0xAA);
VkPhysicalDeviceMemoryProperties memory{};
memory.memoryTypeCount = 1;
memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
VulkanEtc2Emulation emulation;
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x20), true,
memory, FakeGetDeviceProcAddr);
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK;
image_info.extent = {4, 4, 1};
VkImage image = VK_NULL_HANDLE;
ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image),
VK_SUCCESS);
// The 8-byte block sits at memory offset 16, but only [0, 8) is mapped.
ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 16),
VK_SUCCESS);
void* mapped = nullptr;
ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, 8, 0, &mapped),
VK_SUCCESS);
VkBufferImageCopy region{};
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.layerCount = 1;
region.imageExtent = {4, 4, 1};
emulation.CmdCopyBufferToImage(device, command_buffer, source, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&region);
emulation.PrepareSubmit(&command_buffer, 1);
EXPECT_EQ(g_staging[0], 0xAA);
emulation.ReleaseCommandBuffer(command_buffer);
}
TEST(VulkanEtc2EmulationTest, DecodesAgainOnEveryResubmit) {
const VkDevice device = FakeHandle<VkDevice>(0x11);
const VkBuffer source = FakeHandle<VkBuffer>(0x201);
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x701);
g_source.fill(0);
g_staging.fill(0xAA);
VkPhysicalDeviceMemoryProperties memory{};
memory.memoryTypeCount = 1;
memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
VulkanEtc2Emulation emulation;
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x21), true,
memory, FakeGetDeviceProcAddr);
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK;
image_info.extent = {4, 4, 1};
VkImage image = VK_NULL_HANDLE;
ASSERT_EQ(emulation.CreateImage(device, &image_info, nullptr, &image),
VK_SUCCESS);
ASSERT_EQ(emulation.BindBufferMemory(device, source, source_memory, 0),
VK_SUCCESS);
void* mapped = nullptr;
ASSERT_EQ(emulation.MapMemory(device, source_memory, 0, g_source.size(), 0,
&mapped),
VK_SUCCESS);
VkBufferImageCopy region{};
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.layerCount = 1;
region.imageExtent = {4, 4, 1};
emulation.CmdCopyBufferToImage(device, command_buffer, source, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&region);
// Differential block: every texel decodes to 134.
const std::array<std::uint8_t, 8> first = {0x81, 0x81, 0x81, 0x02,
0, 0, 0, 0};
std::memcpy(g_source.data(), first.data(), first.size());
emulation.PrepareSubmit(&command_buffer, 1);
EXPECT_EQ(g_staging[0], 134);
// Individual block: every texel decodes to 138.
const std::array<std::uint8_t, 8> second = {0x88, 0x88, 0x88, 0x00,
0, 0, 0, 0};
std::memcpy(g_source.data(), second.data(), second.size());
emulation.PrepareSubmit(&command_buffer, 1);
EXPECT_EQ(g_staging[0], 138);
emulation.ReleaseCommandBuffer(command_buffer);
}
} // namespace
} // namespace mocktail::graphics