Fix layered image subresource synchronization

This commit is contained in:
nmzik
2026-07-16 22:26:40 +02:00
parent ae86249765
commit 41114be8da
10 changed files with 207 additions and 62 deletions
@@ -1001,7 +1001,7 @@ std::pair<VulkanBuffer*, uint64_t> BufferCache::ObtainBuffer(CommandBuffer* com
" size=0x%016" PRIx64 " read=%d written=%d formatted=%d\n",
vaddr, size, is_read, is_written, is_formatted);
}
m_texture_cache->InvalidateMemoryFromGPU(vaddr, size, true);
(void)m_texture_cache->InvalidateMemoryFromGPU(vaddr, size, true);
}
}
if (is_written) {
@@ -1421,6 +1421,7 @@ void BufferCache::CopyBuffer(CommandBuffer* command, GraphicContext* ctx, uint64
}
ValidateGpuAccess(src_vaddr, size, true, false);
ValidateGpuAccess(dst_vaddr, size, false, true);
bool dst_image_transition = false;
{
std::lock_guard transaction(m_resource_mutex);
const bool src_image_gpu = m_texture_cache->HasGpuModifiedRangeOverlap(src_vaddr, size);
@@ -1452,7 +1453,7 @@ void BufferCache::CopyBuffer(CommandBuffer* command, GraphicContext* ctx, uint64
" dst=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
src_vaddr, dst_vaddr, size);
}
m_texture_cache->InvalidateMemoryFromGPU(dst_vaddr, size);
dst_image_transition = m_texture_cache->InvalidateMemoryFromGPU(dst_vaddr, size);
// A clean target destination is handled above like a protected host write. Target
// aliases that require an actual GPU buffer copy remain unsupported.
if (m_texture_cache->HasGpuTargetPageOverlap(dst_vaddr, size)) {
@@ -1470,7 +1471,8 @@ void BufferCache::CopyBuffer(CommandBuffer* command, GraphicContext* ctx, uint64
}
}
const auto [src, src_offset] = ObtainBuffer(command, ctx, src_vaddr, size, false, true);
const auto [dst, dst_offset] = ObtainBuffer(command, ctx, dst_vaddr, size, true, false);
const auto [dst, dst_offset] =
ObtainBuffer(command, ctx, dst_vaddr, size, true, false, dst_image_transition);
if (src == dst && src_offset < dst_offset + size && dst_offset < src_offset + size) {
EXIT("BufferCache: resolved Vulkan copy ranges overlap, src_offset=0x%016" PRIx64
" dst_offset=0x%016" PRIx64 " size=0x%016" PRIx64 "\n",
@@ -330,6 +330,7 @@ void ResolveRenderColorTarget(uint64_t submit_id, CommandBuffer* buffer, const H
rt.view.base_array_slice_index, rt.view.last_array_slice_index,
render_target_slice_offset);
}
r->base_array_layer = view.base_layer;
const uint32_t levels = rt.attrib2.num_mip_levels + 1u;
if (levels == 0 || levels > 16 || rt.view.current_mip_level >= levels) {
EXIT("unsupported render-target mip range: current=%u levels=%u\n",
+1 -1
View File
@@ -624,7 +624,7 @@ ClassifyRenderTargetOverlap(const RenderTargetInfo& cached, bool cached_gpu_modi
}
// For an equal-address allocation-pool entry, a changed block raster or block size is a new
// image allocation, not a view of the old image. In Kyty we can only retire an
// already-published target with page-isolated storage :)
// already-published target with page-isolated storage.
const bool page_isolated =
cached.address % TRACKER_PAGE_SIZE == 0 && cached.size % TRACKER_PAGE_SIZE == 0 &&
requested.address % TRACKER_PAGE_SIZE == 0 && requested.size % TRACKER_PAGE_SIZE == 0;
+21 -9
View File
@@ -32,6 +32,7 @@
#include "libs/errno.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
@@ -1247,6 +1248,24 @@ void RenderDrawIndexAuto(uint64_t submit_id, CommandBuffer* buffer, HW::Context*
index_bind, false, false, true);
}
bool IsSameColorResolveSubresource(const RenderColorInfo& src, const RenderColorInfo& dst) {
return src.base_addr == dst.base_addr && src.base_mip_level == dst.base_mip_level &&
src.base_array_layer == dst.base_array_layer;
}
ImageImageCopy MakeColorResolveCopy(const RenderColorInfo& src, const RenderColorInfo& dst,
uint32_t width, uint32_t height) {
ImageImageCopy region {};
region.src_image = src.vulkan_buffer;
region.src_level = src.base_mip_level;
region.dst_level = dst.base_mip_level;
region.width = width;
region.height = height;
region.src_layer = src.base_array_layer;
region.dst_layer = dst.base_array_layer;
return region;
}
static bool ResolveColorTargets(uint64_t submit_id, CommandBuffer* buffer, const HW::Context& hw,
uint32_t render_target_slice_offset) {
if (hw.GetColorControl().mode != 3) {
@@ -1268,7 +1287,7 @@ static bool ResolveColorTargets(uint64_t submit_id, CommandBuffer* buffer, const
src.type == RenderColorType::NoColorOutput || dst.type == RenderColorType::NoColorOutput) {
return false;
}
if (src.base_addr == dst.base_addr && src.base_mip_level == dst.base_mip_level) {
if (IsSameColorResolveSubresource(src, dst)) {
return true;
}
@@ -1278,14 +1297,7 @@ static bool ResolveColorTargets(uint64_t submit_id, CommandBuffer* buffer, const
return false;
}
std::vector<ImageImageCopy> regions;
ImageImageCopy region {};
region.src_image = src.vulkan_buffer;
region.src_level = src.base_mip_level;
region.dst_level = dst.base_mip_level;
region.width = width;
region.height = height;
regions.push_back(region);
const std::array regions {MakeColorResolveCopy(src, dst, width, height)};
MarkRenderTargetGpuWritten(dst);
UtilImageToImage(buffer, regions, dst.vulkan_buffer, dst.vulkan_buffer->layout);
@@ -15,6 +15,7 @@ namespace Libs::Graphics {
class CommandBuffer;
struct DepthStencilVulkanImage;
struct ImageImageCopy;
struct RenderTextureVulkanImage;
struct ShaderVertexInputInfo;
struct VulkanImage;
@@ -236,6 +237,7 @@ struct RenderColorInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent = {};
uint32_t base_mip_level = 0;
uint32_t base_array_layer = 0;
uint64_t base_addr = 0;
uint64_t buffer_size = 0;
uint32_t target_slot = 0;
@@ -243,6 +245,12 @@ struct RenderColorInfo {
VkClearColorValue color_clear_value {};
};
[[nodiscard]] bool IsSameColorResolveSubresource(const RenderColorInfo& src,
const RenderColorInfo& dst);
[[nodiscard]] ImageImageCopy MakeColorResolveCopy(const RenderColorInfo& src,
const RenderColorInfo& dst, uint32_t width,
uint32_t height);
struct ScissorRect {
int left = 0;
int top = 0;
+28 -39
View File
@@ -387,18 +387,9 @@ struct TextureCache::ReadbackWorker {
}
download.resize(info.size);
std::fill(download.begin(), download.end(), 0);
std::vector<ImageBufferCopy> regions;
regions.reserve(info.layers);
for (uint32_t layer = 0; layer < info.layers; layer++) {
ImageBufferCopy region {};
region.offset = static_cast<uint32_t>(slice_size * layer);
region.pitch = info.pitch;
region.width = info.width;
region.height = info.height;
region.src_layer = layer;
region.aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
regions.push_back(region);
}
const auto regions =
MakeLayeredImageBufferCopies(info.layers, slice_size, info.pitch, info.width,
info.height, VK_IMAGE_ASPECT_DEPTH_BIT);
UtilFillBuffer(cached.ctx, download.data(), info.size, regions, cached.image,
cached.image->layout);
guest.resize(info.size);
@@ -467,17 +458,8 @@ struct TextureCache::ReadbackWorker {
}
download.resize(info.size);
std::fill(download.begin(), download.end(), 0);
std::vector<ImageBufferCopy> regions;
regions.reserve(layers);
for (uint32_t layer = 0; layer < layers; layer++) {
ImageBufferCopy region {};
region.offset = static_cast<uint32_t>(slice_size * layer);
region.pitch = info.pitch;
region.width = info.width;
region.height = info.height;
region.src_layer = layer;
regions.push_back(region);
}
const auto regions =
MakeLayeredImageBufferCopies(layers, slice_size, info.pitch, info.width, info.height);
UtilFillBuffer(cached.ctx, download.data(), info.size, regions, cached.image,
cached.image->layout);
if (tiled) {
@@ -3046,7 +3028,6 @@ void TextureCache::MarkGpuWritten(VulkanImage* image) {
cached->depth.htile_address, cached->depth.htile_size, true,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
meta->second.gpu_modified = true;
meta->second.clear_mask = 0;
}
}
if (!cached->gpu_modified) {
@@ -3110,29 +3091,34 @@ void TextureCache::SynchronizeRenderTargetToBufferLocked(CachedImage& cached) {
target.tile_mode == Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget);
const auto rows = static_cast<uint64_t>(target.height - 1);
TileSizeAlign exact {};
const bool exact_tiled = tiled &&
TileGetRenderTargetSize(target.width, target.height, target.pitch,
target.bytes_per_element, &exact) &&
exact.align == 65536 && exact.size == target.size;
const bool single_slice = TileGetRenderTargetSize(target.width, target.height, target.pitch,
target.bytes_per_element, &exact);
const bool layered_size = target.layers != 0 && single_slice &&
exact.size <= UINT64_MAX / target.layers &&
exact.size * target.layers == target.size;
const bool exact_tiled = tiled && exact.align == 65536 && layered_size;
if (cached.kind != CachedImage::Kind::RenderTarget || !cached.gpu_modified ||
cached.buffer_modified || cached.ctx == nullptr || cached.image == nullptr ||
(!linear && !exact_tiled) || target.address == 0 || target.size == 0 || target.width == 0 ||
target.height == 0 || target.pitch < target.width || target.bytes_per_element == 0 ||
target.levels != 1 || rows > (UINT64_MAX - target.width) / target.pitch) {
target.layers == 0 || target.size % target.layers != 0 || target.size > UINT32_MAX ||
target.levels != 1 || cached.image->layers != target.layers ||
rows > (UINT64_MAX - target.width) / target.pitch) {
EXIT("TextureCache: unsupported render-target buffer synchronization, "
"addr=0x%016" PRIx64 " size=0x%016" PRIx64
" extent=%ux%u pitch=%u bpe=%u levels=%u tile=%u"
" extent=%ux%u pitch=%u bpe=%u levels=%u layers=%u/%u tile=%u"
" gpu_modified=%d buffer_modified=%d\n",
target.address, target.size, target.width, target.height, target.pitch,
target.bytes_per_element, target.levels, target.tile_mode, cached.gpu_modified,
cached.buffer_modified);
target.bytes_per_element, target.levels, target.layers, cached.image->layers,
target.tile_mode, cached.gpu_modified, cached.buffer_modified);
}
const auto linear_elements = rows * target.pitch + target.width;
if (linear_elements > UINT64_MAX / target.bytes_per_element) {
EXIT("TextureCache: render-target buffer synchronization size overflow\n");
}
const auto linear_size = linear_elements * target.bytes_per_element;
if (linear_size > target.size || cached.image->format != target.format ||
const auto slice_size = target.size / target.layers;
if (linear_size > slice_size || cached.image->format != target.format ||
cached.image->extent.width != target.width ||
cached.image->extent.height != target.height ||
(tiled && target.bytes_per_element != 1 && target.bytes_per_element != 2 &&
@@ -3149,7 +3135,9 @@ void TextureCache::SynchronizeRenderTargetToBufferLocked(CachedImage& cached) {
VulkanDeviceWaitIdle(cached.ctx);
m_buffer_transition_linear.resize(target.size);
std::fill(m_buffer_transition_linear.begin(), m_buffer_transition_linear.end(), 0);
UtilFillBuffer(cached.ctx, m_buffer_transition_linear.data(), linear_size, target.pitch,
const auto regions = MakeLayeredImageBufferCopies(target.layers, slice_size, target.pitch,
target.width, target.height);
UtilFillBuffer(cached.ctx, m_buffer_transition_linear.data(), target.size, regions,
cached.image, cached.image->layout);
if (tiled) {
m_buffer_transition_guest.resize(target.size);
@@ -3171,7 +3159,7 @@ void TextureCache::SynchronizeRenderTargetToBufferLocked(CachedImage& cached) {
cached.buffer_modified = true;
}
void TextureCache::InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
bool TextureCache::InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
bool formatted_buffer_write) {
if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE ||
size > TRACKER_ADDRESS_SIZE - vaddr) {
@@ -3226,23 +3214,24 @@ void TextureCache::InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
sampled_write = sampled;
}
if (match == m_images.end()) {
return;
return false;
}
if (sampled_write) {
(**match).buffer_modified = true;
return;
return true;
}
if (action == BufferImageAlias::VideoOutWrite) {
auto& cached = **match;
cached.buffer_modified = true;
return;
return true;
}
if (action == BufferImageAlias::RenderTargetWrite) {
SynchronizeRenderTargetToBufferLocked(**match);
return;
return true;
}
m_memory_tracker.UntrackMemory(vaddr, size);
m_images.erase(match);
return true;
}
DepthStencilVulkanImage* TextureCache::FindDepthTargetByRange(uint64_t vaddr, uint64_t size) {
@@ -56,7 +56,7 @@ public:
uint64_t size, uint32_t packed_clear);
void MarkGpuWritten(VulkanImage* image);
void PrepareHostWrite(uint64_t vaddr, uint64_t size);
void InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
[[nodiscard]] bool InvalidateMemoryFromGPU(uint64_t vaddr, uint64_t size,
bool formatted_buffer_write = false);
[[nodiscard]] RenderTextureVulkanImage* FindRenderTargetByRange(CommandBuffer* command,
uint64_t vaddr, uint64_t size);
+21
View File
@@ -22,6 +22,27 @@ static Common::Mutex g_scratch_buffer_mutex;
static std::unique_ptr<uint8_t[]> g_scratch_buffer;
static uint64_t g_scratch_buffer_capacity = 0;
std::vector<ImageBufferCopy> MakeLayeredImageBufferCopies(uint32_t layers, uint64_t slice_size,
uint32_t pitch, uint32_t width,
uint32_t height,
VkImageAspectFlags aspect) {
if (layers == 0 || slice_size == 0 || slice_size > UINT32_MAX ||
slice_size > UINT32_MAX / layers || pitch < width || width == 0 || height == 0) {
EXIT("invalid layered image-buffer copy layout\n");
}
std::vector<ImageBufferCopy> regions(layers);
for (uint32_t layer = 0; layer < layers; layer++) {
auto& region = regions[layer];
region.offset = static_cast<uint32_t>(slice_size * layer);
region.pitch = pitch;
region.width = width;
region.height = height;
region.src_layer = layer;
region.aspect = aspect;
}
return regions;
}
UtilScratchBuffer::UtilScratchBuffer(uint64_t size) {
EXIT_IF(size == 0);
+6
View File
@@ -6,6 +6,7 @@
#include <span>
#include <utility>
#include <vector>
#include <vulkan/vulkan_core.h>
namespace Libs::Graphics {
@@ -64,6 +65,11 @@ struct ImageImageCopy {
int dst_z = 0;
};
[[nodiscard]] std::vector<ImageBufferCopy>
MakeLayeredImageBufferCopies(uint32_t layers, uint64_t slice_size, uint32_t pitch, uint32_t width,
uint32_t height,
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT);
enum class StagingBufferType { Texture, Vertex, ReadBack };
class UtilScratchBuffer {
+115 -9
View File
@@ -15,7 +15,9 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderState.h"
#include "graphics/host_gpu/renderer/resourceMutex.h"
#include "graphics/host_gpu/renderer/tiler.h"
#include "graphics/host_gpu/renderer/textureCache.h"
#include "graphics/host_gpu/utils.h"
#include "graphics/shader/recompiler/BindingLayout.h"
#include "graphics/shader/recompiler/ShaderDecoder.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
@@ -9485,6 +9487,31 @@ void CheckStorageTextureVolumeUploadLayout() {
std::printf("[host] %-32s ok\n", "StorageTextureVolumeUpload");
}
void CheckColorResolveLayers() {
RenderColorInfo src{};
RenderColorInfo dst{};
src.base_addr = dst.base_addr = 0x12340000;
src.base_mip_level = dst.base_mip_level = 2;
src.base_array_layer = 1;
dst.base_array_layer = 3;
src.vulkan_buffer = reinterpret_cast<VulkanImage *>(uintptr_t{1});
Require("ColorResolveLayers", "identity",
!IsSameColorResolveSubresource(src, dst),
"different array layers were treated as the same resolve subresource");
const auto copy = MakeColorResolveCopy(src, dst, 128, 64);
Require("ColorResolveLayers", "region",
copy.src_image == src.vulkan_buffer && copy.src_level == 2 &&
copy.dst_level == 2 && copy.src_layer == 1 &&
copy.dst_layer == 3 && copy.width == 128 && copy.height == 64,
"color resolve dropped its source or destination array layer");
dst.base_array_layer = src.base_array_layer;
Require("ColorResolveLayers", "same subresource",
IsSameColorResolveSubresource(src, dst),
"matching color resolve subresources were not recognized");
std::printf("[host] %-32s ok\n", "ColorResolveLayers");
}
void CheckRenderTargetTileRoundTrip() {
struct Case {
uint32_t width;
@@ -9529,6 +9556,64 @@ void CheckRenderTargetTileRoundTrip() {
"linear-to-tiled render-target conversion did not round-trip");
}
}
constexpr uint32_t width = 257;
constexpr uint32_t height = 131;
constexpr uint32_t bytes_per_element = 4;
constexpr uint32_t layers = 3;
const auto pitch = TileGetRenderTargetPitch(width, bytes_per_element);
TileSizeAlign storage{};
Require("RenderTargetTileRoundTrip", "layered layout",
TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
&storage) &&
storage.size <= UINT32_MAX / layers,
"layered render-target test layout was rejected");
RenderTargetInfo info{};
info.address = 1;
info.size = storage.size * layers;
info.format = VK_FORMAT_R8G8B8A8_UNORM;
info.width = width;
info.height = height;
info.pitch = pitch;
info.bytes_per_element = bytes_per_element;
info.tile_mode = Prospero::GpuEnumValue(Prospero::TileMode::kRenderTarget);
info.layers = layers;
std::vector<uint8_t> linear(info.size, 0);
for (uint32_t layer = 0; layer < layers; layer++) {
for (uint32_t y = 0; y < height; y++) {
const auto row = storage.size * layer +
static_cast<uint64_t>(y) * pitch * bytes_per_element;
std::fill_n(linear.data() + row, width * bytes_per_element,
static_cast<uint8_t>(0x31 + layer * 0x27));
}
}
std::vector<uint8_t> tiled(info.size, 0xcd);
std::vector<uint8_t> restored(info.size, 0xa5);
Tiler tiler;
tiler.TileImage(tiled.data(), linear.data(), info);
for (uint32_t layer = 0; layer < layers; layer++) {
TileConvertTiledToLinearRenderTarget(
restored.data() + storage.size * layer,
tiled.data() + storage.size * layer, width, height, pitch,
bytes_per_element, storage.size);
for (uint32_t y = 0; y < height; y++) {
const auto row = storage.size * layer +
static_cast<uint64_t>(y) * pitch * bytes_per_element;
Require("RenderTargetTileRoundTrip", "layered contents",
std::memcmp(linear.data() + row, restored.data() + row,
width * bytes_per_element) == 0,
"layered render-target conversion crossed array slices");
}
}
const auto regions = MakeLayeredImageBufferCopies(
layers, storage.size, pitch, width, height);
Require("RenderTargetTileRoundTrip", "layered regions",
regions.size() == layers && regions[0].offset == 0 &&
regions[1].offset == storage.size &&
regions[2].offset == storage.size * 2 &&
regions[0].src_layer == 0 && regions[1].src_layer == 1 &&
regions[2].src_layer == 2 && regions[2].pitch == pitch &&
regions[2].width == width && regions[2].height == height,
"layered image-buffer regions did not preserve slice offsets");
std::printf("[host] %-32s ok\n", "RenderTargetTileRoundTrip");
}
@@ -10083,7 +10168,8 @@ void CheckOverlappingMetadataViews() {
void CheckGpuMetadataReuse() {
constexpr uintptr_t base = 0x0000000200010000ull;
constexpr uint64_t allocation_size = 0x20000;
constexpr uint64_t metadata_size = 0x8000;
constexpr uint64_t metadata_size = 0x18000;
constexpr uint32_t layers = 3;
auto *memory = static_cast<uint8_t *>(VirtualAlloc(
reinterpret_cast<void *>(base), allocation_size,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
@@ -10100,22 +10186,34 @@ void CheckGpuMetadataReuse() {
buffer_cache.SetTextureCache(texture_cache);
page_manager.OnGpuMap(base, allocation_size);
texture_cache.RegisterMeta(base, metadata_size);
texture_cache.RegisterMeta(base, metadata_size, layers);
Require("GpuMetadataReuse", "clear", texture_cache.ClearMeta(base),
"metadata clear setup failed");
texture_cache.InvalidateMemoryFromGPU(base, allocation_size);
const bool full_image_transition =
texture_cache.InvalidateMemoryFromGPU(base, allocation_size);
Require("GpuMetadataReuse", "discard",
!texture_cache.IsMeta(base) &&
!full_image_transition && !texture_cache.IsMeta(base) &&
!texture_cache.HasMetaRangeOverlap(base, allocation_size),
"fully overwritten metadata identity was retained");
texture_cache.RegisterMeta(base, metadata_size);
"metadata-only overwrite retained identity or claimed an image transition");
texture_cache.RegisterMeta(base, metadata_size, layers);
Require("GpuMetadataReuse", "re-register",
texture_cache.IsMetaRange(base, metadata_size) &&
texture_cache.ClearMeta(base),
"metadata identity could not be reused after a GPU overwrite");
texture_cache.InvalidateMemoryFromGPU(base + 0x1000, 0x1000);
Require("GpuMetadataReuse", "partial discard", !texture_cache.IsMeta(base),
"partially overwritten metadata identity was retained");
Require("GpuMetadataReuse", "layered clear",
texture_cache.IsMetaCleared(base, 0) &&
texture_cache.IsMetaCleared(base, 1) &&
texture_cache.IsMetaCleared(base, 2) &&
texture_cache.TouchMeta(base, 1, false) &&
texture_cache.IsMetaCleared(base, 0) &&
!texture_cache.IsMetaCleared(base, 1) &&
texture_cache.IsMetaCleared(base, 2),
"consuming one metadata slice erased another slice's lazy clear");
const bool partial_image_transition =
texture_cache.InvalidateMemoryFromGPU(base + 0x1000, 0x1000);
Require("GpuMetadataReuse", "partial discard",
!partial_image_transition && !texture_cache.IsMeta(base),
"partial metadata overwrite retained identity or claimed an image transition");
texture_cache.UnmapMemory(base, allocation_size);
page_manager.OnGpuUnmap(base, allocation_size);
@@ -11379,6 +11477,13 @@ int main(int argc, char **argv) {
CheckOverlappingMetadataViews();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--layered-image-only") == 0) {
CheckColorResolveLayers();
CheckRenderTargetTileRoundTrip();
CheckDepthTargetTileRoundTrip();
CheckGpuMetadataReuse();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-view-only") == 0) {
CheckSampledColorViews();
return 0;
@@ -11445,6 +11550,7 @@ int main(int argc, char **argv) {
CheckStorageTextureDepthTileUploadLayout();
CheckStorageTextureLinearReadbackLayout();
CheckStorageImageSwizzleSpecializationId();
CheckColorResolveLayers();
CheckRenderTargetTileRoundTrip();
CheckDepthTargetTileRoundTrip();
CheckStencilTargetTileRoundTrip();