#include "common/assert.h" #include "common/emulatorConfig.h" #include "common/hostException.h" #include "common/logging/log.h" #include "common/subsystems.h" #include "common/threads.h" #include "gpu_test_shaders/gpu_test_ms_depth_spv.h" #include "graphics/guest_gpu/command_processor/commandProcessor.h" #include "graphics/guest_gpu/command_processor/pm4Dispatch.h" #include "graphics/guest_gpu/gpu_defs.h" #include "graphics/guest_gpu/gpu_format.h" #include "graphics/guest_gpu/graphicsRun.h" #include "graphics/guest_gpu/hardwareContext.h" #include "graphics/guest_gpu/pm4.h" #include "graphics/guest_gpu/tile.h" #include "graphics/host_gpu/hostMemory.h" #include "graphics/host_gpu/memoryTracker.h" #include "graphics/host_gpu/pageManager.h" #include "graphics/host_gpu/renderer/cache/bufferCache.h" #include "graphics/host_gpu/renderer/cache/textureCache.h" #include "graphics/host_gpu/renderer/colorRenderTarget.h" #include "graphics/host_gpu/renderer/depthRenderTarget.h" #include "graphics/host_gpu/renderer/image/blitHelper.h" #include "graphics/host_gpu/renderer/image/image.h" #include "graphics/host_gpu/renderer/image/imageView.h" #include "graphics/host_gpu/renderer/image/textureCommon.h" #include "graphics/host_gpu/renderer/image/tiler.h" #include "graphics/host_gpu/renderer/pipeline/descriptors.h" #include "graphics/host_gpu/renderer/pipeline/pipelineCache.h" #include "graphics/host_gpu/renderer/debug.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/renderer/renderDraw.h" #include "graphics/host_gpu/renderer/renderTarget.h" #include "graphics/host_gpu/renderer/sync.h" #include "graphics/host_gpu/vulkanCommon.h" #include "graphics/presentation/window/windowInternal.h" #include "graphics/shader/recompiler/ShaderRecompiler.h" #include "graphics/shader/recompiler/Tessellation.h" #include "graphics/shader/recompiler/backend/spirv/SpirvBuilder.h" #include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h" #include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/BindingLayout.h" #include "graphics/shader/rectListShader.h" #include "graphics/shader/shader.h" #include "graphics/shader/shaderCompiler.h" #include "kernel/eventQueue.h" #include "kernel/memory.h" #include "libs/agc.h" #include "libs/dialog.h" #include "libs/errno.h" #include "spirv-tools/libspirv.hpp" #if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h") #error "legacy render-target barrier API must remain deleted" #endif #if __has_include("graphics/host_gpu/transfer.h") #error "legacy transfer facade must remain deleted" #endif #if __has_include("graphics/host_gpu/renderer/framebufferCache.h") #error "classic framebuffer/render-pass cache must remain deleted" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS #include #include #include #endif #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #ifndef NOMINMAX #define NOMINMAX #endif #include #undef min #undef max #endif namespace Libs::Graphics { template concept HasGetDownloadBuffer = requires(Cache &cache) { cache.GetDownloadBuffer(uint64_t{1}); }; static_assert(!HasGetDownloadBuffer); static_assert(std::same_as); static_assert(std::same_as); template concept HasSynchronizeImageToBuffer = requires(Cache &cache) { cache.SynchronizeImageToBuffer(uint64_t{1}, uint64_t{1}); }; template concept HasObtainBufferForImageCopy = requires(Cache &cache) { cache.ObtainBufferForImageCopy(uint64_t{1}, uint64_t{1}); }; template concept HasObtainBufferForImageWrite = requires(Cache &cache) { cache.ObtainBufferForImageWrite(uint64_t{1}, uint64_t{1}); }; template concept HasDiscardGpuDirtyBytes = requires(Cache &cache) { cache.DiscardGpuDirtyBytes(uint64_t{1}, uint64_t{1}); }; static_assert(!HasSynchronizeImageToBuffer); static_assert(!HasObtainBufferForImageCopy); static_assert(!HasObtainBufferForImageWrite); static_assert(!HasDiscardGpuDirtyBytes); template concept HasLegacyImageLayout = requires(Backing &backing) { backing.layout; }; static_assert(!HasLegacyImageLayout); template concept HasPolicyDetileImage = requires { &Manager::DetileImage; }; template concept HasPolicyTileImageBacking = requires { &Manager::TileImageBacking; }; static_assert(!HasPolicyDetileImage); static_assert(!HasPolicyTileImageBacking); static_assert(BlitHelper::ColorToMsDepthLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal); struct BufferCacheTestAccess { static_assert(std::same_as>); static void SetGarbageCollectionThresholds(BufferCache &cache, uint64_t trigger, uint64_t critical) { cache.m_trigger_gc_memory = trigger; cache.m_critical_gc_memory = critical; } static StreamBuffer &DownloadBuffer(BufferCache &cache) { return cache.m_download_buffer; } static bool SynchronizeBufferFromImage(BufferCache &cache, Buffer &buffer, uint64_t address, uint64_t size) { return cache.SynchronizeBufferFromImage(buffer, address, size); } static BufferId PageOwner(const BufferCache &cache, uint64_t address) { const auto *owner = cache.m_page_table.Find( address >> BufferCache::PageTable::kPageBits); return owner == nullptr ? BufferId{} : *owner; } static bool IsBufferAllocated(const BufferCache &cache, BufferId id) { return cache.m_slot_buffers.is_allocated(id); } }; struct StreamBufferTestAccess { static bool NormalizeReservation(bool coherent, uint64_t atom, uint64_t &size, uint64_t &alignment) { return StreamBuffer::NormalizeReservation(coherent, atom, size, alignment); } }; struct ImageTestAccess { static uint32_t CopyRows(uint64_t row_size, uint32_t rows, uint64_t capacity) { return Image::CopyRows(row_size, rows, capacity); } }; struct TileManagerTestAccess { static uint32_t ConversionRows(uint64_t offset, uint64_t row_stride, uint64_t active, uint32_t remaining, uint64_t alignment, uint64_t max_range, uint32_t max_groups) { return TileManager::ConversionRows(offset, row_stride, active, remaining, alignment, max_range, max_groups); } }; struct TextureCacheTestAccess { static_assert(std::same_as>); static_assert(TextureCache::ImagePageTable::kPageBits == 20); static_assert(TextureCache::ImagePageTable::kAddressSpaceBits == 40); static_assert(TextureCache::ImagePageTable::kFirstLevelBits == 10); static std::unique_lock Lock(TextureCache &cache) { return std::unique_lock(cache.m_lock); } static void ClearImage(TextureCache &cache, CommandBuffer &command, ImageId id, const vk::ImageSubresourceRange &range, const vk::ClearValue &clear) { auto lock = Lock(cache); cache.ClearImage(command, id, cache.GetImage(id).backing.format, range, clear); } static void ConfigureGarbageCollection(TextureCache &cache, std::span oldest, uint64_t tick, uint64_t pressure) { cache.m_trigger_gc_memory = 0; cache.m_pressure_gc_memory = pressure; cache.m_critical_gc_memory = UINT64_MAX; cache.m_gc_tick = tick; std::vector live; cache.m_lru_cache = {}; cache.m_slot_images.ForEach([&](ImageId id, Image &image) { if (image.registered) { image.tick_accessed_last = cache.m_scheduler.CurrentTick(); live.push_back(id); } }); for (const auto id : oldest) { const auto owner = cache.m_slot_images.try_get(id); if (owner != nullptr && owner->registered) { owner->tick_accessed_last = 0; owner->lru_id = cache.m_lru_cache.Insert(id, 0); } } for (const auto id : live) { if (std::ranges::find(oldest, id) == oldest.end()) { cache.m_slot_images[id].lru_id = cache.m_lru_cache.Insert(id, tick); } } } static bool Contains(const TextureCache &cache, ImageId id) { const auto owner = cache.m_slot_images.try_get(id); return owner != nullptr && owner->registered; } static std::vector FindImages(TextureCache &cache, uint64_t address, uint64_t size, bool page_overlap) { std::lock_guard lock(cache.m_lock); const auto found = cache.FindImagesInRegion(address, size, page_overlap); std::vector result; result.reserve(found.size()); for (const auto id : found) { result.push_back(id); } return result; } static size_t PageOwnerCount(TextureCache &cache, uint64_t address) { std::lock_guard lock(cache.m_lock); const auto *owners = cache.m_image_page_table.Find(static_cast( address >> TextureCache::ImagePageTable::kPageBits)); return owners == nullptr ? 0 : owners->size(); } static size_t OwnedPageCount(TextureCache &cache, uint64_t address, uint64_t size, ImageId id) { std::lock_guard lock(cache.m_lock); TextureCache::ImagePageTable::PageRange pages{}; if (!TextureCache::ImagePageTable::TryGetPageRange(address, size, pages)) { return 0; } size_t count = 0; for (size_t page = pages.first; page < pages.last_exclusive; ++page) { const auto *owners = cache.m_image_page_table.Find(page); count += owners != nullptr && owners->Contains(id) ? 1 : 0; } return count; } static void AddPageOwner(TextureCache &cache, uint64_t address, ImageId id) { std::lock_guard lock(cache.m_lock); cache .m_image_page_table[static_cast( address >> TextureCache::ImagePageTable::kPageBits)] .push_back(id); } static bool RemovePageOwner(TextureCache &cache, uint64_t address, ImageId id) { std::lock_guard lock(cache.m_lock); auto *owners = cache.m_image_page_table.Find(static_cast( address >> TextureCache::ImagePageTable::kPageBits)); return owners != nullptr && owners->Erase(id); } static void SetQueryEpoch(TextureCache &cache, uint32_t epoch) { std::lock_guard lock(cache.m_lock); cache.m_image_query_epoch = epoch; } static uint32_t QueryEpoch(TextureCache &cache) { std::lock_guard lock(cache.m_lock); return cache.m_image_query_epoch; } static ImageId InsertImage(TextureCache &cache, const ImageInfo &info) { std::lock_guard lock(cache.m_lock); return cache.InsertImage(info); } static void DeleteImage(TextureCache &cache, ImageId id) { std::lock_guard lock(cache.m_lock); cache.DeleteImage(id); } static const Image *Owner(const TextureCache &cache, ImageId id) { return cache.m_slot_images.try_get(id); } static bool PendingDownload(const TextureCache &cache, ImageId id) { return cache.m_download_images.contains(id); } static size_t NullImageCount(const TextureCache &cache) { return cache.m_null_images.size(); } static void TrackDownload(TextureCache &cache, ImageId id) { std::lock_guard lock(cache.m_lock); cache.TrackImageDownload(id, cache.m_slot_images[id]); } static void AssociateStencil(TextureCache &cache, ImageId depth, GuestRange stencil) { std::lock_guard lock(cache.m_lock); cache.AssociateStencil(depth, stencil); } static void SetLinearReadback(TextureCache &cache, bool enabled) { cache.m_readback_linear_images = enabled; } static std::pair MapDownload(TextureCache &cache, uint64_t size, uint64_t alignment) { auto &download = cache.m_buffer_cache.GetUtilityBuffer(MemoryUsage::Download); auto mapping = download.Map(size, std::max(alignment, 4)); if (mapping.first != nullptr) { download.Commit(); } return mapping; } static bool TryDownload(TextureCache &cache, ImageId id) { return cache.DownloadImageMemory(id); } static void RegisterHtileMeta(TextureCache &cache, uint64_t address) { std::lock_guard lock(cache.m_lock); auto &metadata = cache.m_surface_metas[address]; metadata.type = TextureCache::MetaDataInfo::Type::HTile; metadata.clear_mask = 0; } static TileManager &Tiler(TextureCache &cache) { return cache.m_tiler; } }; struct RenderExecutorTestAccess { static void DrawAuto(RenderExecutor &executor, CommandBuffer &command, const DrawAutoArgs &args) { executor.DrawAuto(0, command, args); } static bool TryConsumeComputeImageClear(RenderExecutor &executor, const ShaderComputeInputInfo &input, CommandBuffer &command, uint32_t x, uint32_t y, uint32_t z, uint32_t mode) { return executor.TryConsumeComputeImageClear(input, command, x, y, z, mode); } static bool TryConsumeComputeMetaClear(RenderExecutor &executor, const ShaderComputeInputInfo &input, const CommandBuffer &buffer) { return executor.TryConsumeComputeMetaClear(input, buffer); } static TextureBinding ResolveTexture(RenderExecutor &executor, const ShaderRecompiler::IR::ImageResource &resource, const ShaderRecompiler::IR::DescriptorValue &value) { return executor.ResolveTexture(resource, value); } static auto PrepareGraphicsBindings(RenderExecutor &executor, const ShaderStageRuntime &vertex, const ShaderStageRuntime &pixel, bool pixel_active) { RenderExecutor::GraphicsBindings result; result.vertex[0] = executor.PrepareBindings(vertex); std::array stages{&result.vertex[0], nullptr}; if (pixel_active) { result.pixel.emplace(executor.PrepareBindings(pixel)); stages[1] = &*result.pixel; } executor.PrepareGraphicsBindings( std::span{stages.data(), pixel_active ? 2u : 1u}, {}); return result; } static void PrepareGraphicsBindings(RenderExecutor &executor, std::span stages, std::span colors) { executor.PrepareGraphicsBindings(stages, colors); } static PipelineCache::Pipeline CreateDescriptorPipeline(RenderExecutor &executor, std::span stages) { std::vector layout_bindings; bool compute = false; for (const auto *prepared : stages) { const auto &program = *prepared->runtime->program; compute |= program.stage == ShaderType::Compute; const auto shader_stage = program.stage == ShaderType::Vertex ? vk::ShaderStageFlagBits::eVertex : program.stage == ShaderType::Pixel ? vk::ShaderStageFlagBits::eFragment : vk::ShaderStageFlagBits::eCompute; for (const auto &binding : program.bindings.descriptors) { layout_bindings.push_back( {ShaderRecompiler::IR::NativeBinding(program.stage, binding.kind), NativeDescriptorType(binding.kind), NativeDescriptorCount(binding), shader_stage}); } } vk::DescriptorSetLayoutCreateInfo descriptor_info{}; descriptor_info.sType = vk::StructureType::eDescriptorSetLayoutCreateInfo; descriptor_info.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR; descriptor_info.bindingCount = static_cast(layout_bindings.size()); descriptor_info.pBindings = layout_bindings.data(); PipelineCache::Pipeline pipeline{}; auto &device = executor.m_context.GetGraphics().device; EXIT_IF(device.createDescriptorSetLayout(&descriptor_info, nullptr, &pipeline.descriptor_set_layout) != vk::Result::eSuccess); vk::PipelineLayoutCreateInfo pipeline_info{}; pipeline_info.sType = vk::StructureType::ePipelineLayoutCreateInfo; pipeline_info.setLayoutCount = 1; pipeline_info.pSetLayouts = &pipeline.descriptor_set_layout; const vk::PushConstantRange push_range { compute ? vk::ShaderStageFlags {vk::ShaderStageFlagBits::eCompute} : vk::ShaderStageFlags {vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, 0, ShaderRecompiler::IR::NativePushConstantSize}; pipeline_info.pushConstantRangeCount = 1; pipeline_info.pPushConstantRanges = &push_range; EXIT_IF(device.createPipelineLayout(&pipeline_info, nullptr, &pipeline.pipeline_layout) != vk::Result::eSuccess); pipeline.uses_push_descriptors = true; return pipeline; } static PipelineCache::Pipeline CommitBindings(RenderExecutor &executor, CommandBuffer &buffer, PreparedBindings &bindings) { std::array stages{&bindings}; auto pipeline = CreateDescriptorPipeline(executor, stages); const auto bind_point = bindings.runtime->program->stage == ShaderType::Compute ? vk::PipelineBindPoint::eCompute : vk::PipelineBindPoint::eGraphics; executor.CommitBindings(buffer, bind_point, pipeline, stages); return pipeline; } static PipelineCache::Pipeline CommitBindings(RenderExecutor &executor, CommandBuffer &buffer, PreparedBindings &vertex, PreparedBindings &pixel) { std::array stages{&vertex, &pixel}; auto pipeline = CreateDescriptorPipeline(executor, stages); executor.CommitBindings(buffer, vk::PipelineBindPoint::eGraphics, pipeline, stages); return pipeline; } static void CommitBindings(RenderExecutor &executor, CommandBuffer &buffer, const PipelineCache::Pipeline &pipeline, PreparedBindings &vertex, PreparedBindings &pixel) { std::array stages{&vertex, &pixel}; executor.CommitBindings(buffer, vk::PipelineBindPoint::eGraphics, pipeline, stages); } static void DestroyDescriptorPipelines( RenderExecutor &executor, std::span pipelines) { auto &device = executor.m_context.GetGraphics().device; for (const auto &pipeline : pipelines) { device.destroyPipelineLayout(pipeline.pipeline_layout, nullptr); device.destroyDescriptorSetLayout(pipeline.descriptor_set_layout, nullptr); } } static void ResolveRenderDepthTarget(RenderExecutor &executor, CommandBuffer &buffer, RenderDepthInfo &depth) { executor.ResolveRenderDepthTarget(buffer, depth); } static bool DepthStencilCopy(RenderExecutor &executor, CommandBuffer &buffer) { return executor.DepthStencilCopy(buffer); } static void ResolveRenderColorTarget(RenderExecutor &executor, CommandBuffer &buffer, RenderColorInfo &color, uint32_t slot) { executor.ResolveRenderColorTarget(buffer, color, 0, slot); } static void BindRenderTarget(RenderExecutor &executor, ImageId id) { executor.BindRenderTarget(id); } static RenderState AcquireRenderTargets(RenderExecutor &executor, CommandBuffer &buffer, RenderColorInfo *colors, uint32_t color_count, RenderDepthInfo &depth, const std::optional &pixel = std::nullopt) { return executor.AcquireRenderTargets(buffer, colors, color_count, depth, pixel); } static void ResetBindings(RenderExecutor &executor) { executor.ResetBindings(); } static bool BoundImagesInOrder(const RenderExecutor &executor, ImageId first, ImageId second) { return executor.m_bound_images.size() == 2 && executor.m_bound_images[0] == first && executor.m_bound_images[1] == second; } }; namespace { using u32 = uint32_t; using BindingType = TextureCache::BindingType; using ImageDesc = TextureCache::ImageDesc; using ShaderOpcode = ShaderRecompiler::Decoder::Opcode; constexpr u32 InlineU32(u32 value) { return 128u + value; } constexpr u32 Vgpr(u32 reg) { return 256u + reg; } constexpr u32 EncodeSMovB32(u32 dst, u32 src) { return 0x80000000u | (0x7du << 23u) | ((dst & 0x7fu) << 16u) | (0x03u << 8u) | (src & 0xffu); } constexpr u32 EncodeSop1(u32 opcode, u32 dst, u32 src) { return 0x80000000u | (0x7du << 23u) | ((dst & 0x7fu) << 16u) | ((opcode & 0xffu) << 8u) | (src & 0xffu); } constexpr u32 EncodeSop2(u32 opcode, u32 dst, u32 src0, u32 src1) { return 0x80000000u | ((opcode & 0x7fu) << 23u) | ((dst & 0x7fu) << 16u) | ((src1 & 0xffu) << 8u) | (src0 & 0xffu); } constexpr u32 EncodeSopc(u32 opcode, u32 src0, u32 src1) { return 0x80000000u | (0x7eu << 23u) | ((opcode & 0x7fu) << 16u) | ((src1 & 0xffu) << 8u) | (src0 & 0xffu); } constexpr u32 EncodeSopp(u32 opcode, u32 simm = 0) { return 0x80000000u | (0x7fu << 23u) | ((opcode & 0x7fu) << 16u) | (simm & 0xffffu); } constexpr u32 EncodeSopk(u32 opcode, u32 dst, u32 imm) { return 0x80000000u | (((opcode + 0x60u) & 0x7fu) << 23u) | ((dst & 0x7fu) << 16u) | (imm & 0xffffu); } constexpr u32 EncodeVop1(u32 opcode, u32 dst, u32 src0) { return (0x3fu << 25u) | ((dst & 0xffu) << 17u) | ((opcode & 0xffu) << 9u) | (src0 & 0x1ffu); } constexpr u32 EncodeVop1Sdwa(u32 src0, u32 dst_sel = 6, u32 dst_u = 0, u32 src0_sel = 6, u32 src0_sext = 0, u32 src0_neg = 0, u32 src0_abs = 0, u32 s0 = 0, u32 clamp = 0, u32 omod = 0) { return (src0 & 0xffu) | ((dst_sel & 0x7u) << 8u) | ((dst_u & 0x3u) << 11u) | ((clamp & 0x1u) << 13u) | ((omod & 0x3u) << 14u) | ((src0_sel & 0x7u) << 16u) | ((src0_sext & 0x1u) << 19u) | ((src0_neg & 0x1u) << 20u) | ((src0_abs & 0x1u) << 21u) | ((s0 & 0x1u) << 23u); } constexpr u32 EncodeVop1Dpp(u32 src0, u32 dpp_ctrl = 0, u32 row_mask = 0xf, u32 bank_mask = 0xf) { return (src0 & 0xffu) | ((dpp_ctrl & 0x1ffu) << 8u) | ((bank_mask & 0xfu) << 24u) | ((row_mask & 0xfu) << 28u); } constexpr u32 EncodeVop2(u32 opcode, u32 dst, u32 src0, u32 src1) { return ((opcode & 0x3fu) << 25u) | ((dst & 0xffu) << 17u) | ((src1 & 0xffu) << 9u) | (src0 & 0x1ffu); } constexpr u32 EncodeVop2Sdwa(u32 src0, u32 dst_sel = 6, u32 dst_u = 0, u32 src0_sel = 6, u32 src1_sel = 6, u32 src0_sext = 0, u32 src1_sext = 0, u32 src0_neg = 0, u32 src0_abs = 0, u32 src1_neg = 0, u32 src1_abs = 0, u32 s0 = 0, u32 s1 = 0, u32 clamp = 0, u32 omod = 0) { return (src0 & 0xffu) | ((dst_sel & 0x7u) << 8u) | ((dst_u & 0x3u) << 11u) | ((clamp & 0x1u) << 13u) | ((omod & 0x3u) << 14u) | ((src0_sel & 0x7u) << 16u) | ((src0_sext & 0x1u) << 19u) | ((src0_neg & 0x1u) << 20u) | ((src0_abs & 0x1u) << 21u) | ((s0 & 0x1u) << 23u) | ((src1_sel & 0x7u) << 24u) | ((src1_sext & 0x1u) << 27u) | ((src1_neg & 0x1u) << 28u) | ((src1_abs & 0x1u) << 29u) | ((s1 & 0x1u) << 31u); } constexpr u32 EncodeVop2Dpp(u32 src0, u32 dpp_ctrl = 0, u32 row_mask = 0xf, u32 bank_mask = 0xf, u32 src0_neg = 0, u32 src0_abs = 0, u32 src1_neg = 0, u32 src1_abs = 0) { return (src0 & 0xffu) | ((dpp_ctrl & 0x1ffu) << 8u) | ((src0_neg & 0x1u) << 20u) | ((src0_abs & 0x1u) << 21u) | ((src1_neg & 0x1u) << 22u) | ((src1_abs & 0x1u) << 23u) | ((bank_mask & 0xfu) << 24u) | ((row_mask & 0xfu) << 28u); } constexpr u32 EncodeVop3Word0(u32 opcode, u32 dst, u32 abs = 0, u32 op_sel = 0, bool clamp = false) { return (0x35u << 26u) | ((opcode & 0x3ffu) << 16u) | ((abs & 0x7u) << 8u) | ((op_sel & 0xfu) << 11u) | (clamp ? (1u << 15u) : 0u) | (dst & 0xffu); } constexpr u32 EncodeVop3BWord0(u32 opcode, u32 vdst, u32 sdst) { return (0x35u << 26u) | ((opcode & 0x3ffu) << 16u) | ((sdst & 0x7fu) << 8u) | (vdst & 0xffu); } constexpr u32 EncodeVop3Word1(u32 src0, u32 src1, u32 src2 = 0, u32 omod = 0, u32 neg = 0) { return (src0 & 0x1ffu) | ((src1 & 0x1ffu) << 9u) | ((src2 & 0x1ffu) << 18u) | ((omod & 0x3u) << 27u) | ((neg & 0x7u) << 29u); } constexpr u32 EncodeVop3pWord0(u32 opcode, u32 dst, u32 op_sel_hi = 0, u32 op_sel = 0, u32 neg_hi = 0, bool clamp = false) { return (0x33u << 26u) | ((opcode & 0x7fu) << 16u) | ((neg_hi & 0x7u) << 8u) | ((op_sel & 0x7u) << 11u) | (((op_sel_hi >> 2u) & 0x1u) << 14u) | (clamp ? (1u << 15u) : 0u) | (dst & 0xffu); } constexpr u32 EncodeVop3pWord1(u32 src0, u32 src1, u32 src2 = 0, u32 op_sel_hi = 0, u32 neg = 0) { return (src0 & 0x1ffu) | ((src1 & 0x1ffu) << 9u) | ((src2 & 0x1ffu) << 18u) | ((op_sel_hi & 0x3u) << 27u) | ((neg & 0x7u) << 29u); } constexpr u32 EncodeVopc(u32 opcode, u32 src0, u32 src1) { return (0x3eu << 25u) | ((opcode & 0xffu) << 17u) | ((src1 & 0xffu) << 9u) | (src0 & 0x1ffu); } constexpr u32 EncodeVopcSdwa(u32 src0, u32 sdst = 0, u32 sd = 0, u32 src0_sel = 6, u32 src1_sel = 6, u32 src0_sext = 0, u32 src1_sext = 0, u32 src0_neg = 0, u32 src0_abs = 0, u32 src1_neg = 0, u32 src1_abs = 0, u32 s0 = 0, u32 s1 = 0) { return (src0 & 0xffu) | ((sdst & 0x7fu) << 8u) | ((sd & 0x1u) << 15u) | ((src0_sel & 0x7u) << 16u) | ((src0_sext & 0x1u) << 19u) | ((src0_neg & 0x1u) << 20u) | ((src0_abs & 0x1u) << 21u) | ((s0 & 0x1u) << 23u) | ((src1_sel & 0x7u) << 24u) | ((src1_sext & 0x1u) << 27u) | ((src1_neg & 0x1u) << 28u) | ((src1_abs & 0x1u) << 29u) | ((s1 & 0x1u) << 31u); } constexpr u32 EncodeMubuf0(u32 opcode, u32 offset = 0, bool idxen = false, bool offen = true, bool glc = false) { return (0x38u << 26u) | ((opcode & 0x7fu) << 18u) | (offen ? (1u << 12u) : 0u) | (idxen ? (1u << 13u) : 0u) | (glc ? (1u << 14u) : 0u) | (offset & 0xfffu); } constexpr u32 EncodeMubuf1(u32 vdata, u32 srsrc, u32 vaddr, u32 soffset = 128) { return ((soffset & 0xffu) << 24u) | ((srsrc & 0x1fu) << 16u) | ((vdata & 0xffu) << 8u) | (vaddr & 0xffu); } constexpr u32 EncodeMtbuf0(u32 opcode, u32 dfmt, u32 nfmt, u32 offset = 0, bool idxen = false, bool offen = true) { return (0x3au << 26u) | (offset & 0xfffu) | (offen ? (1u << 12u) : 0u) | (idxen ? (1u << 13u) : 0u) | ((opcode & 0x7u) << 16u) | ((dfmt & 0xfu) << 19u) | ((nfmt & 0x7u) << 23u); } constexpr u32 EncodeMtbuf1(u32 opcode, u32 vdata, u32 srsrc, u32 vaddr, u32 soffset = 128) { return (((opcode >> 3u) & 1u) << 21u) | ((soffset & 0xffu) << 24u) | ((srsrc & 0x1fu) << 16u) | ((vdata & 0xffu) << 8u) | (vaddr & 0xffu); } constexpr u32 EncodeSmem0(u32 opcode, u32 dst, u32 sbase = 0) { return (0x3du << 26u) | ((opcode & 0xffu) << 18u) | ((dst & 0x7fu) << 6u) | (sbase & 0x3fu); } constexpr u32 EncodeSmem1(u32 offset, u32 soffset = 0) { return (offset & 0x1fffffu) | ((soffset & 0x7fu) << 25u); } constexpr u32 EncodeMimg0(u32 opcode, u32 dmask, u32 nsa_dwords = 0, bool glc = false, u32 dim = 1, bool r128 = false) { return (0x3cu << 26u) | ((opcode >> 7u) & 0x1u) | ((nsa_dwords & 0x3u) << 1u) | ((dim & 0x7u) << 3u) | ((dmask & 0xfu) << 8u) | (glc ? (1u << 13u) : 0u) | (r128 ? (1u << 15u) : 0u) | ((opcode & 0x7fu) << 18u); } constexpr u32 EncodeMimg1(u32 vdata, u32 vaddr, u32 srsrc = 0, u32 ssamp = 0, bool a16 = false, bool d16 = false) { return ((ssamp & 0x1fu) << 21u) | ((srsrc & 0x1fu) << 16u) | ((vdata & 0xffu) << 8u) | (vaddr & 0xffu) | (a16 ? (1u << 30u) : 0u) | (d16 ? (1u << 31u) : 0u); } constexpr u32 EncodeVintrp(u32 opcode, u32 dst, u32 attr, u32 chan, u32 src) { return (0x32u << 26u) | ((dst & 0xffu) << 18u) | ((opcode & 0x3u) << 16u) | ((attr & 0x3fu) << 10u) | ((chan & 0x3u) << 8u) | (src & 0xffu); } constexpr u32 EncodeExp0(u32 target, u32 en, bool done = true, bool compr = false, bool vm = false) { return (0x3eu << 26u) | ((target & 0x3fu) << 4u) | (en & 0xfu) | (compr ? (1u << 10u) : 0u) | (done ? (1u << 11u) : 0u) | (vm ? (1u << 12u) : 0u); } constexpr u32 EncodeExp1(u32 src0, u32 src1, u32 src2, u32 src3) { return (src0 & 0xffu) | ((src1 & 0xffu) << 8u) | ((src2 & 0xffu) << 16u) | ((src3 & 0xffu) << 24u); } constexpr u32 EncodeFlat0(u32 opcode, u32 segment, u32 offset = 0) { return (0x37u << 26u) | ((opcode & 0x7fu) << 18u) | ((segment & 0x3u) << 14u) | (offset & 0xfffu); } constexpr u32 EncodeFlat1(u32 vdst, u32 saddr, u32 data, u32 addr) { return ((vdst & 0xffu) << 24u) | ((saddr & 0x7fu) << 16u) | ((data & 0xffu) << 8u) | (addr & 0xffu); } constexpr u32 EncodeDs0(u32 opcode, u32 offset = 0, bool gds = false) { return (0x36u << 26u) | ((opcode & 0xffu) << 18u) | (gds ? (1u << 17u) : 0u) | (offset & 0xffffu); } constexpr u32 EncodeDs1Ex(u32 vdst, u32 data1, u32 data0, u32 addr) { return ((vdst & 0xffu) << 24u) | ((data1 & 0xffu) << 16u) | ((data0 & 0xffu) << 8u) | (addr & 0xffu); } constexpr u32 EncodeDs1(u32 vdst, u32 data0, u32 addr) { return EncodeDs1Ex(vdst, 0, data0, addr); } void AppendSMovLiteral(std::vector *code, u32 dst, u32 literal) { code->push_back(EncodeSMovB32(dst, 255u)); code->push_back(literal); } void AppendVMovLiteral(std::vector *code, u32 dst, u32 literal) { code->push_back(EncodeVop1(0x01, dst, 255u)); code->push_back(literal); } void AppendVMovU32(std::vector *code, u32 dst, u32 value) { if (value <= 64u) { code->push_back(EncodeVop1(0x01, dst, InlineU32(value))); return; } AppendVMovLiteral(code, dst, value); } void AppendVop3(std::vector *code, u32 opcode, u32 dst, u32 src0, u32 src1, u32 src2 = 0, u32 abs = 0, u32 op_sel = 0, bool clamp = false, u32 omod = 0, u32 neg = 0) { code->push_back(EncodeVop3Word0(opcode, dst, abs, op_sel, clamp)); code->push_back(EncodeVop3Word1(src0, src1, src2, omod, neg)); } void AppendVop3B(std::vector *code, u32 opcode, u32 vdst, u32 sdst, u32 src0, u32 src1, u32 src2 = 0) { code->push_back(EncodeVop3BWord0(opcode, vdst, sdst)); code->push_back(EncodeVop3Word1(src0, src1, src2)); } void AppendVop3p(std::vector *code, u32 opcode, u32 dst, u32 src0, u32 src1, u32 src2 = 0, u32 op_sel_hi = 0, u32 op_sel = 0, u32 neg_hi = 0, u32 neg = 0) { code->push_back(EncodeVop3pWord0(opcode, dst, op_sel_hi, op_sel, neg_hi)); code->push_back(EncodeVop3pWord1(src0, src1, src2, op_sel_hi, neg)); } void AppendBufferLoadDword(std::vector *code, u32 dst_vgpr, u32 address_vgpr) { code->push_back(EncodeMubuf0(0x0cu)); code->push_back(EncodeMubuf1(dst_vgpr, 0, address_vgpr)); } void AppendBufferLoadOpcode(std::vector *code, u32 opcode, u32 dst_vgpr, u32 address_vgpr) { code->push_back(EncodeMubuf0(opcode)); code->push_back(EncodeMubuf1(dst_vgpr, 0, address_vgpr)); } void AppendBufferStoreDword(std::vector *code, u32 value_vgpr, u32 address_vgpr) { code->push_back(EncodeMubuf0(0x1cu)); code->push_back(EncodeMubuf1(value_vgpr, 12, address_vgpr)); } void AppendBufferStoreOpcode(std::vector *code, u32 opcode, u32 value_vgpr, u32 address_vgpr, bool glc = false) { code->push_back(EncodeMubuf0(opcode, 0, false, true, glc)); code->push_back(EncodeMubuf1(value_vgpr, 12, address_vgpr)); } void AppendTBufferLoadOpcode(std::vector *code, u32 opcode, u32 dst_vgpr, u32 address_vgpr) { code->push_back(EncodeMtbuf0(opcode, 14, 7)); code->push_back(EncodeMtbuf1(opcode, dst_vgpr, 0, address_vgpr)); } constexpr u32 BufferFormat(Prospero::BufferFormat format) { return static_cast(format); } void AppendTBufferLoadFormatOpcode(std::vector *code, u32 opcode, u32 dst_vgpr, u32 address_vgpr, Prospero::BufferFormat format) { const auto value = BufferFormat(format); code->push_back(EncodeMtbuf0(opcode, value & 0xfu, (value >> 4u) & 0x7u)); code->push_back(EncodeMtbuf1(opcode, dst_vgpr, 0, address_vgpr)); } void AppendTBufferStoreOpcode(std::vector *code, u32 opcode, u32 value_vgpr, u32 address_vgpr) { code->push_back(EncodeMtbuf0(opcode, 14, 7)); code->push_back(EncodeMtbuf1(opcode, value_vgpr, 0, address_vgpr)); } void AppendSmemLoadOpcode(std::vector *code, u32 opcode, u32 dst_sgpr, u32 byte_offset) { code->push_back(EncodeSmem0(opcode, dst_sgpr)); code->push_back(EncodeSmem1(byte_offset)); } void AppendStoreVgpr(std::vector *code, u32 value_vgpr, u32 dword_index) { AppendVMovU32(code, 31, dword_index * 4u); AppendBufferStoreDword(code, value_vgpr, 31); } void AppendStoreVgprAtLaneDwordOffset(std::vector *code, u32 value_vgpr, u32 lane_vgpr, u32 dword_offset) { if (dword_offset == 0u) { code->push_back(EncodeVop2(0x1a, 31, InlineU32(2), lane_vgpr)); } else { AppendVMovU32(code, 31, dword_offset); code->push_back(EncodeVop2(0x25, 31, Vgpr(lane_vgpr), 31)); code->push_back(EncodeVop2(0x1a, 31, InlineU32(2), 31)); } AppendBufferStoreDword(code, value_vgpr, 31); } void AppendStoreSgpr(std::vector *code, u32 value_sgpr, u32 dword_index) { code->push_back(EncodeVop1(0x01, 30, value_sgpr)); AppendStoreVgpr(code, 30, dword_index); } void AppendStoreSgprAtLaneDwordOffset(std::vector *code, u32 value_sgpr, u32 lane_vgpr, u32 dword_offset) { code->push_back(EncodeVop1(0x01, 30, value_sgpr)); AppendStoreVgprAtLaneDwordOffset(code, 30, lane_vgpr, dword_offset); } void AppendStoreSgprPair(std::vector *code, u32 value_sgpr, u32 first_dword_index) { AppendStoreSgpr(code, value_sgpr, first_dword_index); AppendStoreSgpr(code, value_sgpr + 1u, first_dword_index + 1u); } void AppendEnd(std::vector *code) { code->push_back(0xbf810000u); } std::string Hex(u32 value) { char buffer[32] = {}; std::snprintf(buffer, sizeof(buffer), "0x%08" PRIx32, value); return buffer; } [[noreturn]] void Fail(const char *shader_name, const char *stage, const std::string &message) { std::fprintf(stderr, "ShaderRecompilerComputeTests: %s failed at %s: %s\n", shader_name, stage, message.c_str()); std::abort(); } void Require(const char *shader_name, const char *stage, bool value, const std::string &message); void EnsureConfigInitialized() { static bool config_initialized = false; if (!config_initialized) { static Common::Subsystems subsystems; Common::InitializeThreads(); subsystems.Initialize(); Config::ConfigOptions options; options.printf_direction = Config::LogDirection::Silent; Config::Load(options); subsystems.Initialize(); subsystems.Initialize(); ShaderInit(); config_initialized = true; } } void RequireVk(const char *shader_name, const char *stage, vk::Result result, const char *action) { if (result != vk::Result::eSuccess) { Fail(shader_name, stage, std::string(action) + " returned " + vk::to_string(result)); } } void Require(const char *shader_name, const char *stage, bool value, const std::string &message) { if (!value) { Fail(shader_name, stage, message); } } void CheckErrorDialogLifecycle() { namespace ErrorDialog = Libs::Dialog::ErrorDialog; struct Param { int32_t size = 16; int32_t error_code = static_cast(0x80550006u); int32_t user_id = 1; int32_t reserved = 0; } param; static_assert(sizeof(Param) == 16); constexpr int invalid_state = static_cast(0x80ed0005u); constexpr int invalid_param = static_cast(0x80ed0003u); static std::vector notifications; notifications.clear(); ErrorDialog::SetVisibilityCallback([] { const auto visual = ErrorDialog::GetVisualState(); ErrorDialog::HostSnapshot snapshot{}; Require("ErrorDialog", "callback reentry", ErrorDialog::GetHostSnapshot(&snapshot) == visual.active && (ErrorDialog::ErrorDialogGetStatus() == 2) == visual.active, "visibility callback observed inconsistent dialog state"); notifications.push_back(visual); }); const auto initial_revision = ErrorDialog::GetVisualState().revision; const auto check_state = [&](const char *stage, int status, size_t visibility_changes) { const auto visual = ErrorDialog::GetVisualState(); ErrorDialog::HostSnapshot snapshot{}; Require("ErrorDialog", stage, ErrorDialog::ErrorDialogGetStatus() == status && ErrorDialog::ErrorDialogUpdateStatus() == status && visual.active == (status == 2) && ErrorDialog::GetHostSnapshot(&snapshot) == visual.active && visual.revision == initial_revision + visibility_changes && notifications.size() == visibility_changes && (notifications.empty() || (notifications.back().active == visual.active && notifications.back().revision == visual.revision)), "status, host visibility, or visibility notification changed " "unexpectedly"); }; check_state("uninitialized", 0, 0); Require("ErrorDialog", "uninitialized operations", ErrorDialog::ErrorDialogTerminate() == static_cast(0x80ed0001u) && ErrorDialog::ErrorDialogClose() == invalid_state, "uninitialized dialog accepted termination or close"); Require("ErrorDialog", "initialize", ErrorDialog::ErrorDialogInitialize() == 0 && ErrorDialog::ErrorDialogInitialize() == static_cast(0x80ed0002u), "initialization did not reject a second initialization"); Param invalid = param; invalid.size--; Require("ErrorDialog", "invalid open", ErrorDialog::ErrorDialogOpen(nullptr) == invalid_param && ErrorDialog::ErrorDialogOpen(&invalid) == invalid_param && ErrorDialog::ErrorDialogClose() == invalid_state, "invalid parameters or close were accepted before opening"); check_state("failed open preserves initialized state", 1, 0); Require("ErrorDialog", "open", ErrorDialog::ErrorDialogOpen(¶m) == 0, "valid error dialog did not open"); ErrorDialog::HostSnapshot first{}; Require("ErrorDialog", "copied error code", ErrorDialog::GetHostSnapshot(&first) && first.error_code == param.error_code, "host dialog did not expose the guest error code"); param.error_code = static_cast(0x8055000au); for (int poll = 0; poll < 8; ++poll) { check_state("polling waits for acknowledgement", 2, 1); } Require( "ErrorDialog", "running failures", ErrorDialog::ErrorDialogOpen(¶m) == invalid_state && !ErrorDialog::HostAccept(first.generation + 1), "an invalid open or stale acknowledgement replaced the active dialog"); ErrorDialog::HostSnapshot current{}; Require("ErrorDialog", "failed open preserves active dialog", ErrorDialog::GetHostSnapshot(¤t) && current.generation == first.generation && current.error_code == first.error_code, "active error code or generation changed after a failed open"); check_state("failed operations preserve running state", 2, 1); Require("ErrorDialog", "acknowledge", ErrorDialog::HostAccept(first.generation), "current host acknowledgement did not finish the dialog"); check_state("acknowledged", 3, 2); Require("ErrorDialog", "finished operations", !ErrorDialog::HostAccept(first.generation) && ErrorDialog::ErrorDialogClose() == invalid_state && ErrorDialog::ErrorDialogOpen(&invalid) == invalid_param, "finished dialog accepted duplicate completion or an invalid open"); check_state("failed operations preserve finished state", 3, 2); Require("ErrorDialog", "reopen and stale acknowledgement", ErrorDialog::ErrorDialogOpen(¶m) == 0 && ErrorDialog::GetHostSnapshot(¤t) && current.generation != first.generation && current.error_code == param.error_code && !ErrorDialog::HostAccept(first.generation), "reopened dialog accepted acknowledgement from its predecessor"); check_state("reopened", 2, 3); Require("ErrorDialog", "guest close", ErrorDialog::ErrorDialogClose() == 0, "guest close did not finish the running dialog"); check_state("closed", 3, 4); Require("ErrorDialog", "terminate running dialog", ErrorDialog::ErrorDialogOpen(¶m) == 0 && ErrorDialog::GetHostSnapshot(¤t) && ErrorDialog::ErrorDialogTerminate() == 0 && !ErrorDialog::HostAccept(current.generation), "termination failed to remove the active dialog"); check_state("terminated", 0, 6); Require("ErrorDialog", "reinitialize and stale acknowledgement", ErrorDialog::ErrorDialogInitialize() == 0 && ErrorDialog::ErrorDialogOpen(¶m) == 0 && !ErrorDialog::HostAccept(current.generation), "reinitialization reused an old dialog generation"); check_state("reinitialized", 2, 7); Require("ErrorDialog", "final termination", ErrorDialog::ErrorDialogTerminate() == 0 && ErrorDialog::ErrorDialogTerminate() == static_cast(0x80ed0001u), "termination did not restore the uninitialized state"); check_state("final state", 0, 8); ErrorDialog::SetVisibilityCallback(nullptr); Require("ErrorDialog", "lifecycle without a visibility listener", ErrorDialog::ErrorDialogInitialize() == 0 && ErrorDialog::ErrorDialogOpen(¶m) == 0 && ErrorDialog::ErrorDialogClose() == 0 && ErrorDialog::ErrorDialogTerminate() == 0 && ErrorDialog::ErrorDialogGetStatus() == 0 && !ErrorDialog::GetVisualState().active && ErrorDialog::GetVisualState().revision == initial_revision + 10 && notifications.size() == 8, "guest lifecycle depended on a registered host visibility listener"); std::printf("[host] %-32s ok\n", "ErrorDialogLifecycle"); } #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS template void ExpectFatal(const char *name, Function function) { const pid_t pid = ::fork(); Require(name, "fatal subprocess", pid >= 0, "fork failed while starting fatal shader test"); if (pid == 0) { function(); ::_exit(0); } int status = 0; pid_t waited = -1; do { waited = ::waitpid(pid, &status, 0); } while (waited < 0 && errno == EINTR); Require(name, "fatal subprocess", waited == pid, "waitpid failed while collecting fatal shader test"); Require(name, "fatal exit", WIFEXITED(status) && WEXITSTATUS(status) == (321 & 0xff), "fatal shader path did not terminate with status 321"); std::printf("[host] %-32s ok\n", name); } #endif void CheckLeastRecentlyUsedCacheOrdering() { Common::LeastRecentlyUsedCache cache; const auto first = cache.Insert(1, 1); const auto middle = cache.Insert(2, 2); const auto last = cache.Insert(3, 3); (void)first; (void)last; cache.Touch(middle, 4); std::vector visited; cache.ForEachItemBelow(4, [&](uint32_t value) { visited.push_back(value); return visited.size() > 3; }); Require("LeastRecentlyUsedCache", "non-tail touch traversal", visited == std::vector{1, 3, 2}, "touching a non-tail item left a cycle or changed LRU order"); std::printf("[host] %-32s ok\n", "LeastRecentlyUsedCache"); } struct BdaMapping { uint64_t guest_base = 0; u32 backing_offset = 0; }; struct TestCase { const char *name = ""; std::vector code; std::vector initial; std::vector expected; std::vector opcodes; u32 image_width = 4; u32 image_height = 4; std::vector sampled_image_rgba; std::vector> sampled_image_rgba_mips; vk::Format sampled_image_format = vk::Format::eR32G32B32A32Sfloat; u32 sampled_image_dwords_per_pixel = 4; vk::ImageType sampled_image_type = vk::ImageType::e2D; vk::ImageViewType sampled_image_view_type = vk::ImageViewType::e2D; u32 sampled_image_layers = 1; u32 sampled_image_view_base_layer = 0; u32 sampled_image_view_layers = 0; std::vector storage_image_rgba; std::vector expected_storage_image_rgba; vk::Format storage_image_format = vk::Format::eR32G32B32A32Sfloat; u32 storage_image_dwords_per_pixel = 4; std::vector storage_image_r32ui; std::vector expected_storage_image_r32ui; std::vector required_spirv; std::vector forbidden_spirv; ShaderComputeInputInfo compute_info = [] { ShaderComputeInputInfo info{}; info.lds_size_dwords = 1024; return info; }(); bool has_compute_info = false; u32 dispatch_x = 1; u32 dispatch_y = 1; u32 dispatch_z = 1; std::array user_data{}; bool has_user_data = false; u32 image_descriptor_swizzle = DstSel(4, 5, 6, 7); bool compile_only = false; size_t storage_buffer_range_dwords = 0; std::vector storage_buffer_offsets; std::vector bda_mappings; bool expand_shader_data_storage = false; bool expected_force_point_sampler = false; std::vector gds_initial; std::vector expected_gds; std::vector> decoded_counts; std::vector> ir_counts; u32 expected_storage_mip_descriptors = 0; }; struct GraphicsCase { const char *name = ""; std::vector fragment_code; std::vector expected_pixel; std::vector opcodes; std::array user_data{}; bool has_user_data = false; std::vector push_constants; std::vector pixel_interpolator_settings; bool pixel_no_perspective = false; std::vector vertices; bool pixel_ancillary = false; bool pixel_front_face = false; u32 layers = 1; vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1; bool pixel_position_w = false; float vertex_clip_w = 1.0f; bool pixel_depth_export = false; u32 pixel_perspective_centroid_vgpr = UINT32_MAX; u32 pixel_custom_interpolation_mask = 0; }; struct CompiledShader { std::vector spirv; ShaderRecompiler::IR::Program program; ShaderRecompiler::IR::ResourceSnapshot resources; std::vector packed_user_data; }; std::array MakeNativeUserData(const std::array *source) { std::array data{}; data[50] = 1u << 20u; if (source != nullptr) { data = *source; } return data; } bool ReadTestMemory(void *userdata, uint64_t address, u32 *value) { const auto *data = static_cast *>(userdata); if (data == nullptr || value == nullptr || address % 4u != 0 || address / 4u >= data->size()) { return false; } *value = (*data)[address / 4u]; return true; } void ValidateSpirv(const char *shader_name, const std::vector &spirv) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string messages; tools.SetMessageConsumer([&messages](spv_message_level_t, const char *, const spv_position_t &position, const char *message) { char buffer[1024] = {}; std::snprintf(buffer, sizeof(buffer), "%zu:%zu: %s\n", position.line, position.column, message); messages += buffer; }); if (!tools.Validate(spirv)) { Fail(shader_name, "SPIR-V validation", messages); } } size_t CountText(const std::string &text, const std::string &needle) { size_t count = 0; for (size_t offset = 0; (offset = text.find(needle, offset)) != std::string::npos; offset += needle.size()) { count++; } return count; } void CheckPixelParameterAliases() { constexpr const char *name = "PixelParameterAliases"; ShaderPixelInputInfo pixel{}; pixel.input_num = 2; const std::array pair = {0, 1}; for (const auto flat : {0u, 0x400u}) { pixel.interpolator_settings[0] = flat | 3u; pixel.interpolator_settings[1] = flat | 3u; Require(name, "same-mode nonidentity alias", ShaderPixelParameterLocation(pixel, pair, 0) == 3u && ShaderPixelParameterLocation(pixel, pair, 1) == 3u, "aliases of one export with the same interpolation mode must " "share that physical location"); } pixel.interpolator_settings[0] = 3u; pixel.interpolator_settings[1] = 0x423u; pixel.custom_interpolation_mask = 2u; Require(name, "smooth/custom alias", ShaderPixelParameterLocation(pixel, pair, 0) == 3u && ShaderPixelParameterLocation(pixel, pair, 1) == 3u, "custom interpolation must retain its shared vertex export"); pixel.input_num = 3; pixel.custom_interpolation_mask = 0; pixel.interpolator_settings[0] = 0x400u; pixel.interpolator_settings[1] = 0u; pixel.interpolator_settings[2] = 1u; const std::array active = {0, 1, 2}; Require(name, "reserved physical locations", ShaderPixelParameterLocation(pixel, active, 0) == 0u && ShaderPixelParameterLocation(pixel, active, 1) == 2u && ShaderPixelParameterLocation(pixel, active, 2) == 1u, "a synthesized opposite-mode alias must not occupy another " "vertex export's physical location"); std::printf("[host] %-32s ok\n", name); } void CheckRectListShaders() { constexpr const char *name = "RectListShaders"; ShaderRecompiler::IR::CompiledShaderInfo program{}; program.info.inputs.push_back( {ShaderRecompiler::IR::StageInputKind::Parameter, 0, 4, "in_param_0"}); program.info.inputs.push_back( {ShaderRecompiler::IR::StageInputKind::Parameter, 1, 4, "in_param_1"}); ShaderVertexInputInfo vertex{}; ShaderRecompiler::IR::CompiledShaderInfo vertex_program{.param_export_mask = 1u}; vertex.stage.program = &vertex_program; ShaderPixelInputInfo pixel{}; pixel.input_num = 2; pixel.interpolator_settings[0] = 0x400u; pixel.interpolator_settings[1] = 0; pixel.stage.program = &program; std::vector perspective_specialization; BuildStageStaticKey(pixel, perspective_specialization); pixel.ps_no_perspective = true; std::vector no_perspective_specialization; BuildStageStaticKey(pixel, no_perspective_specialization); pixel.ps_no_perspective = false; Require(name, "pipeline identity", perspective_specialization != no_perspective_specialization, "pixel interpolation mode must participate in the shader and " "pipeline key"); const std::array active_inputs = {0, 1}; Require(name, "duplicate mapping", ShaderPixelParameterLocation(pixel, active_inputs, 0) == 0 && ShaderPixelParameterLocation(pixel, active_inputs, 1) == 1, "duplicate pixel mappings must receive distinct effective output " "locations"); const auto shaders = BuildRectListShaders(vertex, &pixel); Require(name, "SPIR-V version", shaders.control.size() > 1 && shaders.control[1] == 0x00010500u && shaders.evaluation.size() > 1 && shaders.evaluation[1] == 0x00010500u, "vector selection requires SPIR-V 1.5"); ValidateSpirv(name, shaders.control); ValidateSpirv(name, shaders.evaluation); spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string control_text; std::string evaluation_text; Require(name, "control disassembly", tools.Disassemble(shaders.control, &control_text), "failed to disassemble rectangle-list tessellation control shader"); Require( name, "evaluation disassembly", tools.Disassemble(shaders.evaluation, &evaluation_text), "failed to disassemble rectangle-list tessellation evaluation shader"); Require(name, "control execution mode", control_text.find("TessellationControl") != std::string::npos && control_text.find("OutputVertices 4") != std::string::npos, "rectangle-list control shader must produce four control points"); Require(name, "evaluation execution modes", evaluation_text.find("TessellationEvaluation") != std::string::npos && evaluation_text.find("Quads") != std::string::npos && evaluation_text.find("SpacingEqual") != std::string::npos && evaluation_text.find("VertexOrderCw") != std::string::npos, "rectangle-list evaluation shader has the wrong patch modes"); Require(name, "no geometry stage", control_text.find("Geometry") == std::string::npos && evaluation_text.find("Geometry") == std::string::npos, "rectangle-list expansion must not use geometry shaders"); Require(name, "flat broadcast", CountText(control_text, "OpVectorTimesScalar") == 6 && CountText(control_text, "OpSelect %v4float") == 2, "flat parameters must use guest vertex zero instead of reconstructed " "values"); Require(name, "remapped interface", CountText(control_text, " Location 0") == 2 && CountText(control_text, " Location 1") == 1 && CountText(evaluation_text, " Location 0") == 2 && CountText(evaluation_text, " Location 1") == 2, "duplicate pixel mappings must share one vertex input and keep " "distinct patch outputs"); for (const auto flat : {0u, 0x400u}) { pixel.interpolator_settings[0] = flat; pixel.interpolator_settings[1] = flat; const auto shared = BuildRectListShaders(vertex, &pixel); ValidateSpirv(name, shared.control); ValidateSpirv(name, shared.evaluation); Require(name, "same-mode alias disassembly", tools.Disassemble(shared.control, &control_text) && tools.Disassemble(shared.evaluation, &evaluation_text), "failed to disassemble shared rectangle-list parameters"); Require(name, "same-mode interface deduplication", CountText(control_text, " Location 0") == 2 && CountText(evaluation_text, " Location 0") == 2 && control_text.find(" Location 1") == std::string::npos && evaluation_text.find(" Location 1") == std::string::npos, "same-mode aliases must produce one input and one output at " "the shared location in each rectangle-list stage"); } const auto position_only = BuildRectListShaders(vertex, nullptr); ValidateSpirv(name, position_only.control); ValidateSpirv(name, position_only.evaluation); } void CheckSpirvText(const TestCase &test, const std::vector &spirv) { if (test.required_spirv.empty() && test.forbidden_spirv.empty()) { return; } spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string text; if (!tools.Disassemble(spirv, &text)) { Fail(test.name, "SPIR-V disassembly", "failed to disassemble emitted SPIR-V"); } for (const auto &required : test.required_spirv) { if (text.find(required) == std::string::npos) { Fail(test.name, "SPIR-V disassembly", std::string("missing required text: ") + required); } } for (const auto &forbidden : test.forbidden_spirv) { if (text.find(forbidden) != std::string::npos) { Fail(test.name, "SPIR-V disassembly", std::string("found forbidden text: ") + forbidden); } } } CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) { auto user_data = MakeNativeUserData(test.has_user_data ? &test.user_data : nullptr); const auto uses_image = std::any_of(test.opcodes.begin(), test.opcodes.end(), [](auto op) { return op >= ShaderOpcode::IMAGE_GET_RESINFO && op <= ShaderOpcode::IMAGE_GATHER4H; }); if (uses_image && ((user_data[3] >> 28u) & 0xfu) == 0) { user_data[3] = static_cast(Prospero::ImageType::kColor2D) << 28u; } if (uses_image) { user_data[3] = (user_data[3] & ~0xfffu) | test.image_descriptor_swizzle; } if (!test.has_user_data) { user_data[2] = static_cast(test.initial.size() * sizeof(u32)); } ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Compute; options.dump_ir = true; auto compute_info = test.compute_info; compute_info.host_subgroup_size = host_subgroup_size; options.input_info.compute = &compute_info; options.user_data = user_data; if (test.has_compute_info) { options.wave_size = test.compute_info.wave_size; } auto translated = ShaderRecompiler::TranslateProgram(test.code, options); auto resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program); ShaderRecompiler::IR::ResourceSnapshot resources; ShaderRecompiler::IR::ResourceSpecialization specialization; const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(test.code.data()), .read_memory = ReadTestMemory, .userdata = const_cast *>(&test.initial), }; Require(test.name, "resource materialization", ShaderRecompiler::IR::MaterializeResources( resource_plan, runtime, resources, specialization), "translated resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( std::move(translated), options, specialization); for (const auto &[text, expected] : test.decoded_counts) { const auto actual = CountText(result.decoded_dump, text); Require(test.name, "decoded RDNA2", actual == expected, text + " count=" + std::to_string(actual) + ", expected=" + std::to_string(expected)); } for (const auto &[text, expected] : test.ir_counts) { const auto actual = CountText(result.ir_dump, text); Require(test.name, "typed IR", actual == expected, text + " count=" + std::to_string(actual) + ", expected=" + std::to_string(expected)); } if (test.expected_storage_mip_descriptors != 0) { const auto image = std::ranges::find_if( result.program.info.images, [](const auto &resource) { return resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage; }); Require(test.name, "dynamic storage mip specialization", image != result.program.info.images.end() && image->mip_count == test.expected_storage_mip_descriptors, "runtime descriptor range did not specialize the mip count"); const auto resource = static_cast(image - result.program.info.images.begin()); const auto binding = std::ranges::find_if( result.program.bindings.descriptors, [&](const auto &group) { return std::ranges::find(group.resources, resource) != group.resources.end(); }); Require(test.name, "dynamic storage mip binding", binding != result.program.bindings.descriptors.end() && std::ranges::count(binding->resources, resource) == test.expected_storage_mip_descriptors, "dynamic storage image did not receive one descriptor per mip"); } if (test.expand_shader_data_storage) { auto &block = *result.program.blocks.front(); for (u32 reg = 0; reg < 33; reg++) { auto &user_data = block.AppendNewInst( ShaderRecompiler::IR::ValueOpcode::GetUserData, {ShaderRecompiler::IR::Value( static_cast(reg))}); block.AppendNewInst(ShaderRecompiler::IR::ValueOpcode::ReferenceU32, {ShaderRecompiler::IR::Value(&user_data)}); } result.program.bindings = {}; result.program.binding_layout_complete = false; ShaderRecompiler::IR::AllocateBindings(result.program); const auto *shader_data = ShaderRecompiler::IR::FindBinding( result.program.bindings, ShaderRecompiler::IR::DescriptorBindingKind::ShaderData); Require(test.name, "binding layout", result.program.bindings.user_data_registers.size() == 33 && !result.program.bindings.UsesPushData() && shader_data != nullptr, "oversized shader data did not use its storage fallback"); result.spirv = ShaderRecompiler::Spirv::EmitProgram(result.program, options.input_info); } Require(test.name, "SPIR-V emit", !result.spirv.empty(), "recompiler returned empty SPIR-V"); ValidateSpirv(test.name, result.spirv); CheckSpirvText(test, result.spirv); std::vector packed_user_data; for (const auto reg : result.program.bindings.user_data_registers) { packed_user_data.push_back( resources.user_data[reg - result.program.user_data_base]); } packed_user_data.resize(result.program.bindings.ShaderDataDwords()); const auto buffer_count = static_cast(result.program.info.buffers.size()); for (u32 i = 0; i < buffer_count; i++) { u32 offset = 0; if (i < test.storage_buffer_offsets.size()) { offset = test.storage_buffer_offsets[i]; } Require(test.name, "shader data", offset % sizeof(u32) == 0 && offset < 256, "storage buffer offset is not representable"); packed_user_data[result.program.bindings.memory_offset_dword + i / 4u] |= offset << ((i % 4u) * 8u); } return {std::move(result.spirv), std::move(result.program), std::move(resources), std::move(packed_user_data)}; } std::array MakeStructuredStorageBufferData(u32 stride_bytes, u32 num_records, bool add_tid = false, u32 format = 0) { std::array data{}; data[1] = (stride_bytes & 0x3fffu) << 16u; data[2] = num_records; data[3] = DstSel(4, 5, 6, 7) | (1u << 24u); if (add_tid) { data[3] |= 1u << 23u; } data[3] |= (format & 0x7fu) << 12u; return data; } std::array MakeStorageTextureData(Prospero::BufferFormat format) { std::array data{}; data[0] = 0x1000u; data[1] = (static_cast(format) & 0x1ffu) << 20u; data[3] = static_cast(Prospero::ImageType::kColor2D) << 28u; return data; } std::string StorageUint2DImageBindingName(bool atomic) { ShaderRecompiler::IR::ImageResource image{}; image.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; image.numeric_class = Prospero::TextureNumericClass::Uint; image.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; image.atomic = atomic; const auto binding = ShaderRecompiler::IR::DescriptorBindingForImage(image); Require("StorageUint2DImageBindingName", "descriptor binding", binding.has_value(), "storage image has no descriptor binding"); return "image_" + std::to_string(static_cast(*binding)); } CompiledShader CompileFragmentCase(const GraphicsCase &test) { const auto user_data = MakeNativeUserData(test.has_user_data ? &test.user_data : nullptr); ShaderPixelInputInfo pixel_info{}; pixel_info.input_num = test.pixel_interpolator_settings.empty() ? 1u : static_cast(test.pixel_interpolator_settings.size()); pixel_info.ps_no_perspective = test.pixel_no_perspective; pixel_info.ps_ancillary = test.pixel_ancillary; pixel_info.ps_front_face = test.pixel_front_face; pixel_info.ps_pos_w = test.pixel_position_w; pixel_info.ps_depth_export_enable = test.pixel_depth_export; pixel_info.ps_system_input_base = 2; pixel_info.ps_perspective_centroid_vgpr = test.pixel_perspective_centroid_vgpr; pixel_info.custom_interpolation_mask = test.pixel_custom_interpolation_mask; for (u32 i = 0; i < std::size(pixel_info.interpolator_settings); i++) { pixel_info.interpolator_settings[i] = i; } for (u32 i = 0; i < test.pixel_interpolator_settings.size() && i < std::size(pixel_info.interpolator_settings); i++) { pixel_info.interpolator_settings[i] = test.pixel_interpolator_settings[i]; } ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Pixel; options.dump_ir = false; options.input_info.pixel = &pixel_info; options.user_data = user_data; auto translated = ShaderRecompiler::TranslateProgram(test.fragment_code, options); auto resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program); ShaderRecompiler::IR::ResourceSnapshot resources; ShaderRecompiler::IR::ResourceSpecialization specialization; const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(test.fragment_code.data()), }; Require(test.name, "resource materialization", ShaderRecompiler::IR::MaterializeResources( resource_plan, runtime, resources, specialization), "translated resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( std::move(translated), options, specialization); Require(test.name, "SPIR-V emit", !result.spirv.empty(), "recompiler returned empty SPIR-V"); ValidateSpirv(test.name, result.spirv); std::vector packed_user_data; for (const auto reg : result.program.bindings.user_data_registers) { packed_user_data.push_back( resources.user_data[reg - result.program.user_data_base]); } packed_user_data.resize(result.program.bindings.ShaderDataDwords()); return {std::move(result.spirv), std::move(result.program), std::move(resources), std::move(packed_user_data)}; } std::array MakeSampledTextureData(Prospero::BufferFormat format) { return MakeStorageTextureData(format); } namespace TestSpv { std::vector MakePassthroughVertexSpirv(bool layered, float clip_w = 1.0f) { using ShaderRecompiler::Spirv::Builder; Builder b; const auto void_type = b.Type(spv::OpTypeVoid); const auto uint_type = b.Type(spv::OpTypeInt, 32, 0); const auto int_type = b.Type(spv::OpTypeInt, 32, 1); const auto float_type = b.Type(spv::OpTypeFloat, 32); const auto vec2_type = b.Type(spv::OpTypeVector, float_type, 2); const auto vec4_type = b.Type(spv::OpTypeVector, float_type, 4); const auto per_vertex_type = b.DecoratedType(spv::OpTypeStruct, {{spv::OpDecorate, {spv::DecorationBlock}}, {spv::OpMemberDecorate, {0, spv::DecorationBuiltIn, spv::BuiltInPosition}}}, vec4_type); const auto ptr_input_vec2 = b.Type(spv::OpTypePointer, spv::StorageClassInput, vec2_type); const auto ptr_input_vec4 = b.Type(spv::OpTypePointer, spv::StorageClassInput, vec4_type); const auto ptr_output_vec4 = b.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4_type); const auto ptr_output_per_vertex = b.Type(spv::OpTypePointer, spv::StorageClassOutput, per_vertex_type); const auto func_type = b.Type(spv::OpTypeFunction, void_type); const auto const_u32_0 = b.Constant(spv::OpConstant, uint_type, 0); const auto const_f32_0 = b.Constant(spv::OpConstant, float_type, 0x00000000u); const auto const_f32_w = b.Constant(spv::OpConstant, float_type, std::bit_cast(clip_w)); const auto in_pos = b.DefineGlobalVariable(ptr_input_vec2, spv::StorageClassInput); const auto in_color = b.DefineGlobalVariable(ptr_input_vec4, spv::StorageClassInput); const auto out_color = b.DefineGlobalVariable(ptr_output_vec4, spv::StorageClassOutput); const auto per_vertex = b.DefineGlobalVariable(ptr_output_per_vertex, spv::StorageClassOutput); u32 instance = 0; u32 layer = 0; std::vector interfaces = {in_pos, in_color, per_vertex, out_color}; if (layered) { instance = b.DefineGlobalVariable( b.Type(spv::OpTypePointer, spv::StorageClassInput, int_type), spv::StorageClassInput); layer = b.DefineGlobalVariable( b.Type(spv::OpTypePointer, spv::StorageClassOutput, int_type), spv::StorageClassOutput); b.AddAnnotation(spv::OpDecorate, instance, spv::DecorationBuiltIn, spv::BuiltInInstanceIndex); b.AddAnnotation(spv::OpDecorate, layer, spv::DecorationBuiltIn, spv::BuiltInLayer); b.RequireVersion(0x00010500u); b.RequireCapability(spv::CapabilityShaderLayer); interfaces.insert(interfaces.end(), {instance, layer}); } const auto main = b.AllocateId(); const auto label = b.AllocateId(); const auto pos2 = b.AllocateId(); const auto color4 = b.AllocateId(); const auto pos_x = b.AllocateId(); const auto pos_y = b.AllocateId(); const auto position = b.AllocateId(); const auto position_ptr = b.AllocateId(); b.RequireCapability(spv::CapabilityShader); b.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); b.AddEntryPoint(spv::ExecutionModelVertex, main, "main", interfaces); b.AddAnnotation(spv::OpDecorate, in_pos, spv::DecorationLocation, 0); b.AddAnnotation(spv::OpDecorate, in_color, spv::DecorationLocation, 1); b.AddAnnotation(spv::OpDecorate, out_color, spv::DecorationLocation, 0); b.AddFunction(spv::OpFunction, void_type, main, spv::FunctionControlMaskNone, func_type); b.AddFunction(spv::OpLabel, label); b.AddFunction(spv::OpLoad, vec2_type, pos2, in_pos); b.AddFunction(spv::OpLoad, vec4_type, color4, in_color); b.AddFunction(spv::OpCompositeExtract, float_type, pos_x, pos2, 0); b.AddFunction(spv::OpCompositeExtract, float_type, pos_y, pos2, 1); b.AddFunction(spv::OpCompositeConstruct, vec4_type, position, pos_x, pos_y, const_f32_0, const_f32_w); b.AddFunction(spv::OpAccessChain, ptr_output_vec4, position_ptr, per_vertex, const_u32_0); b.AddFunction(spv::OpStore, position_ptr, position); b.AddFunction(spv::OpStore, out_color, color4); if (layered) { const auto index = b.AllocateId(); b.AddFunction(spv::OpLoad, int_type, index, instance); b.AddFunction(spv::OpStore, layer, index); } b.AddFunction(spv::OpReturn); b.AddFunction(spv::OpFunctionEnd); return b.Build(); } } // namespace TestSpv class VulkanHarness { public: VulkanHarness() { Init(); } ~VulkanHarness() { Destroy(); } VulkanHarness(const VulkanHarness &) = delete; VulkanHarness &operator=(const VulkanHarness &) = delete; struct Buffer { vk::Buffer buffer = nullptr; vk::DeviceMemory memory = nullptr; vk::DeviceAddress device_address = 0; vk::DeviceSize size = 0; bool coherent = false; }; struct Image { vk::Image image = nullptr; vk::DeviceMemory memory = nullptr; vk::ImageView view = nullptr; vk::Format format = vk::Format::eUndefined; vk::ImageLayout layout = vk::ImageLayout::eUndefined; u32 width = 0; u32 height = 0; u32 layers = 1; u32 mip_levels = 1; u32 dwords_per_pixel = 0; }; [[nodiscard]] vk::Device Device() const { return m_device; } [[nodiscard]] u32 SubgroupSize() const { vk::PhysicalDeviceSubgroupProperties subgroup{}; vk::PhysicalDeviceProperties2 properties{}; properties.pNext = &subgroup; m_physical_device.getProperties2(&properties); return subgroup.subgroupSize; } [[nodiscard]] GraphicContext &RuntimeContext() { EnsureRuntimeContext(); return m_runtime_context; } [[nodiscard]] RenderContext &RuntimeRenderer() { EnsureRuntimeContext(); return Renderer(); } void CheckHostImageAllocation() { constexpr const char *name = "HostImageAllocation"; auto &graphics = RuntimeContext(); VulkanImage image; vk::ImageCreateInfo create{}; create.flags = vk::ImageCreateFlagBits::eMutableFormat; create.imageType = vk::ImageType::e2D; create.format = vk::Format::eR8G8B8A8Unorm; create.extent = {16, 8, 1}; create.mipLevels = 3; create.arrayLayers = 4; create.samples = vk::SampleCountFlagBits::e1; create.tiling = vk::ImageTiling::eOptimal; create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; create.initialLayout = vk::ImageLayout::eUndefined; for (uint32_t pass = 0; pass < 2; ++pass) { Require(name, "create", graphics.CreateImage(create, image), "could not allocate native image"); Require(name, "allocation metadata", image.image != nullptr && image.allocation != nullptr && image.format == create.format && image.image_type == create.imageType && image.extent == create.extent && image.layers == create.arrayLayers && image.mip_levels == create.mipLevels && image.samples == static_cast(create.samples) && image.usage == create.usage && image.flags == create.flags, "native image metadata did not follow its creation descriptor"); Require(name, "fresh synchronization state", image.state.layout == create.initialLayout && image.state.access_mask == vk::AccessFlags2{} && image.state.pl_stage == vk::PipelineStageFlagBits2::eAllCommands && image.subresource_states.empty(), "new allocation retained synchronization state from an old image"); VkMemoryPropertyFlags properties{}; vmaGetAllocationMemoryProperties(graphics.allocator, image.allocation, &properties); Require(name, "allocation policy", (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, "native image lost its device-local memory requirement"); // A reused presentation image may still describe its previous recording. // No commands use this fixture; seed that bookkeeping before retirement. image.state = {vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite, vk::ImageLayout::eTransferDstOptimal}; image.subresource_states.assign(image.layers * image.mip_levels, image.state); graphics.DeleteImage(image); Require(name, "retirement", image.image == nullptr && image.allocation == nullptr, "retired native image retained an allocation handle"); create.flags = vk::ImageCreateFlagBits::e2DArrayCompatible; create.imageType = vk::ImageType::e3D; create.format = vk::Format::eR32Uint; create.extent = {8, 4, 2}; create.mipLevels = 2; create.arrayLayers = 1; create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst; } std::printf("[host] %-32s ok\n", name); } void CheckDescriptorHeapLargeSet() { EnsureRuntimeContext(); std::array bindings{}; constexpr std::array stages{ vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eFragment, vk::ShaderStageFlagBits::eCompute, }; for (uint32_t i = 0; i < bindings.size(); i++) { bindings[i].binding = i; bindings[i].descriptorType = vk::DescriptorType::eSampler; bindings[i].descriptorCount = 16; bindings[i].stageFlags = stages[i]; } vk::DescriptorSetLayoutCreateInfo create{}; create.sType = vk::StructureType::eDescriptorSetLayoutCreateInfo; create.bindingCount = static_cast(bindings.size()); create.pBindings = bindings.data(); vk::DescriptorSetLayout layout = nullptr; RequireVk("DescriptorHeapLargeSet", "layout", m_device.createDescriptorSetLayout(&create, nullptr, &layout), "vkCreateDescriptorSetLayout"); vk::PipelineLayoutCreateInfo pipeline_create{}; pipeline_create.sType = vk::StructureType::ePipelineLayoutCreateInfo; pipeline_create.setLayoutCount = 1; pipeline_create.pSetLayouts = &layout; vk::PipelineLayout pipeline_layout = nullptr; RequireVk("DescriptorHeapLargeSet", "pipeline layout", m_device.createPipelineLayout(&pipeline_create, nullptr, &pipeline_layout), "vkCreatePipelineLayout"); { RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); const auto commit = [&] { const auto set = context.GetDescriptorHeap().Commit(layout); Require("DescriptorHeapLargeSet", "allocation", set != nullptr, "ordinary descriptor heap could not allocate an oversized sampler set"); scheduler.Current().Handle().bindDescriptorSets( vk::PipelineBindPoint::eCompute, pipeline_layout, 0, 1, &set, 0, nullptr); }; for (uint32_t i = 0; i < 22; i++) { commit(); } scheduler.Finish(); for (uint32_t i = 0; i < 21; i++) { commit(); } } m_device.destroyPipelineLayout(pipeline_layout, nullptr); m_device.destroyDescriptorSetLayout(layout, nullptr); std::printf("[host] %-32s ok\n", "DescriptorHeapLargeSet"); } void CheckGraphicsPushConstantBank() { constexpr const char *name = "GraphicsPushConstantStages"; EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); ShaderRecompiler::IR::CompiledShaderInfo vertex_program{}; vertex_program.stage = ShaderType::Vertex; vertex_program.bindings.push_data_start_dword = 2; vertex_program.bindings.user_data_registers = {0, 1}; vertex_program.bindings.memory_offset_dword = 2; ShaderRecompiler::IR::CompiledShaderInfo pixel_program{}; pixel_program.stage = ShaderType::Pixel; pixel_program.bindings.push_data_start_dword = 0; pixel_program.bindings.user_data_registers = {0, 1}; pixel_program.bindings.memory_offset_dword = 2; ShaderStageRuntime vertex_runtime{.program = &vertex_program}; vertex_runtime.resources.user_data = {0x11111111u, 0x22222222u}; ShaderStageRuntime pixel_runtime{.program = &pixel_program}; pixel_runtime.resources.user_data = {0x33333333u, 0x44444444u}; auto vertex = context.GetRenderExecutor().PrepareBindings(vertex_runtime); auto pixel = context.GetRenderExecutor().PrepareBindings(pixel_runtime); const auto pipeline = RenderExecutorTestAccess::CommitBindings( context.GetRenderExecutor(), scheduler.Current(), vertex, pixel); Require(name, "shared push data", vertex.shader_data == std::vector{0x11111111u, 0x22222222u} && pixel.shader_data == std::vector{0x33333333u, 0x44444444u}, "graphics stages did not commit their shared push data"); scheduler.Finish(); RenderExecutorTestAccess::DestroyDescriptorPipelines( context.GetRenderExecutor(), std::span {&pipeline, 1u}); std::printf("[host] %-32s ok\n", name); } void CheckSchedulerTimeline() { EnsureRuntimeContext(); CommandScheduler scheduler(Renderer(), m_runtime_context); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto *command = &scheduler.Current(); const auto original_handle = command->Handle(); HW::Context next_registers{}; HW::UserConfig next_user_config{}; HW::Shader next_shaders{}; scheduler.Begin(next_registers, next_user_config, next_shaders); Require("SchedulerTimeline", "guest context rebind", &scheduler.Current() == command && command->Handle() == original_handle && &command->GetRegisters() == &next_registers && &command->GetUserConfig() == &next_user_config && &command->GetShaders() == &next_shaders, "rebinding guest state replaced the active command recording"); scheduler.FlushAndWait(); Require("SchedulerTimeline", "guest context after recording restart", scheduler.Active() && &scheduler.Current() == command && &command->GetRegisters() == &next_registers && &command->GetUserConfig() == &next_user_config && &command->GetShaders() == &next_shaders, "restarting command recording lost the current guest context"); scheduler.Begin(registers, user_config, shaders); const auto first_tick = scheduler.CurrentTick(); uint32_t completed = 0; scheduler.DeferOperation([owned = std::make_unique(1), &completed] { completed = *owned; }); scheduler.Flush(); scheduler.Wait(first_tick); Require("SchedulerTimeline", "wait boundary", scheduler.CurrentTick() == first_tick + 1 && scheduler.IsFree(first_tick) && completed == 0, "timeline wait released a resource in the middle of an operation"); scheduler.PopPendingOperations(); Require("SchedulerTimeline", "first tick", completed == 1, "operation boundary did not release its deferred operation"); const auto second_tick = scheduler.CurrentTick(); std::atomic priority_in_callback{false}; scheduler.DeferPriorityOperation([owned = std::make_unique(2), &completed, &priority_in_callback] { priority_in_callback = CommandScheduler::InDeferredOperation(); completed = *owned; }); scheduler.Finish(); scheduler.DrainPriorityOperations(); Require("SchedulerTimeline", "finish", scheduler.CurrentTick() == second_tick + 1 && scheduler.IsFree(second_tick) && completed == 2 && priority_in_callback.load() && !CommandScheduler::InDeferredOperation(), "scheduler completion drain lost its tick or callback context"); const auto implicit_flush_tick = scheduler.CurrentTick(); scheduler.DeferOperation([owned = std::make_unique(3), &completed] { completed = *owned; }); scheduler.Wait(implicit_flush_tick); Require("SchedulerTimeline", "implicit flush", scheduler.CurrentTick() == implicit_flush_tick + 1 && scheduler.IsFree(implicit_flush_tick) && completed == 2, "waiting for the current tick released a deferred resource"); scheduler.PopPendingOperations(); Require("SchedulerTimeline", "implicit boundary", completed == 3, "operation boundary did not release the implicit tick callback"); vk::SemaphoreTypeCreateInfo timeline_type{}; timeline_type.sType = vk::StructureType::eSemaphoreTypeCreateInfo; timeline_type.semaphoreType = vk::SemaphoreType::eTimeline; vk::SemaphoreCreateInfo timeline_create{}; timeline_create.sType = vk::StructureType::eSemaphoreCreateInfo; timeline_create.pNext = &timeline_type; vk::Semaphore external_timeline = nullptr; Require("SchedulerTimeline", "external timeline create", m_runtime_context.device.createSemaphore(&timeline_create, nullptr, &external_timeline) == vk::Result::eSuccess && external_timeline != nullptr, "failed to create the external timeline semaphore"); constexpr uint64_t external_wait_value = 9; const auto external_wait_tick = scheduler.CurrentTick(); SubmitInfo external_wait; external_wait.AddWait(external_timeline, external_wait_value); constexpr size_t blocked_submission_count = 6; std::array blocked_handles{}; for (size_t i = 0; i < blocked_submission_count; ++i) { blocked_handles[i] = scheduler.Current().Handle(); if (i == 0) { scheduler.Flush(external_wait); } else { scheduler.Flush(); } } bool distinct_handles = true; for (size_t i = 0; i < blocked_handles.size(); ++i) { for (size_t j = i + 1; j < blocked_handles.size(); ++j) { distinct_handles &= blocked_handles[i] != blocked_handles[j]; } } const auto last_blocked_tick = scheduler.CurrentTick() - 1; Require("SchedulerTimeline", "external timeline overflow", distinct_handles && last_blocked_tick == external_wait_tick + blocked_submission_count - 1 && !scheduler.IsFree(last_blocked_tick), "blocked submissions did not grow the command-buffer pool"); vk::SemaphoreSignalInfo signal_info{}; signal_info.sType = vk::StructureType::eSemaphoreSignalInfo; signal_info.semaphore = external_timeline; signal_info.value = external_wait_value; Require("SchedulerTimeline", "external timeline signal", m_runtime_context.device.signalSemaphore(&signal_info) == vk::Result::eSuccess, "failed to signal the external timeline semaphore"); scheduler.Wait(last_blocked_tick); m_runtime_context.device.destroySemaphore(external_timeline, nullptr); scheduler.Flush(); scheduler.Flush(); Require( "SchedulerTimeline", "timeline pool reuse", std::ranges::find(blocked_handles, scheduler.Current().Handle()) != blocked_handles.end(), "completed command buffers were not reused after timeline progress"); scheduler.Shutdown(); CommandScheduler draining(Renderer(), m_runtime_context); HW::Context drain_registers{}; HW::UserConfig drain_user_config{}; HW::Shader drain_shaders{}; draining.Begin(drain_registers, drain_user_config, drain_shaders); uint32_t reentrant = 0; draining.DeferOperation([&draining, &reentrant] { reentrant++; draining.DeferOperation([&reentrant] { reentrant++; }); }); std::binary_semaphore priority_entered{0}; std::binary_semaphore release_priority{0}; draining.DeferPriorityOperation( [&draining, &priority_entered, &release_priority] { priority_entered.release(); draining.Shutdown(); release_priority.acquire(); }); std::jthread shutdown_thread([&draining] { draining.Shutdown(); }); priority_entered.acquire(); std::jthread second_shutdown_thread([&draining] { draining.Shutdown(); }); std::atomic concurrent_completed{false}; std::jthread concurrent_defer([&draining, &concurrent_completed] { draining.DeferOperation( [&concurrent_completed] { concurrent_completed = true; }); }); release_priority.release(); shutdown_thread.join(); second_shutdown_thread.join(); concurrent_defer.join(); Require("SchedulerTimeline", "shutdown drain", reentrant == 2 && concurrent_completed.load(), "shutdown lost reentrant or concurrent deferred work"); std::printf("[host] %-32s ok\n", "SchedulerTimeline"); } void CheckGpuMappedRangeLifecycle() { EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); context.InitializeGpu(nullptr); auto &resources = context; constexpr uint64_t base = 0x0000000200000000ull; constexpr uint64_t page = 0x4000; resources.MapMemory(base, page * 4); resources.MapMemory(base + page * 2, page * 4); Require("GpuMappedRangeLifecycle", "union", resources.IsMapped(base, page * 6) && !resources.IsMapped(base, page * 7), "overlapping maps did not form one interval union"); resources.UnmapMemory(base + page * 2, page * 2); Require("GpuMappedRangeLifecycle", "subtract", resources.IsMapped(base, page * 2) && resources.IsMapped(base + page * 4, page * 2) && !resources.IsMapped(base, page * 6), "partial unmap did not punch the expected interval hole"); resources.UnmapMemory(base + page * 2, page * 2); Require("GpuMappedRangeLifecycle", "idempotent unmap", resources.IsMapped(base, page * 2) && resources.IsMapped(base + page * 4, page * 2), "unmapping an absent interval changed neighboring mappings"); resources.UnmapMemory(base, page * 6); Require("GpuMappedRangeLifecycle", "clear", !resources.IsMapped(base, page * 6), "full unmap did not clear the interval union"); constexpr uint64_t old_prt = base + page * 8; constexpr uint64_t new_prt = base + page * 16; resources.MapMemory(old_prt, page * 4); resources.UnmapMemory(old_prt, page * 4); resources.MapMemory(new_prt, page * 6); Require("GpuMappedRangeLifecycle", "PRT replacement", !resources.IsMapped(old_prt, page * 4) && resources.IsMapped(new_prt, page * 6), "old-unmap/new-map did not replace full PRT coverage"); scheduler.Finish(); context.ShutdownGpu(); std::printf("[host] %-32s ok\n", "GpuMappedRangeLifecycle"); } void CheckStreamBufferRing() { EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = resources.GetBufferCache(); constexpr std::array, 4> utilities{{ {MemoryUsage::Upload, 512ull << 20}, {MemoryUsage::Stream, 64ull << 20}, {MemoryUsage::Download, 32ull << 20}, {MemoryUsage::DeviceLocal, 128ull << 20}, }}; std::array handles{}; for (size_t i = 0; i < utilities.size(); i++) { auto &utility = cache.GetUtilityBuffer(utilities[i].first); handles[i] = utility.Handle(); Require("StreamBufferRing", "utility ownership", utility.Usage() == utilities[i].first && utility.Size() == utilities[i].second && utility.Handle() != nullptr, "BufferCache utility buffer does not match the fixed layout"); } for (size_t i = 0; i < handles.size(); i++) { for (size_t j = i + 1; j < handles.size(); j++) { Require("StreamBufferRing", "utility identity", handles[i] != handles[j], "utility usages alias the same native buffer"); } } auto *fixed_download = &cache.GetUtilityBuffer(MemoryUsage::Download); const auto fixed_handle = fixed_download->Handle(); const auto [oversized_download, oversized_offset] = fixed_download->Map(fixed_download->Size() + 4, 64, false); const auto [download_probe, download_probe_offset] = fixed_download->Map(64, 64, false); Require("StreamBufferRing", "fixed download utility", oversized_download == nullptr && oversized_offset == 0 && download_probe != nullptr && download_probe_offset == 0 && &cache.GetUtilityBuffer(MemoryUsage::Download) == fixed_download && fixed_download->Size() == (32ull << 20) && fixed_download->Handle() == fixed_handle, "oversized download replaced or corrupted the fixed shared ring"); fixed_download->Commit(); const std::array overflow_data{ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}; auto &upload_stream = cache.GetUtilityBuffer(MemoryUsage::Stream); const auto stream_offset = upload_stream.Copy(overflow_data.data(), overflow_data.size(), 16); Require("StreamBufferRing", "cache-owned stream upload", upload_stream.Handle() != nullptr && stream_offset < upload_stream.Size() && std::memcmp(upload_stream.Mapped().data() + stream_offset, overflow_data.data(), overflow_data.size()) == 0, "cache-owned stream upload lost bytes"); const auto atom = m_runtime_context.physical_device_properties.limits.nonCoherentAtomSize; uint64_t policy_size = 17; uint64_t policy_alignment = 12; uint64_t coherent_size = policy_size; uint64_t coherent_alignment = policy_alignment; Require("StreamBufferRing", "non-coherent reservation policy", StreamBufferTestAccess::NormalizeReservation( false, 256, policy_size, policy_alignment) && policy_size == 256 && policy_alignment == 768 && StreamBufferTestAccess::NormalizeReservation( true, 256, coherent_size, coherent_alignment) && coherent_size == 17 && coherent_alignment == 12, "non-coherent reservations are not isolated at the atom/texel LCM"); const auto small_ring_size = std::max(64, atom * 2); StreamBuffer stream(m_runtime_context, scheduler, MemoryUsage::Upload, small_ring_size); const auto first_size = stream.IsCoherent() ? small_ring_size - 16 : small_ring_size - atom + 1; constexpr uint64_t blocked_size = 24; const auto [first, first_offset] = stream.Map(first_size, 16, false); Require("StreamBufferRing", "first allocation", first != nullptr && first_offset == 0, "initial stream allocation failed"); std::memset(first, 0x5a, static_cast(first_size)); stream.Commit(); const auto [oversize, oversize_offset] = stream.Map(small_ring_size + 1, 1, false); Require("StreamBufferRing", "oversize", oversize == nullptr && oversize_offset == 0, "oversize allocation was accepted"); const auto [blocked, blocked_offset] = stream.Map(blocked_size, 1, false); const auto [blocked_again, blocked_again_offset] = stream.Map(blocked_size, 1, false); Require("StreamBufferRing", "transactional failure", blocked == nullptr && blocked_offset == 0 && blocked_again == nullptr && blocked_again_offset == 0, "failed wrap mutated stream state"); scheduler.Flush(); const auto [wrapped, wrapped_offset] = stream.Map(blocked_size, 1); Require("StreamBufferRing", "watched wrap", wrapped != nullptr && wrapped_offset == 0, "completed watch did not release the wrapped allocation"); stream.Commit(); const auto download_alignment = atom / std::gcd(atom, 12) * 12; const auto download_ring_size = std::max( 64, download_alignment + std::max(atom, 16)); StreamBuffer download(m_runtime_context, scheduler, MemoryUsage::Download, download_ring_size); const auto [download_data, download_offset] = download.Map(16, 8); Require("StreamBufferRing", "download allocation", download_data != nullptr && download_offset == 0, "download stream is not host visible"); download.Commit(); scheduler.Flush(); const auto [second_download, second_download_offset] = download.Map(16, 12); Require("StreamBufferRing", "download atom isolation", second_download != nullptr && (download.IsCoherent() || (download_offset % atom == 0 && second_download_offset % atom == 0 && second_download_offset >= download_offset + atom)) && second_download_offset % 12 == 0, "download reservations share a non-coherent atom or lost caller " "alignment"); download.Commit(); scheduler.Finish(); download.Invalidate(download_offset, 16); std::printf("[host] %-32s ok\n", "StreamBufferRing"); } void CheckGpuCommandLane() { EnsureRuntimeContext(); auto &context = Renderer(); context.InitializeGpu(nullptr); auto &gpu = context.GetGpu(); const auto caller_thread = std::this_thread::get_id(); std::thread::id gpu_thread; std::thread::id nested_thread; uint32_t order = 0; gpu.SendCommand([&order] { order = 1; }); gpu.SendCommandSync([&] { gpu_thread = std::this_thread::get_id(); Require("GpuCommandLane", "FIFO", order == 1, "synchronous command overtook an older host command"); Require("GpuCommandLane", "GPU context", GuestGpu::IsGpuThread(), "host command did not run on the GPU thread"); gpu.SendCommandSync( [&nested_thread] { nested_thread = std::this_thread::get_id(); }); order = 2; }); Require( "GpuCommandLane", "dispatch thread", order == 2 && gpu_thread != caller_thread && nested_thread == gpu_thread, "host command did not run on the GPU thread or nested sync deadlocked"); { const auto make_release_mem = [](uint32_t data_sel, uint32_t interrupt_selector, void *destination, uint64_t value, uint32_t action = 0x28u, uint32_t gcr_cntl = 1u << 9u) { std::array packet{}; const auto address = reinterpret_cast(destination); const auto event_index = (action >= 0x2fu ? 6u : 5u); packet[0] = KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_RELEASE_MEM); packet[1] = action | (event_index << 8u) | (gcr_cntl << 12u); packet[2] = (interrupt_selector << 24u) | (data_sel << 29u); packet[3] = static_cast(address); packet[4] = static_cast(address >> 32u); packet[5] = static_cast(value); packet[6] = static_cast(value >> 32u); packet[7] = 0x123u; return packet; }; vk::SemaphoreTypeCreateInfo timeline_type{}; timeline_type.sType = vk::StructureType::eSemaphoreTypeCreateInfo; timeline_type.semaphoreType = vk::SemaphoreType::eTimeline; vk::SemaphoreCreateInfo timeline_create{}; timeline_create.sType = vk::StructureType::eSemaphoreCreateInfo; timeline_create.pNext = &timeline_type; vk::Semaphore blocked_timeline = nullptr; Require("GpuCommandLane", "blocked timeline create", m_runtime_context.device.createSemaphore( &timeline_create, nullptr, &blocked_timeline) == vk::Result::eSuccess && blocked_timeline != nullptr, "failed to create the blocked GPU timeline"); constexpr uint64_t release_tick = 1; auto &gpu_scheduler = context.GetCommandScheduler(); auto processor = std::make_unique(context, 0); gpu.SendCommandSync([&] { processor->BufferInit(); SubmitInfo blocked_submit; blocked_submit.AddWait(blocked_timeline, release_tick); gpu_scheduler.Flush(blocked_submit); }); std::binary_semaphore parser_complete{0}; alignas(uint64_t) uint64_t interrupt_only_gds_label = UINT64_MAX; alignas(uint64_t) uint64_t cb_db_release_label = UINT64_MAX; bool parser_kept_nonblocking_boundaries = false; std::jthread no_gpu_wait([&] { gpu.SendCommandSync([&] { processor->BufferInit(); const auto tick_before = gpu_scheduler.CurrentTick(); processor->TriggerEvent(0x16u, 0u); auto packet = make_release_mem(0, 0, nullptr, 0); Pm4Execution execution; const auto result = processor->Process(execution, packet); processor->IncrementDe(); const bool cache_and_counter_did_not_submit = result == Pm4ProcessResult::Complete && gpu_scheduler.CurrentTick() == tick_before; const auto cb_db_tick = gpu_scheduler.CurrentTick(); auto cb_db_release = make_release_mem(2, 0, &cb_db_release_label, 0, 0x14u, 0); Pm4Execution cb_db_execution; const auto cb_db_result = processor->Process(cb_db_execution, cb_db_release); const bool cb_db_release_did_not_submit = cb_db_result == Pm4ProcessResult::Complete && cb_db_release_label == 0 && gpu_scheduler.CurrentTick() == cb_db_tick; auto gds_interrupt_only = make_release_mem(5, 1, &interrupt_only_gds_label, 1ull << 16u); Pm4Execution gds_interrupt_execution; const auto interrupt_tick = gpu_scheduler.CurrentTick(); const auto interrupt_result = processor->Process(gds_interrupt_execution, gds_interrupt_only); parser_kept_nonblocking_boundaries = cache_and_counter_did_not_submit && cb_db_release_did_not_submit && interrupt_result == Pm4ProcessResult::Complete && gpu_scheduler.CurrentTick() == interrupt_tick + 1 && interrupt_only_gds_label == UINT64_MAX; }); parser_complete.release(); }); const bool parser_did_not_wait_gpu = parser_complete.try_acquire_for(std::chrono::seconds(2)); vk::SemaphoreSignalInfo signal_info{}; signal_info.sType = vk::StructureType::eSemaphoreSignalInfo; signal_info.semaphore = blocked_timeline; signal_info.value = release_tick; Require("GpuCommandLane", "blocked timeline signal", m_runtime_context.device.signalSemaphore(&signal_info) == vk::Result::eSuccess, "failed to release the blocked GPU timeline"); no_gpu_wait.join(); gpu.SendCommandSync([&] { gpu_scheduler.Finish(); }); m_runtime_context.device.destroySemaphore(blocked_timeline, nullptr); Require("GpuCommandLane", "nonblocking GPU packets", parser_did_not_wait_gpu && parser_kept_nonblocking_boundaries, "a cache event, GL2 writeback, DE counter, or interrupt boundary " "used the wrong " "submission behavior"); alignas(uint64_t) uint64_t release_label = 0; alignas(uint64_t) uint64_t gds_label = UINT64_MAX; bool release_mem_submission_counts = false; gpu.SendCommandSync([&] { processor->BufferInit(); auto immediate = make_release_mem(1, 0, &release_label, 0x11223344u); Pm4Execution immediate_execution; const auto immediate_tick = gpu_scheduler.CurrentTick(); const auto immediate_result = processor->Process(immediate_execution, immediate); const bool immediate_split_once = gpu_scheduler.CurrentTick() == immediate_tick + 1; auto gds = make_release_mem(5, 0, &gds_label, 1ull << 16u); Pm4Execution gds_execution; const auto gds_tick = gpu_scheduler.CurrentTick(); const auto gds_result = processor->Process(gds_execution, gds); const bool gds_waited_once = gpu_scheduler.CurrentTick() == gds_tick + 1; auto interrupt_only = make_release_mem(0, 4, nullptr, 0); Pm4Execution interrupt_execution; const auto interrupt_tick = gpu_scheduler.CurrentTick(); const auto interrupt_result = processor->Process(interrupt_execution, interrupt_only); const bool interrupt_split_once = gpu_scheduler.CurrentTick() == interrupt_tick + 1; auto gds_interrupt = make_release_mem(5, 2, &gds_label, 1ull << 16u); Pm4Execution gds_interrupt_execution; const auto gds_interrupt_tick = gpu_scheduler.CurrentTick(); const auto gds_interrupt_result = processor->Process(gds_interrupt_execution, gds_interrupt); const bool gds_interrupt_waited_once = gpu_scheduler.CurrentTick() == gds_interrupt_tick + 1; release_mem_submission_counts = immediate_result == Pm4ProcessResult::Complete && immediate_split_once && gds_result == Pm4ProcessResult::Complete && gds_waited_once && interrupt_result == Pm4ProcessResult::Complete && interrupt_split_once && gds_interrupt_result == Pm4ProcessResult::Complete && gds_interrupt_waited_once; }); gpu.SendCommandSync([&] { gpu_scheduler.Finish(); gpu_scheduler.WaitPriorityOperations(gpu_scheduler.CurrentTick() - 1); }); Require("GpuCommandLane", "RELEASE_MEM submission counts", release_mem_submission_counts && static_cast(release_label) == 0x11223344u && static_cast(gds_label) == 0, "RELEASE_MEM lost its required split/readback or retained a " "redundant GPU wait"); } alignas(uint32_t) uint32_t packet_marker_a = 0; alignas(uint32_t) uint32_t packet_marker_b = 0; const auto write_packet = [](uint32_t *packet, uint32_t *destination, uint32_t value) { const auto destination_address = reinterpret_cast(destination); packet[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); packet[1] = 0; packet[2] = static_cast(destination_address); packet[3] = static_cast(destination_address >> 32u); packet[4] = value; }; uint32_t live_graphics_value = 0; std::array live_graphics_commands{}; write_packet(live_graphics_commands.data(), &live_graphics_value, 11); std::binary_semaphore graphics_gate_entered{0}; std::binary_semaphore graphics_gate_release{0}; gpu.SendCommand([&] { graphics_gate_entered.release(); graphics_gate_release.acquire(); }); gpu.Submit(live_graphics_commands, {}); graphics_gate_entered.acquire(); live_graphics_commands[4] = 22; graphics_gate_release.release(); gpu.Done(); Require("GpuCommandLane", "borrowed graphics commands", live_graphics_value == 22, "graphics submission executed a copied PM4 stream"); uint32_t live_compute_value = 0; std::array live_compute_commands{}; write_packet(live_compute_commands.data(), &live_compute_value, 33); std::binary_semaphore compute_gate_entered{0}; std::binary_semaphore compute_gate_release{0}; gpu.SendCommand([&] { compute_gate_entered.release(); compute_gate_release.acquire(); }); gpu.SubmitCompute(0x20, live_compute_commands); compute_gate_entered.acquire(); live_compute_commands[4] = 44; compute_gate_release.release(); gpu.Done(); Require("GpuCommandLane", "borrowed compute commands", live_compute_value == 44, "compute submission executed a copied PM4 stream"); LibKernel::EventQueue::KernelEqueue interrupt_queue = LibKernel::EventQueue::KERNEL_EQUEUE_INVALID; uint32_t graphics_interrupt_udata = 0; uint32_t compute_interrupt_udata = 0; uint32_t other_compute_interrupt_udata = 0; const bool interrupt_queue_ready = LibKernel::EventQueue::KernelCreateEqueue(&interrupt_queue, "pm4-interrupts") == 0 && Sync::AddEqEvent(context, interrupt_queue, 0, &graphics_interrupt_udata) == 0 && Sync::AddEqEvent(context, interrupt_queue, 0x20, &compute_interrupt_udata) == 0 && Sync::AddEqEvent(context, interrupt_queue, 0x21, &other_compute_interrupt_udata) == 0; Require("GpuCommandLane", "interrupt queue", interrupt_queue_ready, "failed to register the PM4 interrupt events"); const auto make_interrupt_packet = [](uint32_t data_selector, uint32_t interrupt_selector, void *destination, uint64_t value, uint32_t context_id) { std::array packet{}; const auto address = reinterpret_cast(destination); packet[0] = KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_RELEASE_MEM); packet[1] = 0x28u | (5u << 8u); packet[2] = (interrupt_selector << 24u) | (data_selector << 29u); packet[3] = static_cast(address); packet[4] = static_cast(address >> 32u); packet[5] = static_cast(value); packet[6] = static_cast(value >> 32u); packet[7] = context_id & 0x07ffffffu; return packet; }; const auto finish_gpu = [&] { gpu.SendCommandSync([&] { auto &scheduler = context.GetCommandScheduler(); scheduler.Finish(); scheduler.WaitPriorityOperations(scheduler.CurrentTick() - 1); }); }; const auto wait_for_interrupt = [&](LibKernel::EventQueue::KernelEvent &event, int &count) { LibKernel::KernelUseconds timeout = 0; return LibKernel::EventQueue::KernelWaitEqueue( interrupt_queue, &event, 1, &count, &timeout); }; uint32_t no_interrupt_graphics_value = 0; std::array no_interrupt_graphics_commands{}; write_packet(no_interrupt_graphics_commands.data(), &no_interrupt_graphics_value, 55); gpu.Submit(no_interrupt_graphics_commands, {}); gpu.Done(); finish_gpu(); LibKernel::EventQueue::KernelEvent interrupt_event{}; int interrupt_count = 0; const auto no_graphics_interrupt_wait = wait_for_interrupt(interrupt_event, interrupt_count); Require("GpuCommandLane", "no implicit graphics interrupt", no_graphics_interrupt_wait == LibKernel::KERNEL_ERROR_ETIMEDOUT && interrupt_count == 0 && no_interrupt_graphics_value == 55, "graphics submission completion synthesized an interrupt"); auto live_interrupt_commands = make_interrupt_packet(0, 1, nullptr, 0, 0x234u); std::binary_semaphore interrupt_gate_entered{0}; std::binary_semaphore interrupt_gate_release{0}; gpu.SendCommand([&] { interrupt_gate_entered.release(); interrupt_gate_release.acquire(); }); gpu.SubmitCompute(0x20, live_interrupt_commands); interrupt_gate_entered.acquire(); live_interrupt_commands[2] = 0; interrupt_gate_release.release(); gpu.Done(); finish_gpu(); interrupt_count = 0; const auto no_interrupt_wait = wait_for_interrupt(interrupt_event, interrupt_count); Require("GpuCommandLane", "no implicit completion interrupt", no_interrupt_wait == LibKernel::KERNEL_ERROR_ETIMEDOUT && interrupt_count == 0, "submission completion synthesized an interrupt absent from the " "executed PM4 stream"); uint32_t graphics_interrupt_label = 0xa5a5a5a5u; auto graphics_interrupt_commands = make_interrupt_packet( 1, 1, &graphics_interrupt_label, 0x11223344u, 0); gpu.Submit(graphics_interrupt_commands, {}); gpu.Done(); finish_gpu(); interrupt_count = 0; const auto graphics_interrupt_wait = wait_for_interrupt(interrupt_event, interrupt_count); Require("GpuCommandLane", "graphics interrupt routing", graphics_interrupt_wait == 0 && interrupt_count == 1 && interrupt_event.ident == 0 && interrupt_event.data == 0 && interrupt_event.udata == &graphics_interrupt_udata && graphics_interrupt_label == 0xa5a5a5a5u, "graphics kOnly interrupt was misrouted or wrote its label"); uint32_t compute_interrupt_label = 0x5a5a5a5au; auto compute_interrupt_commands = make_interrupt_packet( 1, 0, &compute_interrupt_label, 0x55667788u, 0x456u); std::binary_semaphore compute_interrupt_gate_entered{0}; std::binary_semaphore compute_interrupt_gate_release{0}; gpu.SendCommand([&] { compute_interrupt_gate_entered.release(); compute_interrupt_gate_release.acquire(); }); gpu.SubmitCompute(0x20, compute_interrupt_commands); compute_interrupt_gate_entered.acquire(); compute_interrupt_commands[2] |= 1u << 24u; compute_interrupt_gate_release.release(); gpu.Done(); finish_gpu(); interrupt_count = 0; const auto compute_interrupt_wait = wait_for_interrupt(interrupt_event, interrupt_count); const bool compute_interrupt_matches = compute_interrupt_wait == 0 && interrupt_count == 1 && interrupt_event.ident == 0x20 && interrupt_event.data == 0x456u && interrupt_event.udata == &compute_interrupt_udata && compute_interrupt_label == 0x55667788u; uint64_t compute_clock_label = 0; auto compute_clock_commands = make_interrupt_packet( 3, 1, &compute_clock_label, 0, 0x567u); gpu.SubmitCompute(0x20, compute_clock_commands); gpu.Done(); finish_gpu(); interrupt_count = 0; const auto compute_clock_wait = wait_for_interrupt(interrupt_event, interrupt_count); const bool compute_clock_matches = compute_clock_wait == 0 && interrupt_count == 1 && interrupt_event.ident == 0x20 && interrupt_event.data == 0x567u && interrupt_event.udata == &compute_interrupt_udata && compute_clock_label != 0; constexpr uint64_t compute_done_value = 0x1234567887654321ull; uint64_t compute_done_label = 0; auto compute_done_commands = make_interrupt_packet( 2, 2, &compute_done_label, compute_done_value, 0x678u); compute_done_commands[1] = 0x62fu; // CS_DONE, shader-done index, no GCR action. gpu.SubmitCompute(0x20, compute_done_commands); gpu.Done(); finish_gpu(); interrupt_count = 0; const auto compute_done_wait = wait_for_interrupt(interrupt_event, interrupt_count); Require("GpuCommandLane", "CS_DONE write-confirm interrupt", compute_done_wait == 0 && interrupt_count == 1 && interrupt_event.ident == 0x20 && interrupt_event.data == 0x678u && interrupt_event.udata == &compute_interrupt_udata && compute_done_label == compute_done_value, "CS_DONE lost its full 64-bit label write or write-confirm interrupt"); interrupt_count = 0; const auto extra_interrupt_wait = wait_for_interrupt(interrupt_event, interrupt_count); const auto delete_other_compute_event = Sync::DeleteEqEvent(context, interrupt_queue, 0x21); LibKernel::EventQueue::KernelDeleteEqueue(interrupt_queue); context.TriggerInterrupt(0, 0); context.TriggerInterrupt(0x20, 0); context.TriggerInterrupt(0x21, 0); Require("GpuCommandLane", "compute interrupt routing", compute_interrupt_matches && compute_clock_matches && extra_interrupt_wait == LibKernel::KERNEL_ERROR_ETIMEDOUT && interrupt_count == 0 && delete_other_compute_event == 0, "compute kOnly interrupt was misrouted, broadcast, or lost a " "label write"); std::array polling_loop{}; for (size_t i = 0; i < polling_loop.size() - 4; i += 2) { polling_loop[i] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO); polling_loop[i + 1] = 0; } const auto loop_address = reinterpret_cast(polling_loop.data()); polling_loop[1020] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0); polling_loop[1021] = static_cast(loop_address); polling_loop[1022] = static_cast(loop_address >> 32u); polling_loop[1023] = 0x0f200000u | static_cast(polling_loop.size()); std::array polling_commands{}; write_packet(polling_commands.data(), &packet_marker_a, 11); polling_commands[5] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0); polling_commands[6] = static_cast(loop_address); polling_commands[7] = static_cast(loop_address >> 32u); polling_commands[8] = 0x0f200000u | static_cast(polling_loop.size()); write_packet(polling_commands.data() + 9, &packet_marker_b, 22); std::binary_semaphore stream_gate_entered{0}; std::binary_semaphore stream_gate_release{0}; gpu.SendCommand([&] { stream_gate_entered.release(); stream_gate_release.acquire(); }); gpu.Submit(polling_commands, {}); stream_gate_entered.acquire(); stream_gate_release.release(); uint32_t packet_marker_a_at_callback = UINT32_MAX; uint32_t packet_marker_b_at_callback = UINT32_MAX; for (uint32_t attempt = 0; attempt < 8 && packet_marker_a_at_callback != 11; attempt++) { gpu.SendCommandSync([&] { if (packet_marker_a == 11 && packet_marker_b == 0) { packet_marker_a_at_callback = packet_marker_a; packet_marker_b_at_callback = packet_marker_b; polling_commands[13] = 33; polling_loop[1020] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO); polling_loop[1021] = 0; polling_loop[1022] = KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO); polling_loop[1023] = 0; } }); std::this_thread::yield(); } gpu.Done(); Require("GpuCommandLane", "packet-boundary command polling", packet_marker_a_at_callback == 11 && packet_marker_b_at_callback == 0 && packet_marker_a == 11 && packet_marker_b == 33, "host command waited for an entire PM4 stream or resumed a copied " "stream"); uint32_t label = 0; uint32_t prefix = 0; uint32_t suffix = 0; const auto address = [](const void *value) { return reinterpret_cast(value); }; std::array commands{}; commands[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[1] = 0; commands[2] = static_cast(address(&prefix)); commands[3] = static_cast(address(&prefix) >> 32u); commands[4] = 11; commands[5] = KYTY_PM4(7, Pm4::IT_WAIT_REG_MEM, 0); commands[6] = 0x10u | 3u; commands[7] = static_cast(address(&label)); commands[8] = static_cast(address(&label) >> 32u); commands[9] = 1; commands[10] = UINT32_MAX; commands[12] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[13] = 0; commands[14] = static_cast(address(&suffix)); commands[15] = static_cast(address(&suffix) >> 32u); commands[16] = 22; gpu.Submit(commands, {}); std::binary_semaphore ordered_started{0}; std::atomic ordered_finished{false}; uint32_t ordered_suffix = 0; std::jthread ordered([&] { ordered_started.release(); gpu.Done(); ordered_suffix = suffix; ordered_finished = true; }); ordered_started.acquire(); gpu.SendCommandSync([&] { Require("GpuCommandLane", "submit done barrier", !ordered_finished.load(), "submit done returned before a queued submission"); label = 1; }); ordered.join(); Require("GpuCommandLane", "ordered completion", prefix == 11 && suffix == 22 && ordered_suffix == 22 && ordered_finished.load(), "submit done did not drain prior PM4 work"); auto &resources = context; constexpr uint64_t empty_unmap_base = 0x0000000200400000ull; constexpr uint64_t empty_unmap_size = 0x4000; resources.MapMemory(empty_unmap_base, empty_unmap_size); label = 0; prefix = 0; suffix = 0; gpu.Submit(commands, {}); std::binary_semaphore unmap_complete{0}; std::jthread unmap_thread([&] { resources.UnmapMemory(empty_unmap_base, empty_unmap_size); unmap_complete.release(); }); const bool unmap_returned = unmap_complete.try_acquire_for(std::chrono::seconds(2)); gpu.SendCommandSync([&] { label = 1; }); if (!unmap_returned) { unmap_complete.acquire(); } unmap_thread.join(); gpu.Done(); Require("GpuCommandLane", "unmap queue progress", unmap_returned && !resources.IsMapped(empty_unmap_base, empty_unmap_size) && prefix == 11 && suffix == 22, "an unrelated unmap waited for a blocked PM4 submission"); auto &scheduler = context.GetCommandScheduler(); std::atomic normal_completed{false}; gpu.SendCommandSync( [&] { scheduler.DeferOperation([&] { normal_completed = true; }); }); resources.MapMemory(empty_unmap_base, empty_unmap_size); resources.UnmapMemory(empty_unmap_base, empty_unmap_size); Require("GpuCommandLane", "unmap native completion", normal_completed.load() && !resources.IsMapped(empty_unmap_base, empty_unmap_size), "unmap returned before an earlier native guest-memory callback"); std::binary_semaphore priority_entered{0}; std::binary_semaphore release_priority{0}; gpu.SendCommandSync([&] { scheduler.DeferPriorityOperation([&] { priority_entered.release(); release_priority.acquire(); }); scheduler.Flush(); }); priority_entered.acquire(); resources.MapMemory(empty_unmap_base, empty_unmap_size); std::binary_semaphore priority_unmap_entered{0}; std::binary_semaphore priority_unmap_complete{0}; std::jthread priority_unmap_thread([&] { gpu.SendCommandSync([&] { priority_unmap_entered.release(); resources.UnmapMemory(empty_unmap_base, empty_unmap_size); }); priority_unmap_complete.release(); }); priority_unmap_entered.acquire(); const bool unmap_overtook_priority = priority_unmap_complete.try_acquire_for(std::chrono::seconds(1)); release_priority.release(); if (!unmap_overtook_priority) { priority_unmap_complete.acquire(); } priority_unmap_thread.join(); Require("GpuCommandLane", "unmap priority ordering", !unmap_overtook_priority && !resources.IsMapped(empty_unmap_base, empty_unmap_size), "unmap returned before an earlier guest-memory callback"); constexpr uintptr_t fault_base = 0x0000000200500000ull; constexpr uint64_t fault_size = 0x10000; int64_t fault_direct_offset = -1; Require("GpuCommandLane", "processor fault direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), fault_size, fault_size, 0, &fault_direct_offset) == 0, "processor-fault direct-memory allocation failed"); void *fault_memory = reinterpret_cast(fault_base); Require("GpuCommandLane", "processor fault direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &fault_memory, fault_size, 0x3, 0x10, fault_direct_offset, fault_size) == 0, "processor-fault fixed direct-memory mapping failed"); Require("GpuCommandLane", "processor fault allocation", fault_memory == reinterpret_cast(fault_base), "fixed processor-fault allocation failed"); resources.MapMemory(fault_base, fault_size); constexpr uint64_t immediate_dst = fault_base + 0x1000; constexpr uint64_t immediate_memory_dst = fault_base + 0x2000; constexpr uint64_t memory_src = fault_base + 0x4000; constexpr uint64_t memory_copy_dst = fault_base + 0x6000; constexpr uint64_t memory_dst = fault_base + 0x8000; constexpr uint64_t l2_copy_dst = fault_base + 0xa000; constexpr uint64_t nowhere_dst = fault_base + 0xc000; constexpr uint64_t clean_cached_fill = fault_base + 0x3000; constexpr uint64_t clean_cached_copy = fault_base + 0x3008; constexpr uint64_t clean_cached_source = fault_base + 0x3010; constexpr uint64_t clean_cached_readback = fault_base + 0xb000; constexpr std::array source_words{0x11223344u, 0x55667788u}; constexpr std::array nowhere_words{0x89abcdefu, 0x01234567u}; constexpr std::array clean_copy_words{0x13579bdfu, 0x2468ace0u}; std::memcpy(reinterpret_cast(memory_src), source_words.data(), sizeof(source_words)); std::memcpy(reinterpret_cast(nowhere_dst), nowhere_words.data(), sizeof(nowhere_words)); std::memcpy(reinterpret_cast(clean_cached_source), clean_copy_words.data(), sizeof(clean_copy_words)); std::array dma_commands{}; size_t dma_cursor = 0; const auto append_dma = [&](uint8_t dst_sel, uint64_t dst, uint8_t src_sel, uint64_t src, uint32_t bytes, uint32_t command_flags = 0) { auto *packet = dma_commands.data() + dma_cursor; dma_cursor += 7; packet[0] = KYTY_PM4(7, Pm4::IT_DMA_DATA, 0); packet[1] = (static_cast(dst_sel) << 20u) | (static_cast(src_sel) << 29u); packet[2] = static_cast(src); packet[3] = static_cast(src >> 32u); packet[4] = static_cast(dst); packet[5] = static_cast(dst >> 32u); packet[6] = bytes | command_flags; }; constexpr uint64_t gds_immediate_offset = 0; constexpr uint64_t gds_memory_offset = 0x10; constexpr uint32_t immediate_value = 0xa1b2c3d4u; append_dma(1, gds_immediate_offset, 2, immediate_value, sizeof(source_words)); append_dma(0, immediate_dst, 1, gds_immediate_offset, sizeof(source_words)); append_dma(1, gds_memory_offset, 0, memory_src, sizeof(source_words)); append_dma(0, memory_dst, 1, gds_memory_offset, sizeof(source_words)); append_dma(0, immediate_memory_dst, 2, immediate_value, sizeof(source_words)); append_dma(0, memory_copy_dst, 0, memory_src, sizeof(source_words)); append_dma(3, l2_copy_dst, 3, memory_src, sizeof(source_words)); append_dma(2, nowhere_dst, 3, nowhere_dst, sizeof(nowhere_words), 1u << 31u); Require("GpuCommandLane", "DMA_DATA packet assembly", dma_cursor == dma_commands.size(), "DMA_DATA packet stream has the wrong size"); gpu.Submit(dma_commands, {}); gpu.Done(); constexpr uint32_t clean_fill_value = 0xdecafbad; gpu.SendCommandSync([&] { auto &buffer_cache = resources.GetBufferCache(); Require("GpuCommandLane", "clean cached setup", buffer_cache.IsRegionRegistered(clean_cached_fill, sizeof(source_words)) && !buffer_cache.HasGpuDirtyBytes(nowhere_dst, sizeof(nowhere_words)) && buffer_cache.HasGpuDirtyBytes(immediate_dst, sizeof(source_words)), "DMA prefetch modified ownership or did not establish a dirty " "cached page"); buffer_cache.FillBuffer(clean_cached_fill, sizeof(source_words), clean_fill_value, false); buffer_cache.CopyBuffer(clean_cached_copy, clean_cached_source, sizeof(clean_copy_words), false, false); Require("GpuCommandLane", "clean cached DMA ownership", !buffer_cache.HasGpuDirtyBytes(clean_cached_fill, sizeof(source_words)) && !buffer_cache.HasGpuDirtyBytes(clean_cached_copy, sizeof(clean_copy_words)) && buffer_cache.HasGpuDirtyBytes(immediate_dst, sizeof(source_words)), "clean cached DMA acquired ownership or released a byte-disjoint " "dirty sibling"); constexpr uint64_t gds_clean_mirror = 0x20; buffer_cache.CopyBuffer(gds_clean_mirror, clean_cached_fill, 2 * sizeof(source_words), true, false); buffer_cache.CopyBuffer(clean_cached_readback, gds_clean_mirror, 2 * sizeof(source_words), false, true); }); Require("GpuCommandLane", "DMA_DATA immediate GDS readback", resources.HandleFault(PageFaultAccess::Read, immediate_dst), "immediate-to-GDS-to-memory copy did not publish GPU bytes"); Require("GpuCommandLane", "DMA_DATA memory GDS readback", resources.HandleFault(PageFaultAccess::Read, memory_dst), "memory-to-GDS-to-memory copy did not publish GPU bytes"); Require("GpuCommandLane", "DMA_DATA immediate memory readback", resources.HandleFault(PageFaultAccess::Read, immediate_memory_dst), "immediate-to-memory copy did not publish GPU bytes"); Require("GpuCommandLane", "DMA_DATA memory copy readback", resources.HandleFault(PageFaultAccess::Read, memory_copy_dst), "memory-to-memory copy did not publish GPU bytes"); Require("GpuCommandLane", "DMA_DATA L2 readback", resources.HandleFault(PageFaultAccess::Read, l2_copy_dst), "MemoryUsingL2 copy did not publish GPU bytes"); Require("GpuCommandLane", "clean cached mirror readback", resources.HandleFault(PageFaultAccess::Read, clean_cached_readback), "clean host DMA was not reflected in the cached buffer"); std::array immediate_words{}; std::array copied_words{}; std::array immediate_memory_words{}; std::array memory_copy_words{}; std::array l2_copy_words{}; std::array clean_cached_words{}; std::memcpy(immediate_words.data(), reinterpret_cast(immediate_dst), sizeof(immediate_words)); std::memcpy(copied_words.data(), reinterpret_cast(memory_dst), sizeof(copied_words)); std::memcpy(immediate_memory_words.data(), reinterpret_cast(immediate_memory_dst), sizeof(immediate_memory_words)); std::memcpy(memory_copy_words.data(), reinterpret_cast(memory_copy_dst), sizeof(memory_copy_words)); std::memcpy(l2_copy_words.data(), reinterpret_cast(l2_copy_dst), sizeof(l2_copy_words)); std::memcpy(clean_cached_words.data(), reinterpret_cast(clean_cached_readback), sizeof(clean_cached_words)); Require("GpuCommandLane", "DMA_DATA immediate GDS contents", immediate_words == std::array{immediate_value, immediate_value}, "immediate-to-GDS-to-memory bytes do not match"); Require("GpuCommandLane", "DMA_DATA memory GDS contents", copied_words == source_words, "memory-to-GDS-to-memory bytes do not match"); Require("GpuCommandLane", "DMA_DATA immediate memory contents", immediate_memory_words == std::array{immediate_value, immediate_value}, "immediate-to-memory bytes do not match"); Require("GpuCommandLane", "DMA_DATA memory copy contents", memory_copy_words == source_words, "memory-to-memory bytes do not match"); Require("GpuCommandLane", "DMA_DATA L2 contents", l2_copy_words == source_words, "MemoryUsingL2 bytes do not match"); Require("GpuCommandLane", "DMA_DATA nowhere contents", std::memcmp(reinterpret_cast(nowhere_dst), nowhere_words.data(), sizeof(nowhere_words)) == 0, "Nowhere destination modified guest memory"); Require("GpuCommandLane", "clean cached DMA contents", clean_cached_words == std::array{clean_fill_value, clean_fill_value, clean_copy_words[0], clean_copy_words[1]}, "host-memory fill/copy did not update the clean cached mirror"); gpu.SendCommandSync([&] { Require("GpuCommandLane", "GPU memory invalidation", resources.InvalidateMemory(fault_base, sizeof(uint32_t)), "GPU memory invalidation did not find its mapped range"); }); auto &buffer_cache = resources.GetBufferCache(); Require("GpuCommandLane", "clean tracked fault setup", buffer_cache.IsRegionRegistered(immediate_dst, sizeof(uint32_t)) && !buffer_cache.HasGpuDirtyBytes(immediate_dst, sizeof(uint32_t)), "clean write-fault test address is not backed by a clean cached " "Buffer"); constexpr uint64_t dirty_fault_address = fault_base + 0xd000; constexpr uint32_t dirty_fault_value = 0x5a17c0deu; constexpr uint32_t dirty_fault_stale = 0x0ddba11u; std::memcpy(reinterpret_cast(dirty_fault_address), &dirty_fault_stale, sizeof(dirty_fault_stale)); gpu.SendCommandSync([&] { auto dirty = buffer_cache.ObtainBuffer( dirty_fault_address, sizeof(dirty_fault_value), true, false); Require("GpuCommandLane", "dirty tracked fault setup", dirty.first != nullptr, "dirty write-fault test could not create a cached Buffer"); buffer_cache.FillBuffer(dirty_fault_address, sizeof(dirty_fault_value), dirty_fault_value, false); }); Require("GpuCommandLane", "dirty tracked fault ownership", buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)), "dirty write-fault test address did not acquire GPU ownership"); std::binary_semaphore block_entered{0}; std::binary_semaphore block_release{0}; std::binary_semaphore clean_fault_complete{0}; bool clean_fault_handled = false; gpu.SendCommand([&] { block_entered.release(); block_release.acquire(); }); block_entered.acquire(); std::jthread clean_fault_thread([&] { clean_fault_handled = resources.HandleFault(PageFaultAccess::Write, immediate_dst); clean_fault_complete.release(); }); const bool clean_fault_was_direct = clean_fault_complete.try_acquire_for(std::chrono::seconds(1)); if (!clean_fault_was_direct) { block_release.release(); clean_fault_complete.acquire(); } clean_fault_thread.join(); Require("GpuCommandLane", "clean fault direct path", clean_fault_was_direct && clean_fault_handled, "a clean CPU write fault waited for the blocked GPU command lane"); std::binary_semaphore dirty_fault_complete{0}; bool dirty_fault_handled = false; std::jthread dirty_fault_thread([&] { dirty_fault_handled = resources.HandleFault(PageFaultAccess::Write, dirty_fault_address); dirty_fault_complete.release(); }); const bool dirty_fault_bypassed_lane = dirty_fault_complete.try_acquire_for(std::chrono::milliseconds(200)); block_release.release(); if (!dirty_fault_bypassed_lane) { dirty_fault_complete.acquire(); } dirty_fault_thread.join(); uint32_t dirty_fault_backing = 0; std::memcpy(&dirty_fault_backing, reinterpret_cast(dirty_fault_address), sizeof(dirty_fault_backing)); Require("GpuCommandLane", "dirty fault synchronization", !dirty_fault_bypassed_lane && dirty_fault_handled && dirty_fault_backing == dirty_fault_value && !buffer_cache.HasGpuDirtyBytes(dirty_fault_address, sizeof(dirty_fault_value)), "a GPU-dirty write fault bypassed synchronization or lost the " "published bytes"); constexpr uint64_t empty_copy_fault_address = fault_base + 0xe000; constexpr uint32_t empty_copy_fault_value = 0x6b28d1efu; gpu.SendCommandSync([&] { auto dirty = buffer_cache.ObtainBuffer(empty_copy_fault_address, sizeof(empty_copy_fault_value), true, false); Require("GpuCommandLane", "empty-copy write setup", dirty.first != nullptr, "empty-copy write test could not create a cached Buffer"); buffer_cache.FillBuffer(empty_copy_fault_address, sizeof(empty_copy_fault_value), empty_copy_fault_value, false); buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value)); const bool gpu_owned = buffer_cache.IsRegionGpuModified( empty_copy_fault_address, sizeof(empty_copy_fault_value)); const bool cpu_owned = buffer_cache.IsRegionCpuModified( empty_copy_fault_address, sizeof(empty_copy_fault_value)); Require("GpuCommandLane", "empty-copy read ownership", !gpu_owned && !cpu_owned, "readback did not leave clean ownership for the queued write " "invalidation"); buffer_cache.ReadMemory(empty_copy_fault_address, sizeof(empty_copy_fault_value), true); }); uint32_t empty_copy_fault_backing = 0; std::memcpy(&empty_copy_fault_backing, reinterpret_cast(empty_copy_fault_address), sizeof(empty_copy_fault_backing)); const bool empty_copy_cpu_owned = buffer_cache.IsRegionCpuModified( empty_copy_fault_address, sizeof(empty_copy_fault_value)); const bool empty_copy_gpu_owned = buffer_cache.IsRegionGpuModified( empty_copy_fault_address, sizeof(empty_copy_fault_value)); const bool empty_copy_dirty = buffer_cache.HasGpuDirtyBytes( empty_copy_fault_address, sizeof(empty_copy_fault_value)); Require("GpuCommandLane", "empty-copy write ownership", empty_copy_fault_backing == empty_copy_fault_value && empty_copy_cpu_owned && !empty_copy_gpu_owned && !empty_copy_dirty, "write invalidation without a remaining copy did not establish CPU " "ownership"); resources.UnmapMemory(fault_base, fault_size); Require("GpuCommandLane", "processor fault unmap", Libs::LibKernel::Memory::KernelMunmap(fault_base, fault_size) == 0, "processor-fault direct-memory mapping release failed"); Require("GpuCommandLane", "processor fault free", Libs::LibKernel::Memory::KernelReleaseDirectMemory( fault_direct_offset, fault_size) == 0, "processor-fault direct-memory allocation release failed"); std::binary_semaphore command_entered{0}; std::binary_semaphore release_command{0}; std::atomic shutdown_complete{false}; std::atomic submission_lane_entered{false}; bool shutdown_owner_visible = false; resources.MapMemory(empty_unmap_base, empty_unmap_size); gpu.SendCommand([&] { command_entered.release(); release_command.acquire(); shutdown_owner_visible = &context.GetGpu() == &gpu; gpu.Done(); submission_lane_entered = true; }); command_entered.acquire(); std::jthread shutdown_thread([&] { context.ShutdownGpu(); shutdown_complete = true; }); while (!gpu.IsStopping()) { std::this_thread::yield(); } Require("GpuCommandLane", "owned shutdown", !shutdown_complete.load(), "GPU owner returned before an in-flight command completed"); release_command.release(); shutdown_thread.join(); Require( "GpuCommandLane", "owned shutdown completion", shutdown_complete.load() && submission_lane_entered.load() && shutdown_owner_visible, "GPU owner did not remain available while draining queued work"); resources.UnmapMemory(empty_unmap_base, empty_unmap_size); Require("GpuCommandLane", "unmap after shutdown", !resources.IsMapped(empty_unmap_base, empty_unmap_size), "renderer retained mapped coverage after the GPU command lane stopped"); context.ShutdownGpu(); std::printf("[host] %-32s ok\n", "GpuCommandLane"); } void CheckUnifiedImageViewCache() { EnsureRuntimeContext(); constexpr const char *name = "UnifiedImageViewCache"; CommandScheduler scheduler(Renderer(), m_runtime_context); ImageInfo color_info{}; color_info.pixel_format = vk::Format::eR8G8B8A8Unorm; color_info.guest_format = Prospero::BufferFormat::k8_8_8_8UNorm; color_info.type = Prospero::ImageType::kColor2D; color_info.extent = {8, 8, 1}; color_info.resources = {2, 2}; color_info.pitch = 8; color_info.bytes_per_block = 4; color_info.samples = 1; color_info.tile_mode = Prospero::TileMode::kLinear; color_info.mip_layout[0] = {0, 512, 8, 8}; color_info.mip_layout[1] = {512, 128, 4, 4}; Libs::Graphics::Image color(m_runtime_context, scheduler, color_info); const vk::ComponentMapping identity{}; const vk::ComponentMapping bgra{ vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eA}; ImageViewInfo sampled{}; sampled.format = color_info.pixel_format; sampled.type = vk::ImageViewType::e2D; sampled.aspect = vk::ImageAspectFlagBits::eColor; sampled.base_level = 0; sampled.level_count = 1; sampled.base_layer = 0; sampled.layer_count = 1; sampled.mapping = identity; sampled.usage = vk::ImageUsageFlagBits::eSampled; const auto first = color.FindView(sampled); const auto first_again = color.FindView(sampled); auto swizzled_info = sampled; swizzled_info.mapping = bgra; const auto swizzled = color.FindView(swizzled_info); auto mip_info = sampled; mip_info.base_level = 1; const auto mip = color.FindView(mip_info); auto layer_info = sampled; layer_info.base_layer = 1; const auto layer = color.FindView(layer_info); auto array_info = sampled; array_info.type = vk::ImageViewType::e2DArray; array_info.layer_count = 2; const auto array = color.FindView(array_info); auto reinterpreted_info = sampled; reinterpreted_info.format = vk::Format::eR8G8B8A8Uint; const auto reinterpreted = color.FindView(reinterpreted_info); auto storage_info = sampled; storage_info.usage = vk::ImageUsageFlagBits::eStorage; const auto storage = color.FindView(storage_info); auto attachment_info = sampled; attachment_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto attachment = color.FindView(attachment_info); Require(name, "color views", first != nullptr && first_again == first && swizzled != nullptr && swizzled != first && mip != nullptr && mip != first && layer != nullptr && layer != first && array != nullptr && array != first && reinterpreted != nullptr && reinterpreted != first && storage != nullptr && storage != first && attachment == first && color.views.size() == 7, "dynamic view identity omitted mapping, format, mip, layer, type, " "or storage usage"); auto video_out_info = color_info; video_out_info.pixel_format = vk::Format::eA2R10G10B10UnormPack32; video_out_info.guest_format = Prospero::BufferFormat::k10_10_10_2UNorm; Libs::Graphics::Image video_out(m_runtime_context, scheduler, video_out_info); auto video_out_view_info = sampled; video_out_view_info.format = video_out_info.pixel_format; video_out_view_info.usage = vk::ImageUsageFlagBits::eTransferSrc; const auto video_out_view = video_out.FindView(video_out_view_info); auto storage_alias_info = video_out_view_info; storage_alias_info.format = vk::Format::eA2B10G10R10UnormPack32; storage_alias_info.usage = vk::ImageUsageFlagBits::eStorage; const auto storage_alias_view = video_out.FindView(storage_alias_info); Require( name, "video-out storage alias usage", ImageViewOps::FormatsCompatible(video_out_info.pixel_format, storage_alias_info.format) && static_cast(video_out.backing.usage & vk::ImageUsageFlagBits::eStorage) && video_out_view != nullptr && storage_alias_view != nullptr, "compatible video-out backing was created without storage usage"); ImageInfo depth_info{}; depth_info.pixel_format = vk::Format::eD32Sfloat; depth_info.guest_format = Prospero::BufferFormat::k32Float; depth_info.type = Prospero::ImageType::kColor2D; depth_info.extent = {8, 8, 1}; depth_info.resources = {1, 2}; depth_info.pitch = 8; depth_info.bytes_per_block = 4; depth_info.samples = 1; depth_info.tile_mode = Prospero::TileMode::kDepth; depth_info.mip_layout[0] = {0, 512, 8, 8}; Libs::Graphics::Image depth(m_runtime_context, scheduler, depth_info); ImageViewInfo depth_sampled{}; depth_sampled.format = depth_info.pixel_format; depth_sampled.type = vk::ImageViewType::e2D; depth_sampled.aspect = vk::ImageAspectFlagBits::eDepth; depth_sampled.base_level = 0; depth_sampled.level_count = 1; depth_sampled.base_layer = 0; depth_sampled.layer_count = 1; depth_sampled.mapping = {vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR}; depth_sampled.usage = vk::ImageUsageFlagBits::eSampled; const auto depth_first = depth.FindView(depth_sampled); auto depth_alias_info = depth_sampled; depth_alias_info.format = vk::Format::eR32Uint; const auto depth_alias = depth.FindView(depth_alias_info); const auto depth_again = depth.FindView(depth_sampled); auto depth_array_info = depth_sampled; depth_array_info.type = vk::ImageViewType::e2DArray; depth_array_info.layer_count = 2; const auto depth_array = depth.FindView(depth_array_info); auto depth_attachment_info = depth_array_info; depth_attachment_info.mapping = {}; depth_attachment_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto depth_attachment = depth.FindView(depth_attachment_info); Require(name, "depth views", depth_first != nullptr && depth_again == depth_first && depth_alias == depth_first && depth_array != nullptr && depth_array != depth_first && depth_attachment != nullptr && depth_attachment != depth_array && depth.views.size() == 3, "unified depth view cache lost sampled/attachment identity"); auto depth_stencil_info = depth_info; depth_stencil_info.pixel_format = vk::Format::eD24UnormS8Uint; depth_stencil_info.guest_format = Prospero::BufferFormat::k16UNorm; depth_stencil_info.bytes_per_block = 2; Libs::Graphics::Image depth_stencil(m_runtime_context, scheduler, depth_stencil_info); auto depth_only_info = depth_sampled; depth_only_info.format = depth_stencil_info.pixel_format; const auto depth_only = depth_stencil.FindView(depth_only_info); auto combined_info = depth_only_info; combined_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; combined_info.mapping = {}; combined_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto combined = depth_stencil.FindView(combined_info); Require(name, "depth/stencil aspects", depth_only != nullptr && combined != nullptr && depth_only != combined && depth_stencil.views.size() == 2 && depth_stencil.views[0].info.aspect == vk::ImageAspectFlagBits::eDepth && depth_stencil.views[1].info.aspect == (vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil), "explicit depth-only and combined aspects shared one image view"); Require(name, "role-free backing", color.backing.image_type == vk::ImageType::e2D && color.backing.layers == 2 && depth.backing.image_type == vk::ImageType::e2D && ImageViewOps::FormatsCompatible(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Uint) && !ImageViewOps::FormatsCompatible(vk::Format::eD32Sfloat, vk::Format::eR32Sfloat), "role-free image backing or host format compatibility diverged"); auto line_info = color_info; line_info.type = Prospero::ImageType::kColor1D; line_info.extent = {8, 1, 1}; line_info.resources = {2, 2}; line_info.mip_layout[0] = {0, 64, 8, 1}; line_info.mip_layout[1] = {64, 16, 4, 1}; Libs::Graphics::Image line(m_runtime_context, scheduler, line_info); auto line_view_info = sampled; line_view_info.type = vk::ImageViewType::e1D; const auto line_view = line.FindView(line_view_info); auto line_array_info = line_view_info; line_array_info.type = vk::ImageViewType::e1DArray; line_array_info.layer_count = 2; const auto line_array = line.FindView(line_array_info); Require(name, "1D views", line_view != nullptr && line_array != nullptr && line_array != line_view && line.backing.image_type == vk::ImageType::e1D && line.backing.layers == 2, "1D and 1D-array views did not use a first-class 1D backing"); auto volume_info = color_info; volume_info.type = Prospero::ImageType::kColor3D; volume_info.extent = {8, 8, 4}; volume_info.resources = {2, 1}; Libs::Graphics::Image volume(m_runtime_context, scheduler, volume_info); auto slice_info = sampled; slice_info.type = vk::ImageViewType::e2D; slice_info.base_level = 1; slice_info.base_layer = 1; const auto slice = volume.FindView(slice_info); auto slice_array_info = slice_info; slice_array_info.type = vk::ImageViewType::e2DArray; slice_array_info.base_layer = 0; slice_array_info.layer_count = 2; const auto slice_array = volume.FindView(slice_array_info); Require(name, "3D slice views", slice != nullptr && slice_array != nullptr && slice_array != slice && static_cast(volume.backing.flags & vk::ImageCreateFlagBits::e2DArrayCompatible), "2D slice views of a compatible 3D backing were rejected"); std::printf("[host] %-32s ok\n", name); } void CheckBufferCacheDirtyGarbageCollection() { constexpr const char *name = "BufferCacheDirtyGarbageCollection"; constexpr uintptr_t base = 0x0000000200700000ull; constexpr uint64_t allocation_size = 0x2400000; constexpr uint64_t allocation_alignment = 0x10000; constexpr uint64_t first_offset = 0x100; constexpr uint64_t second_offset = 0x200; constexpr uint64_t clean_offset = 0x300; constexpr uint64_t unmap_offset = 0x4100; constexpr uint64_t partial_unmap_offset = 0x8100; constexpr uint64_t partial_unmap_survivor_offset = 0xc100; constexpr uint64_t ring_fault_first_offset = 0x2100; constexpr uint64_t ring_fault_second_offset = 0x2110; constexpr uint32_t first_value = 0x10203040u; constexpr uint32_t second_value = 0x50607080u; constexpr uint32_t first_stale = 0x0badf00du; constexpr uint32_t second_stale = 0xdeadbeefu; constexpr uint32_t clean_value = 0xaabbccddu; constexpr uint32_t unmap_value = 0x91a2b3c4u; constexpr uint32_t remap_value = 0xd5e6f708u; constexpr uint32_t partial_unmap_value = 0x62738495u; constexpr uint32_t partial_unmap_survivor_value = 0xa6b7c8d9u; constexpr uint32_t ring_fault_first_value = 0x0a1b2c3du; constexpr uint32_t ring_fault_second_value = 0x4e5f6071u; EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); context.InitializeGpu(nullptr); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "dirty-GC direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "dirty-GC fixed direct-memory mapping failed"); auto *memory = static_cast(mapped); std::memcpy(memory + first_offset, &first_stale, sizeof(first_stale)); std::memcpy(memory + second_offset, &second_stale, sizeof(second_stale)); std::memcpy(memory + clean_offset, &clean_value, sizeof(clean_value)); { auto &resources = context; auto &cache = resources.GetBufferCache(); resources.MapMemory(base, allocation_size); const auto MarkGpuWrite = [&](uint64_t address, uint64_t size) { auto allocation = cache.ObtainBuffer(address, size, true, false); Require(name, "dirty allocation", allocation.first != nullptr, "dirty-GC buffer allocation failed"); }; const auto ReadNativeValue = [&](const Libs::Graphics::Buffer &buffer, uint64_t offset) { auto readback = CreateHostBuffer(name, sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {0}); const vk::BufferCopy copy{offset, 0, sizeof(uint32_t)}; scheduler.Current().Handle().copyBuffer(buffer.Handle(), readback.buffer, 1, ©); vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = readback.buffer; barrier.size = readback.size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); scheduler.Finish(); const auto value = ReadBuffer(name, readback, 1)[0]; DestroyBuffer(&readback); return value; }; constexpr uint64_t large_copy_source_offset = 0x40000; constexpr uint64_t large_copy_destination_offset = 0x80000; constexpr uint64_t large_copy_size = 96 * 1024; std::vector large_copy_source(large_copy_size); for (uint64_t index = 0; index < large_copy_size; ++index) { large_copy_source[index] = static_cast((index * 37 + index / 251) & 0xffu); } std::memcpy(memory + large_copy_source_offset, large_copy_source.data(), large_copy_size); std::memset(memory + large_copy_destination_offset, 0, large_copy_size); cache.CopyBuffer(base + large_copy_destination_offset, base + large_copy_source_offset, large_copy_size, false, false); Require(name, "large host copy", std::memcmp(memory + large_copy_destination_offset, large_copy_source.data(), large_copy_size) == 0, "chunked host copy lost bytes across its scratch boundary"); constexpr uint64_t index_offset = 0x180000; constexpr uint64_t index_page = BufferCache::CACHING_PAGESIZE; constexpr uint64_t index_span = 3 * index_page; const uint64_t index_begin = base + index_offset; Require(name, "registered-range empty lookup", !cache.IsRegionRegistered(index_begin, index_span) && !BufferCacheTestAccess::PageOwner(cache, index_begin), "empty Buffer cache reported a registered range"); const auto index_left = cache.FindBuffer(index_begin + 0x100, sizeof(uint32_t)); const auto index_right = cache.FindBuffer( index_begin + 2 * index_page + 0x100, sizeof(uint32_t)); Require(name, "registered-range owners", index_left && index_right && cache.FindBuffer(index_begin + 0x200, sizeof(uint32_t)) == index_left && BufferCacheTestAccess::PageOwner(cache, index_begin) == index_left && !BufferCacheTestAccess::PageOwner(cache, index_begin + index_page) && BufferCacheTestAccess::PageOwner( cache, index_begin + 2 * index_page) == index_right, "failed to create disjoint Buffer index owners"); Require( name, "registered-range boundaries", cache.IsRegionRegistered(index_begin, 1) && cache.IsRegionRegistered(index_begin + index_page - 1, 1) && !cache.IsRegionRegistered(index_begin - 1, 1) && !cache.IsRegionRegistered(index_begin + index_page, index_page) && cache.IsRegionRegistered(index_begin + index_page, index_page + 1) && !cache.IsRegionRegistered(index_begin + index_span, 1) && cache.IsRegionRegistered(index_begin - 1, index_span + 2), "indexed lookup mishandled a half-open boundary, gap, or broad " "overlap"); const auto index_bridge = cache.FindBuffer(index_begin + index_page - 1, index_page + 2); Require(name, "registered-range merge", index_bridge && cache.IsRegionRegistered(index_begin + index_page, index_page) && BufferCacheTestAccess::PageOwner(cache, index_begin) == index_bridge && BufferCacheTestAccess::PageOwner( cache, index_begin + index_page) == index_bridge && BufferCacheTestAccess::PageOwner( cache, index_begin + 2 * index_page) == index_bridge, "bridging acquisition did not publish its merged Buffer range"); scheduler.Finish(); constexpr uint64_t recycled_offset = index_offset + 4 * index_page; const auto recycled = cache.FindBuffer(base + recycled_offset + 0x100, sizeof(uint32_t)); Require(name, "slot generation reuse", (recycled.index == index_left.index && recycled != index_left) || (recycled.index == index_right.index && recycled != index_right), "retired Buffer slot was not reused with a new generation"); const auto [resolved_left, resolved_left_offset] = cache.ObtainBuffer( index_begin + 0x100, sizeof(uint32_t), true, false, index_left); const auto [resolved_right, resolved_right_offset] = cache.ObtainBuffer(index_begin + 2 * index_page + 0x100, sizeof(uint32_t), true, false, index_right); Require(name, "two-pass stale id resolution", resolved_left == &cache.GetBuffer(index_bridge) && resolved_right == resolved_left && resolved_left_offset != resolved_right_offset, "saved descriptor IDs did not resolve through the merged owner"); constexpr uint64_t stream_page = BufferCache::CACHING_PAGESIZE; constexpr uint64_t stream_leap = stream_page * 128; for (const bool forward : {true, false}) { const char *stream_name = forward ? "ForwardBufferStream" : "BackwardBufferStream"; const uint64_t stream_begin = base + (forward ? 0x1800000 : 0x2000000); const uint64_t caught_address = forward ? stream_begin + stream_leap / 2 : stream_begin - stream_leap / 2; const uint64_t untouched_address = forward ? stream_begin - stream_leap / 2 : stream_begin + stream_leap / 2; const auto caught = cache.FindBuffer(caught_address, stream_page); const auto untouched = cache.FindBuffer(untouched_address, stream_page); auto grown = cache.FindBuffer(stream_begin, stream_page * 2); constexpr uint32_t stream_value = 0x6a3b92d1u; for (const auto address : {stream_begin, caught_address}) { MarkGpuWrite(address, sizeof(stream_value)); cache.FillBuffer(address, sizeof(stream_value), stream_value, false); } uint64_t requested_begin = stream_begin; uint64_t requested_end = stream_begin + stream_page * 2; for (uint32_t step = 1; step <= 18; ++step) { const auto address = (forward ? requested_end : requested_begin) - 4; grown = cache.FindBuffer(address, 8); if (forward) { requested_end += stream_page; } else { requested_begin -= stream_page; } if (step < 18) { const auto &owner = cache.GetBuffer(grown); Require(stream_name, "growth before threshold", owner.CpuAddress() == requested_begin && owner.Size() == requested_end - requested_begin && owner.StreamScore() == static_cast(step), "sequential requests grew beyond their consumed pages too " "early"); } } const auto &grown_buffer = cache.GetBuffer(grown); Require(stream_name, "growth reserve", grown_buffer.CpuAddress() == (forward ? requested_begin : requested_begin - stream_leap) && grown_buffer.Size() == requested_end - requested_begin + stream_leap && grown_buffer.StreamScore() == 0 && cache.GetBuffer(caught).is_deleted && !cache.GetBuffer(untouched).is_deleted && BufferCacheTestAccess::PageOwner(cache, caught_address) == grown && BufferCacheTestAccess::PageOwner( cache, untouched_address) == untouched, "streaming did not reserve the next pages or retain correct " "neighboring owners"); for (uint32_t step = 0; step < 128; ++step) { const auto address = forward ? requested_end + step * stream_page - 4 : requested_begin - step * stream_page - 4; Require(stream_name, "reuse reserved pages", cache.FindBuffer(address, 8) == grown, "continued sequential requests replaced the buffer within " "its growth reserve"); } for (const auto address : {stream_begin, caught_address}) { Require( stream_name, "preserve GPU contents", ReadNativeValue(grown_buffer, grown_buffer.Offset(address)) == stream_value, "stream growth lost GPU-written contents from a merged owner"); } } resources.UnmapMemory(index_begin, index_span); resources.UnmapMemory(base + recycled_offset, index_page); Require(name, "invalidation retains owners", !resources.IsMapped(index_begin, index_span) && !resources.IsMapped(base + recycled_offset, index_page) && cache.IsRegionRegistered(index_begin, index_span) && BufferCacheTestAccess::PageOwner(cache, index_begin) == index_bridge && BufferCacheTestAccess::PageOwner( cache, index_begin + index_page) == index_bridge && BufferCacheTestAccess::PageOwner( cache, index_begin + 2 * index_page) == index_bridge && BufferCacheTestAccess::PageOwner( cache, base + recycled_offset) == recycled && cache.IsRegionCpuModified(index_begin, index_span) && cache.IsRegionCpuModified(base + recycled_offset, index_page), "CPU invalidation retired a reusable Buffer owner"); resources.MapMemory(index_begin, index_span); resources.MapMemory(base + recycled_offset, index_page); MarkGpuWrite(base + first_offset, sizeof(first_value)); MarkGpuWrite(base + second_offset, sizeof(second_value)); cache.FillBuffer(base + first_offset, sizeof(first_value), first_value, false); cache.FillBuffer(base + second_offset, sizeof(second_value), second_value, false); auto &download = BufferCacheTestAccess::DownloadBuffer(cache); auto *fixed_download = &download; const auto fixed_download_handle = download.Handle(); auto [ring_tail, ring_tail_offset] = download.Map(download.Size() - sizeof(uint32_t), 4); Require(name, "fault-ring tail reservation", ring_tail != nullptr && ring_tail_offset == 0, "failed to position the reusable download ring near wrap"); download.Commit(); MarkGpuWrite(base + ring_fault_first_offset, sizeof(ring_fault_first_value)); MarkGpuWrite(base + ring_fault_second_offset, sizeof(ring_fault_second_value)); cache.FillBuffer(base + ring_fault_first_offset, sizeof(ring_fault_first_value), ring_fault_first_value, false); cache.FillBuffer(base + ring_fault_second_offset, sizeof(ring_fault_second_value), ring_fault_second_value, false); Require(name, "fault-ring wrapped batch", resources.HandleFault(PageFaultAccess::Read, base + ring_fault_first_offset), "fault readback could not wrap a live download-ring tick"); // The widened window collects the two earlier writes and both // fault-adjacent writes. uint64_t expected_packing_offset = 4 * 64; uint64_t expected_packing_alignment = 1; if (!download.IsCoherent()) { Require(name, "fault-ring atom policy", StreamBufferTestAccess::NormalizeReservation( false, m_runtime_context.physical_device_properties.limits .nonCoherentAtomSize, expected_packing_offset, expected_packing_alignment), "failed to calculate non-coherent fault-ring stride"); } auto [packing_probe, packing_offset] = download.Map(1, 1, false); Require(name, "adjacent download reservation stride", packing_probe != nullptr && packing_offset == expected_packing_offset, "adjacent fault downloads did not reserve an atom-safe stride"); download.Commit(); uint32_t widened_first_backing = 0; uint32_t widened_second_backing = 0; uint32_t ring_fault_first_backing = 0; uint32_t ring_fault_second_backing = 0; std::memcpy(&widened_first_backing, memory + first_offset, sizeof(widened_first_backing)); std::memcpy(&widened_second_backing, memory + second_offset, sizeof(widened_second_backing)); std::memcpy(&ring_fault_first_backing, memory + ring_fault_first_offset, sizeof(ring_fault_first_backing)); std::memcpy(&ring_fault_second_backing, memory + ring_fault_second_offset, sizeof(ring_fault_second_backing)); Require(name, "fault-ring wrapped contents", widened_first_backing == first_value && widened_second_backing == second_value && ring_fault_first_backing == ring_fault_first_value && ring_fault_second_backing == ring_fault_second_value && &BufferCacheTestAccess::DownloadBuffer(cache) == fixed_download && download.Handle() == fixed_download_handle && download.Size() == (32ull << 20), "wrapped fault batch published incorrect disjoint ranges"); constexpr uint64_t window_size = 512 * 1024; constexpr uint64_t window_owner_offset = 0x240000; constexpr uint64_t window_owner_size = 0xc0000; constexpr uint64_t window_fault_offset = window_owner_offset + 0x100; constexpr uint64_t window_inside_offset = window_owner_offset + window_size - sizeof(uint32_t); constexpr uint64_t window_outside_offset = window_owner_offset + window_size; constexpr uint32_t window_stale = 0x13579bdfu; constexpr uint32_t window_value = 0x2468ace0u; constexpr std::array window_offsets{ window_fault_offset, window_inside_offset, window_outside_offset}; static_assert((base + window_owner_offset) % window_size == window_size / 2); for (const auto offset : window_offsets) { Libs::LibKernel::Memory::WriteBacking(base + offset, &window_stale, sizeof(window_stale)); } (void)cache.FindBuffer(base + window_owner_offset, window_owner_size); for (const auto offset : window_offsets) { MarkGpuWrite(base + offset, sizeof(window_value)); cache.FillBuffer(base + offset, sizeof(window_value), window_value, false); } cache.ReadMemory(base + window_fault_offset, sizeof(window_value)); uint32_t window_inside_backing = 0; uint32_t window_outside_backing = 0; Libs::LibKernel::Memory::TryReadBacking(base + window_inside_offset, &window_inside_backing, sizeof(window_inside_backing)); Libs::LibKernel::Memory::TryReadBacking(base + window_outside_offset, &window_outside_backing, sizeof(window_outside_backing)); Require(name, "widened-window boundary", window_inside_backing == window_value && window_outside_backing == window_stale && !cache.HasGpuDirtyBytes(base + window_inside_offset, sizeof(window_value)) && cache.HasGpuDirtyBytes(base + window_outside_offset, sizeof(window_value)) && !cache.IsRegionGpuModified(base + window_inside_offset, sizeof(window_value)) && cache.IsRegionGpuModified(base + window_outside_offset, sizeof(window_value)), "readback did not honor the clamped half-open 512 KiB window"); cache.ReadMemory(base + window_outside_offset, sizeof(window_value)); Libs::LibKernel::Memory::WriteBacking(base + first_offset, &first_stale, sizeof(first_stale)); Libs::LibKernel::Memory::WriteBacking(base + second_offset, &second_stale, sizeof(second_stale)); MarkGpuWrite(base + first_offset, sizeof(first_value)); MarkGpuWrite(base + second_offset, sizeof(second_value)); cache.FillBuffer(base + first_offset, sizeof(first_value), first_value, false); cache.FillBuffer(base + second_offset, sizeof(second_value), second_value, false); for (uint32_t tick = 0; tick < 160; tick++) { cache.RunGarbageCollector(); } Require(name, "age before pressure", cache.IsRegionRegistered(base, allocation_size), "buffer was reclaimed without memory pressure"); BufferCacheTestAccess::SetGarbageCollectionThresholds( cache, 0, std::numeric_limits::max()); const auto gc_submission_tick = scheduler.CurrentTick(); cache.RunGarbageCollector(); uint32_t first_before_completion = 0; uint32_t second_before_completion = 0; Libs::LibKernel::Memory::TryReadBacking(base + first_offset, &first_before_completion, sizeof(first_before_completion)); Libs::LibKernel::Memory::TryReadBacking(base + second_offset, &second_before_completion, sizeof(second_before_completion)); Require(name, "normal dirty retention", first_before_completion == first_stale && second_before_completion == second_stale && cache.IsRegionRegistered(base, allocation_size) && cache.IsRegionGpuModified(base + first_offset, sizeof(first_value)) && cache.IsRegionGpuModified(base + second_offset, sizeof(second_value)) && cache.HasGpuDirtyBytes(base + first_offset, sizeof(first_value)) && cache.HasGpuDirtyBytes(base + second_offset, sizeof(second_value)) && scheduler.CurrentTick() == gc_submission_tick, "normal GC retired or synchronized a dirty Buffer"); BufferCacheTestAccess::SetGarbageCollectionThresholds(cache, 0, 0); const auto older_publication_tick = scheduler.CurrentTick(); std::binary_semaphore older_publication_entered{0}; std::binary_semaphore release_older_publication{0}; std::atomic gc_returned{false}; scheduler.DeferPriorityOperation([&] { older_publication_entered.release(); release_older_publication.acquire(); Libs::LibKernel::Memory::WriteBacking( base + first_offset, &first_stale, sizeof(first_stale)); }); std::jthread release_publication([&] { older_publication_entered.acquire(); const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); while (!gc_returned.load() && std::chrono::steady_clock::now() < deadline) { std::this_thread::yield(); } release_older_publication.release(); }); cache.RunGarbageCollector(); gc_returned = true; release_publication.join(); scheduler.WaitPriorityOperations(older_publication_tick); uint32_t first_backing = 0; uint32_t second_backing = 0; uint32_t clean_backing = 0; std::memcpy(&first_backing, memory + first_offset, sizeof(first_backing)); std::memcpy(&second_backing, memory + second_offset, sizeof(second_backing)); std::memcpy(&clean_backing, memory + clean_offset, sizeof(clean_backing)); Require(name, "downloaded contents", first_backing == first_value && second_backing == second_value && clean_backing == clean_value && !cache.IsRegionRegistered(base, allocation_size) && !cache.IsRegionGpuModified(base + first_offset, sizeof(first_value)) && !cache.IsRegionGpuModified(base + second_offset, sizeof(second_value)) && !cache.HasGpuDirtyBytes(base + first_offset, sizeof(first_value)) && !cache.HasGpuDirtyBytes(base + second_offset, sizeof(second_value)) && scheduler.CurrentTick() > gc_submission_tick, "critical GC did not publish exact ranges before clearing " "Buffer ownership"); constexpr uint64_t starvation_offset = 0x100000; constexpr uint64_t starvation_stride = 2 * BufferCache::CACHING_PAGESIZE; constexpr uint32_t starvation_count = 33; constexpr uint32_t starvation_value = 0x31415926u; BufferCacheTestAccess::SetGarbageCollectionThresholds( cache, std::numeric_limits::max(), std::numeric_limits::max()); for (uint32_t index = 0; index < starvation_count; index++) { const auto address = base + starvation_offset + index * starvation_stride; (void)cache.ObtainBuffer(address, sizeof(starvation_value), true); cache.FillBuffer(address, sizeof(starvation_value), starvation_value, false); } for (uint32_t tick = 0; tick < 160; tick++) { cache.RunGarbageCollector(); } constexpr uint64_t starvation_clean_offset = starvation_offset + starvation_count * starvation_stride; const auto starvation_clean = cache.FindBuffer( base + starvation_clean_offset, sizeof(starvation_value)); Require(name, "normal-GC clean candidate", static_cast(starvation_clean), "failed to create the clean starvation candidate"); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } BufferCacheTestAccess::SetGarbageCollectionThresholds( cache, 0, std::numeric_limits::max()); cache.RunGarbageCollector(); Require( name, "normal-GC dirty bypass", cache.IsRegionRegistered(base + starvation_offset, sizeof(starvation_value)) && cache.IsRegionRegistered( base + starvation_offset + (starvation_count - 1) * starvation_stride, sizeof(starvation_value)) && !cache.IsRegionRegistered(base + starvation_clean_offset, sizeof(starvation_value)), "old dirty owners hid an eligible clean Buffer from normal GC"); BufferCacheTestAccess::SetGarbageCollectionThresholds(cache, 0, 0); const auto starvation_retired = BufferCacheTestAccess::PageOwner(cache, base + starvation_offset); cache.RunGarbageCollector(); Require(name, "critical-GC starvation cleanup", !cache.IsRegionRegistered(base + starvation_offset, sizeof(starvation_value)) && !cache.IsRegionRegistered( base + starvation_offset + (starvation_count - 1) * starvation_stride, sizeof(starvation_value)) && !BufferCacheTestAccess::IsBufferAllocated( cache, starvation_retired), "critical GC did not immediately free the skipped dirty owners"); constexpr uint64_t lookup_only_offset = 0x218000; constexpr uint64_t obtained_offset = 0x220000; constexpr uint64_t residency_size = 2 * BufferCache::CACHING_PAGESIZE; BufferCacheTestAccess::SetGarbageCollectionThresholds( cache, std::numeric_limits::max(), std::numeric_limits::max()); const auto lookup_only = cache.FindBuffer( base + lookup_only_offset, residency_size); const auto obtained = cache.FindBuffer(base + obtained_offset, residency_size); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } Require(name, "lookup-only owner identity", cache.FindBuffer(base + lookup_only_offset, residency_size) == lookup_only, "lookup-only discovery replaced its valid Buffer owner"); const auto [obtained_buffer, obtained_buffer_offset] = cache.ObtainBuffer( base + obtained_offset, residency_size, false, false, obtained); BufferCacheTestAccess::SetGarbageCollectionThresholds( cache, 0, std::numeric_limits::max()); cache.RunGarbageCollector(); Require(name, "lookup versus acquisition residency", !cache.IsRegionRegistered(base + lookup_only_offset, residency_size) && !BufferCacheTestAccess::PageOwner( cache, base + lookup_only_offset) && obtained_buffer == &cache.GetBuffer(obtained) && obtained_buffer_offset == 0 && cache.IsRegionRegistered(base + obtained_offset, residency_size) && BufferCacheTestAccess::PageOwner( cache, base + obtained_offset) == obtained, "lookup refreshed residency or actual acquisition did not"); BufferCacheTestAccess::SetGarbageCollectionThresholds(cache, 0, 0); auto unmap_allocation = cache.ObtainBuffer( base + unmap_offset, sizeof(unmap_value), true, false); Require(name, "direct-invalidation allocation", unmap_allocation.first != nullptr, "direct-invalidation buffer allocation failed"); cache.FillBuffer(base + unmap_offset, sizeof(unmap_value), unmap_value, false); resources.UnmapMemory(base + 0x4000, 0x4000); uint32_t unmap_backing = 0; std::memcpy(&unmap_backing, memory + unmap_offset, sizeof(unmap_backing)); Require(name, "direct-invalidation contents", unmap_backing == unmap_value && !resources.IsMapped(base + 0x4000, 0x4000) && cache.IsRegionRegistered(base + 0x4000, 0x4000) && cache.IsRegionCpuModified(base + 0x4000, 0x4000) && !cache.IsRegionGpuModified(base + 0x4000, 0x4000) && !cache.HasGpuDirtyBytes(base + 0x4000, 0x4000), "dirty invalidation did not publish bytes and retain its owner"); std::memcpy(memory + unmap_offset, &remap_value, sizeof(remap_value)); resources.MapMemory(base + 0x4000, 0x4000); const auto remapped = cache.ObtainBuffer(base + unmap_offset, sizeof(remap_value), true); Require(name, "same-address remap allocation", remapped.first == unmap_allocation.first, "same-address remap did not synchronize the retained Buffer"); Require(name, "same-address remap contents", ReadNativeValue(*remapped.first, remapped.second) == remap_value, "same-address remap reused stale native Buffer bytes"); auto partial_unmap_allocation = cache.ObtainBuffer(base + 0x8000, 0x8000, true, false); Require(name, "partial-invalidation allocation", partial_unmap_allocation.first != nullptr, "cross-page cached buffer allocation failed"); cache.FillBuffer(base + partial_unmap_offset, sizeof(partial_unmap_value), partial_unmap_value, false); cache.FillBuffer(base + partial_unmap_survivor_offset, sizeof(partial_unmap_survivor_value), partial_unmap_survivor_value, false); resources.UnmapMemory(base + 0x8000, 0x4000); uint32_t partial_unmap_survivor_backing = 0; std::memcpy(&partial_unmap_survivor_backing, memory + partial_unmap_survivor_offset, sizeof(partial_unmap_survivor_backing)); Require( name, "partial-invalidation ownership", !resources.IsMapped(base + 0x8000, 0x4000) && cache.IsRegionRegistered(base + 0x8000, 0x8000) && cache.IsRegionCpuModified(base + 0x8000, 0x4000) && !cache.IsRegionCpuModified( base + partial_unmap_survivor_offset, sizeof(partial_unmap_survivor_value)) && !cache.HasGpuDirtyBytes(base + partial_unmap_offset, sizeof(partial_unmap_value)) && !cache.HasGpuDirtyBytes(base + partial_unmap_survivor_offset, sizeof(partial_unmap_survivor_value)) && partial_unmap_survivor_backing == partial_unmap_survivor_value, "widened partial invalidation mishandled ownership or backing"); resources.MapMemory(base + 0x8000, 0x4000); auto survivor = cache.ObtainBuffer(base + partial_unmap_survivor_offset, sizeof(partial_unmap_survivor_value), false); Require(name, "partial-invalidation survivor", survivor.first != nullptr, "cached-buffer remainder was not retained"); Require(name, "partial-invalidation survivor contents", ReadNativeValue(*survivor.first, survivor.second) == partial_unmap_survivor_value, "partial invalidation lost disjoint native bytes"); constexpr uint64_t large_offset = 0x10000; constexpr uint64_t large_size = 32ull << 20; constexpr uint32_t large_value = 0x5aa55aa5u; constexpr uint32_t large_stale = 0x12345678u; std::memcpy(memory + large_offset, &large_stale, sizeof(large_stale)); std::memcpy(memory + large_offset + large_size - sizeof(large_stale), &large_stale, sizeof(large_stale)); auto large_allocation = cache.ObtainBuffer(base + large_offset, large_size, true, false); Require(name, "near-capacity dirty allocation", large_allocation.first != nullptr, "failed to allocate the near-capacity dirty native buffer"); cache.FillBuffer(base + large_offset, large_size, large_value, false); const auto large_submission_tick = scheduler.CurrentTick(); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } // Earlier dirty owners in this GC pass may require one ring-wrap drain // before the full-capacity download can reserve the stream. Require(name, "near-capacity synchronized retirement", !cache.IsRegionRegistered(base + large_offset, large_size) && scheduler.CurrentTick() > large_submission_tick && scheduler.CurrentTick() <= large_submission_tick + 2, "capacity-sized dirty Buffer exceeded one ring-wrap drain"); uint32_t large_before_completion = 0; Libs::LibKernel::Memory::TryReadBacking(base + large_offset, &large_before_completion, sizeof(large_before_completion)); Require(name, "near-capacity backing publication", large_before_completion == large_value, "near-capacity Buffer GC retired ownership before publication"); const auto large_image_source = cache.ObtainBufferForImage(base + large_offset, sizeof(large_value)); Require(name, "fixed download after Buffer retirement", large_image_source.first != nullptr && &BufferCacheTestAccess::DownloadBuffer(cache) == fixed_download && fixed_download->Handle() == fixed_download_handle && fixed_download->Size() == (32ull << 20), "image acquisition replaced the shared Buffer download stream"); std::vector large_published(large_size / sizeof(uint32_t)); Require(name, "near-capacity Buffer publication contents", Libs::LibKernel::Memory::TryReadBacking( base + large_offset, large_published.data(), large_size) && std::ranges::all_of(large_published, [](uint32_t value) { return value == large_value; }), "near-capacity Buffer GC did not publish its complete transfer"); constexpr uint64_t grouped_first_offset = 0x10000; constexpr uint64_t grouped_second_offset = 0x1200000; constexpr uint64_t grouped_owner_size = 17ull * 1024 * 1024; constexpr uint32_t grouped_first_value = 0x1122aabbu; constexpr uint32_t grouped_second_value = 0x3344ccddu; constexpr uint32_t grouped_stale = 0; static_assert(grouped_owner_size < (32ull << 20) && grouped_owner_size * 2 > (32ull << 20)); Libs::LibKernel::Memory::WriteBacking( base + grouped_first_offset, &grouped_stale, sizeof(grouped_stale)); Libs::LibKernel::Memory::WriteBacking( base + grouped_second_offset, &grouped_stale, sizeof(grouped_stale)); auto grouped_first = cache.ObtainBuffer(base + grouped_first_offset, grouped_owner_size, true, false); auto grouped_second = cache.ObtainBuffer(base + grouped_second_offset, grouped_owner_size, true, false); Require(name, "per-owner GC allocation", grouped_first.first != nullptr && grouped_second.first != nullptr && grouped_first.first != grouped_second.first, "disjoint GC candidates merged into one Buffer owner"); cache.FillBuffer(base + grouped_first_offset, grouped_owner_size, grouped_first_value, false); cache.FillBuffer(base + grouped_second_offset, grouped_owner_size, grouped_second_value, false); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } Require(name, "per-owner fixed-ring retirement", !cache.IsRegionRegistered(base + grouped_first_offset, grouped_owner_size) && !cache.IsRegionRegistered(base + grouped_second_offset, grouped_owner_size) && &BufferCacheTestAccess::DownloadBuffer(cache) == fixed_download && fixed_download->Handle() == fixed_download_handle, "GC aggregated disjoint retirees or replaced the download ring"); uint32_t grouped_first_backing = 0; uint32_t grouped_second_backing = 0; Libs::LibKernel::Memory::TryReadBacking(base + grouped_first_offset, &grouped_first_backing, sizeof(grouped_first_backing)); Libs::LibKernel::Memory::TryReadBacking(base + grouped_second_offset, &grouped_second_backing, sizeof(grouped_second_backing)); Require(name, "per-owner fixed-ring contents", grouped_first_backing == grouped_first_value && grouped_second_backing == grouped_second_value, "per-owner GC transfers lost data while wrapping the fixed ring"); constexpr uint64_t sparse_offset = 0x2180000; constexpr uint64_t sparse_owner_stride = 2 * BufferCache::CACHING_PAGESIZE; constexpr std::array sparse_starts{1, 7, 13}; constexpr std::array sparse_sizes{3, 2, 1}; constexpr std::array sparse_values{0x41372b19u, 0x957f6953u}; std::array sparse_clean{}; sparse_clean.fill(0xcdu); const auto FillSparseOwner = [&](uint32_t owner_index) { const auto address = base + sparse_offset + owner_index * sparse_owner_stride; Libs::LibKernel::Memory::WriteBacking(address, sparse_clean.data(), sparse_clean.size()); Libs::Graphics::Buffer *owner = nullptr; for (size_t index = 0; index < sparse_starts.size(); ++index) { const auto allocation = cache.ObtainBuffer( address + sparse_starts[index], sparse_sizes[index], true, false); Require(name, "sparse byte-range owner", allocation.first != nullptr && (owner == nullptr || owner == allocation.first), "disjoint byte ranges did not resolve to one cached owner"); owner = allocation.first; } // Deliberately change native clean neighbors too: only the exact dirty // bytes may be published to guest memory. owner->Fill(owner->Offset(address), sparse_clean.size(), sparse_values[owner_index]); }; const auto CheckSparseOwner = [&](uint32_t owner_index) { const auto address = base + sparse_offset + owner_index * sparse_owner_stride; auto expected = sparse_clean; std::array value_bytes{}; std::memcpy(value_bytes.data(), &sparse_values[owner_index], value_bytes.size()); for (size_t index = 0; index < sparse_starts.size(); ++index) { for (uint64_t byte = sparse_starts[index]; byte < sparse_starts[index] + sparse_sizes[index]; ++byte) { expected[byte] = value_bytes[byte % value_bytes.size()]; } } std::array published{}; Require(name, "sparse byte-range publication", Libs::LibKernel::Memory::TryReadBacking( address, published.data(), published.size()) && published == expected && !cache.HasGpuDirtyBytes(address, published.size()) && !cache.IsRegionGpuModified(address, published.size()), "odd-byte readback lost dirty bytes or overwrote clean neighbors"); }; FillSparseOwner(0); bool sparse_callback_ran = false; bool sparse_callback_clean = false; scheduler.DeferOperation([&] { sparse_callback_clean = !cache.HasGpuDirtyBytes(base + sparse_offset, sparse_clean.size()) && !cache.IsRegionGpuModified(base + sparse_offset, sparse_clean.size()); sparse_callback_ran = true; }); const auto sparse_read_tick = scheduler.CurrentTick(); cache.ReadMemory(base + sparse_offset + sparse_starts[0], sparse_sizes[0]); Require(name, "sparse byte-range submission", scheduler.CurrentTick() == sparse_read_tick + 1, "sparse source ranges were downloaded in separate submissions"); scheduler.PopPendingOperations(); Require(name, "readback callback ownership consistency", sparse_callback_ran && sparse_callback_clean, "readback drained a deferred callback before clearing page ownership"); CheckSparseOwner(0); FillSparseOwner(0); FillSparseOwner(1); Require(name, "sparse disjoint GC owners", BufferCacheTestAccess::PageOwner(cache, base + sparse_offset) != BufferCacheTestAccess::PageOwner( cache, base + sparse_offset + sparse_owner_stride), "sparse GC fixtures merged into one source owner"); const auto sparse_gc_tick = scheduler.CurrentTick(); for (uint32_t tick = 0; tick <= 160; ++tick) { cache.RunGarbageCollector(); } Require(name, "sparse multi-owner GC submission", scheduler.CurrentTick() == sparse_gc_tick + 1 && !cache.IsRegionRegistered(base + sparse_offset, sparse_owner_stride + 16), "sparse GC owners did not share one completion drain"); CheckSparseOwner(0); CheckSparseOwner(1); constexpr uint64_t disjoint_owner_offset = 0x2140000; constexpr uint64_t disjoint_owner_size = 0x8000; constexpr uint64_t disjoint_dirty_offset = disjoint_owner_offset + 0x100; constexpr uint64_t disjoint_new_owner_offset = disjoint_owner_offset + 0x5000; constexpr uint32_t disjoint_value = 0x89abcdefu; constexpr uint32_t disjoint_stale = 0x76543210u; std::memcpy(memory + disjoint_dirty_offset, &disjoint_stale, sizeof(disjoint_stale)); auto wide_owner = cache.ObtainBuffer(base + disjoint_owner_offset, disjoint_owner_size, false); Require(name, "disjoint retirement owner", wide_owner.first != nullptr, "failed to create the clean cross-page retirement owner"); (void)cache.ObtainBuffer(base + disjoint_dirty_offset, sizeof(disjoint_value), true, false); cache.FillBuffer(base + disjoint_dirty_offset, sizeof(disjoint_value), disjoint_value, false); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } Require(name, "disjoint synchronized retirement", !cache.IsRegionRegistered(base + disjoint_owner_offset, disjoint_owner_size), "cross-page owner survived pressured collection"); uint32_t disjoint_before_unmap = 0; Libs::LibKernel::Memory::TryReadBacking(base + disjoint_dirty_offset, &disjoint_before_unmap, sizeof(disjoint_before_unmap)); Require( name, "disjoint retirement publication", disjoint_before_unmap == disjoint_value, "whole-owner Buffer retired before publishing its dirty bytes"); resources.UnmapMemory(base + disjoint_owner_offset + 0x4000, 0x4000); uint32_t disjoint_after_unmap = 0; Libs::LibKernel::Memory::TryReadBacking(base + disjoint_dirty_offset, &disjoint_after_unmap, sizeof(disjoint_after_unmap)); Require(name, "disjoint synchronized invalidation", disjoint_after_unmap == disjoint_value && !cache.HasGpuDirtyBytes(base + disjoint_dirty_offset, sizeof(disjoint_value)), "disjoint invalidation lost the completed whole-owner publication"); resources.MapMemory(base + disjoint_owner_offset + 0x4000, 0x4000); auto disjoint_new_owner = cache.ObtainBuffer(base + disjoint_new_owner_offset, sizeof(disjoint_value), true, false); Require(name, "disjoint post-publication reacquire", disjoint_new_owner.first != nullptr, "disjoint acquisition failed after whole-owner publication"); uint32_t disjoint_backing = 0; Libs::LibKernel::Memory::TryReadBacking(base + disjoint_dirty_offset, &disjoint_backing, sizeof(disjoint_backing)); Require(name, "disjoint retirement contents", disjoint_backing == disjoint_value, "old retirement callback lost dirty bytes or retained ownership " "after a disjoint reacquire"); cache.ReadMemory(base + disjoint_new_owner_offset, sizeof(disjoint_value)); constexpr uint64_t reacquire_owner_offset = 0x2200000; constexpr uint64_t reacquire_owner_size = 0x8000; constexpr uint64_t reacquire_dirty_offset = reacquire_owner_offset + 0x100; constexpr uint64_t reacquire_disjoint_offset = reacquire_owner_offset + 0x5000; constexpr uint32_t reacquire_value = 0xc001d00du; constexpr uint32_t reacquire_stale = 0x0badf00du; std::memcpy(memory + reacquire_dirty_offset, &reacquire_stale, sizeof(reacquire_stale)); auto reacquire_owner = cache.ObtainBuffer(base + reacquire_owner_offset, reacquire_owner_size, false); Require(name, "reacquire retirement owner", reacquire_owner.first != nullptr, "failed to create a fresh cross-page retirement owner"); (void)cache.ObtainBuffer(base + reacquire_dirty_offset, sizeof(reacquire_value), true, false); cache.FillBuffer(base + reacquire_dirty_offset, sizeof(reacquire_value), reacquire_value, false); for (uint32_t tick = 0; tick <= 160; tick++) { cache.RunGarbageCollector(); } Require(name, "reacquire synchronized retirement", !cache.IsRegionRegistered(base + reacquire_owner_offset, reacquire_owner_size), "fresh cross-page owner survived pressured collection"); uint32_t reacquire_before = 0; Libs::LibKernel::Memory::TryReadBacking(base + reacquire_dirty_offset, &reacquire_before, sizeof(reacquire_before)); Require(name, "reacquire publication completed", reacquire_before == reacquire_value, "fresh whole-owner ownership retired before publication"); auto reacquired = cache.ObtainBuffer(base + reacquire_disjoint_offset, sizeof(reacquire_value), true, false); Require(name, "independent disjoint post-publication reacquire", reacquired.first != nullptr, "clean disjoint-half acquisition failed after publication"); uint32_t reacquire_after = 0; Libs::LibKernel::Memory::TryReadBacking(base + reacquire_dirty_offset, &reacquire_after, sizeof(reacquire_after)); Require(name, "independent disjoint reacquire contents", reacquire_after == reacquire_value && !cache.HasGpuDirtyBytes(base + reacquire_dirty_offset, sizeof(reacquire_value)), "disjoint-half acquisition failed to publish the retired " "owner's dirty prefix"); cache.ReadMemory(base + reacquire_disjoint_offset, sizeof(reacquire_value)); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } context.ShutdownGpu(); Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "dirty-GC direct-memory mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "dirty-GC direct-memory allocation release failed"); std::printf("[host] %-32s ok\n", name); } void CheckComputeMetaClearClassification() { constexpr const char *name = "ComputeMetaClearClassification"; constexpr uint64_t read_only_meta = 0x0000000204201f00ull; constexpr uint64_t read_write_meta = 0x0000000204202000ull; constexpr uint64_t write_only_meta = 0x0000000204202100ull; EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto &texture_cache = context.GetTextureCache(); TextureCacheTestAccess::RegisterHtileMeta(texture_cache, read_only_meta); TextureCacheTestAccess::RegisterHtileMeta(texture_cache, read_write_meta); TextureCacheTestAccess::RegisterHtileMeta(texture_cache, write_only_meta); // An empty dispatch must not inspect even an unreadable shader address. shaders.SetCsShader({.data_addr = 1}); constexpr std::array, 3> empty_dispatches{{ {0, 1, 1}, {1, 0, 1}, {1, 1, 0}}}; for (const auto mode : {0x41u, 0x61u}) { for (const auto &groups : empty_dispatches) { context.GetRenderExecutor().DispatchDirect( 0, scheduler.Current(), groups[0], groups[1], groups[2], mode); } } Require(name, "HTile fixture", texture_cache.IsMeta(read_only_meta) && texture_cache.IsMeta(read_write_meta) && texture_cache.IsMeta(write_only_meta) && !texture_cache.IsMetaCleared(read_only_meta, 0) && !texture_cache.IsMetaCleared(read_write_meta, 0) && !texture_cache.IsMetaCleared(write_only_meta, 0), "test HTile entries were not registered in their initial state"); const auto MakeInput = [](uint64_t address, bool read, bool written, ShaderRecompiler::IR::CompiledShaderInfo &program) { program.stage = ShaderType::Compute; ShaderRecompiler::IR::BufferResource resource{}; resource.read = read; resource.written = written; resource.formatted = true; program.info.buffers.push_back(resource); ShaderBufferResource descriptor{}; descriptor.UpdateAddress48(address); descriptor.fields[1] |= 16u << 16u; descriptor.fields[2] = 8; descriptor.fields[3] = DstSel(4, 5, 6, 7) | (static_cast(Prospero::BufferFormat::k32_32_32_32UInt) << 12u); ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 4; std::copy_n(descriptor.fields, value.dword_count, value.dwords.begin()); ShaderComputeInputInfo input{}; input.stage.program = &program; input.stage.resources.buffers.push_back(value); return input; }; auto &executor = context.GetRenderExecutor(); auto &command = scheduler.Current(); ShaderRecompiler::IR::CompiledShaderInfo read_only_program{}; const auto read_only_input = MakeInput(read_only_meta, true, false, read_only_program); const bool read_only_consumed = RenderExecutorTestAccess::TryConsumeComputeMetaClear( executor, read_only_input, command); Require(name, "read-only dispatch preserved", !read_only_consumed && !texture_cache.IsMetaCleared(read_only_meta, 0), "a metadata read-only dispatch changed logical clear state"); ShaderRecompiler::IR::CompiledShaderInfo read_write_program{}; const auto read_write_input = MakeInput(read_write_meta, true, true, read_write_program); const bool read_write_consumed = RenderExecutorTestAccess::TryConsumeComputeMetaClear( executor, read_write_input, command); Require(name, "read/write dispatch preserved", !read_write_consumed && !texture_cache.IsMetaCleared(read_write_meta, 0), "a metadata read/modify/write dispatch was replaced by a clear"); ShaderRecompiler::IR::CompiledShaderInfo write_only_program{}; const auto write_only_input = MakeInput(write_only_meta, false, true, write_only_program); const bool write_only_consumed = RenderExecutorTestAccess::TryConsumeComputeMetaClear( executor, write_only_input, command); Require(name, "write-only fill consumed", write_only_consumed && texture_cache.IsMetaCleared(write_only_meta, 0), "a metadata write-only fill was not consumed as a clear"); scheduler.Finish(); std::printf("[host] %-32s ok\n", name); } void CheckUnifiedTextureCacheFlow() { constexpr const char *name = "UnifiedTextureCacheFlow"; constexpr uintptr_t base = 0x0000000200600000ull; constexpr uint64_t allocation_size = 0x2800000; constexpr uint64_t allocation_alignment = 0x200000; EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); context.InitializeGpu(nullptr); auto &gpu = context.GetGpu(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "cache test direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "fixed cache direct-memory mapping failed"); auto *memory = static_cast(mapped); const uint32_t initial = 0x44332211u; std::memcpy(memory, &initial, sizeof(initial)); { auto &resources = context; LibKernel::Memory::InstallGpuResources(&resources); auto &texture_cache = resources.GetTextureCache(); const auto [narrow_download, narrow_download_offset] = TextureCacheTestAccess::MapDownload(texture_cache, 4, 4); const auto [wide_download, wide_download_offset] = TextureCacheTestAccess::MapDownload(texture_cache, 1, 16); Require(name, "image download alignment", narrow_download != nullptr && narrow_download_offset % 4 == 0 && wide_download != nullptr && wide_download_offset % 16 == 0, "wide/block image readback was not aligned to its texel block"); resources.MapMemory(base, allocation_size); ImageDesc sampled{}; sampled.type = BindingType::Texture; sampled.info.data = {base, sizeof(initial)}; sampled.info.pixel_format = vk::Format::eR8G8B8A8Srgb; sampled.info.guest_format = Prospero::BufferFormat::k8_8_8_8Srgb; sampled.info.type = Prospero::ImageType::kColor2D; sampled.info.extent = {1, 1, 1}; sampled.info.resources = {1, 1}; sampled.info.pitch = 1; sampled.info.bytes_per_block = 4; sampled.info.samples = 1; sampled.info.tile_mode = Prospero::TileMode::kLinear; sampled.info.mip_layout[0] = {0, 4, 1, 1}; sampled.view_info.format = sampled.info.pixel_format; sampled.view_info.type = vk::ImageViewType::e2D; sampled.view_info.aspect = vk::ImageAspectFlagBits::eColor; sampled.view_info.usage = vk::ImageUsageFlagBits::eSampled; auto &command = scheduler.Current(); auto first_desc = sampled; const auto first = texture_cache.FindImage(first_desc); auto repeated_desc = sampled; const auto repeated = texture_cache.FindImage(repeated_desc); auto compatible_desc = sampled; compatible_desc.info.pixel_format = vk::Format::eR8G8B8A8Uint; compatible_desc.info.guest_format = Prospero::BufferFormat::k8_8_8_8UInt; compatible_desc.view_info.format = compatible_desc.info.pixel_format; const auto compatible = texture_cache.FindImage(compatible_desc); Require(name, "normalized FindImage", first && repeated == first && compatible == first && texture_cache.GetImage(first).info.pixel_format == vk::Format::eR8G8B8A8Srgb, "registered compatible backing did not reuse one ImageId"); const auto IsOnlyImage = [](const std::vector &ids, ImageId expected) { return ids.size() == 1 && ids.front() == expected; }; const auto exact_byte_miss = TextureCacheTestAccess::FindImages(texture_cache, base + 8, 1, false); const auto touched_page_hit = TextureCacheTestAccess::FindImages(texture_cache, base + 8, 1, true); const auto next_page_miss = TextureCacheTestAccess::FindImages( texture_cache, base + 0x1000, 1, true); Require(name, "exact and 4-KiB page filtering", exact_byte_miss.empty() && IsOnlyImage(touched_page_hit, first) && next_page_miss.empty(), "coarse candidates did not preserve exact-byte and touched-page " "semantics"); const auto MakeOwnershipInfo = [](uint64_t address, uint64_t size) { ImageInfo info{}; info.data = {address, size}; info.extent = {1, 1, 1}; info.resources = {1, 1}; info.samples = 1; return info; }; const auto spanning_info = MakeOwnershipInfo(base + 0x1ff000, 0x2000); const auto spanning = TextureCacheTestAccess::InsertImage(texture_cache, spanning_info); const auto spanning_results = TextureCacheTestAccess::FindImages( texture_cache, spanning_info.data.address, spanning_info.data.size, false); Require(name, "one-MiB cross-page deduplication", spanning && IsOnlyImage(spanning_results, spanning) && TextureCacheTestAccess::PageOwnerCount( texture_cache, spanning_info.data.address) == 1 && TextureCacheTestAccess::PageOwnerCount( texture_cache, spanning_info.data.End() - 1) == 1, "an image spanning two coarse pages was missing or returned more " "than once"); TextureCacheTestAccess::DeleteImage(texture_cache, spanning); Require(name, "cross-page owner cleanup", TextureCacheTestAccess::PageOwnerCount( texture_cache, spanning_info.data.address) == 0 && TextureCacheTestAccess::PageOwnerCount( texture_cache, spanning_info.data.End() - 1) == 0 && TextureCacheTestAccess::FindImages( texture_cache, spanning_info.data.address, spanning_info.data.size, false) .empty(), "cross-page unregister left a stale coarse-page membership"); const auto shared_info = MakeOwnershipInfo(base + 0x500100, 0x100); const auto shared_first = TextureCacheTestAccess::InsertImage(texture_cache, shared_info); const auto shared_second = TextureCacheTestAccess::InsertImage(texture_cache, shared_info); const auto shared_results = TextureCacheTestAccess::FindImages( texture_cache, shared_info.data.address, shared_info.data.size, false); Require(name, "shared coarse-page registration", shared_results.size() == 2 && std::find(shared_results.begin(), shared_results.end(), shared_first) != shared_results.end() && std::find(shared_results.begin(), shared_results.end(), shared_second) != shared_results.end(), "two registered owners were not retained in one coarse page"); TextureCacheTestAccess::DeleteImage(texture_cache, shared_first); const auto shared_survivor = TextureCacheTestAccess::FindImages( texture_cache, shared_info.data.address, shared_info.data.size, false); Require(name, "shared coarse-page unregister", IsOnlyImage(shared_survivor, shared_second) && TextureCacheTestAccess::PageOwnerCount( texture_cache, shared_info.data.address) == 1, "unregistering one owner removed its coarse-page neighbor"); TextureCacheTestAccess::DeleteImage(texture_cache, shared_second); Require(name, "final shared coarse-page unregister", TextureCacheTestAccess::PageOwnerCount( texture_cache, shared_info.data.address) == 0, "the final shared owner remained registered"); constexpr uint64_t large_owner_size = 64ull * 1024 * 1024; const auto large_info = MakeOwnershipInfo(base + 0x800000, large_owner_size); const auto large_owner = TextureCacheTestAccess::InsertImage(texture_cache, large_info); Require(name, "production one-MiB registration granularity", TextureCacheTestAccess::OwnedPageCount( texture_cache, large_info.data.address, large_info.data.size, large_owner) == 64, "64 MiB image registration did not create exactly 64 coarse " "memberships"); TextureCacheTestAccess::DeleteImage(texture_cache, large_owner); Require(name, "production one-MiB unregister granularity", TextureCacheTestAccess::OwnedPageCount( texture_cache, large_info.data.address, large_info.data.size, large_owner) == 0, "large image unregister left coarse memberships behind"); const ImageId stale{first.index, first.generation + 1}; TextureCacheTestAccess::AddPageOwner(texture_cache, base, stale); const auto stale_filtered = TextureCacheTestAccess::FindImages( texture_cache, base, sizeof(initial), false); Require(name, "stale page owner filtering", IsOnlyImage(stale_filtered, first) && TextureCacheTestAccess::RemovePageOwner(texture_cache, base, stale), "a stale generation escaped the direct page-owner lookup"); TextureCacheTestAccess::SetQueryEpoch(texture_cache, UINT32_MAX); const auto wrap_results = TextureCacheTestAccess::FindImages( texture_cache, base, sizeof(initial), false); Require(name, "page-owner query epoch wrap", IsOnlyImage(wrap_results, first) && TextureCacheTestAccess::QueryEpoch(texture_cache) == 1, "query deduplication failed while wrapping its epoch"); auto &image = texture_cache.GetImage(first); constexpr uint32_t final_sampled_value = 0x88776655u; Require(name, "sampled write between discovery and acquisition", resources.HandleFault(PageFaultAccess::Write, sampled.info.data.address), "sampled image did not accept a CPU update after discovery"); std::memcpy(memory, &final_sampled_value, sizeof(final_sampled_value)); Require(name, "sampled final-acquisition precondition", image.IsCpuDirty(), "the between-phase CPU write did not dirty the discovered image"); const auto first_view = texture_cache.FindTexture(first, first_desc); const auto repeated_view = texture_cache.FindTexture(repeated, repeated_desc); auto mapped_view_info = sampled.view_info; mapped_view_info.mapping.r = vk::ComponentSwizzle::eB; mapped_view_info.mapping.b = vk::ComponentSwizzle::eR; const auto mapped_view = image.FindView(mapped_view_info); Require( name, "dynamic views", first_view != nullptr && repeated_view == first_view && mapped_view != nullptr && mapped_view != first_view && image.views.size() == 2 && !image.usage.texture && !image.IsGpuModified() && !image.IsCpuDirty() && !TextureCacheTestAccess::PendingDownload(texture_cache, first), "TextureCache::FindTexture did not preserve sampled view " "identity, final refresh, and cache-only ownership"); constexpr uint64_t resource_replacement_offset = 0x700000; const std::array resource_replacement_data{ 0x01020304u, 0x11121314u, 0x21222324u}; std::memcpy(memory + resource_replacement_offset, resource_replacement_data.data(), sizeof(resource_replacement_data)); auto underspecified = sampled; underspecified.info.data = {base + resource_replacement_offset, sizeof(resource_replacement_data)}; underspecified.info.pixel_format = vk::Format::eR32Uint; underspecified.info.guest_format = Prospero::BufferFormat::k32UInt; underspecified.info.extent = {2, 1, 1}; underspecified.info.resources = {1, 1}; underspecified.info.pitch = 2; underspecified.info.mip_layout[0] = {0, 8, 2, 1}; underspecified.info.mip_layout[1] = {8, 4, 1, 1}; underspecified.view_info.format = underspecified.info.pixel_format; underspecified.view_info.level_count = 1; const auto underspecified_id = texture_cache.FindImage(underspecified); (void)texture_cache.FindTexture(underspecified_id, underspecified); texture_cache.MarkGpuWritten(underspecified_id); auto complete = underspecified; complete.info.resources = {2, 1}; complete.view_info.level_count = 2; const auto complete_id = texture_cache.FindImage(complete); const auto complete_owner = TextureCacheTestAccess::Owner(texture_cache, complete_id); Require(name, "insufficient-resource replacement", complete_id && complete_id != underspecified_id && !TextureCacheTestAccess::Contains(texture_cache, underspecified_id) && complete_owner != nullptr && complete_owner->registered && complete_owner->info.resources == complete.info.resources && !complete_owner->IsGpuModified() && complete_owner->IsDefinitelyCpuDirty(), "an exact backing with insufficient resources was expanded " "instead of being discarded and recreated"); ImageInfo chain = sampled.info; chain.data = {0x2394c1000, 0x384000}; chain.extent = {64, 64, 1}; chain.resources = {7, 150}; chain.pitch = 64; chain.tile_mode = Prospero::TileMode::kStandard4KB; chain.mip_layout[0] = {0x2000, 0x258000, 64, 64}; chain.mip_layout[1] = {0x1000, 0x96000, 32, 32}; for (uint32_t level = 2; level < chain.resources.levels; ++level) { chain.mip_layout[level] = {0, 0x96000, 32, 32}; } ImageInfo subresource = chain; subresource.data = {0x2394c3000, 0x4000}; subresource.resources = {1, 1}; subresource.mip_layout = {}; subresource.mip_layout[0] = {0, 0x4000, 64, 64}; const auto mip = subresource.MipOf(chain); Require(name, "overlap resolution", mip == 0 && subresource.SliceOf(chain, mip) == 0 && ImageRangeOverlaps(chain.data, subresource.data) && ImagePageRangesOverlap(chain.data, subresource.data), "captured PS5 atlas mip/slice overlap did not resolve"); auto exact_desc = compatible_desc; const auto exact = texture_cache.FindImage(exact_desc, true); Require(name, "exact-format coexistence", exact && exact != first && TextureCacheTestAccess::Contains(texture_cache, first) && texture_cache.GetImage(first).info.pixel_format == sampled.info.pixel_format && texture_cache.GetImage(exact).info.pixel_format == compatible_desc.info.pixel_format, "exact-format lookup replaced or synchronized its compatible " "cache record"); auto compatible_after_exact_desc = compatible_desc; const auto compatible_after_exact = texture_cache.FindImage(compatible_after_exact_desc); Require(name, "last compatible format winner", compatible_after_exact == exact, "non-exact lookup did not retain the last compatible " "registration"); auto null_desc = sampled; null_desc.info.data = {}; auto null_repeat_desc = null_desc; const auto null_image = texture_cache.FindImage(null_desc); const auto null_repeat = texture_cache.FindImage(null_repeat_desc); Require(name, "null image", null_image && null_repeat == null_image && null_image != exact, "typed null-image lookup was not stable"); constexpr uint64_t extent_alias_offset = 0x2400000; auto narrow_target = sampled; narrow_target.type = BindingType::RenderTarget; narrow_target.info.data = {base + extent_alias_offset, 0x10000}; narrow_target.info.pixel_format = vk::Format::eR16G16B16A16Sfloat; narrow_target.info.guest_format = Prospero::BufferFormat::k16_16_16_16Float; narrow_target.info.extent = {1, 1, 1}; narrow_target.info.pitch = 128; narrow_target.info.bytes_per_block = 8; narrow_target.info.tile_mode = Prospero::TileMode::kRenderTarget; narrow_target.info.mip_layout[0] = {0, 0x10000, 128, 1}; narrow_target.view_info.format = narrow_target.info.pixel_format; narrow_target.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto narrow_target_id = texture_cache.FindImage(narrow_target); const auto narrow_target_view = texture_cache.FindRenderTarget(narrow_target_id, narrow_target); auto wide_target = narrow_target; wide_target.info.extent.width = 9; const auto wide_target_id = texture_cache.FindImage(wide_target); const auto wide_target_view = texture_cache.FindRenderTarget(wide_target_id, wide_target); const auto wide_owner = TextureCacheTestAccess::Owner(texture_cache, wide_target_id); Require(name, "equal-allocation render-target growth", narrow_target_view != nullptr && wide_target_view != nullptr && wide_target_id != narrow_target_id && !TextureCacheTestAccess::Contains(texture_cache, narrow_target_id) && wide_owner != nullptr && wide_owner->info.extent == vk::Extent3D{9, 1, 1} && wide_owner->backing.extent == vk::Extent3D{9, 1, 1}, "a 9x1 render target reused its equal-size 1x1 native backing"); auto narrow_again = narrow_target; const auto narrow_again_id = texture_cache.FindImage(narrow_again); Require(name, "larger render-target backing reuse", narrow_again_id == wide_target_id && texture_cache.GetImage(narrow_again_id).backing.extent == vk::Extent3D{9, 1, 1}, "a smaller render area replaced its compatible larger backing"); auto MakeLinearDesc = [&](uint64_t address, uint64_t size, vk::Format format, Prospero::BufferFormat guest_format, Prospero::ImageType type, vk::Extent3D extent, uint32_t layers, uint32_t bytes_per_block, uint32_t samples) { ImageDesc desc{}; desc.type = BindingType::Texture; desc.info.data = {address, size}; desc.info.pixel_format = format; desc.info.guest_format = guest_format; desc.info.type = type; desc.info.extent = extent; desc.info.resources = {1, layers}; desc.info.pitch = extent.width; desc.info.bytes_per_block = bytes_per_block; desc.info.samples = samples; desc.info.tile_mode = Prospero::TileMode::kLinear; desc.info.mip_layout[0] = {0, size, extent.width, extent.height}; desc.view_info.format = format; if (type == Prospero::ImageType::kColor3D) { desc.view_info.type = vk::ImageViewType::e3D; } else if (layers > 1) { desc.view_info.type = vk::ImageViewType::e2DArray; } else { desc.view_info.type = vk::ImageViewType::e2D; } desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; desc.view_info.layer_count = layers; desc.view_info.usage = vk::ImageUsageFlagBits::eSampled; return desc; }; const auto HostReadBarrier = [&](vk::Buffer buffer, uint64_t size, vk::PipelineStageFlags source_stage, vk::AccessFlags source_access) { vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = source_access; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = buffer; barrier.offset = 0; barrier.size = size; scheduler.Current().Handle().pipelineBarrier( source_stage, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); }; const auto TransferReadBarrier = [&](vk::Buffer buffer, uint64_t size) { vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = buffer; barrier.offset = 0; barrier.size = size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 1, &barrier, 0, nullptr); }; constexpr uint64_t mip_layout_offset = 0x27f0000; std::vector mip_guest(0x2000 / sizeof(u32)); std::iota(mip_guest.begin(), mip_guest.end(), 0x51000000u); ImageId previous{}; for (const uint32_t width : {32u, 64u, 32u}) { if (previous) { Require(name, "mip-layout CPU overwrite", resources.HandleFault(PageFaultAccess::Write, base + mip_layout_offset), "mip-layout guest overwrite did not invalidate the old texture"); } std::memcpy(memory + mip_layout_offset, mip_guest.data(), mip_guest.size() * sizeof(u32)); TileSurfaceLayout surface{}; Require(name, "mip-tail layout", TileGetTiledTextureLayout( {.format = Prospero::BufferFormat::kBc3Srgb, .tile_mode = Prospero::TileMode::kStandard4KB, .width = width, .height = 64, .levels = 7}, surface) && surface.first_tail_level == (width == 32 ? 0u : 1u) && surface.total_size == (width == 32 ? 0x1000u : 0x2000u), "BC3 width change did not move mip zero into or out of the tail"); ImageDesc desc{}; desc.type = BindingType::Texture; desc.info.data = {base + mip_layout_offset, surface.total_size}; desc.info.pixel_format = vk::Format::eBc3SrgbBlock; desc.info.guest_format = Prospero::BufferFormat::kBc3Srgb; desc.info.extent = {width, 64, 1}; desc.info.resources = {7, 1}; desc.info.pitch = 64; desc.info.bytes_per_block = 16; desc.info.tile_mode = Prospero::TileMode::kStandard4KB; desc.view_info.format = desc.info.pixel_format; desc.view_info.type = vk::ImageViewType::e2D; desc.view_info.level_count = 7; desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; desc.view_info.usage = vk::ImageUsageFlagBits::eSampled; std::vector copies; std::vector expected; for (uint32_t level = 0; level < 7; level++) { const auto &layout = surface.mips[level]; desc.info.mip_layout[level] = { layout.offset, layout.size, layout.padded_width, layout.padded_height}; vk::BufferImageCopy copy{}; copy.bufferOffset = expected.size() * sizeof(u32); copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, level, 0, 1}; copy.imageExtent = {std::max(width >> level, 1u), std::max(64u >> level, 1u), 1}; copies.push_back(copy); for (uint32_t y = 0; y < layout.height; y++) { for (uint32_t x = 0; x < layout.width; x++) { uint32_t offset = 0; Require(name, "mip-tail CPU address", TileGetBlockOffset( surface.texture.block, x + layout.tail_x, y + layout.tail_y, 0, offset), "BC3 mip-tail block address could not be computed"); const auto word = (layout.offset + offset) / sizeof(u32); expected.insert(expected.end(), mip_guest.begin() + word, mip_guest.begin() + word + 4); } } } const auto id = texture_cache.FindImage(desc); Require(name, "distinct mip-layout backing", id && id != previous, "different guest mip layouts reused the same native image"); (void)texture_cache.FindTexture(id, desc); auto readback = CreateHostBuffer( name, expected.size() * sizeof(u32), vk::BufferUsageFlagBits::eTransferDst, std::vector(expected.size())); texture_cache.GetImage(id).Download(copies, readback.buffer, 0, readback.size); HostReadBarrier(readback.buffer, readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); Require(name, "mip-layout GPU contents", ReadBuffer(name, readback, expected.size()) == expected, "cached native copying contaminated the requested guest mip layout"); DestroyBuffer(&readback); if (width == 64) { // Leave only the larger backing so shrinking must reject its layout. TextureCacheTestAccess::DeleteImage(texture_cache, previous); } previous = id; } // A formatted Buffer read must use the private // Exercise the cache-native image-copy path. Use a request larger // than the stream shortcut and poison guest backing // after upload so stale CPU staging cannot accidentally // satisfy the content check. constexpr uint64_t mip_prefix_offset = 0x2740000; constexpr auto mip_format = Prospero::BufferFormat::k32UInt; constexpr auto mip_tile = Prospero::TileMode::kLinear; constexpr uint32_t mip_width = 4097; constexpr uint32_t mip_height = 1; constexpr uint32_t mip_levels = 1; const uint32_t mip_pitch = TileGetTexturePitch(mip_format, mip_width, mip_tile); TileSizeAlign mip_total{}; std::array mip_sizes{}; std::array mip_padded{}; TileGetTextureSize(mip_format, mip_width, mip_height, mip_levels, mip_tile, &mip_total, mip_sizes.data(), mip_padded.data()); const uint64_t mip_prefix_size = mip_sizes[0].offset + mip_sizes[0].size; const uint64_t mip_guest_size = mip_total.size + 256; Require(name, "formatted mip fixture", mip_sizes[0].offset == 0 && mip_prefix_size > BufferCache::CACHING_PAGESIZE && mip_prefix_size < mip_guest_size && mip_guest_size % sizeof(uint32_t) == 0, "single-mip fixture did not expose a cache-sized fitting backing " "prefix"); std::vector mip_native(mip_guest_size / sizeof(uint32_t)); std::iota(mip_native.begin(), mip_native.end(), 0x61000000u); std::memcpy(memory + mip_prefix_offset, mip_native.data(), mip_guest_size); auto mip_desc = MakeLinearDesc( base + mip_prefix_offset, mip_guest_size, vk::Format::eR32Uint, mip_format, Prospero::ImageType::kColor2D, {mip_width, mip_height, 1}, 1, sizeof(uint32_t), 1); mip_desc.info.resources.levels = mip_levels; mip_desc.info.pitch = mip_pitch; mip_desc.view_info.level_count = mip_levels; for (uint32_t level = 0; level < mip_levels; level++) { mip_desc.info.mip_layout[level] = { mip_sizes[level].offset, mip_sizes[level].size, mip_padded[level].width, mip_padded[level].height}; } const auto mip_image = texture_cache.FindImage(mip_desc); (void)texture_cache.FindTexture(mip_image, mip_desc); texture_cache.MarkGpuWritten(mip_image); std::vector mip_stale(mip_native.size(), 0xdeadbeefu); Libs::LibKernel::Memory::WriteBacking(base + mip_prefix_offset, mip_stale.data(), mip_guest_size); Libs::Graphics::Buffer mip_insufficient( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, mip_desc.info.data.address, AllFlags, mip_prefix_size - 1); Libs::Graphics::Buffer mip_prefix( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, mip_desc.info.data.address, AllFlags, mip_prefix_size); Require(name, "formatted mip containment", !BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), mip_insufficient, mip_desc.info.data.address, mip_prefix_size - 1) && BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), mip_prefix, mip_desc.info.data.address, mip_prefix_size) && texture_cache.GetImage(mip_image).IsGpuModified(), "image synchronization accepted a partial first " "mip, rejected a fitting mip " "prefix, or transferred ownership"); auto &buffer_cache = resources.GetBufferCache(); std::pair mip_formatted; bool formatted_owner_was_absent = false; gpu.SendCommandSync([&] { formatted_owner_was_absent = !buffer_cache.IsRegionRegistered( mip_desc.info.data.address, mip_desc.info.data.size); mip_formatted = buffer_cache.ObtainBuffer( mip_desc.info.data.address, mip_desc.info.data.size, false, true); }); Require(name, "formatted cache fixture", formatted_owner_was_absent, "formatted image already had a cached Buffer owner"); Require(name, "formatted Buffer path", mip_formatted.first != nullptr && texture_cache.GetImage(mip_image).IsGpuModified(), "formatted read bypassed the cached image-copy " "path or transferred ownership"); auto mip_prefix_readback = CreateHostBuffer( name, mip_prefix_size, vk::BufferUsageFlagBits::eTransferDst, std::vector(mip_prefix_size / sizeof(uint32_t), 0)); auto mip_formatted_readback = CreateHostBuffer( name, mip_guest_size, vk::BufferUsageFlagBits::eTransferDst, std::vector(mip_guest_size / sizeof(uint32_t), 0)); TransferReadBarrier(mip_prefix.Handle(), mip_prefix_size); const vk::BufferCopy mip_prefix_copy{0, 0, mip_prefix_size}; command.Handle().copyBuffer( mip_prefix.Handle(), mip_prefix_readback.buffer, 1, &mip_prefix_copy); TransferReadBarrier(mip_formatted.first->Handle(), mip_guest_size); const vk::BufferCopy mip_formatted_copy{mip_formatted.second, 0, mip_guest_size}; command.Handle().copyBuffer(mip_formatted.first->Handle(), mip_formatted_readback.buffer, 1, &mip_formatted_copy); HostReadBarrier(mip_prefix_readback.buffer, mip_prefix_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); HostReadBarrier(mip_formatted_readback.buffer, mip_formatted_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); const std::array volume_values{0x10203040u, 0x50607080u}; TileSizeAlign volume_size{}; TileGetTextureTotalSize(Prospero::BufferFormat::k32UInt, 1, 1, 2, 1, Prospero::TileMode::kLinear, true, volume_size); const auto volume_pitch = TileGetTexturePitch( Prospero::BufferFormat::k32UInt, 1, Prospero::TileMode::kLinear); const auto volume_slice_words = volume_size.size / 2 / sizeof(uint32_t); std::vector volume_guest(volume_size.size / sizeof(uint32_t)); volume_guest[0] = volume_values[0]; volume_guest[volume_slice_words] = volume_values[1]; std::memcpy(memory + 0x1000, volume_guest.data(), volume_size.size); auto array_desc = MakeLinearDesc(base + 0x1000, volume_size.size, vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 2, 4, 1); array_desc.info.pitch = volume_pitch; array_desc.info.mip_layout[0].pitch = volume_pitch; const auto array_image = texture_cache.FindImage(array_desc); (void)texture_cache.FindTexture(array_image, array_desc); texture_cache.MarkGpuWritten(array_image); auto volume_desc = array_desc; volume_desc.info.type = Prospero::ImageType::kColor3D; volume_desc.info.extent = {1, 1, 2}; volume_desc.info.resources.layers = 1; volume_desc.view_info.type = vk::ImageViewType::e3D; volume_desc.view_info.layer_count = 1; const auto volume_image = texture_cache.FindImage(volume_desc); Require(name, "2D-array/3D overlap copy", volume_image && volume_image != array_image && texture_cache.GetImage(volume_image).backing.image_type == vk::ImageType::e3D && texture_cache.GetImage(volume_image).IsGpuModified(), "array-to-volume expansion lost slices or GPU " "ownership"); constexpr uint64_t unique_volume_offset = 0x27b0000; std::memcpy(memory + unique_volume_offset, volume_guest.data(), volume_size.size); auto unique_volume_desc = volume_desc; unique_volume_desc.info.data.address = base + unique_volume_offset; const auto unique_volume_image = texture_cache.FindImage(unique_volume_desc); (void)texture_cache.FindTexture(unique_volume_image, unique_volume_desc); texture_cache.MarkGpuWritten(unique_volume_image); Libs::Graphics::Buffer partial_volume( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, unique_volume_desc.info.data.address, AllFlags, unique_volume_desc.info.data.size - sizeof(uint32_t)); Libs::Graphics::Buffer full_volume( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, unique_volume_desc.info.data.address, AllFlags, unique_volume_desc.info.data.size); Require(name, "formatted volume containment", !BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), partial_volume, unique_volume_desc.info.data.address, unique_volume_desc.info.data.size - sizeof(uint32_t)) && BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), full_volume, unique_volume_desc.info.data.address, unique_volume_desc.info.data.size) && texture_cache.GetImage(unique_volume_image).IsGpuModified(), "partial 3D synchronization was accepted, " "full-volume synchronization was " "rejected, or ownership changed"); auto volume_readback = CreateHostBuffer( name, volume_size.size, vk::BufferUsageFlagBits::eTransferDst, std::vector(volume_guest.size(), 0)); TransferReadBarrier(full_volume.Handle(), volume_size.size); const vk::BufferCopy volume_copy{0, 0, volume_size.size}; command.Handle().copyBuffer(full_volume.Handle(), volume_readback.buffer, 1, &volume_copy); HostReadBarrier(volume_readback.buffer, volume_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); constexpr uint64_t depth_containment_offset = 0x27c0000; constexpr std::array depth_containment_values{0.25f, 0.75f, 0.5f}; std::memcpy(memory + depth_containment_offset, depth_containment_values.data(), sizeof(depth_containment_values)); auto depth_containment = MakeLinearDesc( base + depth_containment_offset, sizeof(depth_containment_values), vk::Format::eD32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {2, 1, 1}, 1, sizeof(float), 1); depth_containment.type = BindingType::DepthTarget; depth_containment.info.resources.levels = 2; depth_containment.info.mip_layout[0] = {0, 2 * sizeof(float), 2, 1}; depth_containment.info.mip_layout[1] = {2 * sizeof(float), sizeof(float), 1, 1}; depth_containment.view_info.format = vk::Format::eD32Sfloat; depth_containment.view_info.aspect = vk::ImageAspectFlagBits::eDepth; depth_containment.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; depth_containment.view_info.level_count = 2; const auto depth_containment_image = texture_cache.FindImage(depth_containment); (void)texture_cache.FindDepthTarget(depth_containment_image, depth_containment); texture_cache.MarkGpuWritten(depth_containment_image); Libs::Graphics::Buffer partial_depth( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, depth_containment.info.data.address, AllFlags, depth_containment.info.mip_layout[0].size); Require( name, "formatted depth containment", !BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), partial_depth, depth_containment.info.data.address, depth_containment.info.mip_layout[0].size) && texture_cache.GetImage(depth_containment_image).IsGpuModified(), "partial depth synchronization was accepted or " "transferred ownership"); constexpr uint64_t raw_d16_offset = 0x27d0000; constexpr std::array raw_d16_values{0x2000u, 0xc000u}; std::memcpy(memory + raw_d16_offset, raw_d16_values.data(), sizeof(raw_d16_values)); auto raw_d16 = MakeLinearDesc( base + raw_d16_offset, sizeof(raw_d16_values), vk::Format::eD16Unorm, Prospero::BufferFormat::k16UNorm, Prospero::ImageType::kColor2D, {2, 1, 1}, 1, sizeof(uint16_t), 1); raw_d16.type = BindingType::DepthTarget; raw_d16.view_info.format = vk::Format::eD16Unorm; raw_d16.view_info.aspect = vk::ImageAspectFlagBits::eDepth; raw_d16.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto raw_d16_image = texture_cache.FindImage(raw_d16); (void)texture_cache.FindDepthTarget(raw_d16_image, raw_d16); auto raw_d16_uint = raw_d16; raw_d16_uint.type = BindingType::Texture; raw_d16_uint.info.pixel_format = vk::Format::eR16Uint; raw_d16_uint.info.guest_format = Prospero::BufferFormat::k16UInt; raw_d16_uint.view_info.format = vk::Format::eR16Uint; raw_d16_uint.view_info.aspect = vk::ImageAspectFlagBits::eColor; raw_d16_uint.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto raw_d16_uint_image = texture_cache.FindImage(raw_d16_uint); Require( name, "raw D16 uint texture backing", raw_d16_uint_image && raw_d16_uint_image != raw_d16_image && !TextureCacheTestAccess::Contains(texture_cache, raw_d16_image) && texture_cache.GetImage(raw_d16_uint_image).backing.format == vk::Format::eR16Uint, "D16 uint sampling did not recreate a true integer color backing"); Require(name, "raw D16 uint texture view", texture_cache.FindTexture(raw_d16_uint_image, raw_d16_uint) != nullptr, "D16 uint sampling did not create an integer sampled view"); constexpr uint64_t layered_raw_d16_offset = raw_d16_offset + 0x1000; constexpr std::array layered_raw_d16_values{ 0x0102u, 0x0304u, 0x1112u, 0x1314u, 0x2122u, 0x2324u, 0x3132u, 0x3334u, 0x4142u, 0x4344u, 0x5152u, 0x5354u}; std::memcpy(memory + layered_raw_d16_offset, layered_raw_d16_values.data(), sizeof(layered_raw_d16_values)); auto layered_d16_color = MakeLinearDesc( base + layered_raw_d16_offset, sizeof(layered_raw_d16_values), vk::Format::eR16Unorm, Prospero::BufferFormat::k16UNorm, Prospero::ImageType::kColor2D, {2, 1, 1}, 6, sizeof(uint16_t), 1); const auto layered_d16_color_image = texture_cache.FindImage(layered_d16_color); (void)texture_cache.FindTexture(layered_d16_color_image, layered_d16_color); auto &layered_d16_color_native = texture_cache.GetImage(layered_d16_color_image); layered_d16_color_native.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); vk::ClearColorValue layered_raw_d16_clear_value{}; layered_raw_d16_clear_value.float32[0] = 1.0f; const vk::ImageSubresourceRange layered_raw_d16_clear_range{ vk::ImageAspectFlagBits::eColor, 0, 1, 0, 6}; scheduler.Current().Handle().clearColorImage( layered_d16_color_native.backing.image, vk::ImageLayout::eTransferDstOptimal, &layered_raw_d16_clear_value, 1, &layered_raw_d16_clear_range); texture_cache.MarkGpuWritten(layered_d16_color_image); auto layered_raw_d16_depth = layered_d16_color; layered_raw_d16_depth.type = BindingType::DepthTarget; layered_raw_d16_depth.info.data.size = sizeof(uint16_t) * 2 * 5; layered_raw_d16_depth.info.pixel_format = vk::Format::eD16Unorm; layered_raw_d16_depth.info.guest_format = Prospero::BufferFormat::k16UNorm; layered_raw_d16_depth.info.resources.layers = 5; layered_raw_d16_depth.info.mip_layout[0].size = layered_raw_d16_depth.info.data.size; layered_raw_d16_depth.view_info.format = vk::Format::eD16Unorm; layered_raw_d16_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth; layered_raw_d16_depth.view_info.layer_count = 5; layered_raw_d16_depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto layered_raw_d16_depth_image = texture_cache.FindImage(layered_raw_d16_depth); const auto &layered_depth_owner = texture_cache.GetImage(layered_raw_d16_depth_image); Require(name, "layered raw D16 owner layout", layered_raw_d16_depth_image != layered_d16_color_image && !TextureCacheTestAccess::Contains(texture_cache, layered_d16_color_image) && layered_depth_owner.info.resources.layers == 6 && layered_depth_owner.info.data.size == sizeof(layered_raw_d16_values) && layered_depth_owner.info.mip_layout[0].size == sizeof(layered_raw_d16_values), "a partial depth view detached the retained six-layer owner from " "its full guest range"); (void)texture_cache.FindDepthTarget(layered_raw_d16_depth_image, layered_raw_d16_depth); auto reacquired_layered_raw_d16_uint = layered_d16_color; reacquired_layered_raw_d16_uint.info.pixel_format = vk::Format::eR16Uint; reacquired_layered_raw_d16_uint.info.guest_format = Prospero::BufferFormat::k16UInt; reacquired_layered_raw_d16_uint.view_info.format = vk::Format::eR16Uint; const auto reacquired_layered_raw_d16_uint_image = texture_cache.FindImage(reacquired_layered_raw_d16_uint); const auto reacquired_layered_raw_d16_uint_view = texture_cache.FindTexture(reacquired_layered_raw_d16_uint_image, reacquired_layered_raw_d16_uint); auto layered_raw_d16_readback = CreateHostBuffer( name, sizeof(layered_raw_d16_values), vk::BufferUsageFlagBits::eTransferDst, std::vector(6, 0)); auto &reacquired_layered_raw_d16_native = texture_cache.GetImage(reacquired_layered_raw_d16_uint_image); reacquired_layered_raw_d16_native.Transit( vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, scheduler.Current().Handle()); vk::BufferImageCopy layered_raw_d16_copy{}; layered_raw_d16_copy.bufferRowLength = 2; layered_raw_d16_copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 6}; layered_raw_d16_copy.imageExtent = {2, 1, 1}; scheduler.Current().Handle().copyImageToBuffer( reacquired_layered_raw_d16_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, layered_raw_d16_readback.buffer, 1, &layered_raw_d16_copy); HostReadBarrier(layered_raw_d16_readback.buffer, layered_raw_d16_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); const auto layered_raw_d16_words = ReadBuffer(name, layered_raw_d16_readback, 6); std::array layered_raw_d16_after{}; std::memcpy(layered_raw_d16_after.data(), layered_raw_d16_words.data(), sizeof(layered_raw_d16_after)); std::array layered_raw_d16_expected{}; layered_raw_d16_expected.fill(UINT16_MAX); Require( name, "layered raw D16 round trip", reacquired_layered_raw_d16_uint_image && reacquired_layered_raw_d16_uint_image != layered_raw_d16_depth_image && !TextureCacheTestAccess::Contains(texture_cache, layered_raw_d16_depth_image) && reacquired_layered_raw_d16_native.backing.format == vk::Format::eR16Uint && reacquired_layered_raw_d16_uint_view != nullptr && layered_raw_d16_after == layered_raw_d16_expected, "the partial depth view lost a layer or failed to restore raw UINT " "sampling"); DestroyBuffer(&layered_raw_d16_readback); auto native_array_info = array_desc.info; native_array_info.data = {}; auto native_volume_info = volume_desc.info; native_volume_info.data = {}; Libs::Graphics::Image native_array(m_runtime_context, scheduler, native_array_info); Libs::Graphics::Image native_volume(m_runtime_context, scheduler, native_volume_info); native_volume.CopyImage(native_array); native_array.CopyImage(native_volume); Require(name, "Image-owned 2D-array/3D copy state", native_volume.backing.state.layout == vk::ImageLayout::eTransferSrcOptimal && native_volume.backing.state.access_mask == vk::AccessFlagBits2::eTransferRead && native_array.backing.state.layout == vk::ImageLayout::eGeneral && native_array.backing.state.access_mask == (vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead), "Image::CopyImage did not retain pinned " "source/destination states"); ImageInfo resolve_source_info{}; resolve_source_info.pixel_format = vk::Format::eR8G8B8A8Unorm; resolve_source_info.guest_format = Prospero::BufferFormat::k8_8_8_8UNorm; resolve_source_info.type = Prospero::ImageType::kColor2D; resolve_source_info.extent = {4, 4, 1}; resolve_source_info.resources = {1, 1}; resolve_source_info.pitch = 4; resolve_source_info.bytes_per_block = 4; resolve_source_info.samples = 2; resolve_source_info.tile_mode = Prospero::TileMode::kRenderTarget; ImageInfo resolve_destination_info = resolve_source_info; resolve_destination_info.type = Prospero::ImageType::kColor2D; resolve_destination_info.samples = 1; Libs::Graphics::Image resolve_source(m_runtime_context, scheduler, resolve_source_info); Libs::Graphics::Image resolve_destination(m_runtime_context, scheduler, resolve_destination_info); resolve_destination.Resolve(resolve_source, {}, {}); Require( name, "Image-owned MSAA resolve", resolve_source.backing.state.layout == vk::ImageLayout::eTransferSrcOptimal && resolve_destination.backing.state.layout == vk::ImageLayout::eTransferDstOptimal, "Image::Resolve did not issue the MSAA-to-single-sample transfer"); Libs::Graphics::Image copy_destination(m_runtime_context, scheduler, resolve_destination_info); copy_destination.Resolve(resolve_destination, {}, {}); Require(name, "Image-owned single-sample resolve copy", resolve_destination.backing.state.layout == vk::ImageLayout::eTransferSrcOptimal && copy_destination.backing.state.layout == vk::ImageLayout::eTransferDstOptimal, "Image::Resolve did not retain the single-sample copy path"); const std::array multisample_source{0x4000u, 0xc000u}; std::memcpy(memory + 0x2000, &multisample_source, sizeof(multisample_source)); auto color_desc = MakeLinearDesc( base + 0x2000, sizeof(multisample_source), vk::Format::eR16G16Unorm, Prospero::BufferFormat::k16_16UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto color_image = texture_cache.FindImage(color_desc); (void)texture_cache.FindTexture(color_image, color_desc); texture_cache.MarkGpuWritten(color_image); const std::array refreshed_multisample_source{0x2000u, 0xe000u}; Require(name, "unequal-sample source CPU write", resources.HandleFault(PageFaultAccess::Write, color_desc.info.data.address), "color-to-MS-depth source did not accept a CPU update"); std::memcpy(memory + 0x2000, refreshed_multisample_source.data(), sizeof(refreshed_multisample_source)); constexpr uint64_t ms_stencil_offset = 0x80000; constexpr uint64_t ms_stencil_size = 0x10000; auto ms_stencil_owner = resources.GetBufferCache().ObtainBuffer( base + ms_stencil_offset, ms_stencil_size, false); Require(name, "MS stencil buffer allocation", ms_stencil_owner.first != nullptr, "failed to create an unequal-sample stencil source"); resources.GetBufferCache().FillBuffer( base + ms_stencil_offset, ms_stencil_size, 0x41414141u, false); auto ms_depth_desc = color_desc; ms_depth_desc.type = BindingType::DepthTarget; ms_depth_desc.info.stencil = {base + ms_stencil_offset, ms_stencil_size}; ms_depth_desc.info.pixel_format = vk::Format::eD24UnormS8Uint; ms_depth_desc.info.guest_format = Prospero::BufferFormat::k16UNorm; ms_depth_desc.info.bytes_per_block = 2; ms_depth_desc.info.samples = 2; ms_depth_desc.info.type = Prospero::ImageType::kColor2D; ms_depth_desc.view_info.format = vk::Format::eD24UnormS8Uint; ms_depth_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; ms_depth_desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto ms_depth_image = texture_cache.FindImage(ms_depth_desc); constexpr uint64_t ms_htile_address = base + 0x2600000; constexpr uint64_t ms_htile_size = 0x10000; ms_depth_desc.info.metadata.range = {ms_htile_address, ms_htile_size}; ms_depth_desc.info.metadata.kind = ImageMetadataKind::Htile; const auto ms_depth_with_htile = texture_cache.FindImage(ms_depth_desc); const auto ms_depth_view = texture_cache.FindDepthTarget(ms_depth_with_htile, ms_depth_desc); auto sampled_ms_depth = ms_depth_desc; sampled_ms_depth.type = BindingType::Texture; sampled_ms_depth.info.metadata = {}; sampled_ms_depth.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto sampled_ms_depth_image = texture_cache.FindImage(sampled_ms_depth); Require( name, "unequal-sample depth overlap", ms_depth_image && ms_depth_with_htile == ms_depth_image && sampled_ms_depth_image == ms_depth_image && ms_depth_image != color_image && ms_depth_view != nullptr && texture_cache.GetImage(ms_depth_image).backing.samples == 2 && texture_cache.GetImage(ms_depth_image).backing.state.layout == vk::ImageLayout::eDepthStencilAttachmentOptimal && texture_cache.GetImage(ms_depth_image).IsGpuModified() && texture_cache.GetImage(ms_depth_image).info.htile_clear_mask == 0 && texture_cache.GetImage(ms_depth_image).info.metadata.range == ms_depth_desc.info.metadata.range && texture_cache.IsMeta(ms_htile_address) && !texture_cache.IsMetaCleared(ms_htile_address, 0) && !resources.GetBufferCache().HasGpuDirtyBytes( base + ms_stencil_offset, ms_stencil_size) && !texture_cache.IsRegionGpuModified(base + ms_stencil_offset, ms_stencil_size), "unequal-sample overlap did not run the color-to-MS-depth pass " "without manufacturing stencil ownership"); auto &oversized_ms = texture_cache.GetImage(ms_depth_image); const auto ms_data_size = oversized_ms.info.data.size; oversized_ms.info.data.size = (32ull << 20) + 4; const bool oversized_ms_readback = !TextureCacheTestAccess::TryDownload(texture_cache, ms_depth_image); oversized_ms.info.data.size = ms_data_size; Require(name, "oversized multisample download rejection", oversized_ms_readback && oversized_ms.IsGpuModified() && !oversized_ms.IsBufferModified() && !resources.GetBufferCache().HasGpuDirtyBytes( ms_depth_desc.info.data.address, ms_data_size), "an unsupported multisample source reserved download storage or " "published Buffer ownership"); const std::array multisample_source_4x{0x0000u, 0x4000u, 0x8000u, 0xffffu}; std::memcpy(memory + 0x7000, multisample_source_4x.data(), sizeof(multisample_source_4x)); auto color_desc_4x = MakeLinearDesc(base + 0x7000, sizeof(multisample_source_4x), vk::Format::eR16G16B16A16Unorm, Prospero::BufferFormat::k16_16_16_16UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 8, 1); const auto color_image_4x = texture_cache.FindImage(color_desc_4x); auto ms_depth_desc_4x = color_desc_4x; ms_depth_desc_4x.type = BindingType::DepthTarget; ms_depth_desc_4x.info.pixel_format = vk::Format::eD16Unorm; ms_depth_desc_4x.info.guest_format = Prospero::BufferFormat::k16UNorm; ms_depth_desc_4x.info.bytes_per_block = 2; ms_depth_desc_4x.info.samples = 4; ms_depth_desc_4x.info.type = Prospero::ImageType::kColor2D; ms_depth_desc_4x.view_info.format = vk::Format::eD16Unorm; ms_depth_desc_4x.view_info.aspect = vk::ImageAspectFlagBits::eDepth; ms_depth_desc_4x.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto ms_depth_image_4x = texture_cache.FindImage(ms_depth_desc_4x); Require(name, "four-sample depth overlap", ms_depth_image_4x && ms_depth_image_4x != color_image_4x && texture_cache.GetImage(ms_depth_image_4x).backing.samples == 4, "four-sample color packing did not produce a depth image"); const auto ms_depth_backing = texture_cache.GetImage(ms_depth_image).backing.image; const auto first_stencil_association = texture_cache.FindImageFromRange( base + ms_stencil_offset, ms_stencil_size, false); Require( name, "stencil association shape", first_stencil_association && first_stencil_association != ms_depth_image && texture_cache.GetImage(first_stencil_association) .info.pixel_format == vk::Format::eUndefined && texture_cache.GetImage(first_stencil_association).backing.image == nullptr && texture_cache.GetImage(first_stencil_association).depth_id == ms_depth_image, "stencil address did not create a lightweight depth association"); Require( name, "stencil association download rejection", !TextureCacheTestAccess::TryDownload(texture_cache, first_stencil_association), "a lightweight stencil association entered image download planning"); auto exact_ms_depth_alias = ms_depth_desc; exact_ms_depth_alias.type = BindingType::Texture; exact_ms_depth_alias.info.stencil = {}; exact_ms_depth_alias.info.pixel_format = vk::Format::eR8G8B8A8Unorm; exact_ms_depth_alias.info.guest_format = Prospero::BufferFormat::k8_8_8_8UNorm; exact_ms_depth_alias.info.bytes_per_block = 4; exact_ms_depth_alias.view_info.format = vk::Format::eR8G8B8A8Unorm; exact_ms_depth_alias.view_info.aspect = vk::ImageAspectFlagBits::eColor; exact_ms_depth_alias.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto exact_ms_depth_image = texture_cache.FindImage(exact_ms_depth_alias, true); Require( name, "exact multisample depth/stencil coexistence", exact_ms_depth_image && exact_ms_depth_image != ms_depth_image && TextureCacheTestAccess::Contains(texture_cache, ms_depth_image) && texture_cache.GetImage(ms_depth_image).backing.image == ms_depth_backing && texture_cache.GetImage(first_stencil_association).depth_id == ms_depth_image, "exact multisample depth/stencil lookup replaced the old image " "or stole its stencil association"); constexpr uint64_t second_stencil_offset = 0x90000; auto stencil_storage_desc = MakeLinearDesc( base + second_stencil_offset, ms_stencil_size, vk::Format::eR8Uint, Prospero::BufferFormat::k8UInt, Prospero::ImageType::kColor2D, ms_depth_desc.info.extent, 1, 1, 1); stencil_storage_desc.type = BindingType::Storage; stencil_storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage; const auto stencil_storage_image = texture_cache.FindImage(stencil_storage_desc); (void)texture_cache.FindTexture(stencil_storage_image, stencil_storage_desc); auto switched_ms_depth = ms_depth_desc; switched_ms_depth.info.stencil = {base + second_stencil_offset, ms_stencil_size}; const auto switched_ms_depth_id = texture_cache.FindImage(switched_ms_depth); TextureCacheTestAccess::AssociateStencil( texture_cache, switched_ms_depth_id, switched_ms_depth.info.stencil); const auto second_stencil_association = texture_cache.FindImageFromRange( base + second_stencil_offset, ms_stencil_size, false); Require(name, "stencil association switch", switched_ms_depth_id == ms_depth_image && texture_cache.GetImage(switched_ms_depth_id).backing.image == ms_depth_backing && texture_cache.FindImageFromRange(base + ms_stencil_offset, ms_stencil_size, false) == first_stencil_association && texture_cache.GetImage(first_stencil_association).depth_id == ms_depth_image && second_stencil_association && second_stencil_association == stencil_storage_image && texture_cache.GetImage(second_stencil_association).IsTracked() && texture_cache.GetImage(second_stencil_association).depth_id == ms_depth_image, "changing stencil address recreated depth or discarded an " "existing lightweight association"); Require(name, "CPU write to tracked stencil association", resources.HandleFault(PageFaultAccess::Write, base + second_stencil_offset) && !texture_cache.GetImage(second_stencil_association).IsTracked() && texture_cache.GetImage(second_stencil_association).IsCpuDirty() && texture_cache.GetImage(second_stencil_association).depth_id == ms_depth_image, "CPU invalidation left a reused stencil image write-protected"); memory[second_stencil_offset] = 0x5a; constexpr uint64_t added_stencil_depth_offset = 0x60000; constexpr uint64_t added_stencil_offset = 0x70000; constexpr uint64_t added_stencil_size = 0x10000; constexpr float added_stencil_depth_value = 0.75f; std::memcpy(memory + added_stencil_depth_offset, &added_stencil_depth_value, sizeof(added_stencil_depth_value)); auto depth_only_source = MakeLinearDesc( base + added_stencil_depth_offset, 0x10000, vk::Format::eD32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); depth_only_source.type = BindingType::DepthTarget; depth_only_source.view_info.format = vk::Format::eD32Sfloat; depth_only_source.view_info.aspect = vk::ImageAspectFlagBits::eDepth; depth_only_source.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto depth_only_source_image = texture_cache.FindImage(depth_only_source); auto combined_destination = depth_only_source; combined_destination.info.stencil = {base + added_stencil_offset, added_stencil_size}; combined_destination.info.pixel_format = vk::Format::eD32SfloatS8Uint; combined_destination.view_info.format = vk::Format::eD32SfloatS8Uint; combined_destination.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; const auto combined_destination_image = texture_cache.FindImage(combined_destination); TextureCacheTestAccess::AssociateStencil( texture_cache, combined_destination_image, combined_destination.info.stencil); const auto combined_stencil_association = texture_cache.FindImageFromRange(base + added_stencil_offset, added_stencil_size, false); Require( name, "depth-only to combined association", combined_destination_image != depth_only_source_image && combined_stencil_association && texture_cache.GetImage(combined_stencil_association).depth_id == combined_destination_image && texture_cache.GetImage(combined_stencil_association) .backing.image == nullptr, "stencil addition did not recreate depth and register its " "lightweight association"); const uint32_t mirror_value = 0xa1b2c3d4u; std::memcpy(memory + 0x5000, &mirror_value, sizeof(mirror_value)); auto mirror_desc = MakeLinearDesc(base + 0x5000, sizeof(mirror_value), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto mirror_image = texture_cache.FindImage(mirror_desc); (void)texture_cache.FindTexture(mirror_image, mirror_desc); texture_cache.MarkGpuWritten(mirror_image); constexpr uint32_t mirror_cpu_value = 0x0f1e2d3cu; Require(name, "CPU-dirty formatted mirror fault", resources.HandleFault(PageFaultAccess::Write, mirror_desc.info.data.address), "GPU image did not accept a CPU write before Buffer mirroring"); std::memcpy(memory + 0x5000, &mirror_cpu_value, sizeof(mirror_cpu_value)); auto mirror_binding = resources.GetBufferCache().ObtainBuffer( base + 0x5000, sizeof(mirror_value), false, true); Require(name, "CPU-dirty formatted mirror source", mirror_binding.first != nullptr && !texture_cache.GetImage(mirror_image).IsBufferModified(), "formatted mirror did not expose a readable Buffer source"); auto mirror_cpu_readback = CreateHostBuffer( name, sizeof(mirror_cpu_value), vk::BufferUsageFlagBits::eTransferDst, std::vector{0}); const vk::BufferCopy mirror_cpu_copy{mirror_binding.second, 0, sizeof(mirror_cpu_value)}; command.Handle().copyBuffer(mirror_binding.first->Handle(), mirror_cpu_readback.buffer, 1, &mirror_cpu_copy); HostReadBarrier(mirror_cpu_readback.buffer, mirror_cpu_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); auto mirror_refresh_desc = mirror_desc; const auto mirror_refresh = texture_cache.FindImage(mirror_refresh_desc); (void)texture_cache.FindTexture(mirror_refresh, mirror_refresh_desc); Require(name, "buffer-to-image ownership", mirror_refresh == mirror_image && !texture_cache.GetImage(mirror_refresh).IsBufferModified(), "buffer-backed refresh incorrectly transferred dirty ownership " "to the image"); texture_cache.MarkGpuWritten(mirror_refresh); constexpr uint64_t exact_buffer_offset = 0x90000; constexpr uint32_t exact_buffer_value = 0x3f234567u; std::memcpy(memory + exact_buffer_offset, &exact_buffer_value, sizeof(exact_buffer_value)); auto exact_buffer_desc = MakeLinearDesc(base + exact_buffer_offset, sizeof(exact_buffer_value), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto exact_buffer_image = texture_cache.FindImage(exact_buffer_desc); (void)texture_cache.FindTexture(exact_buffer_image, exact_buffer_desc); texture_cache.MarkGpuWritten(exact_buffer_image); auto exact_buffer_binding = resources.GetBufferCache().ObtainBuffer( exact_buffer_desc.info.data.address, exact_buffer_desc.info.data.size, false, true); Require( name, "exact replacement Buffer synchronization", exact_buffer_binding.first != nullptr && !texture_cache.GetImage(exact_buffer_image).IsBufferModified() && texture_cache.GetImage(exact_buffer_image).IsGpuModified(), "exact replacement copy transferred cache ownership"); auto exact_float_desc = exact_buffer_desc; exact_float_desc.info.pixel_format = vk::Format::eR32Sfloat; exact_float_desc.info.guest_format = Prospero::BufferFormat::k32Float; exact_float_desc.view_info.format = vk::Format::eR32Sfloat; const auto exact_float_image = texture_cache.FindImage(exact_float_desc, true); (void)texture_cache.FindTexture(exact_float_image, exact_float_desc); Require( name, "Buffer-superseded exact coexistence", exact_float_image != exact_buffer_image && TextureCacheTestAccess::Contains(texture_cache, exact_buffer_image) && texture_cache.GetImage(exact_buffer_image).IsGpuModified() && !texture_cache.GetImage(exact_float_image).IsBufferModified() && !texture_cache.GetImage(exact_float_image).IsGpuModified(), "exact-format lookup retired its old record or transferred Buffer " "ownership"); auto exact_buffer_readback = CreateHostBuffer( name, sizeof(exact_buffer_value), vk::BufferUsageFlagBits::eTransferDst, std::vector{0}); auto &exact_float_native = texture_cache.GetImage(exact_float_image); exact_float_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, command.Handle()); vk::BufferImageCopy exact_float_copy{}; exact_float_copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; exact_float_copy.imageSubresource.layerCount = 1; exact_float_copy.imageExtent = {1, 1, 1}; command.Handle().copyImageToBuffer(exact_float_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, exact_buffer_readback.buffer, 1, &exact_float_copy); HostReadBarrier(exact_buffer_readback.buffer, exact_buffer_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); const uint32_t clear_source = 0x01010101u; std::memcpy(memory + 0x6000, &clear_source, sizeof(clear_source)); auto clear_desc = MakeLinearDesc( base + 0x6000, sizeof(clear_source), vk::Format::eR8G8B8A8Unorm, Prospero::BufferFormat::k8_8_8_8UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto clear_image = texture_cache.FindImage(clear_desc); auto buffer_write = resources.GetBufferCache().ObtainBuffer( base + 0x6000, sizeof(clear_source), true, true); texture_cache.InvalidateMemoryFromGPU(base + 0x6000, sizeof(clear_source)); Require(name, "buffer-write ownership", buffer_write.first != nullptr && texture_cache.GetImage(clear_image).IsBufferModified(), "formatted buffer write did not supersede the image"); Require(name, "buffer-owned full image clear", texture_cache.ClearImageFromBuffer( command, base + 0x6000, sizeof(clear_source), 0xaabbccddu) && !texture_cache.GetImage(clear_image).IsBufferModified() && texture_cache.GetImage(clear_image).IsGpuModified(), "full image clear retained dual buffer/image GPU ownership"); texture_cache.MarkGpuWritten(clear_image); auto clear_read_desc = clear_desc; Require(name, "post-clear image reuse", texture_cache.FindImage(clear_read_desc) == clear_image, "cleared image could not be read or written after ownership " "transfer"); auto stencil_clear_desc = MakeLinearDesc( base + 0x27e0000, 0x1000, vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {13, 11, 1}, 2, 4, 1); stencil_clear_desc.info.stencil = {base + 0x27e2000, 0x1000}; stencil_clear_desc.info.resources.levels = 2; stencil_clear_desc.info.mip_layout[0] = {0, 0x800, 13, 11}; stencil_clear_desc.info.mip_layout[1] = {0x800, 0x800, 6, 5}; stencil_clear_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; const auto stencil_clear_id = texture_cache.FindImage(stencil_clear_desc); auto &stencil_clear_image = texture_cache.GetImage(stencil_clear_id); stencil_clear_image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, command.Handle()); const vk::ClearDepthStencilValue initial_depth_stencil{0.75f, 0x91}; const vk::ImageSubresourceRange all_depth_stencil{ stencil_clear_desc.view_info.aspect, 0, 2, 0, 2}; command.Handle().clearDepthStencilImage(stencil_clear_image.backing.image, vk::ImageLayout::eTransferDstOptimal, &initial_depth_stencil, 1, &all_depth_stencil); texture_cache.MarkGpuWritten(stencil_clear_id); vk::ClearValue stencil_value{}; stencil_value.depthStencil = vk::ClearDepthStencilValue{0.f, 0x35}; TextureCacheTestAccess::ClearImage(texture_cache, command, stencil_clear_id, {vk::ImageAspectFlagBits::eStencil, 1, 1, 1, 1}, stencil_value); // Read every mip/layer of both aspects to prove that the stencil clear is local. auto stencil_readback = CreateHostBuffer(name, 0x4000, vk::BufferUsageFlagBits::eTransferDst, {}); std::vector stencil_copies; for (uint32_t mip = 0; mip < 2; mip++) { for (uint32_t layer = 0; layer < 2; layer++) { for (const auto aspect : {vk::ImageAspectFlagBits::eDepth, vk::ImageAspectFlagBits::eStencil}) { vk::BufferImageCopy copy{}; copy.bufferOffset = stencil_copies.size() * 0x800; copy.imageSubresource = {aspect, mip, layer, 1}; copy.imageExtent = {13u >> mip, 11u >> mip, 1}; stencil_copies.push_back(copy); } } } stencil_clear_image.Download(stencil_copies, stencil_readback.buffer, 0, stencil_readback.size); HostReadBarrier(stencil_readback.buffer, stencil_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); const auto stencil_words = ReadBuffer(name, stencil_readback, 0x1000); const auto *stencil_bytes = reinterpret_cast(stencil_words.data()); for (const auto © : stencil_copies) { const auto &sub = copy.imageSubresource; const auto count = copy.imageExtent.width * copy.imageExtent.height; const auto *bytes = stencil_bytes + copy.bufferOffset; const bool depth = sub.aspectMask == vk::ImageAspectFlagBits::eDepth; uint32_t expected = 0x91u; if (depth) { expected = std::bit_cast(0.75f); } else if (sub.mipLevel == 1 && sub.baseArrayLayer == 1) { expected = 0x35u; } for (uint32_t pixel = 0; pixel < count; pixel++) { uint32_t actual = bytes[pixel]; if (depth) std::memcpy(&actual, bytes + pixel * 4, 4); Require(name, "stencil clear preserves depth and neighbors", actual == expected, "selected stencil clear changed depth, another mip, or another layer"); } } DestroyBuffer(&stencil_readback); constexpr uint64_t partial_image_offset = 0xa000; constexpr uint64_t partial_buffer_offset = 0xa010; constexpr uint64_t partial_clean_offset = 0xa020; constexpr uint32_t partial_image_value = 0x31415926u; constexpr uint32_t partial_buffer_value = 0x27182818u; constexpr uint32_t partial_clean_value = 0xabcdef01u; std::memcpy(memory + partial_clean_offset, &partial_clean_value, sizeof(partial_clean_value)); auto partial_write = resources.GetBufferCache().ObtainBuffer( base + partial_image_offset, sizeof(partial_image_value), true); Require(name, "partial-page buffer allocation", partial_write.first != nullptr, "partial-page GPU write did not create a native buffer"); resources.GetBufferCache().FillBuffer(base + partial_image_offset, sizeof(partial_image_value), partial_image_value, false); resources.GetBufferCache().FillBuffer(base + partial_buffer_offset, sizeof(partial_buffer_value), partial_buffer_value, false); auto partial_desc = MakeLinearDesc( base + partial_image_offset, sizeof(partial_image_value), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto partial_image = texture_cache.FindImage(partial_desc); Require(name, "partial-page image upload", !texture_cache.GetImage(partial_image).IsGpuModified(), "image upload incorrectly consumed buffer dirty ownership"); auto partial_image_mirror = resources.GetBufferCache().ObtainBuffer( base + partial_image_offset, sizeof(partial_image_value), false, true); Require(name, "partial-page image mirror", partial_image_mirror.first != nullptr && !texture_cache.GetImage(partial_image).IsGpuModified(), "same-page image upload lost its buffer source"); const auto partial_clean_source = resources.GetBufferCache().ObtainBufferForImage( base + partial_clean_offset, sizeof(partial_clean_value)); Require(name, "partial-page clean source", partial_clean_source.first != nullptr, "clean same-page bytes inherited unrelated buffer ownership"); Require(name, "partial-page remaining fault", resources.HandleFault(PageFaultAccess::Read, base + partial_buffer_offset), "same-page buffer ownership was lost after image transfer"); uint32_t partial_buffer_backing = 0; std::memcpy(&partial_buffer_backing, memory + partial_buffer_offset, sizeof(partial_buffer_backing)); uint32_t partial_image_backing = 0; std::memcpy(&partial_image_backing, memory + partial_image_offset, sizeof(partial_image_backing)); Require(name, "partial-page readback values", partial_image_backing == partial_image_value && partial_buffer_backing == partial_buffer_value, "same-page image/buffer readback published incorrect bytes"); constexpr uint32_t partial_cpu_refresh_value = 0x13579bdfu; Require(name, "partial-page CPU write fault", resources.HandleFault(PageFaultAccess::Write, base + partial_clean_offset), "same-page CPU write did not invalidate cached ownership"); std::memcpy(memory + partial_clean_offset, &partial_cpu_refresh_value, sizeof(partial_cpu_refresh_value)); constexpr std::array partial_neighbors{0x2468ace0u, 0xfedcba98u}; std::memcpy(memory + partial_clean_offset - sizeof(uint32_t), &partial_neighbors[0], sizeof(uint32_t)); std::memcpy(memory + partial_clean_offset + sizeof(uint32_t), &partial_neighbors[1], sizeof(uint32_t)); std::vector partial_cpu_refresh_expected(TRACKER_PAGE_SIZE / sizeof(u32)); std::memcpy(partial_cpu_refresh_expected.data(), memory + partial_image_offset, TRACKER_PAGE_SIZE); const auto partial_cpu_refresh_source = resources.GetBufferCache().ObtainBufferForImage( base + partial_clean_offset, sizeof(partial_cpu_refresh_value)); Require(name, "partial-page CPU refresh source", partial_cpu_refresh_source.first != nullptr, "CPU-dirty same-page bytes did not resolve through the cached " "buffer"); auto partial_cpu_refresh_readback = CreateHostBuffer(name, TRACKER_PAGE_SIZE, vk::BufferUsageFlagBits::eTransferDst, {0}); const vk::BufferCopy partial_cpu_refresh_copy{ partial_cpu_refresh_source.first->Offset(base + partial_image_offset), 0, TRACKER_PAGE_SIZE}; command.Handle().copyBuffer(partial_cpu_refresh_source.first->Handle(), partial_cpu_refresh_readback.buffer, 1, &partial_cpu_refresh_copy); vk::BufferMemoryBarrier partial_cpu_refresh_barrier{}; partial_cpu_refresh_barrier.sType = vk::StructureType::eBufferMemoryBarrier; partial_cpu_refresh_barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; partial_cpu_refresh_barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; partial_cpu_refresh_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; partial_cpu_refresh_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; partial_cpu_refresh_barrier.buffer = partial_cpu_refresh_readback.buffer; partial_cpu_refresh_barrier.size = partial_cpu_refresh_readback.size; command.Handle().pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &partial_cpu_refresh_barrier, 0, nullptr); const uint32_t fault_a = 0x01020304u; const uint32_t fault_b = 0x11121314u; std::memcpy(memory + 0x8000, &fault_a, sizeof(fault_a)); std::memcpy(memory + 0x8010, &fault_b, sizeof(fault_b)); auto fault_a_desc = MakeLinearDesc(base + 0x8000, sizeof(fault_a), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); auto fault_b_desc = fault_a_desc; fault_b_desc.info.data.address = base + 0x8010; const auto fault_a_image = texture_cache.FindImage(fault_a_desc); const auto fault_b_image = texture_cache.FindImage(fault_b_desc); (void)texture_cache.FindTexture(fault_a_image, fault_a_desc); (void)texture_cache.FindTexture(fault_b_image, fault_b_desc); texture_cache.MarkGpuWritten(fault_a_image); texture_cache.MarkGpuWritten(fault_b_image); Require(name, "per-image watcher installation", texture_cache.GetImage(fault_a_image).IsTracked() && texture_cache.GetImage(fault_b_image).IsTracked(), "same-page images did not install independent write watchers"); constexpr uint64_t padding_fault_offset = 0x8080; uint32_t write_only_read_a = 0; uint32_t write_only_read_b = 0; std::memcpy(&write_only_read_a, memory + 0x8000, sizeof(write_only_read_a)); std::memcpy(&write_only_read_b, memory + 0x8010, sizeof(write_only_read_b)); Require(name, "write-only image read policy", write_only_read_a == fault_a && write_only_read_b == fault_b && texture_cache.GetImage(fault_a_image).IsGpuModified() && texture_cache.GetImage(fault_b_image).IsGpuModified(), "TextureCache claimed a CPU read fault instead of preserving " "GPU image ownership"); Require(name, "same-page image write invalidation", resources.HandleFault(PageFaultAccess::Write, base + padding_fault_offset) && texture_cache.GetImage(fault_a_image).IsGpuModified() && texture_cache.GetImage(fault_b_image).IsGpuModified() && !texture_cache.GetImage(fault_a_image).IsTracked() && !texture_cache.GetImage(fault_b_image).IsTracked() && texture_cache.GetImage(fault_a_image).IsMaybeCpuDirty() && texture_cache.GetImage(fault_b_image).IsMaybeCpuDirty(), "a byte-disjoint CPU write discarded authoritative images"); const auto retracked_a = texture_cache.FindImage(fault_a_desc); const auto retracked_b = texture_cache.FindImage(fault_b_desc); (void)texture_cache.FindTexture(retracked_a, fault_a_desc); (void)texture_cache.FindTexture(retracked_b, fault_b_desc); auto fault_a_mirror = resources.GetBufferCache().ObtainBuffer( base + 0x8000, sizeof(fault_a), false, true); Require(name, "same-page image re-track", retracked_a == fault_a_image && retracked_b == fault_b_image && texture_cache.GetImage(fault_a_image).IsTracked() && texture_cache.GetImage(fault_b_image).IsTracked() && !texture_cache.GetImage(fault_a_image).IsCpuDirty() && !texture_cache.GetImage(fault_b_image).IsCpuDirty() && fault_a_mirror.first != nullptr && texture_cache.GetImage(fault_a_image).IsTracked() && texture_cache.GetImage(fault_b_image).IsTracked() && texture_cache.GetImage(fault_a_image).IsGpuModified() && texture_cache.GetImage(fault_b_image).IsGpuModified(), "copying one same-page image lost an authoritative owner"); Require(name, "same-page survivor write fault", resources.HandleFault(PageFaultAccess::Write, base + 0x8010) && texture_cache.GetImage(fault_b_image).IsGpuModified() && texture_cache.GetImage(fault_b_image).IsDefinitelyCpuDirty(), "the surviving image was not protected after its alias retired"); constexpr uint32_t fault_b_cpu = 0xa5a6a7a8u; std::memcpy(memory + 0x8010, &fault_b_cpu, sizeof(fault_b_cpu)); const auto refreshed_b = texture_cache.FindImage(fault_b_desc); (void)texture_cache.FindTexture(refreshed_b, fault_b_desc); Require( name, "same-page survivor refresh", refreshed_b == fault_b_image && !texture_cache.GetImage(fault_b_image).IsDefinitelyCpuDirty() && texture_cache.GetImage(fault_b_image).IsGpuModified(), "the surviving image could not reconcile its CPU write"); constexpr uint64_t publish_image_offset = 0x26000; constexpr uint64_t publish_buffer_offset = 0x26010; constexpr uint32_t publish_image_value = 0x31415926u; constexpr uint32_t publish_buffer_value = 0x27182818u; std::memcpy(memory + publish_image_offset, &publish_image_value, sizeof(publish_image_value)); auto publish_image_desc = MakeLinearDesc( base + publish_image_offset, sizeof(publish_image_value), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto publish_image = texture_cache.FindImage(publish_image_desc); (void)texture_cache.FindTexture(publish_image, publish_image_desc); texture_cache.MarkGpuWritten(publish_image); resources.GetBufferCache().FillBuffer(base + publish_buffer_offset, sizeof(publish_buffer_value), publish_buffer_value, false); auto publish_replacement_desc = publish_image_desc; publish_replacement_desc.info.pixel_format = vk::Format::eR32Sfloat; publish_replacement_desc.info.guest_format = Prospero::BufferFormat::k32Float; publish_replacement_desc.view_info.format = vk::Format::eR32Sfloat; const auto publish_replacement = texture_cache.FindImage(publish_replacement_desc, true); Require(name, "exact-disjoint image replacement", publish_replacement && publish_replacement != publish_image, "exact-format lookup did not replace the image"); Require(name, "exact-disjoint image publication", !resources.GetBufferCache().HasGpuDirtyBytes( base + publish_buffer_offset, sizeof(publish_buffer_value)), "clean neighboring buffer unexpectedly became GPU-owned"); uint32_t published_image_backing = 0; uint32_t published_buffer_backing = 0; std::memcpy(&published_image_backing, memory + publish_image_offset, sizeof(published_image_backing)); std::memcpy(&published_buffer_backing, memory + publish_buffer_offset, sizeof(published_buffer_backing)); Require(name, "exact-disjoint publication contents", published_image_backing == publish_image_value && published_buffer_backing == publish_buffer_value, "image publication or neighboring buffer readback changed bytes"); for (const auto [offset, samples] : std::array, 2>{ {{0x10000, 2}, {0x11000, 4}}}) { std::memset(memory + offset, 0, samples * sizeof(uint16_t)); auto standalone_ms = MakeLinearDesc( base + offset, samples * sizeof(uint16_t), vk::Format::eD16Unorm, Prospero::BufferFormat::k16UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 2, samples); standalone_ms.type = BindingType::DepthTarget; standalone_ms.view_info.aspect = vk::ImageAspectFlagBits::eDepth; standalone_ms.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto image = texture_cache.FindImage(standalone_ms); (void)texture_cache.FindDepthTarget(image, standalone_ms); texture_cache.MarkGpuWritten(image); uint16_t standalone_cpu_read = 1; std::memcpy(&standalone_cpu_read, memory + offset, sizeof(standalone_cpu_read)); Require( name, "standalone MSAA ownership", texture_cache.GetImage(image).IsGpuModified() && standalone_cpu_read == 0 && texture_cache.GetImage(image).IsGpuModified() && resources.HandleFault(PageFaultAccess::Write, base + offset) && texture_cache.GetImage(image).IsGpuModified() && texture_cache.GetImage(image).IsDefinitelyCpuDirty(), "fresh multisample target violated write-only ownership"); } // A one-layer guest descriptor can alias layer 1 of a cached native array. constexpr uint64_t alias_dcc_address = base + 0x2120000; auto dcc_array = MakeLinearDesc( base + 0x2100000, 0x20000, vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, Prospero::ImageType::kColor2D, {128, 64, 1}, 2, 8, 1); dcc_array.type = BindingType::RenderTarget; dcc_array.info.tile_mode = Prospero::TileMode::kRenderTarget; dcc_array.info.pitch = TileGetRenderTargetPitch(128, 8); dcc_array.info.mip_layout[0].pitch = dcc_array.info.pitch; dcc_array.info.metadata.kind = ImageMetadataKind::Dcc; dcc_array.info.metadata.range = {alias_dcc_address, 0x2000}; dcc_array.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; WriteMetadata(context, alias_dcc_address, 0x2000, UINT32_MAX); WriteMetadata(context, alias_dcc_address + 0x2000, 0x1000, 0x5a5a5a5au); const auto dcc_array_id = texture_cache.FindImage(dcc_array); vk::ClearValue array_paint{}; array_paint.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f}; TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(), dcc_array_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}, array_paint); auto dcc_alias = dcc_array; dcc_alias.info.data = {dcc_array.info.data.address + 0x10000, 0x10000}; dcc_alias.info.resources.layers = 1; dcc_alias.info.mip_layout[0].size = 0x10000; dcc_alias.info.metadata.range = {alias_dcc_address + 0x1000, 0x1000}; dcc_alias.view_info.type = vk::ImageViewType::e2D; dcc_alias.view_info.layer_count = 1; WriteMetadata(context, dcc_alias.info.metadata.range.address, 0x1000, 0x40404040u); Require(name, "DCC alias host layer", texture_cache.FindImage(dcc_alias) == dcc_array_id && dcc_alias.view_info.base_layer == 1 && ReadCachedTexel(name, context, dcc_array_id) == std::vector{0x00003c00u, 0x3c003c00u} && ReadCachedTexel(name, context, dcc_array_id, {}, {1, 1, 1}, 1) == std::vector{0, 0x3c000000u}, "guest metadata layer 0 did not clear only remapped native layer 1"); resources.GetBufferCache().ReadMemory(alias_dcc_address, 0x3000, false); std::array alias_metadata{}; Require(name, "DCC alias metadata bounds", LibKernel::Memory::TryReadBacking(alias_dcc_address, alias_metadata.data(), alias_metadata.size()) && std::all_of(alias_metadata.begin(), alias_metadata.begin() + 0x2000, [](uint8_t value) { return value == 0xff; }) && std::all_of(alias_metadata.begin() + 0x2000, alias_metadata.end(), [](uint8_t value) { return value == 0x5a; }), "remapped host layer changed metadata outside the guest alias allocation"); RenderColorInfo retired_alias{}; retired_alias.desc = dcc_alias; retired_alias.image_id = dcc_array_id; retired_alias.guest_mip_level = 0; retired_alias.guest_array_layer = 0; texture_cache.UnmapMemory(dcc_array.info.data.address, dcc_array.info.data.size); WriteMetadata(context, dcc_alias.info.metadata.range.address, 0x1000, 0x40404040u); RenderExecutorTestAccess::PrepareGraphicsBindings( context.GetRenderExecutor(), std::span{}, std::span{&retired_alias, 1u}); RenderDepthInfo alias_depth{}; const auto alias_rendering = RenderExecutorTestAccess::AcquireRenderTargets( context.GetRenderExecutor(), scheduler.Current(), &retired_alias, 1, alias_depth); Require(name, "retired DCC alias native coordinates", retired_alias.image_id != dcc_array_id && retired_alias.desc.view_info.base_level == 0 && retired_alias.desc.view_info.base_layer == 0 && retired_alias.desc.info.resources.layers == 1 && alias_rendering.color_attachments[0].image_view != nullptr && ReadCachedTexel(name, context, retired_alias.image_id) == std::vector{0, 0x3c000000u}, "rediscovery treated a retired host layer as the native DCC slice"); resources.GetBufferCache().ReadMemory(alias_dcc_address, 0x3000); Require(name, "retired DCC alias metadata bounds", LibKernel::Memory::TryReadBacking(alias_dcc_address, alias_metadata.data(), alias_metadata.size()) && std::all_of(alias_metadata.begin(), alias_metadata.begin() + 0x2000, [](uint8_t byte) { return byte == 0xff; }) && std::all_of(alias_metadata.begin() + 0x2000, alias_metadata.end(), [](uint8_t byte) { return byte == 0x5a; }), "retired alias rediscovery overwrote metadata outside its native slice"); RenderExecutorTestAccess::ResetBindings(context.GetRenderExecutor()); constexpr uint64_t metadata_data_a = 0x12000; constexpr uint64_t metadata_data_b = 0x12100; constexpr uint64_t metadata_a = 0x13000; constexpr uint64_t metadata_b = 0x13100; auto MakeMetadataDepth = [&](uint64_t data, uint64_t metadata) { auto desc = MakeLinearDesc( base + data, sizeof(uint32_t), vk::Format::eD32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); desc.type = BindingType::DepthTarget; desc.info.metadata.kind = ImageMetadataKind::Htile; desc.info.metadata.range = {base + metadata, 0x80}; desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth; desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; return desc; }; auto metadata_depth_a = MakeMetadataDepth(metadata_data_a, metadata_a); auto metadata_depth_b = MakeMetadataDepth(metadata_data_b, metadata_b); metadata_depth_a.info.htile_clear_mask = 0; const auto metadata_depth_a_id = texture_cache.FindImage(metadata_depth_a); const auto metadata_depth_b_id = texture_cache.FindImage(metadata_depth_b); WriteMetadata(context, base + metadata_a, 0x80, 0); Require( name, "unbound metadata separation", !texture_cache.IsMeta(base + metadata_a), "unbound metadata bytes were exposed as registered HTile"); const auto metadata_depth_a_view = texture_cache.FindDepthTarget(metadata_depth_a_id, metadata_depth_a); const auto metadata_depth_b_view = texture_cache.FindDepthTarget(metadata_depth_b_id, metadata_depth_b); Require(name, "shared-page metadata state", metadata_depth_a_view != nullptr && metadata_depth_b_view != nullptr && !texture_cache.IsMetaCleared(base + metadata_a, 0) && texture_cache.ClearMeta(base + metadata_a) && texture_cache.ClearMeta(base + metadata_b), "shared-page metadata did not retain logical clear state"); auto metadata_alias = MakeLinearDesc( base + metadata_a, sizeof(uint32_t), vk::Format::eR32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); metadata_alias.type = BindingType::RenderTarget; metadata_alias.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto metadata_alias_id = texture_cache.FindImage(metadata_alias, true); const auto metadata_alias_view = texture_cache.FindRenderTarget(metadata_alias_id, metadata_alias); Require( name, "exact alias metadata coexistence", metadata_alias_view != nullptr && texture_cache.GetImage(metadata_depth_a_id).IsGpuModified() && texture_cache.GetImage(metadata_depth_b_id).IsGpuModified() && texture_cache.IsMeta(base + metadata_a) && texture_cache.IsMetaCleared(base + metadata_a, 0) && texture_cache.IsMeta(base + metadata_b) && texture_cache.IsMetaCleared(base + metadata_b, 0), "image discovery retired aliased metadata or its live depth image"); const bool touched_metadata = texture_cache.TouchMeta(base + metadata_a, 0, false); WriteMetadata(context, base + metadata_a, 0x80, 0); Require( name, "registered DCC/HTile separation", touched_metadata && !texture_cache.IsMetaCleared(base + metadata_a, 0), "a PS5 DCC fill changed registered HTile state"); Require( name, "metadata first-touch state", texture_cache.TouchMeta(base + metadata_b, 0, true) && texture_cache.IsMetaCleared(base + metadata_b, 0) && texture_cache.TouchMeta(base + metadata_b, 0, false) && !texture_cache.IsMetaCleared(base + metadata_b, 0), "per-slice metadata update incorrectly required prior GPU ownership"); resources.GetBufferCache().FillBuffer(base + metadata_b, sizeof(uint32_t), 0, false); Require( name, "metadata buffer fill state", texture_cache.IsMetaCleared(base + metadata_b, 0), "BufferCache fill did not publish an exact-address metadata clear"); // R-Type Final 3 reuses an old color allocation inside a new stencil plane, // with the same metadata address changing from DCC to HTile. auto reused_depth = MakeMetadataDepth(0x12300, 0x28000); reused_depth.info.pixel_format = vk::Format::eD32SfloatS8Uint; reused_depth.info.stencil = {base + 0x12200, 0x80}; reused_depth.info.htile_clear_mask = 0; reused_depth.view_info.format = vk::Format::eD32SfloatS8Uint; reused_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; auto reused_color = MakeLinearDesc( base + 0x12240, sizeof(uint32_t), vk::Format::eR8G8B8A8Unorm, Prospero::BufferFormat::k8_8_8_8UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); reused_color.type = BindingType::RenderTarget; reused_color.info.metadata.kind = ImageMetadataKind::Dcc; reused_color.info.metadata.range = {base + 0x28000, 0x1000}; reused_color.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto reused_color_id = texture_cache.FindImage(reused_color); (void)texture_cache.FindRenderTarget(reused_color_id, reused_color); // Repeated CPU stores must remain visible through ordinary buffer ownership. for (uint32_t repeat = 0; repeat < 2; repeat++) { vk::ClearValue painted{}; painted.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f}; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), reused_color_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, painted); std::memset(memory + 0x28000, 0, 0x1000); Require(name, "CPU metadata rediscovery", texture_cache.FindImage(reused_color) == reused_color_id, "metadata writes replaced the live expanded image"); (void)texture_cache.FindRenderTarget(reused_color_id, reused_color); Require(name, "repeated CPU metadata clear", ReadCachedTexel(name, context, reused_color_id) == std::vector{0}, "a same-value CPU metadata write lost its clear after rendering"); } WriteMetadata(context, base + 0x28000, 0x1000, 0); texture_cache.InvalidateMemoryFromGPU(reused_depth.info.stencil.address, reused_depth.info.stencil.size); const auto reused_depth_id = texture_cache.FindImage(reused_depth); (void)texture_cache.FindDepthTarget(reused_depth_id, reused_depth); Require( name, "DCC allocation reused as HTile", TextureCacheTestAccess::Contains(texture_cache, reused_color_id) && texture_cache.GetImage(reused_color_id).IsBufferModified() && !texture_cache.GetImage(reused_color_id).IsGpuModified() && !texture_cache.IsMetaCleared(base + 0x28000, 0) && texture_cache.ClearMeta(base + 0x28000) && texture_cache.TouchMeta(base + 0x28000, 0, false), "depth binding retained incompatible DCC clear state"); TextureCacheTestAccess::DeleteImage(texture_cache, reused_color_id); (void)texture_cache.FindDepthTarget(reused_depth_id, reused_depth); Require( name, "reused metadata owner retirement", !TextureCacheTestAccess::Contains(texture_cache, reused_color_id) && texture_cache.IsMeta(base + 0x28000) && !texture_cache.IsMetaCleared(base + 0x28000, 0) && texture_cache.IsMetaCleared(base + 0x28000, 1), "retiring the old DCC image erased the live HTile slice state"); texture_cache.UnmapMemory(reused_depth.info.data.address, reused_depth.info.data.size); Require(name, "reused metadata final retirement", !texture_cache.IsMeta(base + 0x28000), "retiring the HTile image left its metadata registered"); constexpr uint64_t partial_unmap_image_offset = 0x2700000; auto partial_unmap_image = MakeLinearDesc(base + partial_unmap_image_offset, 0x2000, vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {2048, 1, 1}, 1, 4, 1); const auto partial_unmap_image_id = texture_cache.FindImage(partial_unmap_image); texture_cache.UnmapMemory(partial_unmap_image.info.data.address, 0x1000); Require(name, "partial image unmap tracking", partial_unmap_image_id && !texture_cache.FindImageFromRange( partial_unmap_image.info.data.address, 0x2000, false), "partial unmap left the deleted image's mapped tail tracked"); constexpr uint64_t unformatted_alias_offset = 0x2500000; auto unformatted_alias = MakeLinearDesc(base + unformatted_alias_offset, sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto unformatted_alias_image = texture_cache.FindImage(unformatted_alias); (void)texture_cache.FindTexture(unformatted_alias_image, unformatted_alias); texture_cache.MarkGpuWritten(unformatted_alias_image); auto unformatted_alias_buffer = resources.GetBufferCache().ObtainBuffer( unformatted_alias.info.data.address, unformatted_alias.info.data.size, false); Require( name, "unformatted buffer/image alias", unformatted_alias_buffer.first != nullptr && texture_cache.GetImage(unformatted_alias_image).IsGpuModified() && !texture_cache.GetImage(unformatted_alias_image) .IsBufferModified(), "read-only unformatted buffer alias did not follow ordinary " "BufferCache acquisition"); constexpr uint64_t layered_offset = 0x14000; constexpr uint64_t layered_guest_size = 0x800; const std::array layered_values{0.0f, 0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0.75f, 0.875f, 1.0f, 0.0625f}; std::memset(memory + layered_offset, 0, layered_guest_size); const auto layered_layout = TextureCalcUploadLayout( Prospero::BufferFormat::k32Float, 2, 2, 2, 2, Prospero::TileMode::kLinear, layered_guest_size, true, false, "UnifiedTextureCacheFlow"); const auto layered_upload_regions = TextureBuildImageCopies(layered_layout); Require(name, "layered guest layout", layered_upload_regions.size() == 4, "unexpected layered/mipped upload-region count"); for (const auto ®ion : layered_upload_regions) { for (uint32_t y = 0; y < region.imageExtent.height; y++) { for (uint32_t x = 0; x < region.imageExtent.width; x++) { const uint32_t logical = region.imageSubresource.mipLevel == 0 ? region.imageSubresource.baseArrayLayer * 4 + y * 2 + x : 8 + region.imageSubresource.baseArrayLayer; const uint64_t byte_offset = region.bufferOffset + (static_cast(y) * region.bufferRowLength + x) * sizeof(float); Require(name, "layered guest layout bounds", byte_offset + sizeof(float) <= layered_guest_size, "layered guest texel lies outside its backing range"); std::memcpy(memory + layered_offset + byte_offset, &layered_values[logical], sizeof(float)); } } } auto layered_color = MakeLinearDesc( base + layered_offset, layered_guest_size, vk::Format::eR32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {2, 2, 1}, 2, 4, 1); layered_color.info.resources.levels = 2; layered_color.info.mip_layout[0] = {0, 32, 2, 2}; layered_color.info.mip_layout[1] = {32, 8, 1, 1}; layered_color.view_info.level_count = 2; const auto layered_color_image = texture_cache.FindImage(layered_color); (void)texture_cache.FindTexture(layered_color_image, layered_color); texture_cache.MarkGpuWritten(layered_color_image); auto layered_depth = layered_color; layered_depth.type = BindingType::DepthTarget; layered_depth.info.resources = {1, 4}; layered_depth.view_info.level_count = 1; layered_depth.info.pixel_format = vk::Format::eD32Sfloat; layered_depth.view_info.format = vk::Format::eD32Sfloat; layered_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth; layered_depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto layered_depth_image = texture_cache.FindImage(layered_depth); const auto &layered_native = texture_cache.GetImage(layered_depth_image); Require(name, "layered mipped depth alias", layered_depth_image != layered_color_image && layered_native.backing.layers == 2 && layered_native.backing.mip_levels == 2 && layered_native.info.resources == layered_color.info.resources && layered_native.IsGpuModified(), "depth/color conversion did not use the lexicographic " "resource maximum"); const volatile auto layered_guest_byte = *reinterpret_cast(memory + layered_offset); (void)layered_guest_byte; Require(name, "layered depth write-only read policy", texture_cache.GetImage(layered_depth_image).IsGpuModified(), "a CPU read invalidated layered depth GPU ownership"); constexpr uint64_t replacement_offset = 0x18000; constexpr uint64_t replacement_size = 0x2000; constexpr uint32_t replacement_pixel = 0xcafebabeu; constexpr uint32_t replacement_tail = 0x0badf00du; std::memset(memory + replacement_offset, 0, replacement_size); std::memcpy(memory + replacement_offset, &replacement_pixel, sizeof(replacement_pixel)); std::memcpy(memory + replacement_offset + replacement_size - sizeof(replacement_tail), &replacement_tail, sizeof(replacement_tail)); auto replacement_source = MakeLinearDesc(base + replacement_offset, sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto replacement_source_image = texture_cache.FindImage(replacement_source); (void)texture_cache.FindTexture(replacement_source_image, replacement_source); texture_cache.MarkGpuWritten(replacement_source_image); auto replacement_desc = MakeLinearDesc(base + replacement_offset, replacement_size, vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 2, 4, 1); const auto replacement = texture_cache.FindImage(replacement_desc); const bool replacement_changed = replacement != replacement_source_image; const bool replacement_gpu_before = texture_cache.GetImage(replacement).IsGpuModified(); const bool replacement_fault = resources.HandleFault( PageFaultAccess::Write, base + replacement_offset + 0x1000); const bool replacement_gpu_after = texture_cache.GetImage(replacement).IsGpuModified(); uint32_t replacement_pixel_after = 0; uint32_t replacement_tail_after = 0; std::memcpy(&replacement_pixel_after, memory + replacement_offset, sizeof(replacement_pixel_after)); std::memcpy(&replacement_tail_after, memory + replacement_offset + replacement_size - sizeof(replacement_tail_after), sizeof(replacement_tail_after)); Require(name, "replacement tracking", replacement_changed && replacement_gpu_before && replacement_fault && replacement_gpu_after && texture_cache.GetImage(replacement).IsDefinitelyCpuDirty() && replacement_pixel_after == replacement_pixel && replacement_tail_after == replacement_tail, "replacement write invalidation lost native authority or guest " "backing"); const uint32_t compressed_value = 0x55667788u; std::memcpy(memory + 0xc000, &compressed_value, sizeof(compressed_value)); auto compressed_desc = MakeLinearDesc( base + 0xc000, sizeof(compressed_value), vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); compressed_desc.type = BindingType::RenderTarget; compressed_desc.info.metadata.kind = ImageMetadataKind::Dcc; compressed_desc.info.metadata.range = {base + 0xd000, 0}; compressed_desc.info.metadata.compression = VideoOutCompression::Dcc256_256_0; compressed_desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto compressed_image = texture_cache.FindImage(compressed_desc); (void)texture_cache.FindRenderTarget(compressed_image, compressed_desc); uint32_t compressed_cpu_read = 0; std::memcpy(&compressed_cpu_read, memory + 0xc000, sizeof(compressed_cpu_read)); Require(name, "compressed write-only read policy", compressed_cpu_read == compressed_value && texture_cache.GetImage(compressed_image).IsGpuModified(), "compressed image incorrectly claimed a CPU read fault"); Require(name, "compressed download rejection", !TextureCacheTestAccess::TryDownload(texture_cache, compressed_image) && texture_cache.GetImage(compressed_image).IsGpuModified() && !texture_cache.GetImage(compressed_image).IsBufferModified(), "a compressed image escaped the unified download guard"); auto compressed_video_desc = compressed_desc; compressed_video_desc.type = BindingType::VideoOut; compressed_video_desc.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto compressed_video = texture_cache.FindImage(compressed_video_desc); const auto &compressed_video_image = texture_cache.GetImage(compressed_video); Require( name, "cache-only video-out discovery", compressed_video == compressed_image && !compressed_video_image.binding.is_bound && !compressed_video_image.binding.is_target && !compressed_video_image.binding.needs_rebind && !compressed_video_image.usage.video_out, "FindImage claimed caller-owned video-out usage or binding state"); constexpr uint64_t mixed_source_offset = 0x20000; constexpr uint64_t mixed_source_size = 0x1004; constexpr uint32_t mixed_source_width = 1025; constexpr uint32_t mixed_cpu_value = 0x1234abcdu; constexpr uint32_t mixed_gpu_value = 0x9876fedcu; auto mixed_owner = resources.GetBufferCache().ObtainBuffer( base + mixed_source_offset, mixed_source_size, true); Require(name, "mixed-page source allocation", mixed_owner.first != nullptr, "mixed CPU/GPU image source did not create a containing buffer"); Require(name, "mixed-page CPU write fault", resources.HandleFault(PageFaultAccess::Write, base + mixed_source_offset), "mixed image source could not dirty its first page"); std::memcpy(memory + mixed_source_offset, &mixed_cpu_value, sizeof(mixed_cpu_value)); resources.GetBufferCache().FillBuffer(base + mixed_source_offset + 0x1000, sizeof(mixed_gpu_value), mixed_gpu_value, false); auto mixed_source_desc = MakeLinearDesc( base + mixed_source_offset, mixed_source_size, vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {mixed_source_width, 1, 1}, 1, 4, 1); const auto mixed_source_image = texture_cache.FindImage(mixed_source_desc); (void)texture_cache.FindTexture(mixed_source_image, mixed_source_desc); Require(name, "mixed-page image upload", !texture_cache.GetImage(mixed_source_image).IsGpuModified(), "clean Buffer source manufactured GPU image ownership"); auto mixed_source_readback = CreateHostBuffer( name, mixed_source_size, vk::BufferUsageFlagBits::eTransferDst, std::vector(mixed_source_size / sizeof(u32), 0)); auto &mixed_source_native = texture_cache.GetImage(mixed_source_image); mixed_source_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, command.Handle()); vk::BufferImageCopy mixed_source_copy{}; mixed_source_copy.bufferRowLength = mixed_source_width; mixed_source_copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; mixed_source_copy.imageSubresource.layerCount = 1; mixed_source_copy.imageExtent = {mixed_source_width, 1, 1}; command.Handle().copyImageToBuffer(mixed_source_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, mixed_source_readback.buffer, 1, &mixed_source_copy); HostReadBarrier(mixed_source_readback.buffer, mixed_source_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); constexpr uint64_t bridged_gpu_offset = 0x101000; constexpr uint64_t bridged_cpu_offset = 0x105000; constexpr uint64_t bridged_texel_size = 16; constexpr uint64_t bridged_source_size = bridged_cpu_offset - bridged_gpu_offset + bridged_texel_size; constexpr uint32_t bridged_source_width = bridged_source_size / bridged_texel_size; constexpr uint32_t bridged_gpu_value = 0x10293847u; constexpr uint32_t bridged_cpu_value = 0x56473829u; constexpr uint32_t bridged_gpu_guest_value = 0xa5a5a5a5u; std::array bridged_cpu_values{}; bridged_cpu_values.fill(bridged_cpu_value); std::memcpy(memory + bridged_cpu_offset, bridged_cpu_values.data(), sizeof(bridged_cpu_values)); auto bridged_cpu_owner = resources.GetBufferCache().ObtainBuffer( base + bridged_cpu_offset, bridged_texel_size, true); Require(name, "bridged CPU owner allocation", bridged_cpu_owner.first != nullptr, "bridged image source did not create its CPU-side cache owner"); resources.GetBufferCache().FillBuffer(base + bridged_cpu_offset, bridged_texel_size, 0, false); Require( name, "bridged CPU write fault", resources.HandleFault(PageFaultAccess::Write, base + bridged_cpu_offset), "bridged image source could not transfer its second owner to CPU"); std::memcpy(memory + bridged_cpu_offset, bridged_cpu_values.data(), sizeof(bridged_cpu_values)); std::array bridged_gpu_guest_values{}; bridged_gpu_guest_values.fill(bridged_gpu_guest_value); std::memcpy(memory + bridged_gpu_offset, bridged_gpu_guest_values.data(), sizeof(bridged_gpu_guest_values)); auto bridged_gpu_owner = resources.GetBufferCache().ObtainBuffer( base + bridged_gpu_offset, bridged_texel_size, true); Require(name, "bridged GPU owner allocation", bridged_gpu_owner.first != nullptr && bridged_gpu_owner.first != bridged_cpu_owner.first, "bridged image source did not retain two disjoint cache owners"); resources.GetBufferCache().FillBuffer(base + bridged_gpu_offset, bridged_texel_size, bridged_gpu_value, false); Require(name, "bridged ownership split", resources.GetBufferCache().HasGpuDirtyBytes( base + bridged_gpu_offset, bridged_texel_size) && !resources.GetBufferCache().HasGpuDirtyBytes( base + bridged_cpu_offset, bridged_texel_size) && resources.GetBufferCache().IsRegionCpuModified( base + bridged_cpu_offset, bridged_texel_size), "bridged image source did not retain distinct GPU and CPU " "ownership domains"); auto bridged_source_desc = MakeLinearDesc( base + bridged_gpu_offset, bridged_source_size, vk::Format::eR32G32B32A32Uint, Prospero::BufferFormat::k32_32_32_32UInt, Prospero::ImageType::kColor2D, {bridged_source_width, 1, 1}, 1, bridged_texel_size, 1); const auto bridged_source_image = texture_cache.FindImage(bridged_source_desc); (void)texture_cache.FindTexture(bridged_source_image, bridged_source_desc); auto bridged_source_readback = CreateHostBuffer( name, bridged_source_size, vk::BufferUsageFlagBits::eTransferDst, std::vector(bridged_source_size / sizeof(u32), 0)); auto &bridged_source_native = texture_cache.GetImage(bridged_source_image); bridged_source_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, scheduler.Current().Handle()); vk::BufferImageCopy bridged_source_copy{}; bridged_source_copy.bufferRowLength = bridged_source_width; bridged_source_copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; bridged_source_copy.imageSubresource.layerCount = 1; bridged_source_copy.imageExtent = {bridged_source_width, 1, 1}; scheduler.Current().Handle().copyImageToBuffer( bridged_source_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, bridged_source_readback.buffer, 1, &bridged_source_copy); HostReadBarrier(bridged_source_readback.buffer, bridged_source_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); constexpr uint64_t byte_mirror_page_offset = 0x24000; constexpr uint64_t byte_mirror_offset = byte_mirror_page_offset + 1; const std::array byte_mirror_guest{0xacu, 0x5au, 0xbdu, 0xceu}; std::memcpy(memory + byte_mirror_page_offset, byte_mirror_guest.data(), byte_mirror_guest.size()); auto byte_mirror_desc = MakeLinearDesc(base + byte_mirror_offset, 1, vk::Format::eR8Unorm, Prospero::BufferFormat::k8UNorm, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 1, 1); const auto byte_mirror_image = texture_cache.FindImage(byte_mirror_desc); (void)texture_cache.FindTexture(byte_mirror_image, byte_mirror_desc); texture_cache.MarkGpuWritten(byte_mirror_image); auto byte_mirror = resources.GetBufferCache().ObtainBuffer( base + byte_mirror_offset, 1, false, true); Require( name, "byte image mirror", byte_mirror.first != nullptr && !texture_cache.GetImage(byte_mirror_image).IsBufferModified() && texture_cache.GetImage(byte_mirror_image).IsGpuModified(), "one-byte image copy transferred cache ownership"); const std::array bgra16_guest{0x3c00u, 0x4000u, 0x4200u, 0x4400u}; std::memcpy(memory + 0xb000, bgra16_guest.data(), sizeof(bgra16_guest)); auto bgra16_desc = MakeLinearDesc( base + 0xb000, sizeof(bgra16_guest), vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 8, 1); bgra16_desc.type = BindingType::RenderTarget; bgra16_desc.info.bgra16 = true; bgra16_desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; const auto bgra16_image = texture_cache.FindImage(bgra16_desc); (void)texture_cache.FindRenderTarget(bgra16_image, bgra16_desc); auto bgra16_readback = CreateHostBuffer(name, sizeof(bgra16_guest), vk::BufferUsageFlagBits::eTransferDst, {0, 0}); auto &bgra16_native = texture_cache.GetImage(bgra16_image); bgra16_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, command.Handle()); vk::BufferImageCopy bgra16_copy{}; bgra16_copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; bgra16_copy.imageSubresource.layerCount = 1; bgra16_copy.imageExtent = {1, 1, 1}; command.Handle().copyImageToBuffer( bgra16_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, bgra16_readback.buffer, 1, &bgra16_copy); HostReadBarrier(bgra16_readback.buffer, bgra16_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); auto layered_readback = CreateHostBuffer( name, sizeof(layered_values), vk::BufferUsageFlagBits::eTransferDst, std::vector(sizeof(layered_values) / sizeof(u32), 0)); auto &layered_depth_native = texture_cache.GetImage(layered_depth_image); layered_depth_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, command.Handle()); std::array layered_copies{}; layered_copies[0].bufferOffset = 0; layered_copies[0].imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth; layered_copies[0].imageSubresource.mipLevel = 0; layered_copies[0].imageSubresource.layerCount = 2; layered_copies[0].imageExtent = {2, 2, 1}; layered_copies[1].bufferOffset = 32; layered_copies[1].imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth; layered_copies[1].imageSubresource.mipLevel = 1; layered_copies[1].imageSubresource.layerCount = 2; layered_copies[1].imageExtent = {1, 1, 1}; command.Handle().copyImageToBuffer( layered_depth_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, layered_readback.buffer, static_cast(layered_copies.size()), layered_copies.data()); HostReadBarrier(layered_readback.buffer, layered_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); vk::ShaderModuleCreateInfo module_info{}; module_info.sType = vk::StructureType::eShaderModuleCreateInfo; module_info.codeSize = sizeof(GPU_TEST_MS_DEPTH_SPV); module_info.pCode = GPU_TEST_MS_DEPTH_SPV; vk::ShaderModule ms_depth_module = nullptr; RequireVk( name, "MS depth content", m_device.createShaderModule(&module_info, nullptr, &ms_depth_module), "vkCreateShaderModule"); std::array observer_bindings{}; observer_bindings[0].binding = 0; observer_bindings[0].descriptorType = vk::DescriptorType::eSampledImage; observer_bindings[0].descriptorCount = 1; observer_bindings[0].stageFlags = vk::ShaderStageFlagBits::eCompute; observer_bindings[1].binding = 1; observer_bindings[1].descriptorType = vk::DescriptorType::eStorageBuffer; observer_bindings[1].descriptorCount = 1; observer_bindings[1].stageFlags = vk::ShaderStageFlagBits::eCompute; vk::DescriptorSetLayoutCreateInfo observer_layout_info{}; observer_layout_info.sType = vk::StructureType::eDescriptorSetLayoutCreateInfo; observer_layout_info.bindingCount = static_cast(observer_bindings.size()); observer_layout_info.pBindings = observer_bindings.data(); vk::DescriptorSetLayout observer_set_layout = nullptr; RequireVk(name, "MS depth content", m_device.createDescriptorSetLayout( &observer_layout_info, nullptr, &observer_set_layout), "vkCreateDescriptorSetLayout"); const vk::PushConstantRange observer_push{ vk::ShaderStageFlagBits::eCompute, 0, sizeof(uint32_t)}; vk::PipelineLayoutCreateInfo observer_pipeline_layout_info{}; observer_pipeline_layout_info.sType = vk::StructureType::ePipelineLayoutCreateInfo; observer_pipeline_layout_info.setLayoutCount = 1; observer_pipeline_layout_info.pSetLayouts = &observer_set_layout; observer_pipeline_layout_info.pushConstantRangeCount = 1; observer_pipeline_layout_info.pPushConstantRanges = &observer_push; vk::PipelineLayout observer_pipeline_layout = nullptr; RequireVk(name, "MS depth content", m_device.createPipelineLayout(&observer_pipeline_layout_info, nullptr, &observer_pipeline_layout), "vkCreatePipelineLayout"); vk::PipelineShaderStageCreateInfo observer_stage{}; observer_stage.sType = vk::StructureType::ePipelineShaderStageCreateInfo; observer_stage.stage = vk::ShaderStageFlagBits::eCompute; observer_stage.module = ms_depth_module; observer_stage.pName = "main"; vk::ComputePipelineCreateInfo observer_pipeline_info{}; observer_pipeline_info.sType = vk::StructureType::eComputePipelineCreateInfo; observer_pipeline_info.stage = observer_stage; observer_pipeline_info.layout = observer_pipeline_layout; vk::Pipeline observer_depth_pipeline = nullptr; RequireVk( name, "MS depth content", m_device.createComputePipelines(nullptr, 1, &observer_pipeline_info, nullptr, &observer_depth_pipeline), "vkCreateComputePipelines"); const std::array observer_pool_sizes{{ {vk::DescriptorType::eSampledImage, 2}, {vk::DescriptorType::eStorageBuffer, 2}, }}; vk::DescriptorPoolCreateInfo observer_pool_info{}; observer_pool_info.sType = vk::StructureType::eDescriptorPoolCreateInfo; observer_pool_info.maxSets = 2; observer_pool_info.poolSizeCount = static_cast(observer_pool_sizes.size()); observer_pool_info.pPoolSizes = observer_pool_sizes.data(); vk::DescriptorPool observer_pool = nullptr; RequireVk(name, "MS depth content", m_device.createDescriptorPool(&observer_pool_info, nullptr, &observer_pool), "vkCreateDescriptorPool"); const std::array observer_layouts{ observer_set_layout, observer_set_layout}; std::array observer_sets{}; vk::DescriptorSetAllocateInfo observer_allocate{}; observer_allocate.sType = vk::StructureType::eDescriptorSetAllocateInfo; observer_allocate.descriptorPool = observer_pool; observer_allocate.descriptorSetCount = static_cast(observer_sets.size()); observer_allocate.pSetLayouts = observer_layouts.data(); RequireVk(name, "MS depth content", m_device.allocateDescriptorSets(&observer_allocate, observer_sets.data()), "vkAllocateDescriptorSets"); std::array ms_observer_outputs{ CreateHostBuffer(name, 4 * sizeof(u32), vk::BufferUsageFlagBits::eStorageBuffer, std::vector(4, 0)), CreateHostBuffer(name, 4 * sizeof(u32), vk::BufferUsageFlagBits::eStorageBuffer, std::vector(4, 0))}; const std::array observed_images{ms_depth_image, ms_depth_image_4x}; const std::array observed_samples{2, 4}; for (uint32_t index = 0; index < observed_images.size(); index++) { auto &observed = texture_cache.GetImage(observed_images[index]); observed.Transit(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, {}, command.Handle()); ImageViewInfo view_info{}; view_info.format = observed.backing.format; view_info.type = vk::ImageViewType::e2D; view_info.aspect = vk::ImageAspectFlagBits::eDepth; view_info.mapping = {vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eR}; view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto view = observed.FindView(view_info); const vk::DescriptorImageInfo image_info{ nullptr, view, vk::ImageLayout::eShaderReadOnlyOptimal}; const vk::DescriptorBufferInfo buffer_info{ ms_observer_outputs[index].buffer, 0, ms_observer_outputs[index].size}; std::array writes{}; writes[0].sType = vk::StructureType::eWriteDescriptorSet; writes[0].dstSet = observer_sets[index]; writes[0].dstBinding = 0; writes[0].descriptorCount = 1; writes[0].descriptorType = vk::DescriptorType::eSampledImage; writes[0].pImageInfo = &image_info; writes[1].sType = vk::StructureType::eWriteDescriptorSet; writes[1].dstSet = observer_sets[index]; writes[1].dstBinding = 1; writes[1].descriptorCount = 1; writes[1].descriptorType = vk::DescriptorType::eStorageBuffer; writes[1].pBufferInfo = &buffer_info; m_device.updateDescriptorSets(static_cast(writes.size()), writes.data(), 0, nullptr); command.Handle().bindPipeline(vk::PipelineBindPoint::eCompute, observer_depth_pipeline); command.Handle().bindDescriptorSets(vk::PipelineBindPoint::eCompute, observer_pipeline_layout, 0, 1, &observer_sets[index], 0, nullptr); command.Handle().pushConstants( observer_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0, sizeof(observed_samples[index]), &observed_samples[index]); command.Handle().dispatch(1, 1, 1); HostReadBarrier(ms_observer_outputs[index].buffer, ms_observer_outputs[index].size, vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eShaderWrite); } scheduler.Finish(); const auto mip_prefix_words = ReadBuffer( name, mip_prefix_readback, mip_prefix_size / sizeof(uint32_t)); const auto volume_words = ReadBuffer(name, volume_readback, volume_guest.size()); Require(name, "formatted volume content", volume_words[0] == volume_values[0] && volume_words[volume_slice_words] == volume_values[1], "volume synchronization lost a padded depth slice"); DestroyBuffer(&volume_readback); const auto mip_formatted_words = ReadBuffer( name, mip_formatted_readback, mip_guest_size / sizeof(uint32_t)); const auto mip0_word = mip_sizes[0].offset / sizeof(uint32_t); Require(name, "formatted mip prefix content", mip0_word < mip_prefix_words.size() && mip0_word < mip_native.size() && mip_prefix_words[mip0_word] == mip_native[mip0_word], "fitting mip-prefix synchronization copied " "stale guest backing"); Require(name, "formatted Buffer content", mip0_word < mip_formatted_words.size() && mip0_word < mip_native.size() && mip_formatted_words[mip0_word] == mip_native[mip0_word], "formatted Buffer read bypassed authoritative " "native image mip data"); Require(name, "CPU-dirty formatted mirror content", ReadBuffer(name, mirror_cpu_readback, 1) == std::vector{mirror_cpu_value}, "formatted Buffer mirror published stale " "native image bytes"); Require(name, "Buffer-superseded exact content", ReadBuffer(name, exact_buffer_readback, 1) == std::vector{exact_buffer_value}, "exact-format recreation initialized from " "stale guest bytes"); Require(name, "partial-page CPU refresh content", ReadBuffer(name, partial_cpu_refresh_readback, partial_cpu_refresh_expected.size()) == partial_cpu_refresh_expected, "cached buffer uploaded bytes outside the exact " "staged guest range"); const auto mixed_source_words = ReadBuffer( name, mixed_source_readback, mixed_source_size / sizeof(u32)); Require(name, "mixed-page image content", mixed_source_words.front() == mixed_cpu_value && mixed_source_words[0x1000 / sizeof(u32)] == mixed_gpu_value, "mixed CPU/GPU source upload lost one " "ownership domain"); const auto bridged_source_words = ReadBuffer( name, bridged_source_readback, bridged_source_size / sizeof(u32)); Require(name, "bridged-owner image content", bridged_source_words.front() == bridged_gpu_value && bridged_source_words.back() == bridged_cpu_value, "image upload bridging separate cache owners lost a GPU or CPU " "ownership domain"); Require(name, "BGRA16 content", ReadBuffer(name, bgra16_readback, 2) == std::vector{0x40004200u, 0x44003c00u}, "GPU BGRA16 swap did not publish RGBA half-word order"); scheduler.Finish(); const auto layered_words = ReadBuffer(name, layered_readback, layered_values.size()); const std::array layered_expected{ layered_values[0], layered_values[1], layered_values[2], layered_values[3], layered_values[4], layered_values[5], layered_values[6], layered_values[7], layered_values[8], layered_values[9]}; bool layered_content = layered_words.size() == layered_expected.size(); for (uint32_t index = 0; layered_content && index < layered_expected.size(); index++) { layered_content &= layered_words[index] == std::bit_cast(layered_expected[index]); } Require(name, "layered mipped depth content", layered_content, "depth/color conversion changed a mip or array-layer value"); const std::array, 2> expected_ms_depth{{ {0x2000 / 65535.0f, 0xe000 / 65535.0f, 0.0f, 0.0f}, {0.0f, 0x4000 / 65535.0f, 0x8000 / 65535.0f, 1.0f}, }}; for (uint32_t image = 0; image < expected_ms_depth.size(); image++) { const auto words = ReadBuffer(name, ms_observer_outputs[image], 4); bool content = words.size() == 4; for (uint32_t sample = 0; content && sample < observed_samples[image]; sample++) { const float actual = std::bit_cast(words[sample]); content &= std::abs(actual - expected_ms_depth[image][sample]) <= 1.5f / 65535.0f; } Require(name, image == 0 ? "two-sample depth content" : "four-sample depth content", content, "color-to-multisample-depth changed a packed sample value"); } auto unrelated_stencil_alias = MakeLinearDesc( base + second_stencil_offset, ms_stencil_size, vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {static_cast(ms_stencil_size / sizeof(uint32_t)), 1, 1}, 1, 4, 1); const auto unrelated_stencil_image = texture_cache.FindImage(unrelated_stencil_alias); Require(name, "stencil association filtering", unrelated_stencil_image && unrelated_stencil_image != ms_depth_image && !texture_cache.GetImage(unrelated_stencil_image).depth_id, "an unrelated color alias was hijacked by the stencil " "association record"); texture_cache.UnmapMemory(base + second_stencil_offset, ms_stencil_size); Require(name, "stencil proxy unmap", !texture_cache.FindImageFromRange(base + second_stencil_offset, ms_stencil_size, false) && texture_cache.GetImage(ms_depth_image).backing.image == ms_depth_backing, "stencil unmap removed the depth backing or retained its proxy"); const auto reassociated_depth = texture_cache.FindImage(switched_ms_depth); TextureCacheTestAccess::AssociateStencil( texture_cache, reassociated_depth, switched_ms_depth.info.stencil); const auto reassociated_proxy = texture_cache.FindImageFromRange( base + second_stencil_offset, ms_stencil_size, false); Require(name, "stencil proxy reassociation", reassociated_depth == ms_depth_image && reassociated_proxy && texture_cache.GetImage(reassociated_proxy).depth_id == ms_depth_image, "stencil proxy did not re-associate with the surviving depth"); texture_cache.UnmapMemory(ms_depth_desc.info.data.address, ms_depth_desc.info.data.size); Require(name, "depth unmap proxy retirement", !texture_cache.FindImageFromRange(base + second_stencil_offset, ms_stencil_size, false), "unmapping depth retained its re-associated stencil proxy"); auto reused_stencil_color = MakeLinearDesc( base + 0x2200000, 0x40000, vk::Format::eR8Unorm, Prospero::BufferFormat::k8UNorm, Prospero::ImageType::kColor2D, {480, 408, 1}, 1, 1, 1); const auto reused_stencil_color_id = texture_cache.FindImage(reused_stencil_color); auto reused_stencil_depth = MakeLinearDesc( base + 0x2240000, 0x10000, vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {128, 128, 1}, 1, 4, 1); reused_stencil_depth.type = BindingType::DepthTarget; reused_stencil_depth.info.stencil = {reused_stencil_color.info.data.address, 0x10000}; reused_stencil_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; reused_stencil_depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto reused_stencil_depth_id = texture_cache.FindImage(reused_stencil_depth); TextureCacheTestAccess::AssociateStencil( texture_cache, reused_stencil_depth_id, reused_stencil_depth.info.stencil); const auto reused_stencil_proxy = texture_cache.FindImageFromRange( reused_stencil_depth.info.stencil.address, reused_stencil_depth.info.stencil.size, false); Require(name, "stencil address reuse preserves color", reused_stencil_proxy && reused_stencil_proxy != reused_stencil_color_id && !texture_cache.GetImage(reused_stencil_color_id).depth_id && texture_cache.GetImage(reused_stencil_proxy).depth_id == reused_stencil_depth_id && texture_cache.GetImage(reused_stencil_proxy).info.data == reused_stencil_depth.info.stencil && texture_cache.GetImage(reused_stencil_proxy).info.extent == reused_stencil_depth.info.extent, "a smaller stencil plane took ownership of an existing R8 image"); vk::ClearValue reused_stencil_color_clear{}; reused_stencil_color_clear.color.float32 = std::array{1.0f, 0.0f, 0.0f, 0.0f}; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), reused_stencil_color_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, reused_stencil_color_clear); Require(name, "reused stencil plane clear", texture_cache.ClearImageFromBuffer( scheduler.Current(), reused_stencil_depth.info.stencil.address, reused_stencil_depth.info.stencil.size, 0x5a5a5a5au) && texture_cache.GetImage(reused_stencil_depth_id).IsGpuModified(), "the separate stencil association did not resolve to depth"); Require(name, "color clear after stencil address reuse", ReadCachedTexel(name, context, reused_stencil_color_id, {}, {4, 1, 1}) == std::vector{0xffffffffu}, "stencil address reuse changed the native R8 clear contents"); texture_cache.UnmapMemory(reused_stencil_depth.info.data.address, reused_stencil_depth.info.data.size); texture_cache.UnmapMemory(reused_stencil_color.info.data.address, reused_stencil_color.info.data.size); constexpr uint64_t exact_image_offset = 0x334100; constexpr uint64_t dirty_sibling_offset = 0x334200; constexpr uint32_t dirty_sibling_value = 0xc001d00du; auto dirty_sibling = resources.GetBufferCache().ObtainBuffer( base + dirty_sibling_offset, sizeof(dirty_sibling_value), true); Require(name, "same-page dirty sibling allocation", dirty_sibling.first != nullptr, "failed to create the disjoint same-page Buffer owner"); resources.GetBufferCache().FillBuffer(base + dirty_sibling_offset, sizeof(dirty_sibling_value), dirty_sibling_value, false); auto exact_image_desc = MakeLinearDesc(base + exact_image_offset, sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto exact_image = texture_cache.FindImage(exact_image_desc); (void)texture_cache.FindTexture(exact_image, exact_image_desc); texture_cache.MarkGpuWritten(exact_image); Require(name, "same-page exact alias ownership", resources.GetBufferCache().HasGpuDirtyBytes( base + dirty_sibling_offset, sizeof(dirty_sibling_value)) && !resources.GetBufferCache().HasGpuDirtyBytes( base + exact_image_offset, sizeof(uint32_t)) && texture_cache.GetImage(exact_image).IsGpuModified(), "image ownership discarded or conflicted with disjoint dirty " "Buffer bytes on the same tracker page"); auto exact_buffer = resources.GetBufferCache().ObtainBuffer( base + exact_image_offset, sizeof(uint32_t), true); Require(name, "overlapping buffer ownership allocation", exact_buffer.first != nullptr, "failed to create the overlapping dirty Buffer owner"); exact_buffer.first->Fill(exact_buffer.second, sizeof(uint32_t), dirty_sibling_value); Libs::Graphics::Buffer exact_download( m_runtime_context, scheduler, MemoryUsage::DeviceLocal, base + exact_image_offset, AllFlags, sizeof(uint32_t)); Require(name, "overlapping buffer ownership rejection", !texture_cache.FindImageFromRange(base + exact_image_offset, sizeof(uint32_t)) && !BufferCacheTestAccess::SynchronizeBufferFromImage( resources.GetBufferCache(), exact_download, base + exact_image_offset, sizeof(uint32_t)), "dirty Buffer bytes did not block the stale image source"); resources.GetBufferCache().ReadMemory(base + exact_image_offset, sizeof(uint32_t)); resources.GetBufferCache().ReadMemory(base + dirty_sibling_offset, sizeof(dirty_sibling_value)); constexpr std::array gc_image_offsets{0x330000, 0x332000}; constexpr std::array gc_image_values{0x76543210u, 0x89abcdefu}; constexpr std::array gc_stale_values{0x10293847u, 0x56473829u}; for (size_t index = 0; index < gc_image_offsets.size(); index++) { std::memcpy(memory + gc_image_offsets[index], &gc_image_values[index], sizeof(uint32_t)); } auto gc_image_desc_a = MakeLinearDesc(base + gc_image_offsets[0], sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); auto gc_image_desc_b = gc_image_desc_a; gc_image_desc_b.info.data.address = base + gc_image_offsets[1]; auto clean_buffer_alias = resources.GetBufferCache().ObtainBuffer( gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size, true); Require(name, "clean pre-image buffer alias", clean_buffer_alias.first != nullptr, "failed to create the clean cached Buffer alias"); resources.GetBufferCache().ReadMemory(gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size); const std::array gc_images{texture_cache.FindImage(gc_image_desc_a), texture_cache.FindImage(gc_image_desc_b)}; Require( name, "non-GPU image range validity", !texture_cache.FindImageFromRange(gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size), "FindImageFromRange accepted an image without current GPU " "contents"); (void)texture_cache.FindTexture(gc_images[0], gc_image_desc_a); (void)texture_cache.FindTexture(gc_images[1], gc_image_desc_b); for (const auto image : gc_images) { texture_cache.MarkGpuWritten(image); } Require(name, "GPU image range validity", texture_cache.FindImageFromRange( gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size) == gc_images[0], "FindImageFromRange rejected a clean GPU-current image"); texture_cache.GetImage(gc_images[0]) .InvalidateCpuWrite(gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size); Require( name, "CPU-dirty image range validity", !texture_cache.FindImageFromRange(gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size), "FindImageFromRange accepted CPU-dirty native contents"); texture_cache.MarkGpuWritten(gc_images[0]); for (size_t index = 0; index < gc_image_offsets.size(); index++) { Libs::LibKernel::Memory::WriteBacking(base + gc_image_offsets[index], &gc_stale_values[index], sizeof(uint32_t)); } TextureCacheTestAccess::ConfigureGarbageCollection( texture_cache, gc_images, 17, UINT64_MAX); texture_cache.RunGarbageCollector(); Require(name, "downloadable image pre-pressure retention", std::ranges::all_of(gc_images, [&](ImageId image) { return TextureCacheTestAccess::Contains( texture_cache, image); }), "GC retired safely downloadable GPU images before pressure"); const auto gc_batch_tick = scheduler.CurrentTick(); TextureCacheTestAccess::ConfigureGarbageCollection(texture_cache, gc_images, 81, 0); texture_cache.RunGarbageCollector(); std::array gc_before_completion{}; for (size_t index = 0; index < gc_image_offsets.size(); index++) { Libs::LibKernel::Memory::TryReadBacking(base + gc_image_offsets[index], &gc_before_completion[index], sizeof(uint32_t)); } Require(name, "batched image pressure retirement", std::ranges::none_of(gc_images, [&](ImageId image) { return TextureCacheTestAccess::Contains( texture_cache, image); }) && scheduler.CurrentTick() == gc_batch_tick && gc_before_completion == gc_stale_values, "GC submitted per image or published a readback before GPU " "completion"); scheduler.Finish(); scheduler.DrainPriorityOperations(); auto refreshed_buffer_alias = resources.GetBufferCache().ObtainBuffer( gc_image_desc_a.info.data.address, gc_image_desc_a.info.data.size, false); Require(name, "post-publication Buffer reacquire", refreshed_buffer_alias.first != nullptr, "Buffer lookup failed after the retired image publication"); std::array gc_after_completion{}; for (size_t index = 0; index < gc_image_offsets.size(); index++) { Libs::LibKernel::Memory::TryReadBacking(base + gc_image_offsets[index], &gc_after_completion[index], sizeof(uint32_t)); } Require(name, "batched image readback publication", scheduler.CurrentTick() == gc_batch_tick + 1 && gc_after_completion == gc_image_values, "one submission did not publish both deferred image readbacks"); Require(name, "refreshed post-image buffer alias", refreshed_buffer_alias.first != nullptr, "failed to reacquire the cached Buffer alias after image " "publication"); auto alias_readback = CreateHostBuffer( name, sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {0}); const vk::BufferCopy alias_copy{refreshed_buffer_alias.second, 0, sizeof(uint32_t)}; scheduler.Current().Handle().copyBuffer( refreshed_buffer_alias.first->Handle(), alias_readback.buffer, 1, &alias_copy); HostReadBarrier(alias_readback.buffer, alias_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); Require(name, "post-image Buffer alias content", ReadBuffer(name, alias_readback, 1) == std::vector{gc_image_values[0]}, "a clean cached Buffer alias survived image ownership with " "stale native bytes"); DestroyBuffer(&alias_readback); constexpr uint64_t submit_readback_offset = 0x336000; constexpr uint32_t submit_readback_value = 0x13579bdfu; constexpr uint32_t submit_readback_stale = 0x2468ace0u; auto submit_readback_desc = MakeLinearDesc(base + submit_readback_offset, sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); submit_readback_desc.type = BindingType::Storage; submit_readback_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage; TextureCacheTestAccess::SetLinearReadback(texture_cache, true); const auto submit_readback_image = texture_cache.FindImage(submit_readback_desc); Require(name, "linear submit image clear", texture_cache.ClearImageFromBuffer( command, submit_readback_desc.info.data.address, submit_readback_desc.info.data.size, submit_readback_value), "failed to create GPU-current linear storage contents"); TextureCacheTestAccess::TrackDownload(texture_cache, submit_readback_image); Libs::LibKernel::Memory::WriteBacking( submit_readback_desc.info.data.address, &submit_readback_stale, sizeof(submit_readback_stale)); const auto submit_readback_tick = scheduler.CurrentTick(); texture_cache.ProcessDownloadImages(); TextureCacheTestAccess::SetLinearReadback(texture_cache, false); uint32_t submit_before_completion = 0; Libs::LibKernel::Memory::TryReadBacking( submit_readback_desc.info.data.address, &submit_before_completion, sizeof(submit_before_completion)); Require(name, "linear submit deferred publication", submit_before_completion == submit_readback_stale && scheduler.CurrentTick() == submit_readback_tick, "submit-time linear readback published synchronously"); scheduler.Finish(); scheduler.DrainPriorityOperations(); uint32_t submit_after_completion = 0; Libs::LibKernel::Memory::TryReadBacking( submit_readback_desc.info.data.address, &submit_after_completion, sizeof(submit_after_completion)); Require(name, "linear submit readback reuse", submit_after_completion == submit_readback_value && TextureCacheTestAccess::Contains(texture_cache, submit_readback_image) && texture_cache.GetImage(submit_readback_image).IsGpuModified(), "submit-time readback did not publish or retained no reusable " "GPU-current image"); constexpr uint64_t linear_depth_offset = 0x33a000; constexpr uint64_t linear_stencil_offset = 0x33b000; constexpr uint32_t linear_depth_width = 3; constexpr uint32_t linear_depth_height = 2; constexpr uint32_t linear_depth_pitch = 4; constexpr uint32_t linear_depth_layers = 2; constexpr uint32_t linear_depth_words = linear_depth_pitch * linear_depth_height * linear_depth_layers; constexpr uint32_t linear_depth_clear = 0x3f400000u; std::array linear_depth_guest{}; for (uint32_t index = 0; index < linear_depth_words; index++) { linear_depth_guest[index] = 0x51000000u + index; } constexpr std::array linear_stencil_guest{ 0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28}; std::memcpy(memory + linear_depth_offset, linear_depth_guest.data(), sizeof(linear_depth_guest)); std::memcpy(memory + linear_stencil_offset, linear_stencil_guest.data(), sizeof(linear_stencil_guest)); auto linear_depth_desc = MakeLinearDesc( base + linear_depth_offset, sizeof(linear_depth_guest), vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {linear_depth_width, linear_depth_height, 1}, linear_depth_layers, 4, 1); linear_depth_desc.type = BindingType::DepthTarget; linear_depth_desc.info.pitch = linear_depth_pitch; linear_depth_desc.info.stencil = {base + linear_stencil_offset, sizeof(linear_stencil_guest)}; linear_depth_desc.info.mip_layout[0] = {0, sizeof(linear_depth_guest), linear_depth_pitch, linear_depth_height}; linear_depth_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; linear_depth_desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto linear_depth_image = texture_cache.FindImage(linear_depth_desc); Require(name, "linear depth native clear", texture_cache.ClearImageFromBuffer( command, linear_depth_desc.info.data.address, linear_depth_desc.info.data.size, linear_depth_clear), "failed to create GPU-current padded linear depth contents"); TextureCacheTestAccess::SetLinearReadback(texture_cache, true); TextureCacheTestAccess::TrackDownload(texture_cache, linear_depth_image); const auto linear_depth_tick = scheduler.CurrentTick(); texture_cache.ProcessDownloadImages(); TextureCacheTestAccess::SetLinearReadback(texture_cache, false); std::array linear_depth_before{}; std::memcpy(linear_depth_before.data(), memory + linear_depth_offset, sizeof(linear_depth_before)); Require(name, "linear depth deferred publication", linear_depth_before == linear_depth_guest && scheduler.CurrentTick() == linear_depth_tick, "linear depth readback published before GPU completion"); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::array linear_depth_after{}; std::array linear_stencil_after{}; std::memcpy(linear_depth_after.data(), memory + linear_depth_offset, sizeof(linear_depth_after)); std::memcpy(linear_stencil_after.data(), memory + linear_stencil_offset, sizeof(linear_stencil_after)); bool linear_depth_matches = true; uint32_t linear_depth_mismatch = UINT32_MAX; for (uint32_t layer = 0; layer < linear_depth_layers; layer++) { for (uint32_t y = 0; y < linear_depth_height; y++) { for (uint32_t x = 0; x < linear_depth_pitch; x++) { const auto index = layer * linear_depth_pitch * linear_depth_height + y * linear_depth_pitch + x; const auto expected = x < linear_depth_width ? linear_depth_clear : linear_depth_guest[index]; if (linear_depth_after[index] != expected && linear_depth_mismatch == UINT32_MAX) { linear_depth_mismatch = index; } linear_depth_matches &= linear_depth_after[index] == expected; } } } Require(name, "linear depth native contents", linear_depth_matches && linear_stencil_after == linear_stencil_guest && TextureCacheTestAccess::Contains(texture_cache, linear_depth_image) && texture_cache.GetImage(linear_depth_image).IsGpuModified(), fmt::format( "linear depth mismatch={} actual=0x{:08x} expected=0x{:08x} " "stencil={} contains={} gpu={}", linear_depth_mismatch, linear_depth_mismatch == UINT32_MAX ? 0 : linear_depth_after[linear_depth_mismatch], linear_depth_mismatch == UINT32_MAX ? 0 : (linear_depth_mismatch % linear_depth_pitch < linear_depth_width ? linear_depth_clear : linear_depth_guest[linear_depth_mismatch]), linear_stencil_after == linear_stencil_guest, TextureCacheTestAccess::Contains(texture_cache, linear_depth_image), texture_cache.GetImage(linear_depth_image).IsGpuModified()) .c_str()); constexpr uint64_t tiled_depth_offset = 0x380000; constexpr uint32_t tiled_depth_width = 3; constexpr uint32_t tiled_depth_height = 2; constexpr uint32_t tiled_depth_pitch = 4; constexpr uint32_t tiled_depth_layers = 2; constexpr uint64_t tiled_depth_slice = 0x10000; constexpr uint64_t tiled_depth_size = tiled_depth_slice * tiled_depth_layers; constexpr uint32_t tiled_depth_clear = 0x3f200000u; constexpr uint32_t tiled_depth_stale = 0xdeadbeefu; std::vector tiled_depth_guest( tiled_depth_size / sizeof(uint32_t), tiled_depth_stale); std::memcpy(memory + tiled_depth_offset, tiled_depth_guest.data(), tiled_depth_size); auto tiled_depth_desc = MakeLinearDesc( base + tiled_depth_offset, tiled_depth_size, vk::Format::eD32Sfloat, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {tiled_depth_width, tiled_depth_height, 1}, tiled_depth_layers, 4, 1); tiled_depth_desc.type = BindingType::DepthTarget; tiled_depth_desc.info.pitch = tiled_depth_pitch; tiled_depth_desc.info.tile_mode = Prospero::TileMode::kDepth; tiled_depth_desc.info.mip_layout[0] = { 0, tiled_depth_size, tiled_depth_pitch, tiled_depth_height}; tiled_depth_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth; tiled_depth_desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto tiled_depth_image = texture_cache.FindImage(tiled_depth_desc); Require(name, "tiled depth native clear", texture_cache.ClearImageFromBuffer( command, tiled_depth_desc.info.data.address, tiled_depth_desc.info.data.size, tiled_depth_clear), "failed to create GPU-current layered tiled depth contents"); const auto [tiled_prefix, tiled_prefix_offset] = TextureCacheTestAccess::MapDownload(texture_cache, 64, 64); Require(name, "tiled depth download prefix", tiled_prefix != nullptr && tiled_prefix_offset != 0, "failed to force a nonzero reusable-download offset"); const auto tiled_depth_tick = scheduler.CurrentTick(); Require( name, "tiled depth download queue", TextureCacheTestAccess::TryDownload(texture_cache, tiled_depth_image), "layered tiled depth readback was rejected"); std::vector tiled_depth_before(tiled_depth_guest.size()); std::memcpy(tiled_depth_before.data(), memory + tiled_depth_offset, tiled_depth_size); Require(name, "tiled depth deferred publication", tiled_depth_before == tiled_depth_guest && scheduler.CurrentTick() == tiled_depth_tick, "tiled depth readback published before GPU completion"); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::vector tiled_depth_after(tiled_depth_guest.size()); std::memcpy(tiled_depth_after.data(), memory + tiled_depth_offset, tiled_depth_size); const auto active_tiled_words = tiled_depth_width * tiled_depth_height * tiled_depth_layers; const auto clear_tiled_words = std::ranges::count(tiled_depth_after, tiled_depth_clear); const auto stale_tiled_words = std::ranges::count(tiled_depth_after, tiled_depth_stale); Require( name, "tiled depth backing preservation", clear_tiled_words == active_tiled_words && stale_tiled_words == tiled_depth_after.size() - active_tiled_words, fmt::format("tiled depth readback changed clear={}/{} stale={}/{}", clear_tiled_words, active_tiled_words, stale_tiled_words, tiled_depth_after.size() - active_tiled_words) .c_str()); TileBlockLayout depth_block{}; Require(name, "tiled depth block layout", TileGetBlockLayout(TileBlockFamily::Depth64KB, sizeof(uint32_t), depth_block), "failed to describe the tiled depth test surface"); std::array tiled_depth_tiles{}; for (uint32_t layer = 0; layer < tiled_depth_layers; layer++) { const uint64_t offset = tiled_depth_slice * layer; tiled_depth_tiles[layer] = {depth_block.family, depth_block.bytes_per_element, offset, tiled_depth_slice, offset, tiled_depth_slice, 0, tiled_depth_width, tiled_depth_height, 1, tiled_depth_pitch}; tiled_depth_tiles[layer].surface_z = layer; } auto tiled_depth_input = CreateHostBuffer(name, tiled_depth_size, AllFlags, tiled_depth_after); auto tiled_depth_linear = TextureCacheTestAccess::Tiler(texture_cache) .Detile(tiled_depth_input.buffer, 0, tiled_depth_size, tiled_depth_size, tiled_depth_tiles); auto tiled_depth_output = CreateHostBuffer(name, tiled_depth_size, AllFlags, std::vector(tiled_depth_guest.size(), 0)); const vk::BufferCopy tiled_depth_copy{tiled_depth_linear.offset, 0, tiled_depth_size}; scheduler.Current().Handle().copyBuffer(tiled_depth_linear.buffer, tiled_depth_output.buffer, 1, &tiled_depth_copy); HostReadBarrier(tiled_depth_output.buffer, tiled_depth_size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); const auto tiled_depth_words = ReadBuffer(name, tiled_depth_output, tiled_depth_guest.size()); bool tiled_depth_matches = true; for (uint32_t layer = 0; layer < tiled_depth_layers; layer++) { for (uint32_t y = 0; y < tiled_depth_height; y++) { for (uint32_t x = 0; x < tiled_depth_width; x++) { const auto index = layer * (tiled_depth_slice / sizeof(uint32_t)) + y * tiled_depth_pitch + x; tiled_depth_matches &= tiled_depth_words[index] == tiled_depth_clear; } } } Require(name, "tiled depth layered contents", tiled_depth_matches && TextureCacheTestAccess::Contains(texture_cache, tiled_depth_image) && texture_cache.GetImage(tiled_depth_image).IsGpuModified(), "tiled depth readback lost a padded slice or retired its image"); DestroyBuffer(&tiled_depth_output); DestroyBuffer(&tiled_depth_input); constexpr uint64_t tiled_d16_offset = 0x3a0000; constexpr uint64_t tiled_d16_stencil_offset = 0x3c0000; constexpr uint16_t tiled_d16_clear = 0x8000u; constexpr uint16_t tiled_d16_stale = 0xbeefu; std::vector tiled_d16_guest(tiled_depth_size / sizeof(uint16_t), tiled_d16_stale); constexpr std::array tiled_d16_stencil{ 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9, 0xba, 0xcb, 0xdc, 0xed, 0xfe, 0x0f}; std::memcpy(memory + tiled_d16_offset, tiled_d16_guest.data(), tiled_depth_size); std::memcpy(memory + tiled_d16_stencil_offset, tiled_d16_stencil.data(), tiled_d16_stencil.size()); auto tiled_d16_desc = MakeLinearDesc( base + tiled_d16_offset, tiled_depth_size, vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k16UNorm, Prospero::ImageType::kColor2D, {tiled_depth_width, tiled_depth_height, 1}, tiled_depth_layers, 2, 1); tiled_d16_desc.type = BindingType::DepthTarget; tiled_d16_desc.info.pitch = tiled_depth_pitch; tiled_d16_desc.info.tile_mode = Prospero::TileMode::kDepth; tiled_d16_desc.info.stencil = {base + tiled_d16_stencil_offset, tiled_d16_stencil.size()}; tiled_d16_desc.info.mip_layout[0] = { 0, tiled_depth_size, tiled_depth_pitch, tiled_depth_height}; tiled_d16_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; tiled_d16_desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto tiled_d16_image = texture_cache.FindImage(tiled_d16_desc); Require(name, "tiled D16 native clear", texture_cache.ClearImageFromBuffer( command, tiled_d16_desc.info.data.address, tiled_d16_desc.info.data.size, EncodeD16AsD32(tiled_d16_clear)), "failed to clear a layered tiled D16 fallback image"); Require( name, "tiled D16 download queue", TextureCacheTestAccess::TryDownload(texture_cache, tiled_d16_image), "layered tiled D16 fallback readback was rejected"); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::vector tiled_d16_after(tiled_d16_guest.size()); std::array tiled_d16_stencil_after{}; std::memcpy(tiled_d16_after.data(), memory + tiled_d16_offset, tiled_depth_size); std::memcpy(tiled_d16_stencil_after.data(), memory + tiled_d16_stencil_offset, tiled_d16_stencil_after.size()); Require(name, "tiled D16 backing preservation", std::ranges::count(tiled_d16_after, tiled_d16_clear) == active_tiled_words && std::ranges::count(tiled_d16_after, tiled_d16_stale) == tiled_d16_after.size() - active_tiled_words && tiled_d16_stencil_after == tiled_d16_stencil, "tiled D16 fallback overwrote inactive depth or stencil backing"); std::vector tiled_d16_words(tiled_depth_size / sizeof(uint32_t)); std::memcpy(tiled_d16_words.data(), tiled_d16_after.data(), tiled_depth_size); std::array tiled_d16_tiles{}; Require(name, "tiled D16 block layout", TileGetBlockLayout(TileBlockFamily::Depth64KB, sizeof(uint16_t), depth_block), "failed to describe the tiled D16 test surface"); for (uint32_t layer = 0; layer < tiled_depth_layers; layer++) { const uint64_t offset = tiled_depth_slice * layer; tiled_d16_tiles[layer] = {depth_block.family, depth_block.bytes_per_element, offset, tiled_depth_slice, offset, tiled_depth_slice, 0, tiled_depth_width, tiled_depth_height, 1, tiled_depth_pitch}; tiled_d16_tiles[layer].surface_z = layer; } auto tiled_d16_input = CreateHostBuffer(name, tiled_depth_size, AllFlags, tiled_d16_words); auto tiled_d16_linear = TextureCacheTestAccess::Tiler(texture_cache) .Detile(tiled_d16_input.buffer, 0, tiled_depth_size, tiled_depth_size, tiled_d16_tiles); auto tiled_d16_output = CreateHostBuffer(name, tiled_depth_size, AllFlags, std::vector(tiled_d16_words.size(), 0)); const vk::BufferCopy tiled_d16_copy{tiled_d16_linear.offset, 0, tiled_depth_size}; scheduler.Current().Handle().copyBuffer( tiled_d16_linear.buffer, tiled_d16_output.buffer, 1, &tiled_d16_copy); HostReadBarrier(tiled_d16_output.buffer, tiled_depth_size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); const auto tiled_d16_linear_words = ReadBuffer(name, tiled_d16_output, tiled_d16_words.size()); std::vector tiled_d16_linear_values(tiled_d16_guest.size()); std::memcpy(tiled_d16_linear_values.data(), tiled_d16_linear_words.data(), tiled_depth_size); bool tiled_d16_matches = true; for (uint32_t layer = 0; layer < tiled_depth_layers; layer++) { for (uint32_t y = 0; y < tiled_depth_height; y++) { for (uint32_t x = 0; x < tiled_depth_width; x++) { const auto index = layer * (tiled_depth_slice / sizeof(uint16_t)) + y * tiled_depth_pitch + x; tiled_d16_matches &= tiled_d16_linear_values[index] == tiled_d16_clear; } } } Require(name, "tiled D16 layered contents", tiled_d16_matches && TextureCacheTestAccess::Contains(texture_cache, tiled_d16_image) && texture_cache.GetImage(tiled_d16_image).IsGpuModified(), "tiled D16 fallback lost a converted layer or retired its image"); DestroyBuffer(&tiled_d16_output); DestroyBuffer(&tiled_d16_input); constexpr uint64_t d16_fallback_offset = 0x33c000; constexpr uint64_t d16_fallback_stencil_offset = 0x33d000; constexpr std::array d16_fallback_values{0x0000u, 0x2468u, 0xabcdu, 0xffffu}; std::memcpy(memory + d16_fallback_offset, d16_fallback_values.data(), sizeof(d16_fallback_values)); std::memset(memory + d16_fallback_stencil_offset, 0x6d, 4); auto d16_depth_desc = MakeLinearDesc( base + d16_fallback_offset, sizeof(d16_fallback_values), vk::Format::eD24UnormS8Uint, Prospero::BufferFormat::k16UNorm, Prospero::ImageType::kColor2D, {4, 1, 1}, 1, 2, 1); d16_depth_desc.type = BindingType::DepthTarget; d16_depth_desc.info.stencil = {base + d16_fallback_stencil_offset, 4}; d16_depth_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; d16_depth_desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto d16_depth_image = texture_cache.FindImage(d16_depth_desc); (void)texture_cache.FindDepthTarget(d16_depth_image, d16_depth_desc); texture_cache.MarkGpuWritten(d16_depth_image); auto d16_storage_desc = d16_depth_desc; d16_storage_desc.type = BindingType::Storage; d16_storage_desc.info.stencil = {}; d16_storage_desc.info.pixel_format = vk::Format::eR16Unorm; d16_storage_desc.view_info.format = vk::Format::eR16Unorm; d16_storage_desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; d16_storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage; const auto d16_storage_image = texture_cache.FindImage(d16_storage_desc, true); auto d16_storage_readback = CreateHostBuffer( name, sizeof(d16_fallback_values), vk::BufferUsageFlagBits::eTransferDst, std::vector(2, 0)); auto &d16_storage_native = texture_cache.GetImage(d16_storage_image); d16_storage_native.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, scheduler.Current().Handle()); vk::BufferImageCopy d16_storage_copy{}; d16_storage_copy.bufferRowLength = 4; d16_storage_copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}; d16_storage_copy.imageExtent = {4, 1, 1}; scheduler.Current().Handle().copyImageToBuffer( d16_storage_native.backing.image, vk::ImageLayout::eTransferSrcOptimal, d16_storage_readback.buffer, 1, &d16_storage_copy); HostReadBarrier(d16_storage_readback.buffer, d16_storage_readback.size, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); const auto d16_storage_words = ReadBuffer(name, d16_storage_readback, 2); std::array d16_storage_values{}; std::memcpy(d16_storage_values.data(), d16_storage_words.data(), sizeof(d16_storage_values)); Require( name, "D16 fallback depth/storage copy", d16_storage_values == d16_fallback_values && d16_storage_image != d16_depth_image && texture_cache.GetImage(d16_storage_image).IsGpuModified(), "D16 fallback reinterpreted four-byte host depth as two-byte color"); DestroyBuffer(&d16_storage_readback); if (!texture_cache.GetImage(combined_destination_image).IsGpuModified()) { texture_cache.MarkGpuWritten(combined_destination_image); } constexpr float stale_added_stencil_depth = 0.125f; Libs::LibKernel::Memory::WriteBacking( combined_destination.info.data.address, &stale_added_stencil_depth, sizeof(stale_added_stencil_depth)); const auto depth_gc_tick = scheduler.CurrentTick(); TextureCacheTestAccess::ConfigureGarbageCollection( texture_cache, std::array{combined_destination_image}, 81, 0); texture_cache.RunGarbageCollector(); float depth_before_completion = 0.0f; Libs::LibKernel::Memory::TryReadBacking( combined_destination.info.data.address, &depth_before_completion, sizeof(depth_before_completion)); const bool depth_image_retired = !TextureCacheTestAccess::Contains( texture_cache, combined_destination_image); const bool depth_proxy_retired = !texture_cache.FindImageFromRange( base + added_stencil_offset, added_stencil_size, false); Require( name, "depth/stencil deferred pressure retirement", depth_image_retired && depth_proxy_retired && scheduler.CurrentTick() == depth_gc_tick && depth_before_completion == stale_added_stencil_depth, fmt::format( "GC failed to retire/defer depth: image={} proxy={} tick={}/{} " "depth={}/{}", depth_image_retired, depth_proxy_retired, scheduler.CurrentTick(), depth_gc_tick, depth_before_completion, stale_added_stencil_depth) .c_str()); scheduler.Finish(); scheduler.DrainPriorityOperations(); float depth_after_completion = 0.0f; Libs::LibKernel::Memory::TryReadBacking( combined_destination.info.data.address, &depth_after_completion, sizeof(depth_after_completion)); Require(name, "depth/stencil depth-plane preservation", depth_after_completion == added_stencil_depth_value, "GC discarded the current depth plane of a depth/stencil " "image"); constexpr size_t gc_depth_pair_count = 6; constexpr uint64_t gc_depth_pair_offset = 0x350000; constexpr uint64_t gc_depth_pair_stride = 0x1000; std::array gc_depth_images{}; std::array gc_stencil_images{}; std::array gc_depth_lru{}; for (size_t index = 0; index < gc_depth_pair_count; index++) { auto depth = MakeLinearDesc( base + gc_depth_pair_offset + index * gc_depth_pair_stride, sizeof(float), vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k32Float, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); depth.type = BindingType::DepthTarget; depth.info.stencil = {depth.info.data.address + gc_depth_pair_stride / 2, sizeof(uint32_t)}; depth.view_info.format = vk::Format::eD32SfloatS8Uint; depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; gc_depth_images[index] = texture_cache.FindImage(depth); TextureCacheTestAccess::AssociateStencil( texture_cache, gc_depth_images[index], depth.info.stencil); gc_stencil_images[index] = texture_cache.FindImageFromRange( depth.info.stencil.address, depth.info.stencil.size, false); gc_depth_lru[index * 2] = gc_depth_images[index]; gc_depth_lru[index * 2 + 1] = gc_stencil_images[index]; } Require( name, "depth/stencil GC fixtures", std::ranges::all_of(gc_depth_images, [](ImageId image) { return bool{image}; }) && std::ranges::all_of(gc_stencil_images, [](ImageId image) { return bool{image}; }), "failed to create six depth/stencil association pairs"); TextureCacheTestAccess::ConfigureGarbageCollection( texture_cache, gc_depth_lru, 81, UINT64_MAX); texture_cache.RunGarbageCollector(); bool gc_depth_budget = true; for (size_t index = 0; index < gc_depth_pair_count; index++) { const bool expected_live = index == gc_depth_pair_count - 1; gc_depth_budget &= TextureCacheTestAccess::Contains( texture_cache, gc_depth_images[index]) == expected_live && TextureCacheTestAccess::Contains( texture_cache, gc_stencil_images[index]) == expected_live; } Require(name, "depth/stencil GC traversal budget", gc_depth_budget, "recursive association deletion stopped LRU traversal or " "exceeded the ten-entry deletion budget"); constexpr uint64_t large_offset = 0x400000; constexpr uint32_t large_width = 4096; constexpr uint32_t large_height = 2047; constexpr uint64_t large_size = uint64_t{large_width} * large_height * sizeof(uint32_t); static_assert(large_size < (32ull << 20)); std::memset(memory + large_offset, 0, static_cast(large_size)); auto large_desc = MakeLinearDesc( base + large_offset, large_size, vk::Format::eR8G8B8A8Unorm, Prospero::BufferFormat::k8_8_8_8UNorm, Prospero::ImageType::kColor2D, {large_width, large_height, 1}, 1, 4, 1); const auto RunLargeReadback = [&](uint32_t clear_value) { const auto image = texture_cache.FindImage(large_desc); Require(name, "near-capacity image clear", texture_cache.ClearImageFromBuffer( command, large_desc.info.data.address, large_desc.info.data.size, clear_value), "failed to write the near-capacity readback source image"); const std::array sample_offsets{ 0, large_size / 2, large_size - sizeof(uint32_t)}; constexpr uint32_t stale = 0; for (const auto offset : sample_offsets) { Libs::LibKernel::Memory::WriteBacking(base + large_offset + offset, &stale, sizeof(stale)); } TextureCacheTestAccess::ConfigureGarbageCollection( texture_cache, std::array{image}, 81, 0); const auto tick = scheduler.CurrentTick(); texture_cache.RunGarbageCollector(); const auto handle = BufferCacheTestAccess::DownloadBuffer(resources.GetBufferCache()) .Handle(); Require( name, "near-capacity deferred retirement", !TextureCacheTestAccess::Contains(texture_cache, image) && scheduler.CurrentTick() == tick, "near-capacity readback was rejected or synchronously submitted"); scheduler.Finish(); scheduler.DrainPriorityOperations(); bool content = true; for (const auto offset : sample_offsets) { uint32_t value = 0; Libs::LibKernel::Memory::TryReadBacking(base + large_offset + offset, &value, sizeof(value)); content &= value == clear_value; } Require( name, "near-capacity readback content", content, "near-capacity image readback did not publish its GPU contents"); return handle; }; const auto large_download = RunLargeReadback(0xa5a5a5a5u); const auto reused_large_download = RunLargeReadback(0x5a5a5a5au); Require(name, "near-capacity shared download reuse", large_download == reused_large_download, "successive near-capacity image transfers replaced the shared " "download buffer"); constexpr uint64_t tile_alias_offset = 0x2000000; constexpr uint64_t tile_alias_size = 0x400000; constexpr uint32_t tile_alias_extent = 1024; std::memset(memory + tile_alias_offset, 0, static_cast(tile_alias_size)); auto render_target_alias = MakeLinearDesc( base + tile_alias_offset, tile_alias_size, vk::Format::eR8G8B8A8Unorm, Prospero::BufferFormat::k8_8_8_8UNorm, Prospero::ImageType::kColor2D, {tile_alias_extent, tile_alias_extent, 1}, 1, 4, 1); render_target_alias.type = BindingType::Storage; render_target_alias.info.tile_mode = Prospero::TileMode::kRenderTarget; render_target_alias.view_info.usage = vk::ImageUsageFlagBits::eStorage; const auto render_target_alias_image = texture_cache.FindImage(render_target_alias); auto standard_4kb_alias = render_target_alias; standard_4kb_alias.type = BindingType::Texture; standard_4kb_alias.info.tile_mode = Prospero::TileMode::kStandard4KB; standard_4kb_alias.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto standard_4kb_alias_image = texture_cache.FindImage(standard_4kb_alias); auto repeated_standard_4kb_alias = standard_4kb_alias; const auto repeated_standard_4kb_alias_image = texture_cache.FindImage(repeated_standard_4kb_alias); Require( name, "equal-size tile-mode alias", render_target_alias_image && standard_4kb_alias_image && standard_4kb_alias_image != render_target_alias_image && repeated_standard_4kb_alias_image == standard_4kb_alias_image && texture_cache.GetImage(render_target_alias_image) .info.tile_mode == Prospero::TileMode::kRenderTarget && texture_cache.GetImage(standard_4kb_alias_image).info.tile_mode == Prospero::TileMode::kStandard4KB, "equal address/size lookup reused an incompatible " "tiled backing"); for (auto &output : ms_observer_outputs) { DestroyBuffer(&output); } DestroyBuffer(&layered_readback); DestroyBuffer(&bgra16_readback); DestroyBuffer(&mixed_source_readback); DestroyBuffer(&bridged_source_readback); DestroyBuffer(&mip_formatted_readback); DestroyBuffer(&mip_prefix_readback); DestroyBuffer(&partial_cpu_refresh_readback); DestroyBuffer(&mirror_cpu_readback); DestroyBuffer(&exact_buffer_readback); m_device.destroyDescriptorPool(observer_pool, nullptr); m_device.destroyPipeline(observer_depth_pipeline, nullptr); m_device.destroyPipelineLayout(observer_pipeline_layout, nullptr); m_device.destroyDescriptorSetLayout(observer_set_layout, nullptr); m_device.destroyShaderModule(ms_depth_module, nullptr); constexpr uint64_t mapped_end = base + allocation_size; constexpr uint64_t final_word_address = mapped_end - sizeof(uint32_t); auto *final_word = reinterpret_cast(final_word_address); auto *final_byte = reinterpret_cast(mapped_end - 1); Require(name, "fault boundary containment", resources.IsMapped(final_word_address, sizeof(uint32_t)) && !resources.IsMapped(final_word_address, sizeof(uint64_t)) && !resources.HandleFault(PageFaultAccess::Write, mapped_end) && !resources.HandleFault(PageFaultAccess::Read, mapped_end), "fault handling changed the mapped range's exclusive end"); Require(name, "fault boundary setup", resources.InvalidateMemory(final_word_address, sizeof(uint32_t)), "the final mapped word could not be initialized"); *final_word = 0; auto final_word_desc = MakeLinearDesc( final_word_address, sizeof(uint32_t), vk::Format::eR32Uint, Prospero::BufferFormat::k32UInt, Prospero::ImageType::kColor2D, {1, 1, 1}, 1, 4, 1); const auto final_word_image = texture_cache.FindImage(final_word_desc); (void)texture_cache.FindTexture(final_word_image, final_word_desc); Require(name, "fault boundary image tracking", texture_cache.GetImage(final_word_image).IsTracked() && !texture_cache.GetImage(final_word_image).IsCpuDirty(), "the final mapped word's image was not clean before its CPU store"); *final_word = 45; Require(name, "fault boundary CPU store", *final_word == 45 && texture_cache.GetImage(final_word_image).IsDefinitelyCpuDirty(), "a valid four-byte store at the mapping end did not invalidate its image"); constexpr uint32_t final_gpu_value = 0xa1b2c3d4u; auto &boundary_buffer_cache = resources.GetBufferCache(); (void)boundary_buffer_cache.ObtainBuffer(final_word_address, sizeof(uint32_t), true); boundary_buffer_cache.FillBuffer(final_word_address, sizeof(uint32_t), final_gpu_value, false); Require(name, "fault boundary GPU ownership", boundary_buffer_cache.HasGpuDirtyBytes(final_word_address, sizeof(uint32_t)) && !HostMemoryIsReadable(mapped_end - 1), "the final mapped byte was not protected by GPU ownership"); const uint8_t final_gpu_byte = *final_byte; Require(name, "fault boundary GPU readback", final_gpu_byte == static_cast(final_gpu_value >> 24u) && *final_word == final_gpu_value && !boundary_buffer_cache.HasGpuDirtyBytes(final_word_address, sizeof(uint32_t)), "a final-byte CPU read did not publish the GPU-owned final word"); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); LibKernel::Memory::InstallGpuResources(nullptr); } context.ShutdownGpu(); Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "cache direct-memory mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "cache direct-memory allocation release failed"); std::printf("[host] %-32s ok\n", name); } void CheckBgra16Readback() { constexpr const char *name = "Bgra16Readback"; constexpr uintptr_t base = 0x0000000204000000ull; constexpr uint64_t allocation_size = 0x200000; constexpr auto format = Prospero::BufferFormat::k16_16_16_16Float; constexpr auto tile = Prospero::TileMode::kRenderTarget; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_size, 0, &direct_offset) == 0, "BGRA16 direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_size) == 0 && mapped == reinterpret_cast(base), "BGRA16 fixed mapping failed"); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); { auto &resources = context; resources.MapMemory(base, allocation_size); const uint32_t pitch = TileGetTexturePitch(format, 1, tile); TileSizeAlign total{}; TileSizeOffset mip{}; TilePaddedSize padded{}; TileGetTextureSize(format, 1, 1, 1, tile, &total, &mip, &padded); Require(name, "tiled layout", total.size >= 8 && total.size <= allocation_size, "BGRA16 tiled layout exceeds its guest allocation"); std::memset(mapped, 0x5a, total.size); ImageDesc desc{}; desc.type = BindingType::RenderTarget; desc.info.data = {base, total.size}; desc.info.pixel_format = vk::Format::eR16G16B16A16Sfloat; desc.info.guest_format = format; desc.info.type = Prospero::ImageType::kColor2D; desc.info.extent = {1, 1, 1}; desc.info.resources = {1, 1}; desc.info.pitch = pitch; desc.info.bytes_per_block = 8; desc.info.samples = 1; desc.info.tile_mode = tile; desc.info.bgra16 = true; desc.info.mip_layout[0] = {mip.offset, mip.size, pitch, 1}; desc.view_info.format = desc.info.pixel_format; desc.view_info.type = vk::ImageViewType::e2D; desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; auto &cache = resources.GetTextureCache(); const auto id = cache.FindImage(desc); auto &image = cache.GetImage(id); image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); vk::ClearColorValue clear{}; clear.float32[0] = 1.0f; clear.float32[1] = 2.0f; clear.float32[2] = 3.0f; clear.float32[3] = 4.0f; const vk::ImageSubresourceRange range{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; scheduler.Current().Handle().clearColorImage( image.backing.image, vk::ImageLayout::eTransferDstOptimal, clear, range); cache.MarkGpuWritten(id); Require(name, "guest readback queue", TextureCacheTestAccess::TryDownload(cache, id), "tiled BGRA16 guest readback was rejected"); auto mirror = resources.GetBufferCache().ObtainBuffer(base, total.size, false, true); Require(name, "mirror owner", mirror.first != nullptr, "tiled BGRA16 mirror has no BufferCache owner"); auto mirror_readback = CreateHostBuffer( name, 8, vk::BufferUsageFlagBits::eTransferDst, {0, 0}); const vk::BufferCopy copy{mirror.second, 0, 8}; scheduler.Current().Handle().copyBuffer(mirror.first->Handle(), mirror_readback.buffer, 1, ©); vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = mirror_readback.buffer; barrier.size = mirror_readback.size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::vector backing(total.size); Require(name, "guest backing", Libs::LibKernel::Memory::TryReadBacking(base, backing.data(), backing.size()), "tiled BGRA16 guest backing is unreadable"); const std::array expected{0x4200u, 0x4000u, 0x3c00u, 0x4400u}; std::array observed{}; std::memcpy(observed.data(), backing.data(), sizeof(observed)); Require(name, "guest component order", observed == expected && std::all_of(backing.begin() + sizeof(observed), backing.end(), [](uint8_t value) { return value == 0x5a; }), "tiled BGRA16 guest readback changed component order or padding"); Require(name, "mirror component order", ReadBuffer(name, mirror_readback, 2) == std::vector{0x40004200u, 0x44003c00u}, "tiled BGRA16 Buffer mirror changed guest component order"); DestroyBuffer(&mirror_readback); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "BGRA16 fixed mapping release failed"); Require(name, "release", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "BGRA16 direct-memory release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckRenderExecutorColorDiscovery() { constexpr const char *name = "RenderExecutorColorDiscovery"; constexpr uintptr_t base = 0x0000000203c00000ull; constexpr uint32_t width = 512; constexpr uint32_t height = 1; constexpr uint32_t bytes_per_element = 4; constexpr uint64_t allocation_alignment = 0x10000; EnsureRuntimeContext(); const auto pitch = TileGetRenderTargetPitch(width, bytes_per_element, 0); TileSizeAlign layout{}; Require(name, "tile layout", pitch != 0 && TileGetRenderTargetSize( width, height, pitch, bytes_per_element, layout, 0), "1D render-target layout is unavailable"); struct TargetCase { uint32_t width, height, dimension, pitch; uint64_t size; Prospero::TileMode tile; }; const std::array cases{ TargetCase{width, height, 0, pitch, layout.size, Prospero::TileMode::kRenderTarget}, // PPSA28068: the 1440x720 agreement target must alias its sampled texture. TargetCase{1440, 720, 1, 1472, 0x40b000, Prospero::TileMode::kLinear}, }; constexpr uint64_t allocation_size = 0x410000; int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "color direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "color fixed mapping failed"); std::memset(mapped, 0, allocation_size); for (const auto &target : cases) { const auto width = target.width; const auto height = target.height; const bool is_1d = target.dimension == 0; RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetColorBase(0, {.addr = base}); registers.SetColorInfo( 0, {.format = Prospero::ChannelLayout::k8_8_8_8, .channel_type = Prospero::ChannelType::kSrgb, .channel_order = Prospero::ChannelOrder::kStandard}); registers.SetColorAttrib2(0, {.height = height - 1, .width = width - 1}); registers.SetColorAttrib3(0, {.tile_mode = target.tile, .dimension = target.dimension, .metadata_pipe_aligned = true}); registers.SetRenderTargetMask(0x0f); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); RenderColorInfo color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), color, 0); const auto attachment = texture_cache.FindRenderTarget(color.image_id, color.desc); const auto &image = texture_cache.GetImage(color.image_id); RenderDepthInfo no_depth{}; const auto rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &color, 1, no_depth); scheduler.Current().BeginRendering(rendering); scheduler.Current().EndRendering(); Require(name, "captured color target", color.image_id && attachment != nullptr && rendering.color_attachments[0].image_view != nullptr && color.desc.info.type == (is_1d ? Prospero::ImageType::kColor1D : Prospero::ImageType::kColor2D) && color.desc.info.extent == vk::Extent3D{width, height, 1} && color.desc.info.resources == ImageSubresources{1, 1} && color.desc.info.pitch == target.pitch && color.desc.info.data.size == target.size && color.desc.info.mip_layout[0].offset == 0 && color.desc.info.mip_layout[0].size == target.size && color.desc.view_info.type == (is_1d ? vk::ImageViewType::e1D : vk::ImageViewType::e2D) && image.backing.image_type == (is_1d ? vk::ImageType::e1D : vk::ImageType::e2D) && rendering.width == width && rendering.height == height && rendering.num_layers == 1 && rendering.num_color_attachments == 1, "color attachment lost its dimensions or padded guest layout"); if (target.tile == Prospero::TileMode::kLinear) { vk::ClearValue clear{}; clear.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f}; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), color.image_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear); RenderExecutorTestAccess::ResetBindings(executor); ShaderTextureResource descriptor{{ static_cast(base >> 8u), (static_cast(Prospero::BufferFormat::k8_8_8_8Srgb) << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), DstSel(4, 5, 6, 7) | (static_cast(Prospero::ImageType::kColor2D) << 28u), 0, 0x00700000u, 0, 0}}; ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 8; std::copy_n(descriptor.fields, 8, value.dwords.begin()); ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; resource.numeric_class = Prospero::TextureNumericClass::Float; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = true; const auto sampled = RenderExecutorTestAccess::ResolveTexture(executor, resource, value); Require(name, "linear target sampled alias", sampled.image_id == color.image_id && sampled.desc.info.pitch == target.pitch && sampled.desc.info.data.size == target.size && sampled.desc.info.mip_layout == color.desc.info.mip_layout && texture_cache.FindTexture(sampled.image_id, sampled.desc) != nullptr, "sampling the linear agreement target created a second image"); Require(name, "linear target sampled contents", ReadCachedTexel(name, context, sampled.image_id, {1439, 719, 0}) == std::vector{0xffff00ffu}, "the sampled linear target lost its GPU-written final texel"); } RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "color direct mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "color direct-memory allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckRenderExecutorColorVolumeDiscovery() { constexpr const char *name = "RenderExecutorColorVolumeDiscovery"; constexpr uintptr_t base = 0x0000000203e00000ull; constexpr uint64_t color_size = 0x200000; constexpr uint64_t metadata_size = 0x20000; constexpr uint64_t allocation_size = color_size + metadata_size; constexpr uint64_t allocation_alignment = 0x10000; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "color-volume direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "color-volume fixed mapping failed"); std::memset(mapped, 0x5a, allocation_size); constexpr uint64_t slice_size = 0x10000; { RenderContext context(m_runtime_context); context.InitializeGpu(nullptr); LibKernel::Memory::InstallGpuResources(&context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetColorBase(0, {.addr = base}); registers.SetColorInfo( 0, {.dcc_compression_enable = true, .format = Prospero::ChannelLayout::k10_10_10_2, .channel_type = Prospero::ChannelType::kUNorm, .channel_order = Prospero::ChannelOrder::kStandard}); registers.SetColorAttrib2(0, {.height = 31, .width = 31}); registers.SetColorAttrib3(0, {.depth = 31, .tile_mode = Prospero::TileMode::kRenderTarget, .dimension = 2, .metadata_pipe_aligned = true}); constexpr uint64_t dcc_address = base + color_size; registers.SetColorDccAddr(0, {.addr = dcc_address}); registers.SetColorClearWord0(0, {.word0 = 0}); registers.SetColorClearWord1(0, {.word1 = 0}); registers.SetRenderTargetMask(0x0f); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); WriteMetadata(context, dcc_address, metadata_size, 0); Require(name, "deferred DCC clear", !texture_cache.IsMeta(dcc_address), "a pre-registration DCC clear was not deferred"); RenderColorInfo color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), color, 0); const auto attachment = texture_cache.FindRenderTarget(color.image_id, color.desc); const auto &image = texture_cache.GetImage(color.image_id); Require( name, "captured 3D target", color.image_id && attachment != nullptr && color.desc.info.type == Prospero::ImageType::kColor3D && color.desc.info.extent == vk::Extent3D{32, 32, 32} && color.desc.info.resources == ImageSubresources{1, 1} && color.desc.info.pitch == 128 && color.desc.info.data.size == color_size && color.desc.info.mip_layout[0].size == 0x10000 && color.desc.info.metadata.kind == ImageMetadataKind::Dcc && color.desc.info.metadata.range.address == dcc_address && !texture_cache.IsMeta(dcc_address) && !texture_cache.ClearMeta(dcc_address) && color.desc.view_info.type == vk::ImageViewType::e2D && color.desc.view_info.layer_count == 1 && image.backing.image_type == vk::ImageType::e3D && static_cast(image.backing.flags & vk::ImageCreateFlagBits::e2DArrayCompatible) && image.usage.render_target && image.IsGpuModified(), "dimension=2/depth=31 did not create the expected 32x32x32 " "backing and 2D " "attachment slice"); constexpr uint64_t metadata_slice_size = metadata_size / 32; std::vector metadata_bytes(metadata_size); resources.GetBufferCache().ReadMemory(dcc_address, metadata_size); Require(name, "first volume slice expanded metadata", LibKernel::Memory::TryReadBacking(dcc_address, metadata_bytes.data(), metadata_bytes.size()) && std::all_of(metadata_bytes.begin(), metadata_bytes.begin() + metadata_slice_size, [](uint8_t byte) { return byte == 0xff; }) && std::all_of(metadata_bytes.begin() + metadata_slice_size, metadata_bytes.end(), [](uint8_t byte) { return byte == 0; }), "consuming volume slice 0 changed unrequested native metadata slices"); RenderExecutorTestAccess::ResetBindings(executor); registers.SetColorView( 0, {.base_array_slice_index = 7, .last_array_slice_index = 7}); RenderColorInfo sliced_color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), sliced_color, 0); RenderDepthInfo no_depth{}; const auto sliced_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &sliced_color, 1, no_depth); Require( name, "3D slice transition", sliced_color.image_id == color.image_id && sliced_rendering.color_attachments[0].image_view != nullptr && sliced_color.desc.view_info.base_layer == 7 && sliced_rendering.num_color_attachments == 1 && sliced_rendering.num_layers == 1, "a nonzero 3D attachment slice was treated as a Vulkan array layer"); resources.GetBufferCache().ReadMemory(dcc_address, metadata_size); Require(name, "mixed volume metadata readback", LibKernel::Memory::TryReadBacking(dcc_address, metadata_bytes.data(), metadata_bytes.size()), "mixed volume metadata is unavailable"); for (uint32_t slice = 0; slice < 32; slice++) { const uint8_t expected = slice == 0 || slice == 7 ? 0xff : 0; const auto first = metadata_bytes.begin() + slice * metadata_slice_size; Require(name, "independent volume metadata consumption", std::all_of(first, first + metadata_slice_size, [expected](uint8_t byte) { return byte == expected; }), "mixed DCC metadata lost a pending slice or consumed an unrequested slice"); } Require(name, "3D slice clear and preservation", ReadCachedTexel(name, context, color.image_id, {0, 0, 7}) == std::vector{0} && ReadCachedTexel(name, context, color.image_id, {0, 0, 31}) == std::vector{0x5a5a5a5a}, "clearing one volume slice changed untouched slice contents"); auto storage_desc = color.desc; storage_desc.type = BindingType::Storage; storage_desc.info.metadata = {}; storage_desc.info.guest_format = Prospero::BufferFormat::k10_10_10_2UNorm; storage_desc.view_info.type = vk::ImageViewType::e3D; storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage; const auto storage_id = texture_cache.FindImage(storage_desc); const auto storage_view = texture_cache.FindTexture(storage_id, storage_desc); const auto &shared_image = texture_cache.GetImage(storage_id); Require(name, "storage alias reuse", storage_id == color.image_id && storage_view != nullptr && shared_image.IsGpuModified() && shared_image.usage.render_target, "the matching 3D storage binding did not reuse the live " "render-target image"); Require( name, "volume readback queue", TextureCacheTestAccess::TryDownload(texture_cache, storage_id), "the 3D render target could not be queued for guest-layout readback"); auto mirror = resources.GetBufferCache().ObtainBuffer( base, color_size, false, true); Require(name, "volume mirror", mirror.first != nullptr, "the 3D render-target readback has no BufferCache owner"); auto slice_probe = CreateHostBuffer(name, 4, vk::BufferUsageFlagBits::eTransferDst, {0}); const vk::BufferCopy copy{mirror.second + 31 * slice_size, 0, 4}; scheduler.Current().Handle().copyBuffer(mirror.first->Handle(), slice_probe.buffer, 1, ©); vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = slice_probe.buffer; barrier.size = slice_probe.size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); scheduler.Finish(); scheduler.DrainPriorityOperations(); Require(name, "volume Z transfer", ReadBuffer(name, slice_probe, 1) == std::vector{0x5a5a5a5a}, "render-target upload/readback lost the final Z slice"); DestroyBuffer(&slice_probe); vk::ClearValue painted{}; painted.color.float32 = std::array{1.0f, 1.0f, 1.0f, 0.0f}; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), color.image_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, painted); WriteMetadata(context, dcc_address, metadata_size, 0xffffffffu); color.image_id = texture_cache.FindImage(color.desc); (void)texture_cache.FindRenderTarget(color.image_id, color.desc); resources.GetBufferCache().ReadMemory(dcc_address, metadata_size); Require(name, "expanded volume metadata preserves rendering", ReadCachedTexel(name, context, color.image_id) == std::vector{0x3fffffffu} && LibKernel::Memory::TryReadBacking(dcc_address, metadata_bytes.data(), metadata_bytes.size()) && std::all_of(metadata_bytes.begin(), metadata_bytes.end(), [](uint8_t byte) { return byte == 0xff; }), "uncompressed native keys reapplied an old volume clear"); WriteMetadata(context, dcc_address, metadata_size, 0); color.image_id = texture_cache.FindImage(color.desc); (void)texture_cache.FindRenderTarget(color.image_id, color.desc); resources.GetBufferCache().ReadMemory(dcc_address, metadata_size); Require(name, "native volume re-clear", ReadCachedTexel(name, context, color.image_id) == std::vector{0} && LibKernel::Memory::TryReadBacking(dcc_address, metadata_bytes.data(), metadata_bytes.size()) && std::all_of(metadata_bytes.begin(), metadata_bytes.begin() + metadata_slice_size, [](uint8_t byte) { return byte == 0xff; }) && std::all_of(metadata_bytes.begin() + metadata_slice_size, metadata_bytes.end(), [](uint8_t byte) { return byte == 0; }), "a native metadata overwrite did not clear only its requested volume slice"); for (const uint32_t base_slice : {0u, 7u}) { RenderExecutorTestAccess::ResetBindings(executor); registers.SetColorView(0, {.base_array_slice_index = base_slice, .last_array_slice_index = 32}); RenderColorInfo volume_color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), volume_color, 0); const auto volume_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &volume_color, 1, no_depth); Require(name, "3D export bound exceeds depth", volume_color.image_id == color.image_id && volume_color.desc.info.extent == vk::Extent3D{32, 32, 32} && volume_color.desc.info.data.size == color_size && volume_color.desc.view_info.type == vk::ImageViewType::e2DArray && volume_color.desc.view_info.base_layer == base_slice && volume_color.desc.view_info.layer_count == 32 - base_slice && volume_rendering.num_layers == 32 - base_slice && registers.GetRenderTarget(0).view.last_array_slice_index == 32, "3D export bounds changed the backing or exceeded its attachment depth"); scheduler.Current().BeginRendering(volume_rendering); scheduler.Current().EndRendering(); } resources.GetBufferCache().ReadMemory(dcc_address, metadata_size); Require(name, "complete volume expansion", LibKernel::Memory::TryReadBacking(dcc_address, metadata_bytes.data(), metadata_bytes.size()) && std::all_of(metadata_bytes.begin(), metadata_bytes.end(), [](uint8_t byte) { return byte == 0xff; }), "full volume acquisition left an unconsumed native clear slice"); RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); context.ShutdownGpu(); LibKernel::Memory::InstallGpuResources(nullptr); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "color-volume direct mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "color-volume direct-memory allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } static void WriteMetadata(RenderContext &context, uint64_t address, uint64_t size, uint32_t value) { Require("DccMetadata", "guest write range", size % sizeof(value) == 0 && context.InvalidateMemory(address, size), "metadata write is outside the mapped allocation"); const std::vector words(size / sizeof(value), value); LibKernel::Memory::WriteBacking(address, words.data(), size); } std::vector ReadCachedTexel(const char *name, RenderContext &context, ImageId id, vk::Offset3D offset = {}, vk::Extent3D extent = {1, 1, 1}, uint32_t layer = 0, uint32_t mip = 0) { auto &scheduler = context.GetCommandScheduler(); auto &image = context.GetTextureCache().GetImage(id); const auto bytes = image.info.bytes_per_block * extent.width * extent.height * extent.depth; auto probe = CreateHostBuffer(name, bytes, vk::BufferUsageFlagBits::eTransferDst, {}); scheduler.Current().EndRendering(); image.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}, scheduler.Current().Handle()); vk::BufferImageCopy copy{}; copy.imageSubresource = {image.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor, mip, layer, 1}; copy.imageOffset = offset; copy.imageExtent = extent; scheduler.Current().Handle().copyImageToBuffer( image.backing.image, vk::ImageLayout::eTransferSrcOptimal, probe.buffer, 1, ©); vk::BufferMemoryBarrier barrier{}; barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = probe.buffer; barrier.size = probe.size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); scheduler.Finish(); auto result = ReadBuffer(name, probe, bytes / 4); DestroyBuffer(&probe); return result; } void CheckRenderExecutorDccFixedClearFloat() { constexpr const char *name = "RenderExecutorDccFixedClearFloat"; constexpr uintptr_t base = 0x0000000204100000ull; constexpr uint64_t allocation_size = 0x200000; constexpr uint64_t allocation_alignment = 0x10000; constexpr uint64_t dcc_address = base + 0x100000; struct FillCase { uint32_t fill; std::array texel; bool reuse_unorm = false; }; constexpr std::array cases{ FillCase{0x40404040u, {0, 0x3c000000u}}, FillCase{0x80808080u, {0x3c003c00u, 0x00003c00u}}, FillCase{0x40404040u, {0, 0x3c000000u}, true}, }; EnsureRuntimeContext(); // Astro's generic metadata fill, through S_ENDPGM; trailing debug data is omitted. static constexpr std::array native_fill{ 0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u, 0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu, 0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u, 0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u, 0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u, 0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u, 0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d860609u, 0x7d8c02f9u, 0x06068c02u, 0x87ea6a0cu, 0x50000080u, 0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u, 0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u, 0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au, 0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u, 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u, }; static constexpr auto raw_fill = [] { auto words = native_fill; for (auto &word : words) { if (word == 0xe0102000u) { word = 0xe0702000u; // BUFFER_STORE_DWORD in place of BUFFER_STORE_FORMAT_X. } } return words; }(); for (const auto *shader : {&native_fill, &raw_fill}) { ShaderMapUserData(reinterpret_cast(shader->data()), {.type = Prospero::ShaderBinaryType::kCs, .code_size_bytes = sizeof(native_fill)}); } TileSizeAlign metadata_size{}; (void)TileGetDccSize(512, 256, 1, 8, 1, Prospero::TileMode::kRenderTarget, metadata_size, 0); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "fixed-clear direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "fixed-clear direct mapping failed"); std::memset(mapped, 0, allocation_size); for (const auto &fill_case : cases) { RenderContext context(m_runtime_context); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; context.InitializeGpu(nullptr); LibKernel::Memory::InstallGpuResources(&context); context.GetGpu().SendCommandSync([&] { auto &scheduler = context.GetCommandScheduler(); registers.SetColorBase(0, {.addr = base}); registers.SetColorInfo( 0, {.dcc_compression_enable = true, .format = Prospero::ChannelLayout::k16_16_16_16, .channel_type = Prospero::ChannelType::kFloat, .channel_order = Prospero::ChannelOrder::kStandard}); registers.SetColorAttrib2(0, {.height = 255, .width = 511}); registers.SetColorAttrib3(0, {.tile_mode = Prospero::TileMode::kRenderTarget, .dimension = 1, .metadata_pipe_aligned = true}); registers.SetColorDccAddr(0, {.addr = dcc_address}); registers.SetColorClearWord0(0, {.word0 = 0}); registers.SetColorClearWord1(0, {.word1 = 0}); registers.SetRenderTargetMask(0x0f); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); ImageId unorm_id{}; if (fill_case.reuse_unorm) { const auto float_info = registers.GetRenderTarget(0).info; auto unorm_info = float_info; unorm_info.dcc_compression_enable = false; unorm_info.channel_type = Prospero::ChannelType::kUNorm; registers.SetColorInfo(0, unorm_info); RenderColorInfo unorm{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), unorm, 0); unorm_id = unorm.image_id; (void)texture_cache.FindRenderTarget(unorm_id, unorm.desc); RenderExecutorTestAccess::ResetBindings(executor); registers.SetColorInfo(0, float_info); } const auto fill_metadata = [&](uint32_t count, bool raw = false) { const auto *shader = &native_fill; if (raw) { shader = &raw_fill; } shaders.SetCsShader({.data_addr = reinterpret_cast(shader->data()), .num_thread_x = 64, .num_thread_y = 1, .num_thread_z = 1, .wave_size = 64, .user_sgpr = 10, .tgid_x_en = true}); const std::array user_data{ static_cast(dcc_address), static_cast(dcc_address >> 32u) | (4u << 16u), count, 0x00014004u, fill_case.fill, 0, 0x200, 0, count, 1}; for (uint32_t i = 0; i < user_data.size(); i++) { shaders.SetCsUserSgpr(i, user_data[i], HW::UserSgprType::Unknown); } executor.DispatchDirect(0, scheduler.Current(), (count + 63) / 64, 1, 1, 0x41u); }; const auto metadata_words = static_cast(metadata_size.size / 4); fill_metadata(metadata_words); Require(name, "metadata before descriptor", !texture_cache.IsMeta(dcc_address), "the native fill guessed an unbound metadata allocation's type"); RenderColorInfo color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), color, 0); RenderDepthInfo no_depth{}; const auto rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &color, 1, no_depth); if (fill_case.reuse_unorm) { Require(name, "FLOAT clear reuses UNORM image", color.image_id == unorm_id && texture_cache.GetImage(unorm_id).backing.format == vk::Format::eR16G16B16A16Unorm, "the aliased clear did not reuse its existing UNORM allocation"); } Require(name, "fixed clear on a float target", color.image_id && color.desc.info.metadata.kind == ImageMetadataKind::Dcc && color.desc.info.metadata.range.address == dcc_address && rendering.num_color_attachments == 1 && !texture_cache.IsMeta(dcc_address), "a DCC fixed clear code was not materialised on an RGBA16F " "target"); const auto cleared = ReadCachedTexel(name, context, color.image_id); Require(name, "GPU float clear value", cleared == std::vector(fill_case.texel.begin(), fill_case.texel.end()), "expected " + Hex(fill_case.texel[0]) + ", " + Hex(fill_case.texel[1]) + "; got " + Hex(cleared[0]) + ", " + Hex(cleared[1])); context.GetBufferCache().ReadMemory(dcc_address, metadata_size.size, false); std::vector expanded_metadata(metadata_size.size); Require(name, "expanded native metadata", LibKernel::Memory::TryReadBacking(dcc_address, expanded_metadata.data(), expanded_metadata.size()) && std::all_of(expanded_metadata.begin(), expanded_metadata.end(), [](uint8_t value) { return value == 0xff; }), "materialized color retained stale native DCC clear codes"); const auto bind = [&] { RenderExecutorTestAccess::ResetBindings(executor); color.image_id = texture_cache.FindImage(color.desc); (void)texture_cache.FindRenderTarget(color.image_id, color.desc); }; const auto paint = [&] { vk::ClearValue clear{}; clear.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f}; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), color.image_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear); }; std::vector painted{0x00003c00u, 0x3c003c00u}; if (fill_case.reuse_unorm) { painted = {0x0000ffffu, 0xffffffffu}; } const auto retains_painted_texel = [&] { const auto texels = ReadCachedTexel(name, context, color.image_id, {}, {512, 256, 1}); for (size_t i = 0; i < texels.size(); i += 2) { if (texels[i] == painted[0] && texels[i + 1] == painted[1]) { return true; } } return false; }; paint(); bind(); Require(name, "unchanged metadata preserves rendering", ReadCachedTexel(name, context, color.image_id) == painted, "rebinding an unchanged DCC allocation reapplied an old clear"); fill_metadata(metadata_words - 1); bind(); Require(name, "partial same-value metadata fill", retains_painted_texel(), "a partial same-value write reused stale clear bytes after rendering"); fill_metadata(metadata_words, true); bind(); Require(name, "same-value raw native re-clear", ReadCachedTexel(name, context, color.image_id) == std::vector(fill_case.texel.begin(), fill_case.texel.end()), "a repeated metadata fill did not clear an already drawn target"); paint(); WriteMetadata(context, dcc_address, metadata_size.size, UINT32_MAX); fill_metadata(metadata_words - 1); bind(); Require(name, "partial native metadata fill", retains_painted_texel(), "a nonuniform metadata allocation cleared the whole target"); fill_metadata(metadata_words); bind(); Require(name, "complete fill after partial fill", ReadCachedTexel(name, context, color.image_id) == std::vector(fill_case.texel.begin(), fill_case.texel.end()), "completing the metadata overwrite did not restore its clear"); ImageDesc previous_depth{}; previous_depth.type = BindingType::DepthTarget; previous_depth.info.data = {base + 0x180000, 0x10000}; previous_depth.info.pixel_format = vk::Format::eD32Sfloat; previous_depth.info.guest_format = Prospero::BufferFormat::k32Float; previous_depth.info.type = Prospero::ImageType::kColor2D; previous_depth.info.extent = {128, 128, 1}; previous_depth.info.resources = {1, 1}; previous_depth.info.pitch = 128; previous_depth.info.bytes_per_block = 4; previous_depth.info.samples = 1; previous_depth.info.tile_mode = Prospero::TileMode::kLinear; previous_depth.info.mip_layout[0] = {0, 0x10000, 128, 128}; previous_depth.info.metadata.kind = ImageMetadataKind::Htile; previous_depth.info.metadata.range = {dcc_address, metadata_size.size}; previous_depth.info.htile_clear_mask = 0; previous_depth.view_info.format = vk::Format::eD32Sfloat; previous_depth.view_info.type = vk::ImageViewType::e2D; previous_depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth; previous_depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto previous_depth_id = texture_cache.FindImage(previous_depth); Require(name, "old HTile interpretation", texture_cache.FindDepthTarget(previous_depth_id, previous_depth) != nullptr && texture_cache.IsMeta(dcc_address), "the old depth target did not register its metadata allocation"); paint(); bind(); Require(name, "HTile allocation reused as expanded DCC", !texture_cache.IsMeta(dcc_address) && !texture_cache.ClearMeta(dcc_address) && ReadCachedTexel(name, context, color.image_id) == painted, "DCC discovery retained the old HTile classification or erased rendered color"); fill_metadata(metadata_words); bind(); context.GetBufferCache().ReadMemory(dcc_address, metadata_size.size); Require(name, "native DCC dispatch after HTile reuse", ReadCachedTexel(name, context, color.image_id) == std::vector(fill_case.texel.begin(), fill_case.texel.end()) && LibKernel::Memory::TryReadBacking(dcc_address, expanded_metadata.data(), expanded_metadata.size()) && std::all_of(expanded_metadata.begin(), expanded_metadata.end(), [](uint8_t byte) { return byte == 0xff; }), "the former HTile entry swallowed the native DCC fill dispatch"); RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); }); context.ShutdownGpu(); LibKernel::Memory::InstallGpuResources(nullptr); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "fixed-clear direct mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "fixed-clear direct-memory allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckSampledDccClear() { constexpr const char *name = "SampledDccClear"; constexpr uintptr_t base = 0x0000000205000000ull; constexpr uint64_t color_size = 0x3fc0000; constexpr uint64_t metadata_size = 0x48000; constexpr uint64_t dcc_address = base + color_size; constexpr uint64_t allocation_size = 0x4020000; constexpr uint64_t alignment = 0x10000; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, alignment, 0, &direct_offset) == 0, "captured scene allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, alignment) == 0 && mapped == reinterpret_cast(base), "captured scene mapping failed"); for (const auto initial_size : {metadata_size - 0x1000, metadata_size}) { std::memset(mapped, 0x5a, color_size); std::memset(reinterpret_cast(dcc_address), 0xff, metadata_size); RenderContext context(m_runtime_context); context.InitializeGpu(nullptr); LibKernel::Memory::InstallGpuResources(&context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); WriteMetadata(context, dcc_address, initial_size, 0x40404040u); Require(name, "deferred metadata fill", !cache.IsMeta(dcc_address), "unknown metadata was consumed before descriptor discovery"); // PPSA19244's first sampled scene descriptor, with relocated allocations. ShaderTextureResource descriptor{{static_cast(base >> 8u), 0xc4700000u, 0x021bc3bfu, 0x91b00facu, 0, 0x00700000u, 0x006b0000u | (static_cast(dcc_address >> 8u) << 24u), static_cast(dcc_address >> 16u)}}; ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 8; std::copy_n(descriptor.fields, 8, value.dwords.begin()); ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; resource.numeric_class = Prospero::TextureNumericClass::Float; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = true; auto binding = RenderExecutorTestAccess::ResolveTexture(executor, resource, value); Require(name, "sampled metadata discovery", binding.desc.info.extent == vk::Extent3D{3840, 2160, 1} && binding.desc.info.data.size == color_size && binding.desc.info.metadata.kind == ImageMetadataKind::Dcc && binding.desc.info.metadata.range.address == dcc_address && binding.desc.info.metadata.range.size == metadata_size, "captured sampled descriptor lost its DCC allocation"); Require(name, "sampled view", cache.FindTexture(binding.image_id, binding.desc) != nullptr, "captured scene did not produce a sampled view"); if (initial_size != metadata_size) { Require(name, "partial metadata coverage", ReadCachedTexel(name, context, binding.image_id) == std::vector{0x5a5a5a5au, 0x5a5a5a5au}, "an incomplete metadata fill cleared the whole scene"); WriteMetadata(context, dcc_address, metadata_size, 0x40404040u); binding.image_id = cache.FindImage(binding.desc); (void)cache.FindTexture(binding.image_id, binding.desc); } Require(name, "opaque black before first sample", ReadCachedTexel(name, context, binding.image_id, {3839, 2159, 0}) == std::vector{0, 0x3c000000u}, "sampling read stale color bytes instead of the DCC opaque black clear"); context.GetBufferCache().ReadMemory(dcc_address, metadata_size); std::vector expanded_metadata(metadata_size); Require(name, "sampled clear expands native metadata", LibKernel::Memory::TryReadBacking(dcc_address, expanded_metadata.data(), expanded_metadata.size()) && std::all_of(expanded_metadata.begin(), expanded_metadata.end(), [](uint8_t byte) { return byte == 0xff; }), "the sampled clear retained native clear keys after materialization"); WriteMetadata(context, dcc_address, metadata_size, 0x80808080u); binding.image_id = cache.FindImage(binding.desc); (void)cache.FindTexture(binding.image_id, binding.desc); Require(name, "later sampled clear", ReadCachedTexel(name, context, binding.image_id) == std::vector{0x3c003c00u, 0x00003c00u}, "an existing sampled image retained the previous DCC clear"); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); context.ShutdownGpu(); LibKernel::Memory::InstallGpuResources(nullptr); } Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "captured scene mapping release failed"); Require(name, "release", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "captured scene allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckPackedTextureComponents() { constexpr const char *name = "PackedTextureComponents"; constexpr uintptr_t base = 0x0000000204200000ull; constexpr uint64_t allocation_size = 0x10000; struct FormatCase { Prospero::BufferFormat format; std::array words; std::array, 4> channels; std::array maxima; }; // Packed format channels start at the least significant bit. // PPSA24515's blue, white, and masked black exposed the 5551 reversal. constexpr std::array cases{ FormatCase{Prospero::BufferFormat::k5_5_5_1UNorm, {0x7c00, 0x7fff, 0x8000, 0x8c41}, {{{0, 0, 31, 0}, {31, 31, 31, 0}, {0, 0, 0, 1}, {1, 2, 3, 1}}}, {31, 31, 31, 1}}, FormatCase{Prospero::BufferFormat::k4_4_4_4UNorm, {0x000f, 0x00f0, 0x0f00, 0x4321}, {{{15, 0, 0, 0}, {0, 15, 0, 0}, {0, 0, 15, 0}, {1, 2, 3, 4}}}, {15, 15, 15, 15}}, }; const auto argb = TextureGetComponentMapping(DstSel(5, 1, 7, 0), Prospero::ColorMappingArgb); Require(name, "inverse mapping", argb == vk::ComponentMapping{vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eOne, vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eZero}, "cyclic host mapping reordered output slots or descriptor constants"); const auto target = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k1_5_5_5, Prospero::ChannelType::kUNorm, Prospero::ChannelOrder::kStandard); Require(name, "1555 render target", target.format == vk::Format::eR5G5B5A1UnormPack16 && target.export_mapping == Prospero::ColorMappingAbgr, "shared format resolution changed the existing 1555 render target"); EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_size, 0, &direct_offset) == 0, "packed texture allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_size) == 0 && mapped == reinterpret_cast(base), "packed texture mapping failed"); for (const auto &format : cases) { std::memset(mapped, 0, allocation_size); std::memcpy(mapped, format.words.data(), sizeof(format.words)); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); for (const auto swizzle : {DstSel(4, 5, 6, 7), DstSel(7, 1, 4, 0)}) { ShaderTextureResource descriptor{{static_cast(base >> 8u), (static_cast(format.format) << 20u) | (3u << 30u), 0, swizzle | (static_cast(Prospero::ImageType::kColor2D) << 28u), 0, 0x00700000u, 0, 0}}; TestCase test; test.name = name; test.has_user_data = true; test.image_descriptor_swizzle = swizzle; std::copy_n(descriptor.fields, 8, test.user_data.begin()); test.user_data[50] = 16 * sizeof(uint32_t); test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_LOAD, ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM}; for (uint32_t pixel = 0; pixel < format.words.size(); ++pixel) { AppendVMovU32(&test.code, 20, pixel); AppendVMovU32(&test.code, 21, 0); test.code.push_back(EncodeMimg0(0x00, 0xf)); test.code.push_back(EncodeMimg1(0, 20)); for (uint32_t channel = 0; channel < 4; ++channel) { AppendStoreVgpr(&test.code, channel, pixel * 4 + channel); } } AppendEnd(&test.code); const auto compiled = CompileCase(test, SubgroupSize()); ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 8; std::copy_n(descriptor.fields, 8, value.dwords.begin()); const auto binding = RenderExecutorTestAccess::ResolveTexture( executor, compiled.program.info.images.at(0), value); Image sampled; sampled.view = cache.FindTexture(binding.image_id, binding.desc); sampled.layout = cache.GetImage(binding.image_id).backing.state.layout; scheduler.Finish(); auto output = CreateStorageBuffer(name, {}, 16); Dispatch(test, compiled, output, nullptr, &sampled); const auto actual = ReadBuffer(name, output, 16); for (uint32_t pixel = 0; pixel < format.words.size(); ++pixel) { for (uint32_t channel = 0; channel < 4; ++channel) { const auto selector = GetDstSel(swizzle, channel); const auto maximum = selector < 4 ? 1u : format.maxima[selector - 4]; const auto expected = selector < 4 ? selector : format.channels[pixel][selector - 4]; const float observed = std::bit_cast(actual[pixel * 4 + channel]); // Recover the source bits without testing the host's UNORM rounding precision. const bool matches = maximum == 1 ? observed == expected : observed >= 0.0f && observed <= 1.0f && std::round(observed * maximum) == expected; if (!matches) { std::ostringstream message; message << "format=" << static_cast(format.format) << " swizzle=" << swizzle << " word=" << format.words[pixel] << " channel=" << channel << " expected=" << expected << " actual=" << observed; Fail(name, "GPU components", message.str()); } } } DestroyBuffer(&output); } RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } Require(name, "release", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0 && Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "packed texture backing release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckComparisonDepthTexture() { constexpr const char *name = "ComparisonDepthTexture"; constexpr uintptr_t base = 0x0000000204200000ull; constexpr uint64_t allocation_size = 0x20000; constexpr uint64_t ordinary_address = base + 0x10000; constexpr std::array depths{0, 13107, 26214, 39321, 52428, 65535}; constexpr std::array references{0.1f, 0.3f, 0.5f, 0.7f, 0.9f}; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_size, 0, &direct_offset) == 0, "comparison texture allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_size) == 0 && mapped == reinterpret_cast(base), "comparison texture mapping failed"); // Sea of Stars' 1D comparison texture and the existing six-face cube. const auto check_layout = [&](bool cube) { const uint32_t layers = cube ? depths.size() : 1; const auto depth_at = [&](uint32_t layer, bool reversed) { const auto index = cube ? layer : 2u; return depths[reversed ? depths.size() - 1u - index : index]; }; std::memset(mapped, 0, allocation_size); for (uint32_t face = 0; face < layers; face++) { const auto depth = depth_at(face, false); for (const auto address : {base, ordinary_address}) { std::memcpy(reinterpret_cast(address + face * 0x100), &depth, sizeof(depth)); } } RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); ShaderTextureResource descriptor{{static_cast(base >> 8u), 0x00700000u, 0, cube ? 0xb0100204u : 0x81800924u, layers - 1u, 0x00700000u, 0, 0}}; ShaderSamplerResource sampler_descriptor{{1u << 12u, 0, 1u << 24u, 0}}; TestCase test; test.name = name; test.has_user_data = true; test.image_descriptor_swizzle = descriptor.DstSelXYZW(); std::copy_n(descriptor.fields, 8, test.user_data.begin()); std::copy_n(sampler_descriptor.fields, 4, test.user_data.begin() + 8); test.user_data[50] = layers * references.size() * sizeof(uint32_t); test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_SAMPLE, ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM}; test.required_spirv = {"OpImageSampleDrefExplicitLod"}; std::vector promoted_expected; for (uint32_t face = 0; face < layers; face++) { for (const auto reference : references) { AppendVMovLiteral(&test.code, 20, std::bit_cast(reference)); AppendVMovLiteral(&test.code, 21, std::bit_cast(cube ? 1.5f : 0.5f)); if (cube) { AppendVMovLiteral(&test.code, 22, std::bit_cast(1.5f)); AppendVMovLiteral(&test.code, 23, std::bit_cast(static_cast(face))); } test.code.push_back(EncodeMimg0(0x2f, 1, 0, false, cube ? 3 : 0)); test.code.push_back(EncodeMimg1(0, 20, 0, 2)); AppendStoreVgpr(&test.code, 0, static_cast(test.expected.size())); test.expected.push_back( reference < static_cast(depth_at(face, false)) / 65535.f ? 0x3f800000u : 0u); promoted_expected.push_back( reference < static_cast(depth_at(face, true)) / 65535.f ? 0x3f800000u : 0u); } } AppendEnd(&test.code); const auto compiled = CompileCase(test, SubgroupSize()); Require(name, "comparison resource", compiled.program.info.images.size() == 1 && compiled.program.info.images[0].depth_compare && compiled.program.info.images[0].cube == cube && compiled.program.info.images[0].dimension == (cube ? ShaderRecompiler::Decoder::ImageDimension::Dim2DArray : ShaderRecompiler::Decoder::ImageDimension::Dim1D), "native comparison did not retain its resource semantics"); ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 8; std::copy_n(descriptor.fields, 8, value.dwords.begin()); auto binding = RenderExecutorTestAccess::ResolveTexture( executor, compiled.program.info.images[0], value); const auto view = cache.FindTexture(binding.image_id, binding.desc); auto &image = cache.GetImage(binding.image_id); Require(name, "fresh depth backing", view != nullptr && image.info.pixel_format == vk::Format::eD16Unorm && image.info.guest_format == Prospero::BufferFormat::k16UNorm && image.info.tile_mode == descriptor.TileMode() && image.info.type == (cube ? Prospero::ImageType::kColor2D : Prospero::ImageType::kColor1D) && image.info.data.size == (cube ? 0x600u : 0x10000u) && image.info.resources.layers == layers, "comparison texture lost its guest layout or depth representation"); auto ordinary_resource = compiled.program.info.images[0]; ordinary_resource.depth_compare = false; value.dwords[0] = static_cast(ordinary_address >> 8u); auto ordinary = RenderExecutorTestAccess::ResolveTexture(executor, ordinary_resource, value); (void)cache.FindTexture(ordinary.image_id, ordinary.desc); auto &color = cache.GetImage(ordinary.image_id); Require(name, "ordinary R16", color.info.pixel_format == vk::Format::eR16Unorm, "ordinary R16 sampling unexpectedly acquired a depth format"); scheduler.Finish(); auto output = CreateStorageBuffer(name, {}, test.expected.size()); const auto sampler = context.GetSamplerCache().GetSampler(sampler_descriptor); Image sampled; sampled.view = view; sampled.layout = image.backing.state.layout; Dispatch(test, compiled, output, nullptr, &sampled, nullptr, nullptr, sampler); Require(name, "native comparisons", ReadBuffer(name, output, test.expected.size()) == test.expected, "comparison samples lost a depth value or guest layer stride"); image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); color.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); for (uint32_t face = 0; face < layers; face++) { const vk::ImageSubresourceRange depth_range{vk::ImageAspectFlagBits::eDepth, 0, 1, face, 1}; const vk::ClearDepthStencilValue clear_depth{ static_cast(depth_at(face, true)) / 65535.f, 0}; scheduler.Current().Handle().clearDepthStencilImage(image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear_depth, 1, &depth_range); const vk::ImageSubresourceRange color_range{vk::ImageAspectFlagBits::eColor, 0, 1, face, 1}; const vk::ClearColorValue clear_color{std::array{ static_cast(depth_at(face, true)) / 65535.f, 0.f, 0.f, 0.f}}; scheduler.Current().Handle().clearColorImage(color.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear_color, 1, &color_range); } cache.MarkGpuWritten(binding.image_id); cache.MarkGpuWritten(ordinary.image_id); Require(name, "depth readback", TextureCacheTestAccess::TryDownload(cache, binding.image_id), "ordinary tiled depth readback did not use the shared transfer path"); scheduler.Finish(); scheduler.DrainPriorityOperations(); for (uint32_t face = 0; face < layers; face++) { uint16_t actual = 0; std::memcpy(&actual, reinterpret_cast(base + face * 0x100), sizeof(actual)); Require(name, "depth guest roundtrip", actual == depth_at(face, true), "GPU depth clear was not written to the correct guest face"); } auto promoted = RenderExecutorTestAccess::ResolveTexture( executor, compiled.program.info.images[0], value); sampled.view = cache.FindTexture(promoted.image_id, promoted.desc); auto &promoted_image = cache.GetImage(promoted.image_id); Require(name, "GPU color owner promotion", promoted.image_id != ordinary.image_id && promoted_image.info.pixel_format == vk::Format::eD16Unorm && promoted_image.IsGpuModified(), "comparison binding did not preserve the GPU-current color owner"); sampled.layout = promoted_image.backing.state.layout; scheduler.Finish(); Dispatch(test, compiled, output, nullptr, &sampled, nullptr, nullptr, sampler); Require(name, "promoted comparisons", ReadBuffer(name, output, promoted_expected.size()) == promoted_expected, "color-to-depth ownership conversion changed comparison results"); DestroyBuffer(&output); RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); }; check_layout(false); check_layout(true); Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "comparison texture unmap failed"); Require(name, "release", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "comparison texture release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckRenderExecutorColorStandardTileDiscovery() { constexpr const char *name = "RenderExecutorColorStandardTile"; constexpr uintptr_t base = 0x0000000203b00000ull; constexpr uint64_t allocation_size = 0x190000; constexpr uint64_t allocation_alignment = 0x10000; constexpr auto format = Prospero::BufferFormat::k32Float; struct TileCase { Prospero::TileMode tile; uint64_t target_offset; uint64_t alignment; }; constexpr std::array cases{ TileCase{Prospero::TileMode::kStandard4KB, 0x4000, 0x1000}, TileCase{Prospero::TileMode::kStandard64KB, 0x10000, 0x10000}, }; EnsureRuntimeContext(); const auto odd_pitch = TileGetTexturePitch(format, 257, Prospero::TileMode::kStandard4KB); TileSizeAlign odd_slice{}; TileGetTextureSize(format, 257, 131, 1, Prospero::TileMode::kStandard4KB, &odd_slice, nullptr, nullptr); Require(name, "tile layout", odd_pitch == 288 && odd_slice.size == 0x2d000 && odd_slice.align == 0x1000, "Standard4KB pitch, footprint, or block alignment is incorrect"); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "standard-tiled color allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "standard-tiled color mapping failed"); std::memset(mapped, 0, allocation_size); for (const auto &tile_case : cases) { const auto pitch = TileGetTexturePitch(format, 256, tile_case.tile); TileSizeAlign slice{}; TileGetTextureSize(format, 256, 256, 1, tile_case.tile, &slice, nullptr, nullptr); Require( name, "tile layout", pitch == 256 && slice.size == 0x40000 && slice.align == tile_case.alignment, "standard tile pitch, footprint, or block alignment is incorrect"); const auto target_address = base + tile_case.target_offset; const auto target_size = slice.size * 6ull; RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetColorBase(0, {.addr = target_address}); registers.SetColorInfo( 0, {.format = Prospero::ChannelLayout::k32, .channel_type = Prospero::ChannelType::kFloat, .channel_order = Prospero::ChannelOrder::kStandard}); registers.SetColorView( 0, {.base_array_slice_index = 5, .last_array_slice_index = 5}); registers.SetColorAttrib2(0, {.height = 255, .width = 255}); registers.SetColorAttrib3(0, {.tile_mode = tile_case.tile, .dimension = 1, .metadata_pipe_aligned = true}); registers.SetRenderTargetMask(0x0f); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); RenderColorInfo color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), color, 0); const auto attachment = texture_cache.FindRenderTarget(color.image_id, color.desc); const auto &image = texture_cache.GetImage(color.image_id); Require(name, "captured target", color.image_id && attachment != nullptr && color.guest_array_layer == 5 && color.desc.view_info.base_layer == 5 && color.desc.info.data.address == target_address && color.desc.info.data.size == target_size && color.desc.info.tile_mode == tile_case.tile && color.desc.info.extent == vk::Extent3D{256, 256, 1} && color.desc.info.resources == ImageSubresources{1, 6} && color.desc.info.pitch == pitch && color.desc.info.mip_layout[0].offset == 0 && color.desc.info.mip_layout[0].size == target_size && color.desc.view_info.type == vk::ImageViewType::e2D && color.desc.view_info.base_layer == 5 && color.desc.view_info.layer_count == 1 && (target_address & (tile_case.alignment - 1)) == 0 && image.usage.render_target && image.IsGpuModified(), "slice 5 standard-tiled render target was not resolved"); RenderExecutorTestAccess::ResetBindings(executor); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "standard-tiled color mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "standard-tiled color allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckRenderExecutorColorDepthTileDiscovery() { constexpr const char *name = "RenderExecutorColorDepthTile"; constexpr uintptr_t base = 0x0000000203d00000ull; constexpr uint64_t allocation_size = 0x10000; constexpr uint64_t allocation_alignment = 0x10000; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "depth-tiled color allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "depth-tiled color mapping failed"); constexpr uint32_t width = 64; constexpr uint32_t height = 64; TileTextureBlockLayout tile_layout{}; Require(name, "tile layout", TileGetTextureBlockLayout(Prospero::BufferFormat::k32Float, Prospero::TileMode::kDepth, false, tile_layout), "depth-tiled color block layout is unavailable"); uint32_t block_xor = 0; Require(name, "block xor", TileGetBlockXor(tile_layout.block, 0, 0, 0, block_xor), "depth-tiled color block XOR is unavailable"); std::vector expected(allocation_size / sizeof(uint32_t)); for (uint32_t y = 0; y < height; ++y) { for (uint32_t x = 0; x < width; ++x) { uint32_t offset = 0; Require(name, "texel offset", TileGetBlockOffset(tile_layout.block, x, y, 0, offset), "depth-tiled color texel offset is unavailable"); const auto index = (offset ^ block_xor) / sizeof(uint32_t); Require(name, "texel address", index < expected.size(), "depth-tiled color texel address exceeds its backing"); expected[index] = 0x3f000000u + y * width + x; } } std::memcpy(mapped, expected.data(), allocation_size); { RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetColorBase(0, {.addr = base}); registers.SetColorInfo( 0, {.format = Prospero::ChannelLayout::k32, .channel_type = Prospero::ChannelType::kFloat, .channel_order = Prospero::ChannelOrder::kStandard}); registers.SetColorAttrib2(0, {.height = 63, .width = 63}); registers.SetColorAttrib3(0, {.tile_mode = Prospero::TileMode::kDepth, .dimension = 1, .metadata_pipe_aligned = true}); registers.SetRenderTargetMask(0x0f); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); RenderColorInfo color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), color, 0); const auto attachment = texture_cache.FindRenderTarget(color.image_id, color.desc); const auto &image = texture_cache.GetImage(color.image_id); Require(name, "captured target", color.image_id && attachment != nullptr && color.desc.info.tile_mode == Prospero::TileMode::kDepth && color.desc.info.pitch == 128 && color.desc.info.data.size == allocation_size && color.desc.info.mip_layout[0].size == allocation_size && image.usage.render_target && image.IsGpuModified(), "supported depth-tiled color target was not resolved"); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::vector cleared(allocation_size); Require(name, "clear guest backing", Libs::LibKernel::Memory::TryWriteBacking(base, cleared.data(), allocation_size), "depth-tiled color backing could not be cleared"); Require( name, "readback queue", TextureCacheTestAccess::TryDownload(texture_cache, color.image_id), "depth-tiled color target could not be queued for readback"); RenderExecutorTestAccess::ResetBindings(executor); scheduler.Finish(); scheduler.DrainPriorityOperations(); std::vector observed(expected.size()); Require(name, "readback backing", Libs::LibKernel::Memory::TryReadBacking(base, observed.data(), allocation_size), "depth-tiled color readback is unavailable"); bool recovered = true; for (uint32_t y = 0; y < height; ++y) { for (uint32_t x = 0; x < width; ++x) { uint32_t offset = 0; const auto valid = TileGetBlockOffset(tile_layout.block, x, y, 0, offset); const auto index = (offset ^ block_xor) / sizeof(uint32_t); recovered &= valid && index < observed.size() && observed[index] == expected[index]; } } Require(name, "round trip", recovered, "depth-tiled color upload/readback changed active texels"); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "depth-tiled color mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "depth-tiled color allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } void CheckRenderExecutorStencilBindingDiscovery() { constexpr const char *name = "RenderExecutorStencilBindingDiscovery"; constexpr uintptr_t base = 0x0000000203600000ull; constexpr uint64_t allocation_size = 0x200000; constexpr uint64_t allocation_alignment = 0x10000; constexpr uint64_t depth_address = base + 0x40000; constexpr uint64_t stencil_address = base + 0x70000; constexpr uint64_t mipped_storage_address = base + 0x100000; constexpr uint32_t tail_backing_value = 0x2468ace0u; constexpr uint32_t tail_gpu_value = 0x13579bdfu; EnsureRuntimeContext(); int64_t direct_offset = -1; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_alignment, 0, &direct_offset) == 0, "descriptor discovery direct-memory allocation failed"); void *mapped = reinterpret_cast(base); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_alignment) == 0 && mapped == reinterpret_cast(base), "descriptor discovery fixed mapping failed"); std::memset(mapped, 0, allocation_size); std::fill_n(reinterpret_cast(mipped_storage_address), 0x10000 / 4, tail_backing_value); { RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; context.InitializeGpu(nullptr); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &texture_cache = resources.GetTextureCache(); auto &executor = context.GetRenderExecutor(); resources.MapMemory(base, allocation_size); std::vector descriptor_pipelines; // The virtual-texture atlas is written as raw BC3 blocks, then sampled as BC3. constexpr uint64_t block_alias_address = base + 0xf0000; constexpr std::array block_alias_data{ 0x01234567u, 0x89abcdefu, 0xfedcba98u, 0x76543210u}; const auto resolve_block_alias = [&](bool compressed) { const uint32_t side = compressed ? 256 : 64; const auto format = compressed ? Prospero::BufferFormat::kBc3UNorm : Prospero::BufferFormat::k32_32_32_32UInt; ShaderRecompiler::IR::DescriptorValue value{}; value.dword_count = 8; value.dwords = {static_cast(block_alias_address >> 8u), (static_cast(format) << 20u) | (3u << 30u), ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0x90900facu, 0, 0x00700000u, 0, 0}; ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = compressed ? ShaderRecompiler::IR::ImageResourceClass::Sampled : ShaderRecompiler::IR::ImageResourceClass::Storage; resource.numeric_class = compressed ? Prospero::TextureNumericClass::Float : Prospero::TextureNumericClass::Uint; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = compressed; resource.written = !compressed; return RenderExecutorTestAccess::ResolveTexture(executor, resource, value); }; auto raw_blocks = resolve_block_alias(false); (void)texture_cache.FindTexture(raw_blocks.image_id, raw_blocks.desc); auto &raw_blocks_native = texture_cache.GetImage(raw_blocks.image_id); raw_blocks_native.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); vk::ClearColorValue block_clear{}; std::copy(block_alias_data.begin(), block_alias_data.end(), block_clear.uint32.begin()); const vk::ImageSubresourceRange block_range{ vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; scheduler.Current().Handle().clearColorImage( raw_blocks_native.backing.image, vk::ImageLayout::eTransferDstOptimal, &block_clear, 1, &block_range); texture_cache.MarkGpuWritten(raw_blocks.image_id); auto compressed_blocks = resolve_block_alias(true); (void)texture_cache.FindTexture(compressed_blocks.image_id, compressed_blocks.desc); auto raw_blocks_again = resolve_block_alias(false); Require(name, "compressed atlas storage reuse", compressed_blocks.image_id != raw_blocks.image_id && raw_blocks_again.image_id == compressed_blocks.image_id && texture_cache.FindTexture(raw_blocks_again.image_id, raw_blocks_again.desc) != nullptr, "BC3 sampling replaced the GPU atlas on its next raw-block write"); Require(name, "compressed atlas download", TextureCacheTestAccess::TryDownload(texture_cache, raw_blocks_again.image_id), "the retained BC3 atlas could not publish its native contents"); scheduler.Finish(); scheduler.DrainPriorityOperations(); const auto *block_words = reinterpret_cast(block_alias_address); Require(name, "compressed atlas GPU contents", std::equal(block_alias_data.begin(), block_alias_data.end(), block_words) && std::equal(block_alias_data.begin(), block_alias_data.end(), block_words + 0x10000 / sizeof(uint32_t) - 4), "compressed atlas aliases reloaded stale CPU bytes over GPU-written blocks"); const auto allocate_bindings = [&](ShaderRecompiler::IR::Program &program) { program.shader_info_complete = true; ShaderRecompiler::IR::AllocateBindings(program); }; { ShaderRecompiler::IR::Program buffer_ir{}; buffer_ir.stage = ShaderType::Compute; buffer_ir.resource_tracking_complete = true; buffer_ir.info.buffers.resize(1); buffer_ir.info.buffers[0].read = true; allocate_bindings(buffer_ir); ShaderRecompiler::IR::CompiledShaderInfo buffer_program{}; buffer_program.stage = buffer_ir.stage; buffer_program.info = std::move(buffer_ir.info); buffer_program.bindings = std::move(buffer_ir.bindings); constexpr uint64_t buffer_address = base + allocation_size - 0x5000; ShaderBufferResource buffer_descriptor{}; buffer_descriptor.UpdateAddress48(buffer_address); buffer_descriptor.fields[2] = 0x8000; ShaderStageRuntime buffer_runtime{.program = &buffer_program}; auto &value = buffer_runtime.resources.buffers.emplace_back(); std::memcpy(value.dwords.data(), buffer_descriptor.fields, sizeof(buffer_descriptor.fields)); value.dword_count = 4; auto buffer_bindings = executor.PrepareBindings(buffer_runtime); executor.FindBuffers(buffer_bindings); const auto original_id = buffer_bindings.buffer_sources[0].id; auto &buffer_cache = resources.GetBufferCache(); const auto merged_id = buffer_cache.FindBuffer(base + allocation_size - 0x10000, 0x10000); Require(name, "buffer discovery invalidation", merged_id != original_id, "overlapping buffer discovery did not replace its smaller owner"); executor.RebindBuffers(buffer_bindings); const auto &binding = buffer_bindings.buffers[0]; const auto &owner = buffer_cache.GetBuffer(merged_id); Require(name, "clamped buffer rebind", binding.buffer == owner.Handle() && binding.offset == owner.Offset(buffer_address) && binding.range == 0x5000, "descriptor rebind lost its clamped guest range or retained a stale host owner"); } constexpr auto stencil_format = Prospero::BufferFormat::k8UInt; constexpr auto linear = Prospero::TileMode::kLinear; TileSizeAlign stencil_layout{}; TileGetTextureTotalSize(stencil_format, 1, 1, 1, 1, linear, false, stencil_layout); Require(name, "stencil footprint", stencil_layout.size != 0 && stencil_layout.align != 0 && (stencil_address & (stencil_layout.align - 1u)) == 0, "linear stencil descriptor produced an invalid footprint"); ShaderRecompiler::IR::Program null_program{}; null_program.stage = ShaderType::Compute; null_program.resource_tracking_complete = true; ShaderRecompiler::IR::ImageResource null_resource{}; null_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; null_resource.numeric_class = Prospero::TextureNumericClass::Uint; null_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; null_resource.read = true; null_program.info.images.push_back(null_resource); auto null_volume = null_resource; null_volume.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim3D; null_program.info.images.push_back(null_volume); auto null_storage = null_resource; null_storage.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; null_storage.read = false; null_storage.written = true; null_program.info.images.push_back(null_storage); ShaderRecompiler::IR::ResourceSnapshot null_snapshot{}; ShaderRecompiler::IR::DescriptorValue null_descriptor{}; null_descriptor.dword_count = 8; null_snapshot.images.assign(3, null_descriptor); ShaderRecompiler::IR::ResourceSpecialization null_specialization{}; null_specialization.images.resize(3); for (auto &image : null_specialization.images) { image.numeric_class = Prospero::TextureNumericClass::Float; image.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; } ShaderRecompiler::IR::ApplyResourceSpecialization( null_program, null_specialization); allocate_bindings(null_program); ShaderRecompiler::IR::CompiledShaderInfo null_info{}; null_info.stage = null_program.stage; null_info.info = std::move(null_program.info); null_info.bindings = std::move(null_program.bindings); ShaderStageRuntime null_runtime{&null_info, std::move(null_snapshot)}; auto null_bindings = executor.PrepareBindings(null_runtime); executor.RebindImages(null_bindings); Require(name, "null descriptor count", null_bindings.images.size() == 3, "null descriptor preparation lost an image binding"); const auto null_image_id = null_bindings.images[0].image_id; const auto &null_image = texture_cache.GetImage(null_image_id); Require(name, "one null image per format", null_bindings.images[1].image_id == null_image_id && null_bindings.images[2].image_id == null_image_id && TextureCacheTestAccess::NullImageCount(texture_cache) == 1, "null view dimension or usage created another image allocation"); Require(name, "null descriptors share final cache acquisition", null_image.info.data.Empty() && !null_image.binding.is_bound && !null_image.binding.is_target && !null_image.binding.needs_rebind && null_image.usage.texture && null_image.usage.storage && null_image.IsGpuModified() && !TextureCacheTestAccess::PendingDownload(texture_cache, null_image_id), "the shared null image did not preserve texture/storage " "acquisition without guest readback or RenderExecutor ownership"); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), null_bindings)); Require(name, "null descriptor general layouts", std::ranges::all_of(null_bindings.images, [](const auto &binding) { const auto info = MakeImageInfo(binding); return binding.layout == vk::ImageLayout::eGeneral && info.imageView == binding.image_view && info.imageLayout == binding.layout; }) && null_image.backing.state.layout == vk::ImageLayout::eGeneral, "shared sampled/storage null descriptors did not retain " "the general layout through descriptor generation"); constexpr uint64_t target_mip_size = 0x10000; const auto make_target_desc = [&](uint64_t address, uint64_t size, vk::Extent3D extent) { ImageDesc desc{}; desc.type = BindingType::RenderTarget; desc.info.data = {address, size}; desc.info.pixel_format = vk::Format::eR32Uint; desc.info.guest_format = Prospero::BufferFormat::k32UInt; desc.info.type = Prospero::ImageType::kColor2D; desc.info.extent = extent; desc.info.resources = {1, 1}; desc.info.pitch = extent.width; desc.info.bytes_per_block = 4; desc.info.samples = 1; desc.info.tile_mode = linear; desc.info.mip_layout[0] = {0, size, extent.width, extent.height}; desc.view_info.format = desc.info.pixel_format; desc.view_info.type = vk::ImageViewType::e2D; desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; return desc; }; TextureCacheTestAccess::SetLinearReadback(texture_cache, true); ShaderTextureResource storage{}; const uint64_t storage_address = base + 0x60000; constexpr uint32_t storage_native_value = 0x13579bdfu; constexpr uint32_t storage_stale_value = 0x2468ace0u; std::memcpy(reinterpret_cast(mapped) + 0x60000, &storage_native_value, sizeof(storage_native_value)); const uint64_t encoded_storage_address = storage_address >> 8u; storage.fields[0] = static_cast(encoded_storage_address); storage.fields[1] = static_cast(encoded_storage_address >> 32u) | (static_cast(stencil_format) << 20u); storage.fields[3] = DstSel(4, 5, 6, 7) | (static_cast(linear) << 20u) | (static_cast(Prospero::ImageType::kColor2D) << 28u); storage.fields[5] = 0x00700000u; ShaderRecompiler::IR::Program storage_program{}; storage_program.stage = ShaderType::Vertex; storage_program.resource_tracking_complete = true; ShaderRecompiler::IR::ImageResource storage_resource{}; storage_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; storage_resource.numeric_class = Prospero::TextureNumericClass::Uint; storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; storage_resource.written = true; storage_program.info.images.push_back(storage_resource); allocate_bindings(storage_program); ShaderRecompiler::IR::DescriptorValue storage_descriptor{}; std::copy(std::begin(storage.fields), std::end(storage.fields), storage_descriptor.dwords.begin()); storage_descriptor.dword_count = 8; auto mipped_storage = storage; const auto encoded_mipped_storage_address = mipped_storage_address >> 8u; mipped_storage.fields[0] = static_cast(encoded_mipped_storage_address); mipped_storage.fields[1] = static_cast(encoded_mipped_storage_address >> 32u) | (static_cast(Prospero::BufferFormat::k32UInt) << 20u) | (3u << 30u); mipped_storage.fields[2] = 15u | (63u << 14u); mipped_storage.fields[3] = DstSel(4, 5, 6, 7) | (1u << 12u) | (3u << 16u) | (static_cast(Prospero::TileMode::kStandard64KB) << 20u) | (static_cast(Prospero::ImageType::kColor2D) << 28u); mipped_storage.fields[5] = 0x00700000u | (3u << 4u); ShaderRecompiler::IR::DescriptorValue mipped_storage_descriptor{}; std::copy(std::begin(mipped_storage.fields), std::end(mipped_storage.fields), mipped_storage_descriptor.dwords.begin()); mipped_storage_descriptor.dword_count = 8; auto overwide_mipped_storage = mipped_storage; overwide_mipped_storage.fields[3] = (overwide_mipped_storage.fields[3] & ~(0xfu << 16u)) | (4u << 16u); ShaderRecompiler::IR::DescriptorValue overwide_mipped_storage_descriptor{}; std::copy(std::begin(overwide_mipped_storage.fields), std::end(overwide_mipped_storage.fields), overwide_mipped_storage_descriptor.dwords.begin()); overwide_mipped_storage_descriptor.dword_count = 8; #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS ExpectFatal("MipViewPhysicalLayoutChange", [&] { auto linear_view = overwide_mipped_storage_descriptor; linear_view.dwords[3] &= ~(0x1fu << 20u); (void)RenderExecutorTestAccess::ResolveTexture(executor, storage_resource, linear_view); }); #endif auto mipped_storage_resource = storage_resource; mipped_storage_resource.mip_mode = ShaderRecompiler::IR::ImageMipMode::DynamicStorage; mipped_storage_resource.mip_count = 3; auto sampled_overwide_resource = storage_resource; sampled_overwide_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; sampled_overwide_resource.numeric_class = Prospero::TextureNumericClass::Uint; sampled_overwide_resource.read = true; sampled_overwide_resource.written = false; auto plain_mipped_storage_binding = RenderExecutorTestAccess::ResolveTexture(executor, storage_resource, mipped_storage_descriptor); vk::ClearValue tail_clear{}; tail_clear.color.uint32[0] = tail_gpu_value; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), plain_mipped_storage_binding.image_id, {vk::ImageAspectFlagBits::eColor, 0, 4, 0, 1}, tail_clear); auto mipped_storage_binding = RenderExecutorTestAccess::ResolveTexture( executor, mipped_storage_resource, mipped_storage_descriptor); auto overwide_mipped_storage_binding = RenderExecutorTestAccess::ResolveTexture( executor, storage_resource, overwide_mipped_storage_descriptor); auto sampled_overwide_resolved = RenderExecutorTestAccess::ResolveTexture( executor, sampled_overwide_resource, overwide_mipped_storage_descriptor); const auto tail0 = ReadCachedTexel(name, context, overwide_mipped_storage_binding.image_id); const auto tail3 = ReadCachedTexel(name, context, overwide_mipped_storage_binding.image_id, {}, {1, 1, 1}, 0, 3); const auto tail4 = ReadCachedTexel(name, context, overwide_mipped_storage_binding.image_id, {}, {1, 1, 1}, 0, 4); Require(name, "expanded tail preserves GPU and backing data", tail0 == std::vector{tail_gpu_value} && tail3 == std::vector{tail_gpu_value} && tail4 == std::vector{tail_backing_value}, "expanding a shared mip tail lost GPU writes or the added mip's backing bytes"); PreparedBindings mipped_prepared{}; ShaderRecompiler::IR::CompiledShaderInfo mipped_program{}; mipped_program.info.images.push_back(storage_resource); mipped_program.info.images.push_back(mipped_storage_resource); mipped_program.info.images.push_back(storage_resource); mipped_program.info.images.push_back(sampled_overwide_resource); ShaderRecompiler::IR::ResourceSnapshot mipped_snapshot{}; mipped_snapshot.images = {mipped_storage_descriptor, mipped_storage_descriptor, overwide_mipped_storage_descriptor, overwide_mipped_storage_descriptor}; ShaderStageRuntime mipped_runtime{&mipped_program, std::move(mipped_snapshot)}; mipped_prepared.runtime = &mipped_runtime; mipped_prepared.images.push_back( std::move(plain_mipped_storage_binding)); mipped_prepared.images.push_back( std::move(mipped_storage_binding)); mipped_prepared.images.push_back( std::move(overwide_mipped_storage_binding)); mipped_prepared.images.push_back( std::move(sampled_overwide_resolved)); executor.RebindImages(mipped_prepared); auto &plain_mipped_binding = mipped_prepared.images[0]; auto &mipped_binding = mipped_prepared.images[1]; auto &overwide_mipped_binding = mipped_prepared.images[2]; auto &sampled_overwide_binding = mipped_prepared.images[3]; plain_mipped_binding.layout = vk::ImageLayout::eGeneral; mipped_binding.layout = vk::ImageLayout::eGeneral; overwide_mipped_binding.layout = vk::ImageLayout::eGeneral; sampled_overwide_binding.layout = vk::ImageLayout::eGeneral; auto plain_view_desc = plain_mipped_binding.desc; plain_view_desc.view_info.level_count = 1; Require(name, "dynamic storage mip views", plain_mipped_binding.desc.view_info.base_level == 1 && plain_mipped_binding.desc.view_info.level_count == 3 && plain_mipped_binding.mip_views.empty() && plain_mipped_binding.image_view == texture_cache.FindTexture(plain_mipped_binding.image_id, plain_view_desc) && mipped_binding.desc.info.resources.levels == 4 && mipped_binding.desc.view_info.base_level == 1 && mipped_binding.desc.view_info.level_count == 3 && mipped_binding.mip_views.size() == 3 && mipped_binding.mip_views[0] != nullptr && mipped_binding.mip_views[1] != nullptr && mipped_binding.mip_views[2] != nullptr && mipped_binding.mip_views[0] != mipped_binding.mip_views[1] && mipped_binding.mip_views[1] != mipped_binding.mip_views[2] && MakeImageInfo(mipped_binding, 0).imageView == mipped_binding.mip_views[0] && MakeImageInfo(mipped_binding, 1).imageView == mipped_binding.mip_views[1] && MakeImageInfo(mipped_binding, 2).imageView == mipped_binding.mip_views[2], "base-1 through last-3 storage view did not create three " "ordered one-level descriptors"); Require(name, "over-wide fixed storage mip view", overwide_mipped_storage.LastLevel() == 4 && overwide_mipped_storage.MaxMip() == 3 && overwide_mipped_binding.image_id == plain_mipped_binding.image_id && overwide_mipped_binding.desc.info.resources.levels == 5 && overwide_mipped_binding.desc.view_info.base_level == 1 && overwide_mipped_binding.desc.view_info.level_count == 4 && overwide_mipped_binding.mip_views.empty() && overwide_mipped_binding.image_view == plain_mipped_binding.image_view, "fixed storage view lost addressable mips in the allocated tail"); Require(name, "over-wide sampled mip view", sampled_overwide_binding.image_id == plain_mipped_binding.image_id && sampled_overwide_binding.desc.info.resources.levels == 5 && sampled_overwide_binding.desc.view_info.base_level == 1 && sampled_overwide_binding.desc.view_info.level_count == 4 && sampled_overwide_binding.mip_views.empty() && sampled_overwide_binding.image_view != nullptr, "sampled view lost addressable mips in the allocated tail"); // Streaming T# clamps apply after S# max LOD, without changing the view // base. { constexpr uint64_t lod_address = base + 0x170000; auto lod_descriptor = mipped_storage_descriptor; lod_descriptor.dwords[0] = static_cast(lod_address >> 8u); lod_descriptor.dwords[1] = static_cast(lod_address >> 40u) | (static_cast(Prospero::BufferFormat::k32Float) << 20u) | (3u << 30u); lod_descriptor.dwords[3] &= ~(0xfu << 12u); ShaderSamplerResource lod_sampler{ {0, 0, static_cast(Prospero::SamplerMipFilter::kLinear) << 26u, 0}}; TestCase lod_test; lod_test.name = "TextureMinLodAfterSamplerClamp"; lod_test.has_user_data = true; lod_test.image_descriptor_swizzle = mipped_storage.DstSelXYZW(); std::copy_n(lod_descriptor.dwords.begin(), 8, lod_test.user_data.begin()); std::copy_n(lod_sampler.fields, 4, lod_test.user_data.begin() + 8); lod_test.user_data[50] = sizeof(uint32_t); lod_test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_SAMPLE, ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM}; lod_test.required_spirv = {"OpImageSampleExplicitLod"}; AppendVMovLiteral(&lod_test.code, 20, std::bit_cast(0.5f)); AppendVMovLiteral(&lod_test.code, 21, std::bit_cast(0.5f)); lod_test.code.push_back(EncodeMimg0(0x27, 1)); lod_test.code.push_back(EncodeMimg1(0, 20, 0, 2)); AppendStoreVgpr(&lod_test.code, 0, 0); AppendEnd(&lod_test.code); const auto lod_program = CompileCase(lod_test, SubgroupSize()); #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS ExpectFatal("TextureMinLodBeyondView", [&] { auto invalid_lod = lod_descriptor; invalid_lod.dwords[1] |= 1024u << 8u; (void)RenderExecutorTestAccess::ResolveTexture( executor, lod_program.program.info.images[0], invalid_lod); }); #endif const auto lod_binding = RenderExecutorTestAccess::ResolveTexture( executor, lod_program.program.info.images[0], lod_descriptor); for (uint32_t mip = 0; mip < 4; ++mip) { vk::ClearValue clear{}; clear.color.float32[0] = static_cast(1u << mip); TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), lod_binding.image_id, {vk::ImageAspectFlagBits::eColor, mip, 1, 0, 1}, clear); } const auto sampler = context.GetSamplerCache().GetSampler(lod_sampler); auto output = CreateStorageBuffer(lod_test.name, {}, 1); struct LodCase { uint32_t base_level; uint32_t min_lod; float expected; float integer_min_lod_expected; }; constexpr std::array lod_cases{ LodCase{0, 0, 1.0f, 1.0f}, LodCase{0, 256, 2.0f, 2.0f}, LodCase{0, 384, 3.0f, 2.0f}, LodCase{1, 384, 3.0f, 2.0f}, LodCase{1, 512, 4.0f, 4.0f}, LodCase{1, 0, 2.0f, 2.0f}, LodCase{0, 256, 2.0f, 2.0f}}; std::array views{}; for (size_t index = 0; index < lod_cases.size(); ++index) { const auto &test = lod_cases[index]; lod_descriptor.dwords[1] = (lod_descriptor.dwords[1] & ~0xfff00u) | (test.min_lod << 8u); lod_descriptor.dwords[3] = (lod_descriptor.dwords[3] & ~(0xfu << 12u)) | (test.base_level << 12u); std::copy_n(lod_descriptor.dwords.begin(), 8, lod_test.user_data.begin()); const auto binding = RenderExecutorTestAccess::ResolveTexture( executor, lod_program.program.info.images[0], lod_descriptor); views[index] = texture_cache.FindTexture(binding.image_id, binding.desc); auto &image = texture_cache.GetImage(binding.image_id); image.Transit(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, {}, scheduler.Current().Handle()); Image sampled; sampled.view = views[index]; sampled.layout = image.backing.state.layout; scheduler.Finish(); Dispatch(lod_test, lod_program, output, nullptr, &sampled, nullptr, nullptr, sampler); const auto result = ReadBuffer(lod_test.name, output, 1)[0]; // Vulkan permits flooring imageViewMinLod instead of retaining its // fraction. Require(lod_test.name, "sampled mip", result == std::bit_cast(test.expected) || result == std::bit_cast( test.integer_min_lod_expected), "texture clamp was lost, applied before sampler max LOD, or " "rebased incorrectly"); } Require(lod_test.name, "view identity", views[0] != views[1] && views[1] != views[2] && views[2] != views[3] && views[1] == views.back(), "different texture clamps aliased or identical clamped views " "were not reused"); DestroyBuffer(&output); RenderExecutorTestAccess::ResetBindings(executor); } auto srgb_storage = storage; constexpr auto srgb_format = static_cast(Prospero::BufferFormat::k8_8_8_8Srgb); constexpr uint64_t srgb_storage_address = base + 0xc0000; const auto encoded_srgb_storage_address = srgb_storage_address >> 8u; srgb_storage.fields[0] = static_cast(encoded_srgb_storage_address); srgb_storage.fields[1] = static_cast(encoded_srgb_storage_address >> 32u) | (srgb_format << 20u); ShaderRecompiler::IR::DescriptorValue srgb_storage_descriptor{}; std::copy(std::begin(srgb_storage.fields), std::end(srgb_storage.fields), srgb_storage_descriptor.dwords.begin()); srgb_storage_descriptor.dword_count = 8; auto srgb_storage_resource = storage_resource; srgb_storage_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; srgb_storage_resource.numeric_class = Prospero::TextureNumericClass::Float; const auto srgb_storage_binding = RenderExecutorTestAccess::ResolveTexture( executor, srgb_storage_resource, srgb_storage_descriptor); const auto srgb_storage_view = texture_cache.FindTexture( srgb_storage_binding.image_id, srgb_storage_binding.desc); Require(name, "sRGB storage view", srgb_storage_view != nullptr && srgb_storage_binding.desc.info.pixel_format == vk::Format::eR8G8B8A8Srgb && srgb_storage_binding.desc.view_info.format == vk::Format::eR8G8B8A8Unorm && texture_cache.GetImage(srgb_storage_binding.image_id) .backing.format == vk::Format::eR8G8B8A8Srgb, "storage descriptor did not preserve its sRGB backing and " "select an UNORM Vulkan view"); ShaderTextureResource sint_storage{{0x01514b00u, 0xc1500000u, 0x000bc00bu, 0x91b00204u, 0x00000000u, 0x00700000u, 0x102b0000u, 0x0001514au}}; Require(name, "PPSA06888 R32 SINT descriptor", sint_storage.Base40() == 0x1514b0000ull && sint_storage.Width5() + 1u == 48 && sint_storage.Height5() + 1u == 48 && sint_storage.Depth() + 1u == 1 && sint_storage.Format() == Prospero::BufferFormat::k32SInt && sint_storage.Type() == Prospero::ImageType::kColor2D && sint_storage.TileMode() == Prospero::TileMode::kRenderTarget && sint_storage.DstSelXYZW() == DstSel(4, 0, 0, 1), "captured write-only signed storage descriptor was decoded " "incorrectly"); const uint64_t mapped_sint_address = base + 0xe0000; const auto encoded_sint_address = mapped_sint_address >> 8u; sint_storage.fields[0] = static_cast(encoded_sint_address); sint_storage.fields[1] = (sint_storage.fields[1] & ~0xffu) | static_cast(encoded_sint_address >> 32u); constexpr uint64_t sint_metadata_address = base + 0x160000; const auto encoded_sint_metadata = sint_metadata_address >> 8u; sint_storage.fields[6] = (sint_storage.fields[6] & 0x00ffffffu) | (static_cast(encoded_sint_metadata & 0xffu) << 24u); sint_storage.fields[7] = static_cast(encoded_sint_metadata >> 8u); WriteMetadata(context, sint_metadata_address, 0x1000, UINT32_MAX); ShaderRecompiler::IR::DescriptorValue sint_storage_descriptor{}; std::copy(std::begin(sint_storage.fields), std::end(sint_storage.fields), sint_storage_descriptor.dwords.begin()); sint_storage_descriptor.dword_count = 8; auto sint_storage_resource = srgb_storage_resource; sint_storage_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; sint_storage_resource.numeric_class = Prospero::TextureNumericClass::Uint; const auto sint_storage_binding = RenderExecutorTestAccess::ResolveTexture( executor, sint_storage_resource, sint_storage_descriptor); const auto sint_storage_view = texture_cache.FindTexture( sint_storage_binding.image_id, sint_storage_binding.desc); Require(name, "PPSA06888 raw R32 SINT storage view", sint_storage_view != nullptr && sint_storage_binding.desc.info.metadata.range == GuestRange{sint_metadata_address, 0x1000} && sint_storage_binding.desc.info.data.size == 0x10000 && sint_storage_binding.desc.info.pixel_format == vk::Format::eR32Sint && sint_storage_binding.desc.view_info.format == vk::Format::eR32Uint && texture_cache.GetImage(sint_storage_binding.image_id) .backing.format == vk::Format::eR32Sint, "write-only R32 SINT storage did not select a bit-compatible " "uint view"); auto narrowed_storage = storage; constexpr uint64_t narrowed_storage_address = base + 0xd0000; const auto encoded_narrowed_address = narrowed_storage_address >> 8u; narrowed_storage.fields[0] = static_cast(encoded_narrowed_address); narrowed_storage.fields[1] = static_cast(encoded_narrowed_address >> 32u) | (static_cast(stencil_format) << 20u); narrowed_storage.fields[3] = DstSel(4, 5, 6, 7) | (static_cast(linear) << 20u) | (static_cast(Prospero::ImageType::kColor1DArray) << 28u); narrowed_storage.fields[4] = 1u | (1u << 16u); ShaderRecompiler::IR::DescriptorValue narrowed_storage_descriptor{}; std::copy(std::begin(narrowed_storage.fields), std::end(narrowed_storage.fields), narrowed_storage_descriptor.dwords.begin()); narrowed_storage_descriptor.dword_count = 8; auto narrowed_storage_resource = storage_resource; narrowed_storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim1D; const auto narrowed_storage_binding = RenderExecutorTestAccess::ResolveTexture( executor, narrowed_storage_resource, narrowed_storage_descriptor); const auto narrowed_storage_view = texture_cache.FindTexture( narrowed_storage_binding.image_id, narrowed_storage_binding.desc); const auto &narrowed_storage_image = texture_cache.GetImage(narrowed_storage_binding.image_id); Require(name, "narrowed 1D array storage view", narrowed_storage_view != nullptr && narrowed_storage_binding.desc.info.type == Prospero::ImageType::kColor1D && narrowed_storage_binding.desc.info.resources.layers == 2 && narrowed_storage_binding.desc.view_info.type == vk::ImageViewType::e1D && narrowed_storage_binding.desc.view_info.base_layer == 1 && narrowed_storage_binding.desc.view_info.layer_count == 1 && narrowed_storage_image.backing.image_type == vk::ImageType::e1D, "non-array 1D specialization did not select the descriptor " "base layer from its 1D-array backing"); ShaderRecompiler::IR::ResourceSnapshot storage_snapshot{}; storage_snapshot.images.push_back(storage_descriptor); ShaderRecompiler::IR::CompiledShaderInfo storage_info{}; storage_info.stage = storage_program.stage; storage_info.info = std::move(storage_program.info); storage_info.bindings = std::move(storage_program.bindings); ShaderStageRuntime storage_runtime{&storage_info, std::move(storage_snapshot)}; auto storage_discovery = executor.PrepareBindings(storage_runtime); const auto storage_id = storage_discovery.images[0].image_id; Require(name, "storage prefetch purity", storage_discovery.images[0].image_view == nullptr && texture_cache.GetImage(storage_id).binding.is_bound && !texture_cache.GetImage(storage_id).usage.storage && !TextureCacheTestAccess::PendingDownload(texture_cache, storage_id), "the first descriptor pass performed final storage acquisition"); RenderExecutorTestAccess::ResetBindings(executor); ShaderRecompiler::IR::Program sampled_program{}; sampled_program.stage = ShaderType::Pixel; sampled_program.resource_tracking_complete = true; ShaderRecompiler::IR::ImageResource sampled_resource{}; sampled_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; sampled_resource.numeric_class = Prospero::TextureNumericClass::Uint; sampled_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; sampled_resource.read = true; sampled_program.info.images.push_back(sampled_resource); allocate_bindings(sampled_program); ShaderRecompiler::IR::ResourceSnapshot sampled_snapshot{}; sampled_snapshot.images.push_back(storage_descriptor); ShaderRecompiler::IR::CompiledShaderInfo sampled_info{}; sampled_info.stage = sampled_program.stage; sampled_info.info = std::move(sampled_program.info); sampled_info.bindings = std::move(sampled_program.bindings); ShaderStageRuntime sampled_runtime{&sampled_info, std::move(sampled_snapshot)}; constexpr uint64_t ordered_sampled_address = base + 0x10000; const uint32_t ordered_sampled_value = 0x89abcdefu; std::memcpy(reinterpret_cast(mapped) + 0x10000, &ordered_sampled_value, sizeof(ordered_sampled_value)); auto ordered_sampled = storage; const auto encoded_ordered_address = ordered_sampled_address >> 8u; ordered_sampled.fields[0] = static_cast(encoded_ordered_address); ordered_sampled.fields[1] = static_cast(encoded_ordered_address >> 32u) | (static_cast(stencil_format) << 20u); ShaderRecompiler::IR::DescriptorValue ordered_descriptor{}; std::copy(std::begin(ordered_sampled.fields), std::end(ordered_sampled.fields), ordered_descriptor.dwords.begin()); ordered_descriptor.dword_count = 8; ShaderRecompiler::IR::ResourceSnapshot ordered_snapshot{}; ordered_snapshot.images.push_back(ordered_descriptor); ShaderStageRuntime ordered_sampled_runtime{sampled_runtime.program, std::move(ordered_snapshot)}; auto ordered_bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, storage_runtime, ordered_sampled_runtime, true); const auto ordered_sampled_id = ordered_bindings.pixel->images[0].image_id; Require( name, "VS-before-PS retained-owner order", ordered_bindings.vertex[0].images[0].image_id == storage_id && ordered_sampled_id != storage_id && RenderExecutorTestAccess::BoundImagesInOrder(executor, storage_id, ordered_sampled_id), "production graphics binding did not retain vertex resources " "before pixel resources"); RenderExecutorTestAccess::ResetBindings(executor); auto graphics_bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, storage_runtime, sampled_runtime, true); const auto &storage_binding = graphics_bindings.vertex[0].images[0]; const auto &sampled_binding = graphics_bindings.pixel->images[0]; Require(name, "storage final acquisition", storage_binding.image_view != nullptr && texture_cache.GetImage(storage_id).usage.storage && texture_cache.GetImage(storage_id).IsGpuModified() && TextureCacheTestAccess::PendingDownload(texture_cache, storage_id), "the production graphics binding path did not establish GPU " "ownership before storage readback"); Require(name, "VS-to-PS image acquisition order", storage_binding.image_id == storage_id && sampled_binding.image_id == storage_id && texture_cache.GetImage(storage_id).binding.force_general && texture_cache.GetImage(storage_id).binding.shader_write && texture_cache.GetImage(storage_id).IsGpuModified() && texture_cache.GetImage(storage_id).usage.storage && texture_cache.GetImage(storage_id).usage.texture, "the production graphics binding path did not complete vertex " "storage acquisition before pixel sampling"); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), graphics_bindings.vertex[0], *graphics_bindings.pixel)); Require(name, "early writable alias retention", texture_cache.GetImage(storage_id).backing.state.access_mask == (vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite), "a later sampled alias dropped an earlier storage write access"); RenderExecutorTestAccess::ResetBindings(executor); ShaderRecompiler::IR::Program vertex_sampled_program{}; vertex_sampled_program.stage = ShaderType::Vertex; vertex_sampled_program.resource_tracking_complete = true; vertex_sampled_program.info.images.push_back(sampled_resource); allocate_bindings(vertex_sampled_program); ShaderRecompiler::IR::CompiledShaderInfo vertex_sampled_info{}; vertex_sampled_info.stage = vertex_sampled_program.stage; vertex_sampled_info.info = std::move(vertex_sampled_program.info); vertex_sampled_info.bindings = std::move(vertex_sampled_program.bindings); ShaderStageRuntime vertex_sampled_runtime{ &vertex_sampled_info, sampled_runtime.resources}; ShaderRecompiler::IR::Program pixel_storage_program{}; pixel_storage_program.stage = ShaderType::Pixel; pixel_storage_program.resource_tracking_complete = true; pixel_storage_program.info.images.push_back(storage_resource); allocate_bindings(pixel_storage_program); ShaderRecompiler::IR::CompiledShaderInfo pixel_storage_info{}; pixel_storage_info.stage = pixel_storage_program.stage; pixel_storage_info.info = std::move(pixel_storage_program.info); pixel_storage_info.bindings = std::move(pixel_storage_program.bindings); ShaderStageRuntime pixel_storage_runtime{&pixel_storage_info, storage_runtime.resources}; auto writable_alias_bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, vertex_sampled_runtime, pixel_storage_runtime, true); Require(name, "late writable alias promotion", texture_cache.GetImage(storage_id).binding.force_general, "an already sampled image was not promoted when a later " "storage alias bound the same backing"); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), writable_alias_bindings.vertex[0], *writable_alias_bindings.pixel)); Require( name, "forced-general descriptor capture", writable_alias_bindings.vertex[0].images[0].layout == vk::ImageLayout::eGeneral && writable_alias_bindings.pixel->images[0].layout == vk::ImageLayout::eGeneral && MakeImageInfo(writable_alias_bindings.vertex[0].images[0]) .imageLayout == vk::ImageLayout::eGeneral && MakeImageInfo(writable_alias_bindings.pixel->images[0]) .imageLayout == vk::ImageLayout::eGeneral && texture_cache.GetImage(storage_id).backing.state.layout == vk::ImageLayout::eGeneral && texture_cache.GetImage(storage_id).backing.state.access_mask == (vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite), "sampled/storage aliases did not retain the promoted general " "layout and writable access in both generated descriptors"); RenderExecutorTestAccess::ResetBindings(executor); const std::array split_mip_data{0x01020304u, 0x05060708u, 0x090a0b0cu}; std::memcpy(reinterpret_cast(mapped) + 0x50000, split_mip_data.data(), sizeof(split_mip_data)); ImageDesc split_desc{}; split_desc.type = BindingType::Texture; split_desc.info.data = {base + 0x50000, sizeof(split_mip_data)}; split_desc.info.pixel_format = vk::Format::eR32Uint; split_desc.info.guest_format = Prospero::BufferFormat::k32UInt; split_desc.info.type = Prospero::ImageType::kColor2D; split_desc.info.extent = {2, 1, 1}; split_desc.info.resources = {2, 1}; split_desc.info.pitch = 2; split_desc.info.bytes_per_block = 4; split_desc.info.samples = 1; split_desc.info.tile_mode = linear; split_desc.info.mip_layout[0] = {0, 8, 2, 1}; split_desc.info.mip_layout[1] = {8, 4, 1, 1}; split_desc.view_info.format = split_desc.info.pixel_format; split_desc.view_info.type = vk::ImageViewType::e2D; split_desc.view_info.aspect = vk::ImageAspectFlagBits::eColor; split_desc.view_info.level_count = 2; split_desc.view_info.layer_count = 1; split_desc.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto split_id = texture_cache.FindImage(split_desc); auto split_storage_desc = split_desc; split_storage_desc.type = BindingType::Storage; split_storage_desc.view_info.base_level = 0; split_storage_desc.view_info.level_count = 1; split_storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage; auto split_sampled_desc = split_desc; split_sampled_desc.view_info.base_level = 1; split_sampled_desc.view_info.level_count = 1; split_sampled_desc.view_info.usage = vk::ImageUsageFlagBits::eSampled; ShaderRecompiler::IR::Program split_ir{}; split_ir.stage = ShaderType::Vertex; split_ir.resource_tracking_complete = true; split_ir.info.images = {storage_resource, sampled_resource}; allocate_bindings(split_ir); ShaderRecompiler::IR::CompiledShaderInfo split_program{}; split_program.stage = split_ir.stage; split_program.info = std::move(split_ir.info); split_program.bindings = std::move(split_ir.bindings); PreparedBindings split_bindings{}; ShaderStageRuntime split_runtime{.program = &split_program}; split_bindings.runtime = &split_runtime; split_bindings.images.push_back( {split_id, texture_cache.FindTexture(split_id, split_storage_desc), split_storage_desc}); split_bindings.images.push_back( {split_id, texture_cache.FindTexture(split_id, split_sampled_desc), split_sampled_desc}); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), split_bindings)); const auto &split_image = texture_cache.GetImage(split_id); Require( name, "per-binding subresource layouts", split_bindings.images[0].layout == vk::ImageLayout::eGeneral && split_bindings.images[1].layout == vk::ImageLayout::eShaderReadOnlyOptimal && MakeImageInfo(split_bindings.images[0]).imageLayout == vk::ImageLayout::eGeneral && MakeImageInfo(split_bindings.images[1]).imageLayout == vk::ImageLayout::eShaderReadOnlyOptimal && split_image.backing.subresource_states.size() == 2 && split_image.backing.subresource_states[0].layout == vk::ImageLayout::eGeneral && split_image.backing.subresource_states[1].layout == vk::ImageLayout::eShaderReadOnlyOptimal, "descriptor layouts were queried after a later mip transition " "instead of being captured at each binding"); Libs::LibKernel::Memory::WriteBacking( storage_address, &storage_stale_value, sizeof(storage_stale_value)); texture_cache.ProcessDownloadImages(); Require( name, "storage acquisition download consumption", !TextureCacheTestAccess::PendingDownload(texture_cache, storage_id), "submit-time processing retained an acquired storage request"); RenderExecutorTestAccess::ResetBindings(executor); scheduler.Finish(); scheduler.DrainPriorityOperations(); uint32_t storage_downloaded_value = 0; Libs::LibKernel::Memory::TryReadBacking(storage_address, &storage_downloaded_value, sizeof(storage_downloaded_value)); const uint32_t storage_expected_value = (storage_stale_value & 0xffffff00u) | (storage_native_value & 0xffu); Require(name, "storage acquisition readback window", storage_downloaded_value == storage_expected_value && texture_cache.GetImage(storage_id).IsGpuModified(), "readback enrollment was consumed before final storage " "ownership became visible"); constexpr uint64_t phased_depth_address = base + 0x90000; constexpr uint64_t phased_stencil_address = base + 0xa0000; constexpr uint64_t phased_htile_address = base + 0xb0000; HW::DepthRenderTarget phased_depth_target{}; phased_depth_target.z_info.format = Prospero::DepthFormat::kZ32F; phased_depth_target.z_info.texture_compatibility = Prospero::TextureCompatiblePlaneCompression::kEnable; phased_depth_target.z_info.htile_acceleration = true; phased_depth_target.z_info.z_compare_base = Prospero::ZCompareBase::kZMax; phased_depth_target.stencil_info.format = Prospero::StencilFormat::k8UInt; phased_depth_target.stencil_info.texture_compatibility = Prospero::TextureCompatibleStencil::kEnable; phased_depth_target.stencil_info.htile_stencil_disabled = false; phased_depth_target.z_read_base_addr = phased_depth_address; phased_depth_target.z_write_base_addr = phased_depth_address; phased_depth_target.stencil_read_base_addr = phased_stencil_address; phased_depth_target.stencil_write_base_addr = phased_stencil_address; phased_depth_target.htile_data_base_addr = phased_htile_address; phased_depth_target.size.valid = true; registers.SetDepthRenderTarget(phased_depth_target); HW::DepthControl phased_depth_control{}; phased_depth_control.z_write_enable = true; registers.SetDepthControl(phased_depth_control); registers.SetRenderControl({}); RenderDepthInfo disabled_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), disabled_depth); Require(name, "disabled tests with stale depth write enable", !disabled_depth.image_id, "a dormant depth write bit bound a depth attachment"); phased_depth_control.z_enable = true; registers.SetDepthControl(phased_depth_control); HW::RenderControl phased_render_control{}; phased_render_control.resummarize_enable = true; registers.SetRenderControl(phased_render_control); RenderDepthInfo phased_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), phased_depth); auto non_texture_compatible_target = phased_depth_target; non_texture_compatible_target.z_info.texture_compatibility = Prospero::TextureCompatiblePlaneCompression::kDisable; non_texture_compatible_target.stencil_info.texture_compatibility = Prospero::TextureCompatibleStencil::kDisable; registers.SetDepthRenderTarget(non_texture_compatible_target); RenderDepthInfo non_texture_compatible_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), non_texture_compatible_depth); Require( name, "depth texture compatibility identity", phased_depth.image_id && (phased_depth.desc.info.metadata.kind == ImageMetadataKind::Htile) && non_texture_compatible_depth.image_id == phased_depth.image_id && non_texture_compatible_depth.desc.info.data == phased_depth.desc.info.data && non_texture_compatible_depth.desc.info.stencil == phased_depth.desc.info.stencil && non_texture_compatible_depth.desc.info.metadata.range == phased_depth.desc.info.metadata.range && phased_depth.desc.info.metadata.stencil_compressed && !phased_depth.depth_clear_enable && !texture_cache.IsMeta(phased_depth.desc.info.metadata.range.address) && !texture_cache.GetImage(phased_depth.image_id).IsGpuModified() && !texture_cache.GetImage(phased_depth.image_id).usage.depth_target, "valid PS5 texture-compatibility policy changed logical depth image " "identity or discovery side effects"); auto read_only_depth_target = phased_depth_target; read_only_depth_target.z_write_base_addr = 0; read_only_depth_target.stencil_write_base_addr = 0; read_only_depth_target.depth_view.depth_write_disable = true; read_only_depth_target.depth_view.stencil_write_disable = true; registers.SetDepthRenderTarget(read_only_depth_target); auto read_only_depth_control = phased_depth_control; read_only_depth_control.z_write_enable = false; read_only_depth_control.zfunc = static_cast(vk::CompareOp::eAlways); read_only_depth_control.stencil_enable = true; read_only_depth_control.stencilfunc = static_cast(vk::CompareOp::eAlways); registers.SetDepthControl(read_only_depth_control); HW::StencilControl read_only_stencil_control{}; read_only_stencil_control.stencil_zpass = static_cast(Prospero::StencilOp::kReplaceOp); registers.SetStencilControl(read_only_stencil_control); HW::StencilMask read_only_stencil_mask{}; read_only_stencil_mask.stencil_testval = 0x12; read_only_stencil_mask.stencil_mask = 0xff; read_only_stencil_mask.stencil_writemask = 0xff; read_only_stencil_mask.stencil_opval = 0x34; registers.SetStencilMask(read_only_stencil_mask); RenderDepthInfo read_only_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), read_only_depth); Require( name, "read-only depth/stencil target without write addresses", read_only_depth.image_id == phased_depth.image_id && read_only_depth.desc.info.data.address == phased_depth_address && read_only_depth.desc.info.stencil.address == phased_stencil_address && !read_only_depth.depth_write_enable && !read_only_depth.stencil_clear_enable && read_only_depth.stencil_test_enable && read_only_depth.stencil_front.writeMask == 0 && read_only_depth.stencil_back.writeMask == 0 && read_only_depth.stencil_front.passOp == vk::StencilOp::eKeep && !read_only_depth.AttachmentWriteAspects() && depth_attachment_layout(read_only_depth) == vk::ImageLayout::eDepthStencilReadOnlyOptimal, "a PS5 read-only depth/stencil target required write addresses or " "retained host writes"); auto read_only_render_control = phased_render_control; read_only_render_control.stencil_clear_enable = true; registers.SetRenderControl(read_only_render_control); RenderDepthInfo read_only_clear_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), read_only_clear_depth); Require(name, "read-only stencil clear suppression", read_only_clear_depth.image_id == phased_depth.image_id && !read_only_clear_depth.stencil_clear_enable && !read_only_clear_depth.AttachmentWriteAspects() && depth_attachment_layout(read_only_clear_depth) == vk::ImageLayout::eDepthStencilReadOnlyOptimal, "a PS5 target-level stencil write disable retained a host clear"); registers.SetDepthRenderTarget(phased_depth_target); registers.SetDepthControl(phased_depth_control); registers.SetStencilControl({}); registers.SetStencilMask({}); registers.SetRenderControl(phased_render_control); RenderColorInfo no_color{}; auto phased_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &no_color, 0, phased_depth); Require( name, "HTile final acquisition", phased_rendering.depth_stencil_attachment.image_view != nullptr && phased_rendering.num_color_attachments == 0 && phased_rendering.depth_stencil_attachment.has_depth && phased_rendering.depth_stencil_attachment.depth_clear && phased_depth.depth_load_clear_enable && texture_cache.IsMeta(phased_depth.desc.info.metadata.range.address) && !texture_cache.IsMetaCleared( phased_depth.desc.info.metadata.range.address, phased_depth.desc.view_info.base_layer) && texture_cache.GetImage(phased_depth.image_id).IsGpuModified() && texture_cache.GetImage(phased_depth.image_id).usage.depth_target, "final depth acquisition did not publish and consume HTile " "state at the final acquisition boundary"); RenderExecutorTestAccess::ResetBindings(executor); constexpr uint64_t depth_only_address = base + 0x140000; constexpr uint64_t depth_only_htile_address = base + 0x150000; constexpr uint32_t captured_depth_only_z_info = 0x22900983u; constexpr uint32_t captured_depth_only_stencil_info = 0x00100980u; HW::DepthRenderTarget depth_only_target{}; depth_only_target.z_info = HW::DepthZInfo::Decode(captured_depth_only_z_info); depth_only_target.stencil_info = HW::DepthStencilInfo::Decode(captured_depth_only_stencil_info); depth_only_target.z_read_base_addr = depth_only_address; depth_only_target.z_write_base_addr = depth_only_address; depth_only_target.htile_data_base_addr = depth_only_htile_address; depth_only_target.size = {63, 63, true}; registers.SetDepthRenderTarget(depth_only_target); HW::DepthControl depth_only_control{}; depth_only_control.stencil_enable = true; depth_only_control.z_enable = true; depth_only_control.z_write_enable = true; depth_only_control.zfunc = static_cast(vk::CompareOp::eAlways); registers.SetDepthControl(depth_only_control); HW::RenderControl depth_only_render_control{}; depth_only_render_control.depth_clear_enable = true; depth_only_render_control.stencil_clear_enable = true; registers.SetRenderControl(depth_only_render_control); RenderDepthInfo depth_only{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), depth_only); Require( name, "depth-only target with stale stencil state", depth_only.image_id && depth_only.desc.view_info.format == vk::Format::eD32Sfloat && (depth_only.desc.info.metadata.kind == ImageMetadataKind::Htile) && depth_only.depth_test_enable && !depth_only.depth_write_enable && depth_only.depth_clear_enable && !depth_only.stencil_test_enable && !depth_only.stencil_clear_enable && depth_only.desc.info.stencil.Empty() && depth_only.desc.info.metadata.range.address == depth_only_htile_address && depth_only.desc.info.data.size != 0 && depth_only_address + depth_only.desc.info.data.size <= base + allocation_size && depth_only.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eDepth, "inactive stencil state changed a depth-only HTile attachment"); RenderExecutorTestAccess::ResetBindings(executor); const auto sampled_width = depth_only.desc.info.extent.width - 1u; const auto sampled_height = depth_only.desc.info.extent.height - 1u; const auto sampled_address = depth_only_address >> 8u; ShaderTextureResource sampled_depth_descriptor{}; sampled_depth_descriptor.fields[0] = static_cast(sampled_address); sampled_depth_descriptor.fields[1] = static_cast(sampled_address >> 32u) | (static_cast(Prospero::BufferFormat::k32UInt) << 20u) | ((sampled_width & 3u) << 30u); sampled_depth_descriptor.fields[2] = (sampled_width >> 2u) | (sampled_height << 14u); sampled_depth_descriptor.fields[3] = DstSel(4, 4, 4, 4) | (static_cast(Prospero::TileMode::kDepth) << 20u) | (static_cast(Prospero::ImageType::kColor2D) << 28u); sampled_depth_descriptor.fields[5] = 0x00700000u; ShaderRecompiler::IR::DescriptorValue sampled_depth_value{}; std::copy(std::begin(sampled_depth_descriptor.fields), std::end(sampled_depth_descriptor.fields), sampled_depth_value.dwords.begin()); sampled_depth_value.dword_count = 8; ShaderRecompiler::IR::ImageResource sampled_depth_resource{}; sampled_depth_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; sampled_depth_resource.numeric_class = Prospero::TextureNumericClass::Uint; sampled_depth_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; sampled_depth_resource.read = true; const auto sampled_depth_backing = texture_cache.GetImage(depth_only.image_id).backing.image; auto sampled_depth_binding = RenderExecutorTestAccess::ResolveTexture( executor, sampled_depth_resource, sampled_depth_value); const auto sampled_depth_view = texture_cache.FindTexture( sampled_depth_binding.image_id, sampled_depth_binding.desc); const auto &sampled_depth_image = texture_cache.GetImage(sampled_depth_binding.image_id); const bool normalized_depth_view = std::ranges::any_of( sampled_depth_image.views, [&](const auto &cached) { return cached.view == sampled_depth_view && cached.info.format == vk::Format::eD32Sfloat && cached.info.aspect == vk::ImageAspectFlagBits::eDepth && cached.info.usage == vk::ImageUsageFlags{}; }); Require( name, "R32 uint depth-backed view", sampled_depth_binding.image_id == depth_only.image_id && sampled_depth_binding.desc.info.guest_format == Prospero::BufferFormat::k32UInt && sampled_depth_binding.desc.info.pixel_format == vk::Format::eR32Uint && sampled_depth_binding.desc.view_info.format == vk::Format::eR32Uint && sampled_depth_binding.desc.view_info.aspect == vk::ImageAspectFlagBits::eColor && sampled_depth_binding.desc.view_info.usage == vk::ImageUsageFlagBits::eSampled && sampled_depth_image.info.pixel_format == vk::Format::eD32Sfloat && sampled_depth_image.backing.format == vk::Format::eD32Sfloat && sampled_depth_image.backing.image == sampled_depth_backing && sampled_depth_view != nullptr && normalized_depth_view, "uint T# did not retain its requested format while the concrete " "view followed the D32 backing"); RenderExecutorTestAccess::ResetBindings(executor); // Attachment acquisition consumes pending HTile clears using DB_DEPTH_CLEAR. (void)texture_cache.FindDepthTarget(depth_only.image_id, depth_only.desc); auto bounds_target = depth_only_target; bounds_target.z_write_base_addr = 0; bounds_target.depth_view.depth_write_disable = true; registers.SetDepthRenderTarget(bounds_target); HW::DepthControl bounds_control{}; bounds_control.depth_bounds_enable = true; registers.SetDepthControl(bounds_control); registers.SetRenderControl({}); registers.SetDepthClearValue(0.375f); ShaderStageRuntime bounds_vertex{&vertex_sampled_info, {}}; ShaderStageRuntime bounds_pixel{&sampled_info, {}}; bounds_vertex.resources.images.push_back(sampled_depth_value); bounds_pixel.resources.images.push_back(sampled_depth_value); const auto depth_texels = depth_only.desc.info.extent.width * depth_only.desc.info.extent.height; const auto depth_bytes = depth_texels * sizeof(uint32_t); auto bounds_readback = CreateHostBuffer(name, depth_bytes * 2, vk::BufferUsageFlagBits::eTransferDst, {}); for (uint32_t pass = 0; pass < 2; ++pass) { Require(name, "bounds prior depth contents", texture_cache.ClearImageFromBuffer(scheduler.Current(), depth_only.desc.info.data.address, depth_only.desc.info.data.size, std::bit_cast(0.625f)), "failed to seed depth before the deferred HTile clear"); if (pass == 0) { Require(name, "bounds deferred HTile clear", texture_cache.ClearMeta(depth_only_htile_address), "depth-only HTile was not registered"); } else { bounds_target.z_write_base_addr = depth_only_address; bounds_target.depth_view.depth_write_disable = false; bounds_control.z_write_enable = true; registers.SetDepthRenderTarget(bounds_target); registers.SetDepthControl(bounds_control); } RenderDepthInfo bounds_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), bounds_depth); auto bounds_bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, bounds_vertex, bounds_pixel, true); const auto bounds_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &no_color, 0, bounds_depth, bounds_bindings.pixel); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), bounds_bindings.vertex[0], *bounds_bindings.pixel)); const auto &vertex_depth = bounds_bindings.vertex[0].images[0]; const auto &pixel_depth = bounds_bindings.pixel->images[0]; constexpr auto readonly_layout = vk::ImageLayout::eDepthReadOnlyOptimal; Require(name, "deferred clear with sampled read-only depth bounds", bounds_depth.image_id == depth_only.image_id && bounds_depth.depth_bounds_test_enable && !bounds_depth.depth_write_enable && bounds_depth.depth_load_clear_enable == (pass == 0) && !texture_cache.IsMetaCleared(depth_only_htile_address, 0) && bounds_rendering.depth_stencil_attachment.image_layout == readonly_layout && bounds_rendering.depth_stencil_attachment.depth_clear == (pass == 0) && vertex_depth.image_id == depth_only.image_id && pixel_depth.image_id == depth_only.image_id && MakeImageInfo(vertex_depth).imageLayout == readonly_layout && MakeImageInfo(pixel_depth).imageLayout == readonly_layout, "deferred clear changed guest depth writes, sampled layouts, or repeated"); scheduler.BeginRendering(bounds_rendering); scheduler.EndRendering(); RenderExecutorTestAccess::ResetBindings(executor); const vk::BufferImageCopy copy{pass * depth_bytes, 0, 0, {vk::ImageAspectFlagBits::eDepth, 0, 0, 1}, {}, depth_only.desc.info.extent}; texture_cache.GetImage(depth_only.image_id).Download( std::span{©, 1}, bounds_readback.buffer, 0, bounds_readback.size); } vk::MemoryBarrier2 bounds_host_barrier{}; bounds_host_barrier.sType = vk::StructureType::eMemoryBarrier2; bounds_host_barrier.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; bounds_host_barrier.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; bounds_host_barrier.dstStageMask = vk::PipelineStageFlagBits2::eHost; bounds_host_barrier.dstAccessMask = vk::AccessFlagBits2::eHostRead; vk::DependencyInfo bounds_dependency{}; bounds_dependency.sType = vk::StructureType::eDependencyInfo; bounds_dependency.memoryBarrierCount = 1; bounds_dependency.pMemoryBarriers = &bounds_host_barrier; scheduler.Current().Handle().pipelineBarrier2(bounds_dependency); scheduler.Finish(); const auto bounds_result = ReadBuffer(name, bounds_readback, depth_texels * 2); Require(name, "deferred HTile clear occurs once", std::ranges::all_of(std::span{bounds_result}.first(depth_texels), [](uint32_t value) { return value == std::bit_cast(0.375f); }) && std::ranges::all_of(std::span{bounds_result}.subspan(depth_texels), [](uint32_t value) { return value == std::bit_cast(0.625f); }), "pending clear did not use DB_DEPTH_CLEAR or cleared the next acquisition again"); DestroyBuffer(&bounds_readback); registers.SetDepthClearValue(0.0f); // DB override copies run with depth and stencil tests disabled. constexpr uint64_t copy_source_depth = base + 0x180000; constexpr uint64_t copy_source_stencil = base + 0x1a0000; constexpr uint64_t copy_destination_depth = base + 0x1c0000; constexpr uint64_t copy_destination_stencil = base + 0x1e0000; constexpr uint32_t copy_side = 8; constexpr uint32_t copy_layer_texels = copy_side * copy_side; constexpr uint32_t copy_depth_bytes = copy_layer_texels * 2 * sizeof(uint32_t); constexpr uint32_t copy_readback_stride = copy_depth_bytes + copy_layer_texels * 2; HW::DepthRenderTarget copy_target{}; copy_target.z_info.format = Prospero::DepthFormat::kZ32F; copy_target.z_info.texture_compatibility = Prospero::TextureCompatiblePlaneCompression::kEnable; copy_target.stencil_info.format = Prospero::StencilFormat::k8UInt; copy_target.stencil_info.texture_compatibility = Prospero::TextureCompatibleStencil::kEnable; copy_target.stencil_info.htile_stencil_disabled = true; copy_target.depth_view.slice_max = 1; copy_target.size = {copy_side - 1, copy_side - 1, true}; HW::DepthControl copy_seed_control{}; copy_seed_control.z_enable = true; copy_seed_control.z_write_enable = true; copy_seed_control.stencil_enable = true; registers.SetDepthControl(copy_seed_control); registers.SetRenderControl({}); const auto make_copy_target = [&](uint64_t depth, uint64_t stencil) { auto target = copy_target; target.z_read_base_addr = depth; target.z_write_base_addr = depth; target.stencil_read_base_addr = stencil; target.stencil_write_base_addr = stencil; registers.SetDepthRenderTarget(target); RenderDepthInfo resolved{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), resolved); (void)texture_cache.FindDepthTarget(resolved.image_id, resolved.desc); RenderExecutorTestAccess::ResetBindings(executor); return resolved; }; const auto copy_source = make_copy_target(copy_source_depth, copy_source_stencil); const auto copy_destination = make_copy_target(copy_destination_depth, copy_destination_stencil); const auto clear_copy_target = [&](ImageId id, bool source) { auto &image = texture_cache.GetImage(id); image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}, scheduler.Current().Handle()); for (uint32_t layer = 0; layer < 2; ++layer) { const vk::ClearDepthStencilValue clear{ source ? (layer == 0 ? 0.75f : 0.25f) : (layer == 0 ? 0.875f : 0.625f), source ? (layer == 0 ? 0xa3u : 0x5au) : (layer == 0 ? 0x31u : 0xc4u)}; const vk::ImageSubresourceRange range{ vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil, 0, 1, layer, 1}; scheduler.Current().Handle().clearDepthStencilImage( image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear, 1, &range); } texture_cache.MarkGpuWritten(id); }; struct CopyCase { const char *label; HW::DepthRenderOverride override; uint32_t color_mode; bool depth; bool stencil; }; constexpr std::array copy_cases{ CopyCase{"no override", {}, 0, false, false}, CopyCase{ "depth valid only", {true, false, false, false}, 0, false, false}, CopyCase{ "depth dirty only", {false, true, false, false}, 0, false, false}, CopyCase{"stencil valid only", {false, false, true, false}, 0, false, false}, CopyCase{"stencil dirty only", {false, false, false, true}, 0, false, false}, CopyCase{ "unpaired aspects", {true, false, false, true}, 0, false, false}, CopyCase{"color enabled", {true, true, true, true}, 1, false, false}, CopyCase{"depth only", {true, true, false, false}, 0, true, false}, CopyCase{"stencil only", {false, false, true, true}, 0, false, true}, CopyCase{ "depth and stencil", {true, true, true, true}, 0, true, true}}; auto copy_readback = CreateHostBuffer(name, copy_cases.size() * copy_readback_stride, vk::BufferUsageFlagBits::eTransferDst, {}); copy_target.z_read_base_addr = copy_source_depth; copy_target.z_write_base_addr = copy_destination_depth; copy_target.stencil_read_base_addr = copy_source_stencil; copy_target.stencil_write_base_addr = copy_destination_stencil; copy_target.depth_view.slice_start = 1; registers.SetDepthRenderTarget(copy_target); registers.SetDepthControl({}); RenderDepthInfo copy_disabled_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), copy_disabled_depth); Require(name, "copy without attachment tests", !copy_disabled_depth.image_id, "disabled depth/stencil tests acquired a regular attachment"); for (size_t index = 0; index < copy_cases.size(); ++index) { const auto &test = copy_cases[index]; clear_copy_target(copy_source.image_id, true); clear_copy_target(copy_destination.image_id, false); registers.SetDepthRenderOverride(test.override); auto color_control = registers.GetColorControl(); color_control.mode = test.color_mode; registers.SetColorControl(color_control); Require(name, test.label, RenderExecutorTestAccess::DepthStencilCopy( executor, scheduler.Current()) == (test.depth || test.stencil), "DB override copy did not honor paired flags and disabled " "color output"); RenderExecutorTestAccess::ResetBindings(executor); const auto offset = index * copy_readback_stride; const std::array copies{ {{offset, 0, 0, {vk::ImageAspectFlagBits::eDepth, 0, 0, 2}, {}, copy_destination.desc.info.extent}, {offset + copy_depth_bytes, 0, 0, {vk::ImageAspectFlagBits::eStencil, 0, 0, 2}, {}, copy_destination.desc.info.extent}}}; texture_cache.GetImage(copy_destination.image_id) .Download(copies, copy_readback.buffer, 0, copy_readback.size); } scheduler.Current().Handle().pipelineBarrier2(bounds_dependency); scheduler.Finish(); const auto copy_result = ReadBuffer( name, copy_readback, copy_cases.size() * copy_readback_stride / sizeof(uint32_t)); for (size_t index = 0; index < copy_cases.size(); ++index) { const auto &test = copy_cases[index]; const auto *depth = copy_result.data() + index * copy_readback_stride / sizeof(uint32_t); const auto *stencil = reinterpret_cast(depth) + copy_depth_bytes; for (uint32_t layer = 0; layer < 2; ++layer) { const auto expected_depth = std::bit_cast(layer == 0 ? 0.875f : test.depth ? 0.25f : 0.625f); const auto expected_stencil = layer == 0 ? 0x31u : test.stencil ? 0x5au : 0xc4u; for (uint32_t texel = 0; texel < copy_layer_texels; ++texel) { const auto position = layer * copy_layer_texels + texel; Require(name, test.label, depth[position] == expected_depth && stencil[position] == expected_stencil, "DB override copy changed an unselected layer/aspect or " "missed copied texels"); } } } DestroyBuffer(©_readback); ShaderTextureResource copy_sample_descriptor{}; copy_sample_descriptor.fields[0] = static_cast(copy_destination_depth >> 8u); copy_sample_descriptor.fields[1] = static_cast(copy_destination_depth >> 40u) | (static_cast(Prospero::BufferFormat::k32Float) << 20u) | (((copy_side - 1u) & 3u) << 30u); copy_sample_descriptor.fields[2] = ((copy_side - 1u) >> 2u) | ((copy_side - 1u) << 14u); copy_sample_descriptor.fields[3] = DstSel(4, 4, 4, 4) | (static_cast(Prospero::TileMode::kDepth) << 20u) | (static_cast(Prospero::ImageType::kColor2DArray) << 28u); copy_sample_descriptor.fields[4] = 1; copy_sample_descriptor.fields[5] = 0x00700000u; TestCase copy_sample_test; copy_sample_test.name = "DepthOverrideR32Sampling"; copy_sample_test.has_user_data = true; copy_sample_test.image_descriptor_swizzle = copy_sample_descriptor.DstSelXYZW(); std::copy_n(copy_sample_descriptor.fields, 8, copy_sample_test.user_data.begin()); copy_sample_test.user_data[50] = 2 * sizeof(uint32_t); copy_sample_test.opcodes = { ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_SAMPLE, ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM}; copy_sample_test.required_spirv = {"OpImageSampleExplicitLod"}; for (uint32_t layer = 0; layer < 2; ++layer) { AppendVMovLiteral(©_sample_test.code, 20, std::bit_cast(0.5f)); AppendVMovLiteral(©_sample_test.code, 21, std::bit_cast(0.5f)); AppendVMovLiteral(©_sample_test.code, 22, std::bit_cast(static_cast(layer))); copy_sample_test.code.push_back(EncodeMimg0(0x20, 1, 0, false, 5)); copy_sample_test.code.push_back(EncodeMimg1(0, 20, 0, 2)); AppendStoreVgpr(©_sample_test.code, 0, layer); copy_sample_test.expected.push_back( std::bit_cast(layer == 0 ? 0.875f : 0.25f)); } AppendEnd(©_sample_test.code); const auto copy_sample_program = CompileCase(copy_sample_test, SubgroupSize()); ShaderRecompiler::IR::DescriptorValue copy_sample_value{}; copy_sample_value.dword_count = 8; std::copy_n(copy_sample_descriptor.fields, 8, copy_sample_value.dwords.begin()); const auto copy_sample_binding = RenderExecutorTestAccess::ResolveTexture( executor, copy_sample_program.program.info.images[0], copy_sample_value); const auto copy_sample_view = texture_cache.FindTexture( copy_sample_binding.image_id, copy_sample_binding.desc); Require( name, "copied depth sampled owner", copy_sample_binding.image_id == copy_destination.image_id && copy_sample_binding.desc.info.pixel_format == vk::Format::eR32Sfloat, "R32 sampling did not reuse the GPU-written depth-copy destination"); auto ©_sample_image = texture_cache.GetImage(copy_destination.image_id); copy_sample_image.Transit(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, {}, scheduler.Current().Handle()); Image copied_depth_sample; copied_depth_sample.view = copy_sample_view; copied_depth_sample.layout = copy_sample_image.backing.state.layout; scheduler.Finish(); auto copy_sample_output = CreateStorageBuffer(name, {}, 2); const auto copy_sampler = CreateNearestSampler(name); Dispatch(copy_sample_test, copy_sample_program, copy_sample_output, nullptr, &copied_depth_sample, nullptr, nullptr, copy_sampler); Require( name, "copied depth R32 samples", ReadBuffer(name, copy_sample_output, 2) == copy_sample_test.expected, "R32 post-effect sampling saw stale depth after the override copy"); m_device.destroySampler(copy_sampler); DestroyBuffer(©_sample_output); RenderExecutorTestAccess::ResetBindings(executor); registers.SetDepthRenderOverride({}); registers.SetColorControl({}); auto shared_depth_descriptor = sampled_depth_descriptor; shared_depth_descriptor.fields[0] = static_cast(phased_depth_address >> 8u); shared_depth_descriptor.fields[1] = static_cast(phased_depth_address >> 40u) | (static_cast(Prospero::BufferFormat::k32UInt) << 20u); shared_depth_descriptor.fields[2] = 0; ShaderRecompiler::IR::DescriptorValue shared_depth_value{}; std::copy(std::begin(shared_depth_descriptor.fields), std::end(shared_depth_descriptor.fields), shared_depth_value.dwords.begin()); shared_depth_value.dword_count = 8; ShaderStageRuntime shared_depth_vertex{&vertex_sampled_info, {}}; ShaderStageRuntime shared_depth_pixel{&sampled_info, {}}; shared_depth_vertex.resources.images.push_back(shared_depth_value); shared_depth_pixel.resources.images.push_back(shared_depth_value); for (const bool stencil_write : {true, false}) { registers.SetDepthRenderTarget(phased_depth_target); registers.SetRenderControl({}); registers.SetDepthControl(read_only_depth_control); HW::StencilControl shared_stencil_control{}; shared_stencil_control.stencil_zpass = static_cast(Prospero::StencilOp::kReplaceTest); registers.SetStencilControl(shared_stencil_control); HW::StencilMask shared_stencil_mask{}; shared_stencil_mask.stencil_testval = 0x20; shared_stencil_mask.stencil_mask = 0xff; shared_stencil_mask.stencil_writemask = stencil_write ? 0xf4 : 0; registers.SetStencilMask(shared_stencil_mask); RenderDepthInfo shared_depth{}; RenderExecutorTestAccess::ResolveRenderDepthTarget( executor, scheduler.Current(), shared_depth); auto shared_bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, shared_depth_vertex, shared_depth_pixel, true); const auto shared_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &no_color, 0, shared_depth, shared_bindings.pixel); descriptor_pipelines.push_back(RenderExecutorTestAccess::CommitBindings( executor, scheduler.Current(), shared_bindings.vertex[0], *shared_bindings.pixel)); const auto expected_layout = stencil_write ? vk::ImageLayout::eDepthReadOnlyStencilAttachmentOptimal : vk::ImageLayout::eDepthStencilReadOnlyOptimal; const auto expected_access = vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eDepthStencilAttachmentRead | vk::AccessFlagBits2::eDepthStencilAttachmentWrite; const auto &shared_image = texture_cache.GetImage(shared_depth.image_id); const auto &vertex_image = shared_bindings.vertex[0].images[0]; const auto &pixel_image = shared_bindings.pixel->images[0]; Require(name, "sampled depth and stencil attachment layout", shared_depth.image_id == phased_depth.image_id && vertex_image.image_id == shared_depth.image_id && pixel_image.image_id == shared_depth.image_id && MakeImageInfo(vertex_image).imageLayout == expected_layout && MakeImageInfo(pixel_image).imageLayout == expected_layout && shared_rendering.depth_stencil_attachment.image_layout == expected_layout && shared_image.backing.state.layout == expected_layout && shared_image.backing.state.access_mask == expected_access, "a shader alias replaced the depth/stencil attachment layout " "or dropped an attachment access"); scheduler.BeginRendering(shared_rendering); scheduler.EndRendering(); RenderExecutorTestAccess::ResetBindings(executor); Require(name, "attachment binding reset", !shared_image.binding.is_target && shared_image.binding.attachment_layout == vk::ImageLayout::eUndefined && !shared_image.binding.attachment_access, "attachment usage leaked into the next draw"); } auto video_subresource = make_target_desc(base + 0x20000, target_mip_size, {1, 1, 1}); video_subresource.type = BindingType::VideoOut; video_subresource.info.pixel_format = vk::Format::eR8G8B8A8Srgb; video_subresource.info.guest_format = Prospero::BufferFormat::k8_8_8_8Srgb; video_subresource.view_info.format = video_subresource.info.pixel_format; video_subresource.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto video_subresource_id = texture_cache.FindImage(video_subresource); const auto video_subresource_owner = TextureCacheTestAccess::Owner(texture_cache, video_subresource_id); auto video_parent = make_target_desc(base + 0x20000, target_mip_size * 2, {2, 2, 1}); video_parent.type = BindingType::VideoOut; video_parent.info.pixel_format = vk::Format::eR8G8B8A8Srgb; video_parent.info.guest_format = Prospero::BufferFormat::k8_8_8_8Srgb; video_parent.info.resources.levels = 2; video_parent.info.mip_layout[0] = {0, target_mip_size, 2, 2}; video_parent.info.mip_layout[1] = {target_mip_size, target_mip_size, 1, 1}; video_parent.view_info.format = video_parent.info.pixel_format; video_parent.view_info.usage = vk::ImageUsageFlagBits::eSampled; const auto video_parent_id = texture_cache.FindImage(video_parent); const auto video_parent_owner = TextureCacheTestAccess::Owner(texture_cache, video_parent_id); Require(name, "video-out replacement remains cache-only", video_subresource_owner != nullptr && video_parent_owner != nullptr && video_parent_id != video_subresource_id && !video_subresource_owner->binding.is_bound && !video_subresource_owner->binding.is_target && !video_subresource_owner->binding.needs_rebind && !video_subresource_owner->usage.video_out && !video_parent_owner->binding.is_bound && !video_parent_owner->binding.is_target && !video_parent_owner->binding.needs_rebind && !video_parent_owner->usage.video_out, "FindImage leaked caller-owned state across a video-out " "replacement"); constexpr uint64_t array_target_address = base + 0xd0000; auto array_target = make_target_desc(array_target_address, target_mip_size, {128, 128, 1}); const auto array_target_id = texture_cache.FindImage(array_target); const auto array_target_owner = TextureCacheTestAccess::Owner(texture_cache, array_target_id); RenderExecutorTestAccess::BindRenderTarget(executor, array_target_id); const auto array_target_view = texture_cache.FindRenderTarget(array_target_id, array_target); ShaderTextureResource array_texture{}; const auto encoded_array_address = array_target_address >> 8u; constexpr auto array_format = static_cast(Prospero::BufferFormat::k32UInt); array_texture.fields[0] = static_cast(encoded_array_address); array_texture.fields[1] = static_cast(encoded_array_address >> 32u) | (array_format << 20u) | (3u << 30u); array_texture.fields[2] = (127u >> 2u) | (127u << 14u); array_texture.fields[3] = DstSel(4, 5, 6, 7) | (static_cast(linear) << 20u) | (static_cast(Prospero::ImageType::kColor2DArray) << 28u); array_texture.fields[4] = 1; ShaderRecompiler::IR::CompiledShaderInfo array_program{}; array_program.stage = ShaderType::Compute; ShaderRecompiler::IR::ImageResource array_resource{}; array_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; array_resource.numeric_class = Prospero::TextureNumericClass::Uint; array_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; array_resource.read = true; array_program.info.images.push_back(array_resource); ShaderRecompiler::IR::DescriptorValue array_descriptor{}; std::copy(std::begin(array_texture.fields), std::end(array_texture.fields), array_descriptor.dwords.begin()); array_descriptor.dword_count = 8; ShaderRecompiler::IR::ResourceSnapshot array_snapshot{}; array_snapshot.images.push_back(array_descriptor); ShaderStageRuntime array_runtime{&array_program, std::move(array_snapshot)}; auto array_binding = executor.PrepareBindings(array_runtime); executor.RebindImages(array_binding); const auto expanded_array_id = array_binding.images[0].image_id; const auto &expanded_array = texture_cache.GetImage(expanded_array_id); Require( name, "2D target to array backing expansion", array_target_view != nullptr && array_target_owner != nullptr && array_target_owner->binding.needs_rebind && expanded_array_id != array_target_id && !TextureCacheTestAccess::Contains(texture_cache, array_target_id) && array_binding.images[0].image_view != nullptr && array_binding.images[0].desc.info.type == Prospero::ImageType::kColor2D && array_binding.images[0].desc.info.resources == ImageSubresources{1, 2} && array_binding.images[0].desc.view_info.type == vk::ImageViewType::e2DArray && array_binding.images[0].desc.view_info.layer_count == 2 && expanded_array.backing.layers == 2 && expanded_array.IsGpuModified() && expanded_array.usage.texture, "raw array view did not expand and reuse the Color2D backing"); RenderColorInfo rebound_array_target{}; rebound_array_target.desc = array_target; rebound_array_target.image_id = array_target_id; RenderDepthInfo no_array_depth{}; const auto acquired_array_view = array_binding.images[0].image_view; RenderExecutorTestAccess::PrepareGraphicsBindings( executor, std::span{}, std::span{&rebound_array_target, 1u}); auto array_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &rebound_array_target, 1, no_array_depth); Require(name, "expanded array target rebind", rebound_array_target.image_id == expanded_array_id && array_binding.images[0].image_id == expanded_array_id && array_binding.images[0].image_view == acquired_array_view && array_rendering.color_attachments[0].image_view != nullptr && array_rendering.num_color_attachments == 1 && array_rendering.width == 128 && array_rendering.height == 128 && array_rendering.num_layers == 1 && texture_cache.GetImage(expanded_array_id).binding.is_bound && texture_cache.GetImage(expanded_array_id).binding.is_target, "the retained single-layer target did not rebind to the expanded " "backing"); RenderExecutorTestAccess::ResetBindings(executor); { constexpr uint32_t before = 0x13579bdfu; constexpr uint32_t guard = 0x2468ace0u; constexpr uint32_t after = 0xa1b2c3d4u; vk::ClearValue clear{}; clear.color.uint32[0] = before; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), expanded_array_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 2}, clear); clear.color.uint32[0] = guard; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), expanded_array_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 1, 1}, clear); ShaderRecompiler::IR::Program buffer_ir{}; buffer_ir.stage = ShaderType::Vertex; buffer_ir.resource_tracking_complete = true; buffer_ir.info.buffers.resize(1); buffer_ir.info.buffers[0].read = true; buffer_ir.info.buffers[0].formatted = true; allocate_bindings(buffer_ir); ShaderRecompiler::IR::CompiledShaderInfo buffer_program{}; buffer_program.stage = buffer_ir.stage; buffer_program.info = std::move(buffer_ir.info); buffer_program.bindings = std::move(buffer_ir.bindings); constexpr uint64_t buffer_size = 2 * target_mip_size; static_assert(buffer_size > BufferCache::CACHING_PAGESIZE); ShaderBufferResource descriptor{}; descriptor.UpdateAddress48(array_target_address); descriptor.fields[1] |= 4u << 16u; descriptor.fields[2] = buffer_size / sizeof(uint32_t); descriptor.fields[3] = 0x00014004u; ShaderStageRuntime buffer_runtime{.program = &buffer_program}; auto &value = buffer_runtime.resources.buffers.emplace_back(); std::memcpy(value.dwords.data(), descriptor.fields, sizeof(descriptor.fields)); value.dword_count = 4; auto buffer_bindings = executor.PrepareBindings(buffer_runtime); std::array stages{&buffer_bindings}; RenderColorInfo target{}; target.desc = array_target; target.image_id = array_target_id; RenderExecutorTestAccess::PrepareGraphicsBindings( executor, stages, std::span{&target, 1u}); const auto &buffer = buffer_bindings.buffers[0]; Require(name, "rediscovered target formatted-buffer read", target.image_id == expanded_array_id && buffer.range == buffer_size && buffer_bindings.images.empty() && texture_cache.GetImage(target.image_id).backing.state.layout == vk::ImageLayout::eTransferSrcOptimal, "target discovery did not precede the formatted image-to-buffer copy"); auto readback = CreateHostBuffer(name, 2 * sizeof(uint32_t), vk::BufferUsageFlagBits::eTransferDst, {0, 0}); const std::array copies{ vk::BufferCopy{buffer.offset, 0, sizeof(uint32_t)}, vk::BufferCopy{buffer.offset + target_mip_size, sizeof(uint32_t), sizeof(uint32_t)}}; scheduler.Current().Handle().copyBuffer(buffer.buffer, readback.buffer, copies.size(), copies.data()); vk::BufferMemoryBarrier readback_barrier{}; readback_barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; readback_barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; readback_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; readback_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; readback_barrier.buffer = readback.buffer; readback_barrier.size = readback.size; scheduler.Current().Handle().pipelineBarrier( vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &readback_barrier, 0, nullptr); const auto rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &target, 1, no_array_depth); Require(name, "attachment layout after formatted-buffer read", rendering.color_attachments[0].image_layout == vk::ImageLayout::eColorAttachmentOptimal && texture_cache.GetImage(target.image_id).backing.state.layout == vk::ImageLayout::eColorAttachmentOptimal, "the formatted buffer copy left the render attachment in its transfer layout"); vk::ClearAttachment clear_attachment{}; clear_attachment.aspectMask = vk::ImageAspectFlagBits::eColor; clear_attachment.colorAttachment = 0; clear_attachment.clearValue.color.uint32[0] = after; const vk::ClearRect clear_rect{{{0, 0}, {128, 128}}, 0, 1}; scheduler.BeginRendering(rendering); scheduler.Current().Handle().clearAttachments(1, &clear_attachment, 1, &clear_rect); scheduler.EndRendering(); scheduler.Finish(); Require(name, "formatted alias observes prior GPU contents", ReadBuffer(name, readback, 2) == std::vector{before, guard}, "formatted buffer acquisition copied stale guest bytes instead of the image"); DestroyBuffer(&readback); Require(name, "rendering after formatted-buffer acquisition", ReadCachedTexel(name, context, target.image_id) == std::vector{after} && ReadCachedTexel(name, context, target.image_id, {}, {1, 1, 1}, 1) == std::vector{guard}, "attachment rendering lost its new color or changed the untouched array layer"); RenderExecutorTestAccess::ResetBindings(executor); } auto colliding_msaa_texture = array_texture; colliding_msaa_texture.fields[3] = DstSel(4, 5, 6, 7) | (1u << 16u) | (static_cast(Prospero::TileMode::kRenderTarget) << 20u) | (static_cast(Prospero::ImageType::kColor2DMsaa) << 28u); colliding_msaa_texture.fields[4] = 0; colliding_msaa_texture.fields[5] = (7u << 20u) | (1u << 4u); ShaderRecompiler::IR::DescriptorValue colliding_msaa_descriptor{}; std::copy(std::begin(colliding_msaa_texture.fields), std::end(colliding_msaa_texture.fields), colliding_msaa_descriptor.dwords.begin()); colliding_msaa_descriptor.dword_count = 8; ShaderRecompiler::IR::CompiledShaderInfo colliding_msaa_program{}; colliding_msaa_program.stage = ShaderType::Compute; auto colliding_msaa_resource = array_resource; colliding_msaa_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa; colliding_msaa_program.info.images.push_back(colliding_msaa_resource); ShaderRecompiler::IR::ResourceSnapshot colliding_msaa_snapshot{}; colliding_msaa_snapshot.images.push_back(colliding_msaa_descriptor); ShaderStageRuntime colliding_msaa_runtime{ &colliding_msaa_program, std::move(colliding_msaa_snapshot)}; auto colliding_msaa_binding = executor.PrepareBindings(colliding_msaa_runtime); executor.RebindImages(colliding_msaa_binding); const auto &resolved_colliding_msaa = colliding_msaa_binding.images[0]; Require( name, "equal-footprint sample-count identity", resolved_colliding_msaa.image_id != expanded_array_id && resolved_colliding_msaa.image_view != nullptr && resolved_colliding_msaa.desc.info.type == Prospero::ImageType::kColor2D && resolved_colliding_msaa.desc.info.resources == ImageSubresources{1, 1} && resolved_colliding_msaa.desc.info.samples == 2 && resolved_colliding_msaa.desc.info.data.size == 0x20000 && texture_cache.GetImage(resolved_colliding_msaa.image_id) .backing.samples == 2 && texture_cache.GetImage(expanded_array_id).backing.samples == 1 && texture_cache.GetImage(expanded_array_id).backing.layers == 2, "equal-size 1x array and 2x image shared an incompatible backing"); RenderExecutorTestAccess::ResetBindings(executor); constexpr uint64_t msaa_target_address = base + 0x100000; constexpr uint32_t msaa_samples = 4; auto msaa_target = make_target_desc(msaa_target_address, 0x40000, {128, 128, 1}); msaa_target.info.samples = msaa_samples; msaa_target.info.tile_mode = Prospero::TileMode::kRenderTarget; msaa_target.info.mip_layout[0].size = msaa_target.info.data.size; const auto msaa_target_id = texture_cache.FindImage(msaa_target); RenderExecutorTestAccess::BindRenderTarget(executor, msaa_target_id); const auto msaa_target_view = texture_cache.FindRenderTarget(msaa_target_id, msaa_target); auto msaa_texture = array_texture; const auto encoded_msaa_address = msaa_target_address >> 8u; msaa_texture.fields[0] = static_cast(encoded_msaa_address); msaa_texture.fields[1] = static_cast(encoded_msaa_address >> 32u) | (array_format << 20u) | (3u << 30u); msaa_texture.fields[3] = DstSel(4, 5, 6, 7) | (2u << 16u) | (static_cast(Prospero::TileMode::kRenderTarget) << 20u) | (static_cast(Prospero::ImageType::kColor2DMsaa) << 28u); msaa_texture.fields[4] = 0; msaa_texture.fields[5] = (7u << 20u) | (2u << 4u); ShaderRecompiler::IR::DescriptorValue msaa_descriptor{}; std::copy(std::begin(msaa_texture.fields), std::end(msaa_texture.fields), msaa_descriptor.dwords.begin()); msaa_descriptor.dword_count = 8; ShaderRecompiler::IR::CompiledShaderInfo msaa_program{}; msaa_program.stage = ShaderType::Compute; auto msaa_resource = array_resource; msaa_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaa; msaa_program.info.images.push_back(msaa_resource); ShaderRecompiler::IR::ResourceSnapshot msaa_snapshot{}; msaa_snapshot.images.push_back(msaa_descriptor); ShaderStageRuntime msaa_runtime{&msaa_program, std::move(msaa_snapshot)}; auto msaa_binding = executor.PrepareBindings(msaa_runtime); executor.RebindImages(msaa_binding); const auto &resolved_msaa = msaa_binding.images[0]; Require( name, "MSAA descriptor backing reuse", msaa_target_view != nullptr && resolved_msaa.image_id == msaa_target_id && resolved_msaa.image_view != nullptr && resolved_msaa.desc.info.type == Prospero::ImageType::kColor2D && resolved_msaa.desc.info.resources == ImageSubresources{1, 1} && resolved_msaa.desc.info.samples == msaa_samples && resolved_msaa.desc.info.data.size == 0x40000 && resolved_msaa.desc.view_info.level_count == 1 && texture_cache.GetImage(msaa_target_id).backing.samples == msaa_samples, "MSAA view fields did not resolve to the matching multisample " "backing"); RenderExecutorTestAccess::ResetBindings(executor); auto msaa_array_texture = msaa_texture; msaa_array_texture.fields[3] = (msaa_array_texture.fields[3] & ~(0xfu << 28u)) | (static_cast(Prospero::ImageType::kColor2DMsaaArray) << 28u); msaa_array_texture.fields[4] = 1; ShaderRecompiler::IR::DescriptorValue msaa_array_descriptor{}; std::copy(std::begin(msaa_array_texture.fields), std::end(msaa_array_texture.fields), msaa_array_descriptor.dwords.begin()); msaa_array_descriptor.dword_count = 8; ShaderRecompiler::IR::CompiledShaderInfo msaa_array_program{}; msaa_array_program.stage = ShaderType::Compute; auto msaa_array_resource = array_resource; msaa_array_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DMsaaArray; msaa_array_program.info.images.push_back(msaa_array_resource); ShaderRecompiler::IR::ResourceSnapshot msaa_array_snapshot{}; msaa_array_snapshot.images.push_back(msaa_array_descriptor); ShaderStageRuntime msaa_array_runtime{&msaa_array_program, std::move(msaa_array_snapshot)}; auto msaa_array_binding = executor.PrepareBindings(msaa_array_runtime); executor.RebindImages(msaa_array_binding); const auto &resolved_msaa_array = msaa_array_binding.images[0]; Require(name, "MSAA array backing expansion", resolved_msaa_array.image_id != msaa_target_id && resolved_msaa_array.image_view != nullptr && resolved_msaa_array.desc.info.type == Prospero::ImageType::kColor2D && resolved_msaa_array.desc.info.resources == ImageSubresources{1, 2} && resolved_msaa_array.desc.info.samples == msaa_samples && resolved_msaa_array.desc.info.data.size == 0x80000 && resolved_msaa_array.desc.view_info.type == vk::ImageViewType::e2DArray && resolved_msaa_array.desc.view_info.layer_count == 2 && texture_cache.GetImage(resolved_msaa_array.image_id) .backing.layers == 2 && texture_cache.GetImage(resolved_msaa_array.image_id) .backing.samples == msaa_samples, "MSAA array view did not expand the matching Color2D backing"); RenderExecutorTestAccess::ResetBindings(executor); ImageDesc depth{}; depth.type = BindingType::DepthTarget; depth.info.data = {depth_address, 0x10000}; depth.info.stencil = {stencil_address, stencil_layout.size}; depth.info.pixel_format = vk::Format::eD32SfloatS8Uint; depth.info.guest_format = Prospero::BufferFormat::k32Float; depth.info.type = Prospero::ImageType::kColor2D; depth.info.extent = {1, 1, 1}; depth.info.resources = {1, 1}; depth.info.pitch = 1; depth.info.bytes_per_block = 4; depth.info.samples = 1; depth.info.tile_mode = linear; depth.info.mip_layout[0] = {0, depth.info.data.size, 1, 1}; depth.view_info.format = depth.info.pixel_format; depth.view_info.type = vk::ImageViewType::e2D; depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth; depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; const auto depth_id = texture_cache.FindImage(depth); Require(name, "cache-only depth discovery", !texture_cache.FindImageFromRange(stencil_address, stencil_layout.size, false) && !texture_cache.GetImage(depth_id).binding.is_bound && !texture_cache.GetImage(depth_id).binding.is_target && !texture_cache.GetImage(depth_id).usage.depth_target, "FindImage created depth acquisition state or a stencil proxy"); RenderExecutorTestAccess::BindRenderTarget(executor, depth_id); Require(name, "depth prefetch purity", texture_cache.GetImage(depth_id).binding.is_target && !texture_cache.GetImage(depth_id).usage.depth_target && !texture_cache.FindImageFromRange(stencil_address, stencil_layout.size, false), "depth prefetch performed final target acquisition"); auto target_parent = make_target_desc(base, 1536, {4, 4, 1}); target_parent.type = BindingType::RenderTarget; target_parent.info.resources.levels = 2; // PS5 linear rows align to 256 bytes; smaller mips precede larger ones. target_parent.info.pitch = 64; target_parent.info.mip_layout[0] = {512, 1024, 64, 4}; target_parent.info.mip_layout[1] = {0, 512, 64, 2}; target_parent.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; auto target_base_subresource = make_target_desc(base + 512, 1024, {4, 4, 1}); target_base_subresource.info.pitch = 64; target_base_subresource.info.mip_layout[0] = {0, 1024, 64, 4}; const auto target_base_subresource_id = texture_cache.FindImage(target_base_subresource); auto target_subresource = make_target_desc(base, 512, {2, 2, 1}); target_subresource.info.pitch = 64; target_subresource.info.mip_layout[0] = {0, 512, 64, 2}; const auto target_subresource_id = texture_cache.FindImage(target_subresource); const auto *target_subresource_owner = TextureCacheTestAccess::Owner(texture_cache, target_subresource_id); const auto &discovered_target = texture_cache.GetImage(target_subresource_id); Require(name, "cache-only target discovery", !discovered_target.binding.is_bound && !discovered_target.binding.is_target && !discovered_target.binding.needs_rebind && !discovered_target.usage.render_target && !TextureCacheTestAccess::PendingDownload( texture_cache, target_subresource_id), "FindImage claimed RenderExecutor-owned render-target state"); vk::ClearValue target_clear{}; target_clear.color.uint32[0] = 0x12345678; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), target_base_subresource_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, target_clear); target_clear.color.uint32[0] = 0x89abcdef; TextureCacheTestAccess::ClearImage( texture_cache, scheduler.Current(), target_subresource_id, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, target_clear); RenderExecutorTestAccess::BindRenderTarget(executor, target_subresource_id); Require(name, "target prefetch purity", texture_cache.GetImage(target_subresource_id).binding.is_target && !texture_cache.GetImage(target_subresource_id) .usage.render_target && !TextureCacheTestAccess::PendingDownload( texture_cache, target_subresource_id), "target prefetch performed final target acquisition"); const auto target_parent_id = texture_cache.FindImage(target_parent); Require(name, "active target overlap", target_parent_id && target_parent_id != target_base_subresource_id && target_parent_id != target_subresource_id && target_subresource_owner != nullptr && target_subresource_owner->binding.needs_rebind && texture_cache.GetImage(target_parent_id).binding.is_target && !TextureCacheTestAccess::PendingDownload(texture_cache, target_parent_id), "target overlap did not transfer target state to the merged " "owner"); auto target_readback = CreateHostBuffer( name, 2 * sizeof(u32), vk::BufferUsageFlagBits::eTransferDst, {0xaaaaaaaa, 0xaaaaaaaa}); const std::array target_probes{ vk::BufferImageCopy{0, 0, 0, {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, {}, {1, 1, 1}}, vk::BufferImageCopy{sizeof(u32), 0, 0, {vk::ImageAspectFlagBits::eColor, 1, 0, 1}, {}, {1, 1, 1}}}; texture_cache.GetImage(target_parent_id) .Download(target_probes, target_readback.buffer, 0, target_readback.size); const vk::MemoryBarrier2 target_host_barrier{ .srcStageMask = vk::PipelineStageFlagBits2::eCopy, .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, .dstStageMask = vk::PipelineStageFlagBits2::eHost, .dstAccessMask = vk::AccessFlagBits2::eHostRead}; vk::DependencyInfo target_dependency{}; target_dependency.memoryBarrierCount = 1; target_dependency.pMemoryBarriers = &target_host_barrier; scheduler.Current().Handle().pipelineBarrier2(target_dependency); RenderColorInfo rebound_color{}; rebound_color.desc = target_subresource; rebound_color.image_id = target_subresource_id; RenderDepthInfo rebound_depth{}; rebound_depth.desc = depth; rebound_depth.image_id = depth_id; scheduler.Finish(); Require(name, "promoted target mip contents", ReadBuffer(name, target_readback, 2) == std::vector{0x12345678, 0x89abcdef}, "promotion discarded GPU contents from a child mip"); DestroyBuffer(&target_readback); Require(name, "deferred target slot erasure", TextureCacheTestAccess::Owner(texture_cache, target_subresource_id) == nullptr, "the displaced target slot was not erased after GPU completion"); RenderExecutorTestAccess::PrepareGraphicsBindings( executor, std::span{}, std::span{&rebound_color, 1u}); auto rebound_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &rebound_color, 1, rebound_depth); Require(name, "guest target extent after host mip remap", rebound_color.desc.view_info.base_level == 1 && rebound_color.guest_mip_level == 0 && rebound_color.Extent() == vk::Extent2D{2, 2}, "remapping a guest target into its parent mip shrank the guest " "render area twice"); const auto stencil_proxy_id = texture_cache.FindImageFromRange( stencil_address, stencil_layout.size, false); Require( name, "production target rebind", rebound_color.image_id == target_parent_id && rebound_rendering.color_attachments[0].image_view != nullptr && rebound_rendering.depth_stencil_attachment.image_view != nullptr && rebound_rendering.num_color_attachments == 1 && rebound_rendering.width == 1 && rebound_rendering.height == 1 && texture_cache.GetImage(target_parent_id).binding.is_target && texture_cache.GetImage(target_parent_id).usage.render_target && texture_cache.GetImage(target_parent_id).IsGpuModified() && texture_cache.GetImage(depth_id).usage.depth_target && texture_cache.GetImage(depth_id).IsGpuModified() && TextureCacheTestAccess::PendingDownload(texture_cache, target_parent_id) && stencil_proxy_id && texture_cache.GetImage(stencil_proxy_id).depth_id == depth_id, "the final target pass did not acquire color before depth and " "build the dynamic render state"); scheduler.BeginRendering(rebound_rendering); scheduler.EndRendering(); RenderExecutorTestAccess::ResetBindings(executor); Require(name, "draw-scoped target reset", TextureCacheTestAccess::Owner(texture_cache, target_subresource_id) == nullptr && !texture_cache.GetImage(target_parent_id).binding.is_target && !texture_cache.GetImage(depth_id).binding.is_target, "RenderExecutor retained target binding state after reset"); constexpr uint64_t ordered_color_address = base + 0x80000; auto ordered_color_desc = make_target_desc(ordered_color_address, target_mip_size, {1, 1, 1}); const auto stale_ordered_color = texture_cache.FindImage(ordered_color_desc); RenderExecutorTestAccess::BindRenderTarget(executor, stale_ordered_color); resources.UnmapMemory(ordered_color_address, target_mip_size); resources.MapMemory(ordered_color_address, target_mip_size); auto ordered_depth_desc = depth; ordered_depth_desc.info.stencil = {ordered_color_address, target_mip_size}; const auto ordered_depth_id = texture_cache.FindImage(ordered_depth_desc); RenderExecutorTestAccess::BindRenderTarget(executor, ordered_depth_id); RenderColorInfo ordered_color{}; ordered_color.desc = ordered_color_desc; ordered_color.image_id = stale_ordered_color; RenderDepthInfo ordered_depth{}; ordered_depth.desc = ordered_depth_desc; ordered_depth.image_id = ordered_depth_id; RenderExecutorTestAccess::PrepareGraphicsBindings( executor, std::span{}, std::span{&ordered_color, 1u}); auto ordered_rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &ordered_color, 1, ordered_depth); Require(name, "color-before-depth acquisition order", ordered_color.image_id != stale_ordered_color && ordered_rendering.color_attachments[0].image_view != nullptr && ordered_rendering.depth_stencil_attachment.image_view != nullptr && texture_cache.GetImage(ordered_color.image_id).depth_id == ordered_depth.image_id, "depth acquisition ran before the stale color target was " "recreated and associated"); RenderExecutorTestAccess::ResetBindings(executor); TextureCacheTestAccess::SetLinearReadback(texture_cache, false); ShaderTextureResource stencil{}; const uint64_t encoded_address = stencil_address >> 8u; stencil.fields[0] = static_cast(encoded_address); stencil.fields[1] = static_cast(encoded_address >> 32u) | (static_cast(stencil_format) << 20u); stencil.fields[3] = DstSel(4, 5, 6, 7) | (static_cast(linear) << 20u) | (static_cast(Prospero::ImageType::kColor2D) << 28u); ShaderRecompiler::IR::CompiledShaderInfo program{}; program.stage = ShaderType::Compute; ShaderRecompiler::IR::ImageResource stencil_resource{}; stencil_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; stencil_resource.numeric_class = Prospero::TextureNumericClass::Uint; stencil_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; stencil_resource.read = true; program.info.images.push_back(stencil_resource); ShaderRecompiler::IR::ResourceSnapshot snapshot{}; ShaderRecompiler::IR::DescriptorValue descriptor{}; std::copy(std::begin(stencil.fields), std::end(stencil.fields), descriptor.dwords.begin()); descriptor.dword_count = 8; snapshot.images.push_back(descriptor); ShaderStageRuntime runtime{&program, std::move(snapshot)}; auto prepared = context.GetRenderExecutor().PrepareBindings(runtime); const auto sampled_stencil_id = prepared.images[0].image_id; Require(name, "first stencil discovery", prepared.images.size() == 1 && sampled_stencil_id != depth_id && prepared.images[0].image_view == nullptr && texture_cache.GetImage(sampled_stencil_id).binding.is_bound && !texture_cache.GetImage(stencil_proxy_id).binding.is_bound && !texture_cache.GetImage(stencil_proxy_id).binding.is_target, "the first sampled-stencil lookup did not remain an ordinary " "discovery before final depth acquisition"); context.GetRenderExecutor().RebindImages(prepared); Require(name, "first stencil acquisition", prepared.images[0].image_id == sampled_stencil_id && prepared.images[0].image_view != nullptr && texture_cache.GetImage(sampled_stencil_id).usage.texture, "the first sampled-stencil image was not finally acquired"); RenderExecutorTestAccess::ResetBindings(executor); RenderDepthInfo reassociated_depth = rebound_depth; RenderColorInfo no_reassociated_color{}; (void)RenderExecutorTestAccess::AcquireRenderTargets( executor, scheduler.Current(), &no_reassociated_color, 0, reassociated_depth); Require(name, "existing stencil association selection", texture_cache.GetImage(sampled_stencil_id).depth_id == reassociated_depth.image_id, "final depth acquisition did not associate the existing image " "at the stencil guest address"); RenderExecutorTestAccess::ResetBindings(executor); auto redirected = context.GetRenderExecutor().PrepareBindings(runtime); Require(name, "redirected owner discovery", redirected.images.size() == 1 && redirected.images[0].image_id == depth_id && redirected.images[0].image_view == nullptr && texture_cache.GetImage(depth_id).binding.is_bound && !texture_cache.GetImage(sampled_stencil_id).binding.is_bound, "the established stencil association did not redirect the next " "discovery to the depth owner"); context.GetRenderExecutor().RebindImages(redirected); Require(name, "second-pass stencil acquisition", redirected.images[0].image_id == depth_id && redirected.images[0].image_view != nullptr && texture_cache.GetImage(depth_id).usage.texture, "RebindImages did not acquire the associated depth owner"); RenderExecutorTestAccess::ResetBindings(executor); // Execute the captured IMAGE_STORE clear through production fill recognition. const std::array stencil_shader{ 0xd7460000u, 0x0401060cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu, 0x7e06026au, 0xd7460001u, 0x0405060du, 0x7e04020eu, 0xf0200128u, 0x00000300u, 0xbf810000u}; uint32_t stencil_byte = 0x35; const auto value_address = reinterpret_cast(&stencil_byte); auto storage_stencil = stencil; storage_stencil.fields[3] = (storage_stencil.fields[3] & 0x0fffffffu) | (static_cast(Prospero::ImageType::kColor2DArray) << 28u); storage_stencil.fields[5] = 0x00700000u; std::array stencil_userdata{}; std::copy_n(storage_stencil.fields, 8, stencil_userdata.begin()); stencil_userdata[8] = static_cast(value_address); stencil_userdata[9] = static_cast(value_address >> 32) | (4u << 16); stencil_userdata[10] = 1; stencil_userdata[11] = 0x14204u; ShaderComputeInputInfo stencil_compute{}; stencil_compute.threads_num[0] = stencil_compute.threads_num[1] = 8; stencil_compute.threads_num[2] = 1; stencil_compute.workgroup_register = 12; stencil_compute.thread_ids_num = 2; std::fill_n(stencil_compute.group_id, 3, true); ShaderRecompiler::CompileOptions stencil_options; stencil_options.stage = ShaderType::Compute; stencil_options.user_data = stencil_userdata; stencil_options.input_info.compute = &stencil_compute; auto stencil_translated = ShaderRecompiler::TranslateProgram(stencil_shader, stencil_options); auto stencil_plan = ShaderRecompiler::IR::ExtractResourcePlan(stencil_translated.program); ShaderRecompiler::IR::ResourceSpecialization stencil_specialization; Require(name, "stencil fill materialization", ShaderRecompiler::IR::MaterializeResources(stencil_plan, {.user_data = stencil_userdata, .userdata = &stencil_byte, .read_specialization_memory = +[](void *data, uint64_t address, uint32_t *word) { if (address != reinterpret_cast(data)) return false; *word = *static_cast(data); return true; }}, stencil_compute.stage.resources, stencil_specialization), "captured stencil shader could not resolve its clear byte"); ShaderRecompiler::IR::ApplyResourceSpecialization(stencil_translated.program, stencil_specialization); ShaderRecompiler::IR::CompiledShaderInfo stencil_program{}; stencil_program.stage = ShaderType::Compute; stencil_program.info = std::move(stencil_translated.program.info); stencil_compute.stage.program = &stencil_program; vk::ClearValue stencil_initial{}; stencil_initial.depthStencil = vk::ClearDepthStencilValue{0.625f, 0x91}; TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(), depth_id, {vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, stencil_initial); Require(name, "stencil fill coverage", !RenderExecutorTestAccess::TryConsumeComputeImageClear(executor, stencil_compute, scheduler.Current(), 0, 1, 1, 0x41u) && !RenderExecutorTestAccess::TryConsumeComputeImageClear(executor, stencil_compute, scheduler.Current(), 2, 1, 1, 0x41u), "incomplete or excessive stencil dispatch was consumed"); auto &stencil_source = stencil_compute.stage.resources.buffers[0]; const auto clean_source = stencil_source; stencil_source.dwords[0] = static_cast(stencil_address); stencil_source.dwords[1] = static_cast(stencil_address >> 32) | (4u << 16); Require(name, "stencil fill source alias", !RenderExecutorTestAccess::TryConsumeComputeImageClear(executor, stencil_compute, scheduler.Current(), 1, 1, 1, 0x41u), "an aliased scalar load was consumed as a uniform image clear"); stencil_source = clean_source; Require(name, "stencil IMAGE_STORE clear", RenderExecutorTestAccess::TryConsumeComputeImageClear(executor, stencil_compute, scheduler.Current(), 1, 1, 1, 0x41u), "captured clear attempted an unsupported depth/stencil storage binding"); auto stencil_readback = CreateHostBuffer(name, 8, vk::BufferUsageFlagBits::eTransferDst, {}); const std::array stencil_copies{{ {0, 0, 0, {vk::ImageAspectFlagBits::eDepth, 0, 0, 1}, {}, {1, 1, 1}}, {4, 0, 0, {vk::ImageAspectFlagBits::eStencil, 0, 0, 1}, {}, {1, 1, 1}}}}; texture_cache.GetImage(depth_id).Download(stencil_copies, stencil_readback.buffer, 0, 8); vk::MemoryBarrier2 stencil_host_barrier{}; stencil_host_barrier.sType = vk::StructureType::eMemoryBarrier2; stencil_host_barrier.srcStageMask = vk::PipelineStageFlagBits2::eTransfer; stencil_host_barrier.srcAccessMask = vk::AccessFlagBits2::eTransferWrite; stencil_host_barrier.dstStageMask = vk::PipelineStageFlagBits2::eHost; stencil_host_barrier.dstAccessMask = vk::AccessFlagBits2::eHostRead; vk::DependencyInfo stencil_dependency{}; stencil_dependency.sType = vk::StructureType::eDependencyInfo; stencil_dependency.memoryBarrierCount = 1; stencil_dependency.pMemoryBarriers = &stencil_host_barrier; scheduler.Current().Handle().pipelineBarrier2(stencil_dependency); scheduler.Finish(); const auto stencil_result = ReadBuffer(name, stencil_readback, 2); Require(name, "stencil clear retains depth", stencil_result[0] == std::bit_cast(0.625f) && (stencil_result[1] & 0xffu) == 0x35 && !texture_cache.GetImage(depth_id).usage.storage, "stencil clear altered depth, lost its scalar value, or used a storage view"); DestroyBuffer(&stencil_readback); resources.UnmapMemory(base, allocation_size); scheduler.Finish(); RenderExecutorTestAccess::DestroyDescriptorPipelines( executor, descriptor_pipelines); context.ShutdownGpu(); } Require(name, "unmap direct backing", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0, "descriptor discovery direct mapping release failed"); Require(name, "release direct backing", Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "descriptor discovery direct-memory allocation release failed"); std::printf("[host] %-32s ok\n", name); } Buffer CreateStorageBuffer(const char *shader_name, const std::vector &initial, size_t dword_count, bool device_address = false) { Buffer ret; ret.size = static_cast(std::max(dword_count, 1u) * sizeof(u32)); vk::BufferCreateInfo buffer_info{}; buffer_info.sType = vk::StructureType::eBufferCreateInfo; buffer_info.size = ret.size; buffer_info.usage = vk::BufferUsageFlagBits::eStorageBuffer; if (device_address) { buffer_info.usage |= vk::BufferUsageFlagBits::eShaderDeviceAddress; } buffer_info.sharingMode = vk::SharingMode::eExclusive; RequireVk(shader_name, "dispatch", m_device.createBuffer(&buffer_info, nullptr, &ret.buffer), "vkCreateBuffer"); vk::MemoryRequirements req{}; m_device.getBufferMemoryRequirements(ret.buffer, &req); u32 memory_type = 0; ret.coherent = FindMemoryType(req.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, &memory_type); if (!ret.coherent) { Require(shader_name, "dispatch", FindMemoryType(req.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible, &memory_type), "no host-visible memory type for storage buffer"); } vk::MemoryAllocateInfo alloc{}; alloc.sType = vk::StructureType::eMemoryAllocateInfo; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type; vk::MemoryAllocateFlagsInfo flags{}; if (device_address) { flags.sType = vk::StructureType::eMemoryAllocateFlagsInfo; flags.flags = vk::MemoryAllocateFlagBits::eDeviceAddress; alloc.pNext = &flags; } RequireVk(shader_name, "dispatch", m_device.allocateMemory(&alloc, nullptr, &ret.memory), "vkAllocateMemory"); RequireVk(shader_name, "dispatch", m_device.bindBufferMemory(ret.buffer, ret.memory, 0), "vkBindBufferMemory"); if (device_address) { vk::BufferDeviceAddressInfo address_info{}; address_info.sType = vk::StructureType::eBufferDeviceAddressInfo; address_info.buffer = ret.buffer; ret.device_address = m_device.getBufferAddress(address_info); Require(shader_name, "dispatch", ret.device_address != 0, "storage buffer has no device address"); } std::vector contents(dword_count, 0); for (size_t i = 0; i < initial.size() && i < contents.size(); i++) { contents[i] = initial[i]; } WriteBuffer(shader_name, ret, contents); return ret; } void DestroyBuffer(Buffer *buffer) { if (buffer == nullptr) { return; } if (buffer->buffer != nullptr) { m_device.destroyBuffer(buffer->buffer, nullptr); buffer->buffer = nullptr; } if (buffer->memory != nullptr) { m_device.freeMemory(buffer->memory, nullptr); buffer->memory = nullptr; } } Image CreateImage2D(const char *shader_name, u32 width, u32 height, vk::Format format, vk::ImageUsageFlags usage, const std::vector &initial, u32 dwords_per_pixel, vk::ImageLayout final_layout) { std::vector> mips; if (!initial.empty()) { mips.push_back(initial); } return CreateImageMips(shader_name, width, height, format, usage, mips, dwords_per_pixel, final_layout, vk::ImageType::e2D, vk::ImageViewType::e2D, 1); } static u32 MipExtent(u32 value, u32 level) { for (u32 i = 0; i < level && value > 1u; i++) { value >>= 1u; } return std::max(value, 1u); } static size_t ImageMipDwordCount(u32 width, u32 height, u32 dwords_per_pixel, u32 level, u32 layers = 1) { return static_cast(MipExtent(width, level)) * static_cast(MipExtent(height, level)) * layers * dwords_per_pixel; } Image CreateImageMips(const char *shader_name, u32 width, u32 height, vk::Format format, vk::ImageUsageFlags usage, const std::vector> &initial_mips, u32 dwords_per_pixel, vk::ImageLayout final_layout, vk::ImageType image_type, vk::ImageViewType view_type, u32 layers, u32 view_base_layer = 0, u32 view_layers = 0, vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1) { Image ret; ret.format = format; ret.width = width; ret.height = height; ret.layers = layers; ret.mip_levels = std::max(static_cast(initial_mips.size()), 1u); ret.dwords_per_pixel = dwords_per_pixel; vk::ImageCreateInfo image_info{}; image_info.sType = vk::StructureType::eImageCreateInfo; image_info.imageType = image_type; image_info.format = format; image_info.extent.width = width; image_info.extent.height = height; image_info.extent.depth = 1; image_info.mipLevels = ret.mip_levels; image_info.arrayLayers = layers; image_info.samples = samples; image_info.tiling = vk::ImageTiling::eOptimal; image_info.usage = usage | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc; image_info.sharingMode = vk::SharingMode::eExclusive; image_info.initialLayout = vk::ImageLayout::eUndefined; RequireVk(shader_name, "dispatch", m_device.createImage(&image_info, nullptr, &ret.image), "vkCreateImage"); vk::MemoryRequirements req{}; m_device.getImageMemoryRequirements(ret.image, &req); u32 memory_type = 0; if (!FindMemoryType(req.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal, &memory_type)) { Require(shader_name, "dispatch", FindMemoryType(req.memoryTypeBits, {}, &memory_type), "no memory type for image"); } vk::MemoryAllocateInfo alloc{}; alloc.sType = vk::StructureType::eMemoryAllocateInfo; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type; RequireVk(shader_name, "dispatch", m_device.allocateMemory(&alloc, nullptr, &ret.memory), "vk::Device::allocateMemory(image)"); RequireVk(shader_name, "dispatch", m_device.bindImageMemory(ret.image, ret.memory, 0), "vkBindImageMemory"); vk::ImageViewCreateInfo view_info{}; view_info.sType = vk::StructureType::eImageViewCreateInfo; view_info.image = ret.image; view_info.viewType = view_type; view_info.format = format; view_info.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; view_info.subresourceRange.baseMipLevel = 0; view_info.subresourceRange.levelCount = ret.mip_levels; Require(shader_name, "dispatch", view_base_layer < layers, "image view base layer is out of bounds"); if (view_layers == 0) { view_layers = layers - view_base_layer; } Require(shader_name, "dispatch", view_layers <= layers - view_base_layer, "image view layer count is out of bounds"); view_info.subresourceRange.baseArrayLayer = view_base_layer; view_info.subresourceRange.layerCount = view_layers; RequireVk(shader_name, "dispatch", m_device.createImageView(&view_info, nullptr, &ret.view), "vkCreateImageView"); if (!initial_mips.empty()) { size_t total_dwords = 0; for (u32 level = 0; level < ret.mip_levels; level++) { total_dwords += ImageMipDwordCount(width, height, dwords_per_pixel, level, layers); } std::vector contents(total_dwords, 0); size_t offset = 0; for (u32 level = 0; level < ret.mip_levels; level++) { const auto level_dwords = ImageMipDwordCount(width, height, dwords_per_pixel, level, layers); const auto &src = initial_mips[level]; for (size_t i = 0; i < src.size() && i < level_dwords; i++) { contents[offset + i] = src[i]; } offset += level_dwords; } auto staging = CreateHostBuffer(shader_name, contents.size() * sizeof(u32), vk::BufferUsageFlagBits::eTransferSrc, contents); UploadImageMips(shader_name, &ret, staging.buffer, final_layout); DestroyBuffer(&staging); } else { TransitionImage(shader_name, &ret, final_layout, vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eComputeShader, {}, AccessForLayout(final_layout)); } return ret; } void DestroyImage(Image *image) { if (image == nullptr) { return; } if (image->view != nullptr) { m_device.destroyImageView(image->view, nullptr); image->view = nullptr; } if (image->image != nullptr) { m_device.destroyImage(image->image, nullptr); image->image = nullptr; } if (image->memory != nullptr) { m_device.freeMemory(image->memory, nullptr); image->memory = nullptr; } } std::vector ReadImage(const char *shader_name, Image *image) { const auto dword_count = static_cast(image->width) * static_cast(image->height) * image->layers * image->dwords_per_pixel; auto staging = CreateHostBuffer(shader_name, dword_count * sizeof(u32), vk::BufferUsageFlagBits::eTransferDst, {}); vk::CommandBuffer cmd = BeginCommands(shader_name, "readback"); AddImageBarrier( cmd, image->image, image->layout, vk::ImageLayout::eTransferSrcOptimal, vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite, vk::AccessFlagBits::eTransferRead, image->mip_levels, image->layers); vk::BufferImageCopy copy{}; copy.bufferOffset = 0; copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; copy.imageSubresource.mipLevel = 0; copy.imageSubresource.baseArrayLayer = 0; copy.imageSubresource.layerCount = image->layers; copy.imageExtent.width = image->width; copy.imageExtent.height = image->height; copy.imageExtent.depth = 1; cmd.copyImageToBuffer(image->image, vk::ImageLayout::eTransferSrcOptimal, staging.buffer, 1, ©); EndSubmitAndFree(shader_name, "readback", cmd); image->layout = vk::ImageLayout::eTransferSrcOptimal; auto ret = ReadBuffer(shader_name, staging, dword_count); DestroyBuffer(&staging); return ret; } vk::Sampler CreateNearestSampler(const char *shader_name, u32 mip_levels = 1) { vk::SamplerCreateInfo sampler_info{}; sampler_info.sType = vk::StructureType::eSamplerCreateInfo; sampler_info.magFilter = vk::Filter::eNearest; sampler_info.minFilter = vk::Filter::eNearest; sampler_info.mipmapMode = vk::SamplerMipmapMode::eNearest; sampler_info.addressModeU = vk::SamplerAddressMode::eClampToEdge; sampler_info.addressModeV = vk::SamplerAddressMode::eClampToEdge; sampler_info.addressModeW = vk::SamplerAddressMode::eClampToEdge; sampler_info.minLod = 0.0f; sampler_info.maxLod = static_cast(mip_levels - 1u); vk::Sampler sampler = nullptr; RequireVk(shader_name, "dispatch", m_device.createSampler(&sampler_info, nullptr, &sampler), "vkCreateSampler"); return sampler; } std::vector ReadBuffer(const char *shader_name, const Buffer &buffer, size_t dword_count) { if (!buffer.coherent) { vk::MappedMemoryRange range{}; range.sType = vk::StructureType::eMappedMemoryRange; range.memory = buffer.memory; range.offset = 0; range.size = VK_WHOLE_SIZE; RequireVk(shader_name, "readback", m_device.invalidateMappedMemoryRanges(1, &range), "vkInvalidateMappedMemoryRanges"); } void *data = nullptr; RequireVk(shader_name, "readback", m_device.mapMemory(buffer.memory, 0, buffer.size, {}, &data), "vkMapMemory"); std::vector ret(dword_count, 0); std::memcpy(ret.data(), data, dword_count * sizeof(u32)); m_device.unmapMemory(buffer.memory); return ret; } void Dispatch(const TestCase &test, const CompiledShader &compiled, const Buffer &buffer, const Buffer *gds_buffer = nullptr, const Image *sampled_image = nullptr, const Image *storage_image = nullptr, const Image *storage_image_uint = nullptr, vk::Sampler sampler = nullptr) { using Kind = ShaderRecompiler::IR::DescriptorBindingKind; const auto &layout = compiled.program.bindings; auto Binding = [&](Kind kind) { return ShaderRecompiler::IR::FindBinding(layout, kind); }; auto Native = [](Kind kind) { return ShaderRecompiler::IR::NativeBinding(ShaderType::Compute, kind); }; for (const auto &binding : layout.descriptors) { const auto resource_class = ShaderRecompiler::IR::ImageBindingResourceClass(binding.kind); if (resource_class == ShaderRecompiler::IR::ImageResourceClass::None) { continue; } const auto &image = compiled.program.info.images.at(binding.resources.front()); bool supported = image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2D; if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) { supported = supported || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1D || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim1DArray || image.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; } Require(test.name, "dispatch", supported, "unsupported image dimension needs a matching Vulkan test view"); } vk::ShaderModuleCreateInfo module_info{}; module_info.sType = vk::StructureType::eShaderModuleCreateInfo; module_info.codeSize = compiled.spirv.size() * sizeof(u32); module_info.pCode = compiled.spirv.data(); vk::ShaderModule module = nullptr; RequireVk(test.name, "SPIR-V module", m_device.createShaderModule(&module_info, nullptr, &module), "vkCreateShaderModule"); std::vector layout_bindings; auto add_layout_binding = [&layout_bindings](u32 binding, vk::DescriptorType type, u32 count) { if (count == 0) { return; } vk::DescriptorSetLayoutBinding item{}; item.binding = binding; item.descriptorType = type; item.descriptorCount = count; item.stageFlags = vk::ShaderStageFlagBits::eCompute; layout_bindings.push_back(item); }; for (const auto &binding : layout.descriptors) { add_layout_binding(ShaderRecompiler::IR::NativeBinding( ShaderType::Compute, binding.kind), NativeDescriptorType(binding.kind), NativeDescriptorCount(binding)); } vk::DescriptorSetLayoutCreateInfo layout_info{}; layout_info.sType = vk::StructureType::eDescriptorSetLayoutCreateInfo; layout_info.bindingCount = static_cast(layout_bindings.size()); layout_info.pBindings = layout_bindings.empty() ? nullptr : layout_bindings.data(); vk::DescriptorSetLayout descriptor_layout = nullptr; RequireVk(test.name, "dispatch", m_device.createDescriptorSetLayout(&layout_info, nullptr, &descriptor_layout), "vkCreateDescriptorSetLayout"); vk::PipelineLayoutCreateInfo pipeline_layout_info{}; pipeline_layout_info.sType = vk::StructureType::ePipelineLayoutCreateInfo; pipeline_layout_info.setLayoutCount = 1; pipeline_layout_info.pSetLayouts = &descriptor_layout; vk::PushConstantRange push_range{}; if (layout.UsesPushData()) { push_range.stageFlags = vk::ShaderStageFlagBits::eCompute; push_range.offset = 0; push_range.size = ShaderRecompiler::IR::NativePushConstantSize; pipeline_layout_info.pushConstantRangeCount = 1; pipeline_layout_info.pPushConstantRanges = &push_range; } vk::PipelineLayout pipeline_layout = nullptr; RequireVk(test.name, "dispatch", m_device.createPipelineLayout(&pipeline_layout_info, nullptr, &pipeline_layout), "vkCreatePipelineLayout"); vk::PipelineShaderStageCreateInfo stage{}; stage.sType = vk::StructureType::ePipelineShaderStageCreateInfo; stage.stage = vk::ShaderStageFlagBits::eCompute; stage.module = module; stage.pName = "main"; vk::ComputePipelineCreateInfo pipeline_info{}; pipeline_info.sType = vk::StructureType::eComputePipelineCreateInfo; pipeline_info.stage = stage; pipeline_info.layout = pipeline_layout; vk::Pipeline pipeline = nullptr; RequireVk(test.name, "dispatch", m_device.createComputePipelines(nullptr, 1, &pipeline_info, nullptr, &pipeline), "vkCreateComputePipelines"); std::vector pool_sizes; auto add_pool_size = [&pool_sizes](vk::DescriptorType type, u32 count) { if (count == 0) { return; } for (auto &size : pool_sizes) { if (size.type == type) { size.descriptorCount += count; return; } } pool_sizes.push_back({type, count}); }; for (const auto &binding : layout.descriptors) { add_pool_size(NativeDescriptorType(binding.kind), NativeDescriptorCount(binding)); } vk::DescriptorPoolCreateInfo pool_info{}; pool_info.sType = vk::StructureType::eDescriptorPoolCreateInfo; pool_info.maxSets = 1; pool_info.poolSizeCount = static_cast(pool_sizes.size()); pool_info.pPoolSizes = pool_sizes.empty() ? nullptr : pool_sizes.data(); vk::DescriptorPool descriptor_pool = nullptr; RequireVk( test.name, "dispatch", m_device.createDescriptorPool(&pool_info, nullptr, &descriptor_pool), "vkCreateDescriptorPool"); vk::DescriptorSetAllocateInfo set_info{}; set_info.sType = vk::StructureType::eDescriptorSetAllocateInfo; set_info.descriptorPool = descriptor_pool; set_info.descriptorSetCount = 1; set_info.pSetLayouts = &descriptor_layout; vk::DescriptorSet descriptor_set = nullptr; RequireVk(test.name, "dispatch", m_device.allocateDescriptorSets(&set_info, &descriptor_set), "vkAllocateDescriptorSets"); std::vector writes; std::vector buffer_infos; std::vector sampled_infos; std::vector storage_infos; std::vector storage_uint_infos; std::vector storage_atomic_infos; std::vector sampler_infos; Buffer flattened_buffer; Buffer user_data_buffer; vk::DescriptorBufferInfo flattened_info{}; vk::DescriptorBufferInfo user_data_info{}; vk::DescriptorBufferInfo gds_info{}; vk::DescriptorBufferInfo bda_pagetable_info{}; vk::DescriptorBufferInfo fault_buffer_info{}; const bool uses_bda = Binding(Kind::BdaPagetable) != nullptr; Require(test.name, "dispatch", uses_bda == (Binding(Kind::FaultBuffer) != nullptr), "BDA page table and fault buffer must be bound together"); if (uses_bda) { EnsureBdaBuffers(test.name); bda_pagetable_info = {m_bda_pagetable_buffer.buffer, 0, m_bda_pagetable_buffer.size}; fault_buffer_info = {m_fault_buffer.buffer, 0, m_fault_buffer.size}; const std::array special_buffers{ std::pair{Kind::BdaPagetable, &bda_pagetable_info}, std::pair{Kind::FaultBuffer, &fault_buffer_info}, }; for (const auto &[kind, info] : special_buffers) { vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(kind); write.descriptorCount = 1; write.descriptorType = vk::DescriptorType::eStorageBuffer; write.pBufferInfo = info; writes.push_back(write); } } const auto *buffers = Binding(Kind::Buffers); if (buffers != nullptr) { buffer_infos.resize(buffers->resources.size()); for (u32 i = 0; i < buffer_infos.size(); i++) { auto &info = buffer_infos[i]; info.buffer = buffer.buffer; info.offset = 0; info.range = buffer.size; if (test.storage_buffer_range_dwords != 0) { const auto offset = i < test.storage_buffer_offsets.size() ? test.storage_buffer_offsets[i] : 0u; info.range = static_cast( test.storage_buffer_range_dwords * sizeof(u32) + offset); Require(test.name, "dispatch", info.range <= buffer.size, "storage buffer descriptor range exceeds backing buffer"); } } vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(Kind::Buffers); write.descriptorCount = static_cast(buffer_infos.size()); write.descriptorType = vk::DescriptorType::eStorageBuffer; write.pBufferInfo = buffer_infos.data(); writes.push_back(write); } if (const auto *flattened = Binding(Kind::FlattenedSrt); flattened != nullptr) { flattened_buffer = CreateStorageBuffer(test.name, compiled.resources.flattened_srt, compiled.resources.flattened_srt.size()); flattened_info = {flattened_buffer.buffer, 0, flattened_buffer.size}; vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(Kind::FlattenedSrt); write.descriptorCount = 1; write.descriptorType = vk::DescriptorType::eStorageBuffer; write.pBufferInfo = &flattened_info; writes.push_back(write); } if (const auto *user = Binding(Kind::ShaderData); user != nullptr) { user_data_buffer = CreateStorageBuffer(test.name, compiled.packed_user_data, compiled.packed_user_data.size()); user_data_info = {user_data_buffer.buffer, 0, user_data_buffer.size}; vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(Kind::ShaderData); write.descriptorCount = 1; write.descriptorType = vk::DescriptorType::eStorageBuffer; write.pBufferInfo = &user_data_info; writes.push_back(write); } if (const auto *gds = Binding(Kind::Gds); gds != nullptr) { Require(test.name, "dispatch", gds_buffer != nullptr, "GDS descriptor requested but no GDS buffer was provided"); gds_info = {gds_buffer->buffer, 0, gds_buffer->size}; vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(Kind::Gds); write.descriptorCount = 1; write.descriptorType = vk::DescriptorType::eStorageBuffer; write.pBufferInfo = &gds_info; writes.push_back(write); } const ShaderRecompiler::IR::DescriptorBinding *sampled = nullptr; const ShaderRecompiler::IR::DescriptorBinding *storage = nullptr; const ShaderRecompiler::IR::DescriptorBinding *storage_uint = nullptr; const ShaderRecompiler::IR::DescriptorBinding *storage_atomic = nullptr; for (const auto &binding : layout.descriptors) { const auto resource_class = ShaderRecompiler::IR::ImageBindingResourceClass(binding.kind); if (resource_class == ShaderRecompiler::IR::ImageResourceClass::None) { continue; } const auto &image = compiled.program.info.images.at(binding.resources.front()); if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) { Require(test.name, "dispatch", sampled == nullptr, "Vulkan test harness needs separate sampled images for mixed " "descriptor classes"); sampled = &binding; } else if (image.atomic) { storage_atomic = &binding; } else if (image.numeric_class == Prospero::TextureNumericClass::Float) { storage = &binding; } else { storage_uint = &binding; } } if (sampled != nullptr) { Require(test.name, "dispatch", sampled_image != nullptr, "sampled image descriptor requested but no sampled image was " "provided"); sampled_infos.resize(sampled->resources.size()); for (auto &info : sampled_infos) { info.imageView = sampled_image->view; info.imageLayout = sampled_image->layout; } vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(sampled->kind); write.descriptorCount = static_cast(sampled_infos.size()); write.descriptorType = vk::DescriptorType::eSampledImage; write.pImageInfo = sampled_infos.data(); writes.push_back(write); } const auto BindStorage = [&](const ShaderRecompiler::IR::DescriptorBinding *binding, const Image *image, std::vector *infos) { if (binding == nullptr) { return; } Require( test.name, "dispatch", image != nullptr, "storage image descriptor requested but no matching image was " "provided"); infos->resize(binding->resources.size()); for (auto &info : *infos) { info.imageView = image->view; info.imageLayout = image->layout; } vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(binding->kind); write.descriptorCount = static_cast(infos->size()); write.descriptorType = vk::DescriptorType::eStorageImage; write.pImageInfo = infos->data(); writes.push_back(write); }; BindStorage(storage, storage_image, &storage_infos); BindStorage(storage_uint, storage_image_uint, &storage_uint_infos); BindStorage(storage_atomic, storage_image_uint, &storage_atomic_infos); const auto *samplers = Binding(Kind::Samplers); if (samplers != nullptr) { Require(test.name, "dispatch", sampler != nullptr, "sampler descriptor requested but no sampler was provided"); sampler_infos.resize(samplers->resources.size()); for (auto &info : sampler_infos) { info.sampler = sampler; } vk::WriteDescriptorSet write{}; write.sType = vk::StructureType::eWriteDescriptorSet; write.dstSet = descriptor_set; write.dstBinding = Native(Kind::Samplers); write.descriptorCount = static_cast(sampler_infos.size()); write.descriptorType = vk::DescriptorType::eSampler; write.pImageInfo = sampler_infos.data(); writes.push_back(write); } if (!writes.empty()) { m_device.updateDescriptorSets(static_cast(writes.size()), writes.data(), 0, nullptr); } vk::CommandBuffer cmd = BeginCommands(test.name, "dispatch"); if (uses_bda) { Require(test.name, "dispatch", buffer.device_address != 0, "BDA test backing has no device address"); Require(test.name, "dispatch", !test.bda_mappings.empty(), "BDA test has no explicit guest mapping"); cmd.fillBuffer(m_bda_pagetable_buffer.buffer, 0, m_bda_pagetable_buffer.size, 0); cmd.fillBuffer(m_fault_buffer.buffer, 0, m_fault_buffer.size, 0); std::array barriers{}; barriers[0].sType = vk::StructureType::eBufferMemoryBarrier; barriers[0].srcAccessMask = vk::AccessFlagBits::eTransferWrite; barriers[0].dstAccessMask = vk::AccessFlagBits::eTransferWrite; barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barriers[0].buffer = m_bda_pagetable_buffer.buffer; barriers[0].offset = 0; barriers[0].size = m_bda_pagetable_buffer.size; barriers[1] = barriers[0]; barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; barriers[1].buffer = m_fault_buffer.buffer; barriers[1].size = m_fault_buffer.size; cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 1, barriers.data(), 0, nullptr); for (const auto &[guest_base, backing] : test.bda_mappings) { const auto page_offset = guest_base & (BufferCache::CACHING_PAGESIZE - 1); Require(test.name, "dispatch", backing >= page_offset && backing < buffer.size, "BDA test mapping begins outside its backing buffer"); const auto bytes = page_offset + buffer.size - backing; const auto pages = (bytes + BufferCache::CACHING_PAGESIZE - 1) / BufferCache::CACHING_PAGESIZE; auto address = buffer.device_address + backing - page_offset; auto page = guest_base >> BufferCache::CACHING_PAGEBITS; for (uint64_t mapped = 0; mapped < pages; mapped++) { cmd.updateBuffer(m_bda_pagetable_buffer.buffer, (page + mapped) * sizeof(vk::DeviceAddress), sizeof(address), &address); address += BufferCache::CACHING_PAGESIZE; } } barriers[0].dstAccessMask = vk::AccessFlagBits::eShaderRead; cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, static_cast(barriers.size()), barriers.data(), 0, nullptr); } cmd.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline); cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute, pipeline_layout, 0, 1, &descriptor_set, 0, nullptr); if (layout.UsesPushData()) { ShaderRecompiler::IR::PushData push_data; std::copy(compiled.packed_user_data.begin(), compiled.packed_user_data.end(), push_data.dwords.begin() + layout.push_data_start_dword); cmd.pushConstants(pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0, sizeof(push_data), push_data.dwords.data()); } cmd.dispatch(test.dispatch_x, test.dispatch_y, test.dispatch_z); if (buffers != nullptr) { vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = buffer.buffer; barrier.offset = 0; barrier.size = buffer.size; cmd.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); } if (gds_buffer != nullptr) { vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = gds_buffer->buffer; barrier.offset = 0; barrier.size = gds_buffer->size; cmd.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); } EndSubmitAndFree(test.name, "dispatch", cmd); if (flattened_buffer.buffer != nullptr) { DestroyBuffer(&flattened_buffer); } if (user_data_buffer.buffer != nullptr) { DestroyBuffer(&user_data_buffer); } m_device.destroyDescriptorPool(descriptor_pool, nullptr); m_device.destroyPipeline(pipeline, nullptr); m_device.destroyPipelineLayout(pipeline_layout, nullptr); m_device.destroyDescriptorSetLayout(descriptor_layout, nullptr); m_device.destroyShaderModule(module, nullptr); } void CheckRasterization(bool depth_feedback, bool packed_vertex_color = false) { const char *name = packed_vertex_color ? "PackedFloatVertexColor" : depth_feedback ? "DepthAttachmentFeedback" : "PolygonModeRasterization"; const uint32_t extent = depth_feedback ? 8 : 32; constexpr uintptr_t depth_address = 0x0000000204400000ull; constexpr uint64_t allocation_size = 0x40000; EnsureRuntimeContext(); RenderContext context(m_runtime_context); auto &scheduler = context.GetCommandScheduler(); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetRenderTargetMask(0xf); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = context.GetTextureCache(); auto &executor = context.GetRenderExecutor(); int64_t direct_offset = -1; RenderDepthInfo depth{}; Require(name, "direct allocation", Libs::LibKernel::Memory::KernelAllocateDirectMemory( 0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(), allocation_size, allocation_size, 0, &direct_offset) == 0, "depth allocation failed"); void *mapped = reinterpret_cast(depth_address); Require(name, "direct mapping", Libs::LibKernel::Memory::KernelMapDirectMemory( &mapped, allocation_size, 0x3, 0x10, direct_offset, allocation_size) == 0 && mapped == reinterpret_cast(depth_address), "depth mapping failed"); std::memset(mapped, 0, allocation_size); const auto initial_depth = [](uint32_t x, uint32_t y) { return 0.125f + static_cast(x + 2 * y) / 128.0f; }; for (uint32_t y = 0; y < extent; y++) { for (uint32_t x = 0; x < extent; x++) { static_cast(mapped)[y * 64 + x] = initial_depth(x, y); } } resources.MapMemory(depth_address, allocation_size); if (depth_feedback) { depth.desc.type = BindingType::DepthTarget; depth.desc.info.data = {depth_address, 256 * extent}; depth.desc.info.pixel_format = vk::Format::eD32Sfloat; depth.desc.info.guest_format = Prospero::BufferFormat::k32Float; depth.desc.info.extent = {extent, extent, 1}; depth.desc.info.pitch = 64; depth.desc.info.bytes_per_block = 4; depth.desc.info.tile_mode = Prospero::TileMode::kLinear; depth.desc.info.mip_layout[0] = {0, 256 * extent, 64, extent}; depth.desc.view_info.format = depth.desc.info.pixel_format; depth.desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth; depth.desc.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; depth.depth_test_enable = depth.depth_write_enable = true; depth.depth_compare_op = vk::CompareOp::eAlways; depth.image_id = cache.FindImage(depth.desc); } GraphicsCase test; test.name = name; test.pixel_depth_export = depth_feedback; if (depth_feedback) { const ShaderTextureResource descriptor{{ static_cast(depth_address >> 8u), (static_cast(Prospero::BufferFormat::k32Float) << 20u) | (((extent - 1u) & 3u) << 30u), ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), DstSel(4, 4, 4, 4) | (static_cast(Prospero::ImageType::kColor2D) << 28u), 0, 0, 0, 0}}; test.has_user_data = true; std::copy_n(descriptor.fields, 8, test.user_data.begin()); // Each fragment samples its own texel through interpolated screen UVs. for (uint32_t component = 0; component < 2; component++) { test.fragment_code.push_back(EncodeVintrp(0, 20 + component, 0, component, 0)); test.fragment_code.push_back(EncodeVintrp(1, 20 + component, 0, component, 1)); } test.fragment_code.push_back(EncodeMimg0(0x27, 1)); test.fragment_code.push_back(EncodeMimg1(0, 20, 0, 2)); AppendVMovLiteral(&test.fragment_code, 1, 0x3e000000u); test.fragment_code.push_back(EncodeVop2(0x03, 1, Vgpr(0), 1)); test.fragment_code.push_back(EncodeExp0(0x08, 0x1, false)); test.fragment_code.push_back(EncodeExp1(1, 1, 1, 1)); } else { test.pixel_interpolator_settings = {0x400u}; for (uint32_t component = 0; component < (packed_vertex_color ? 4u : 1u); component++) { test.fragment_code.push_back(EncodeVintrp(0x02, component, 0, component, 2)); } } test.fragment_code.push_back(EncodeExp0(0x00, 0xf)); test.fragment_code.push_back(packed_vertex_color ? EncodeExp1(0, 1, 2, 3) : EncodeExp1(0, 0, 0, 0)); AppendEnd(&test.fragment_code); auto fragment = CompileFragmentCase(test); const auto vertex_spirv = TestSpv::MakePassthroughVertexSpirv(false); ShaderProgram vertex_shader{1, CreateShaderModule(name, vertex_spirv)}; ShaderProgram pixel_shader{2, CreateShaderModule(name, fragment.spirv)}; ShaderRecompiler::IR::CompiledShaderInfo vertex_program{}; vertex_program.stage = ShaderType::Vertex; vertex_program.info.vertex_fetch_components[0] = 2; vertex_program.info.vertex_fetch_components[1] = packed_vertex_color ? 3 : 4; ShaderRecompiler::IR::CompiledShaderInfo pixel_program{}; pixel_program.stage = ShaderType::Pixel; pixel_program.info = fragment.program.info; pixel_program.bindings = fragment.program.bindings; ShaderVertexInputInfo vertex{}; vertex.stage.program = &vertex_program; vertex.resources_num = 2; vertex.buffers_num = 1; vertex.buffers[0].stride = 6 * sizeof(float); vertex.buffers[0].attr_num = 2; vertex.buffers[0].attr_indices[1] = 1; vertex.buffers[0].attr_offsets[1] = 2 * sizeof(float); for (uint32_t i = 0; i < 2; i++) { const auto format = i == 0 ? Prospero::BufferFormat::k32_32Float : packed_vertex_color ? Prospero::BufferFormat::k11_11_10Float : Prospero::BufferFormat::k32_32_32_32Float; vertex.resources[i].fields[3] = DstSel(4, 5, 6, 7) | (static_cast(format) << 12u); vertex.resources_dst[i].registers_num = i == 0 ? 2 : vertex_program.info.vertex_fetch_components[1]; } ShaderPixelInputInfo pixel{}; pixel.stage.program = &pixel_program; pixel.stage.resources = std::move(fragment.resources); RenderColorInfo color{}; color.desc.type = BindingType::RenderTarget; color.desc.info.data = {depth_address + 0x10000, extent * extent * 16}; color.desc.info.mip_layout[0] = {0, color.desc.info.data.size, extent, extent}; color.desc.info.pixel_format = vk::Format::eR32G32B32A32Sfloat; color.desc.info.guest_format = Prospero::BufferFormat::k32_32_32_32Float; color.desc.info.extent = {extent, extent, 1}; color.desc.info.pitch = extent; color.desc.info.bytes_per_block = 16; color.desc.view_info.format = color.desc.info.pixel_format; color.desc.view_info.usage = vk::ImageUsageFlagBits::eColorAttachment; color.image_id = cache.FindImage(color.desc); std::array vertices{ -0.75f, -0.75f, 1, 1, 1, 1, 0.75f, -0.75f, 0.5f, 1, 1, 1, 0.0f, 0.75f, 0.25f, 1, 1, 1}; if (depth_feedback) { vertices = {-1, -1, 0, 0, 0, 1, 3, -1, 2, 0, 0, 1, -1, 3, 0, 2, 0, 1}; } std::vector vertex_words(vertices.size()); std::memcpy(vertex_words.data(), vertices.data(), sizeof(vertices)); if (packed_vertex_color) { // R11/G11/B10 unsigned floats: 0.5, 1.0, 2.0; Vulkan supplies the missing alpha as 1. for (uint32_t i = 0; i < 3; i++) { vertex_words[i * 6 + 2] = 0x801e0380u; } } auto buffer = CreateHostBuffer(name, sizeof(vertices), vk::BufferUsageFlagBits::eVertexBuffer, vertex_words); const auto pipeline = [&](bool enabled, uint8_t front, uint8_t back, bool provoking_last = false, bool clockwise = false, vk::PrimitiveTopology topology = vk::PrimitiveTopology::eTriangleList) -> PipelineCache::Pipeline & { HW::ModeControl mode{}; mode.face = clockwise; mode.poly_mode = enabled; mode.polymode_front_ptype = front; mode.polymode_back_ptype = back; mode.provoking_vtx_last = provoking_last; registers.SetModeControl(mode); return context.GetPipelineCache().GetGraphicsPipeline( std::span{&color, 1u}, depth, std::span{&vertex, 1u}, scheduler.Current(), &pixel, topology, false, PipelineCache::GraphicsPrograms{{vertex_shader}, pixel_shader}); }; auto &filled = pipeline(true, 2, 2); const auto draw = [&](const PipelineCache::Pipeline &selected, uint32_t vertex_count = 3) { RenderExecutorTestAccess::BindRenderTarget(executor, color.image_id); if (depth.image_id) { RenderExecutorTestAccess::BindRenderTarget(executor, depth.image_id); } auto bindings = RenderExecutorTestAccess::PrepareGraphicsBindings( executor, vertex.stage, pixel.stage, true); Require(name, "sampled attachment identity", !depth_feedback || bindings.pixel->images[0].image_id == depth.image_id, "the fragment must sample the depth attachment's native image"); auto &command = scheduler.Current(); auto rendering = RenderExecutorTestAccess::AcquireRenderTargets( executor, command, &color, 1, depth, bindings.pixel); const bool feedback_enabled = rendering.depth_stencil_attachment.image_layout == vk::ImageLayout::eAttachmentFeedbackLoopOptimalEXT; Require(name, "per-draw depth feedback", feedback_enabled == (depth_feedback && depth.depth_write_enable), "feedback was not selected only for an overlapping fragment depth read/write"); RenderExecutorTestAccess::CommitBindings( executor, command, selected, bindings.vertex[0], *bindings.pixel); rendering.color_attachments[0].is_clear = true; command.BeginRendering(rendering); auto cmd = command.Handle(); cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, selected.pipeline); const vk::Viewport viewport{0, 0, static_cast(extent), static_cast(extent), 0, 1}; const vk::Rect2D scissor{{0, 0}, {extent, extent}}; cmd.setViewportWithCount(1, &viewport); cmd.setScissorWithCount(1, &scissor); cmd.setLineWidth(1); cmd.setDepthTestEnable(depth.depth_test_enable); cmd.setDepthWriteEnable(depth.depth_write_enable); cmd.setDepthCompareOp(depth.depth_compare_op); cmd.setDepthBiasEnable(false); cmd.setStencilTestEnable(depth.stencil_test_enable); if (depth.stencil_test_enable) { const auto set_stencil = [&](vk::StencilFaceFlagBits face, const vk::StencilOpState &state) { cmd.setStencilOp(face, state.failOp, state.passOp, state.depthFailOp, state.compareOp); cmd.setStencilCompareMask(face, state.compareMask); cmd.setStencilWriteMask(face, state.writeMask); cmd.setStencilReference(face, state.reference); }; set_stencil(vk::StencilFaceFlagBits::eFront, depth.stencil_front); set_stencil(vk::StencilFaceFlagBits::eBack, depth.stencil_back); } const vk::Bool32 write = true; cmd.setColorWriteEnableEXT(1, &write); cmd.setAttachmentFeedbackLoopEnableEXT( feedback_enabled ? vk::ImageAspectFlags{vk::ImageAspectFlagBits::eDepth} : vk::ImageAspectFlags{}); const vk::DeviceSize offset = 0; cmd.bindVertexBuffers(0, 1, &buffer.buffer, &offset); cmd.draw(vertex_count, 1, 0, 0); command.EndRendering(); RenderExecutorTestAccess::ResetBindings(executor); }; const auto read_color = [&] { return ReadCachedTexel(name, context, color.image_id, {}, {extent, extent, 1}); }; draw(filled); const auto solid_pixels = read_color(); if (packed_vertex_color) { const std::array expected{0.5f, 1.0f, 2.0f, 1.0f}; const auto center = 4 * ((extent / 2) * extent + extent / 2); for (uint32_t component = 0; component < expected.size(); component++) { Require(name, "packed vertex fetch and coverage", std::abs(std::bit_cast(solid_pixels[center + component]) - expected[component]) < 0.0001f && solid_pixels[component] == 0, "packed vertex color changed channels, clamped HDR, or filled outside the triangle"); } } else if (depth_feedback) { // Consecutive draws in the same submission must see each earlier depth write. draw(filled); draw(filled); depth.depth_write_enable = false; draw(filled); // Reset feedback in the same command buffer and preserve the depth writes. const auto repeated_pixels = read_color(); const auto stored_depth = ReadCachedTexel( name, context, depth.image_id, {}, {extent, extent, 1}); for (uint32_t y = 0; y < extent; y++) { for (uint32_t x = 0; x < extent; x++) { const auto index = y * extent + x; Require(name, "sampled and stored depth", solid_pixels[index * 4] == std::bit_cast(initial_depth(x, y)) && repeated_pixels[index * 4] == std::bit_cast(initial_depth(x, y) + 0.375f) && stored_depth[index] == repeated_pixels[index * 4], "depth feedback lost a prior write or the read-only draw changed depth"); } } } else { auto &wireframe = pipeline(true, 1, 1); Require(name, "pipeline cache", filled.pipeline != wireframe.pipeline && pipeline(true, 2, 2).pipeline == filled.pipeline && pipeline(false, 1, 0).pipeline == filled.pipeline, "polygon mode was lost from the cache key or dormant face modes were applied"); draw(wireframe); const auto line_pixels = read_color(); const auto interior = 4 * (16 * extent + 16); Require(name, "GPU coverage", solid_pixels[interior] == 0x3f800000u && line_pixels[interior] == 0 && std::ranges::any_of(line_pixels, [](u32 value) { return value == 0x3f800000u; }), "wireframe did not preserve triangle edges while leaving its interior empty"); auto &last_vertex = pipeline(true, 2, 2, true); Require(name, "provoking vertex pipeline cache", last_vertex.pipeline != filled.pipeline && pipeline(true, 2, 2, true).pipeline == last_vertex.pipeline && pipeline(true, 2, 2).pipeline == filled.pipeline, "first and last provoking vertices did not keep distinct cached pipelines"); draw(last_vertex); const auto last_pixels = read_color(); for (size_t i = 0; i < solid_pixels.size(); i++) { Require(name, "flat provoking vertex output", last_pixels[i] == (solid_pixels[i] == 0 ? 0 : 0x3e800000u), "last-vertex flat shading changed coverage or did not use vertex two"); } const auto blend_pipeline = [&](bool enabled, bool bypass) -> PipelineCache::Pipeline & { auto blend = registers.GetBlendControl(0); blend.enable = enabled; blend.color_srcblend = static_cast(Prospero::BlendFactor::kZero); blend.alpha_srcblend = static_cast(Prospero::BlendFactor::kZero); registers.SetBlendControl(0, blend); auto target = registers.GetRenderTarget(0).info; target.blend_bypass = bypass; registers.SetColorInfo(0, target); return pipeline(true, 2, 2); }; auto &disabled_blend = blend_pipeline(false, false); Require(name, "effective blend pipeline cache", blend_pipeline(false, true).pipeline == disabled_blend.pipeline && blend_pipeline(true, true).pipeline == disabled_blend.pipeline, "disabled and bypassed blending did not share the same host pipeline"); auto &enabled_blend = blend_pipeline(true, false); Require(name, "enabled blend pipeline cache", enabled_blend.pipeline != disabled_blend.pipeline && blend_pipeline(true, false).pipeline == enabled_blend.pipeline, "active blending was lost from the pipeline key or missed its cached pipeline"); draw(enabled_blend); const auto blended_pixels = read_color(); Require(name, "active blend output", std::ranges::all_of(blended_pixels, [](u32 value) { return value == 0; }), "zero-factor blending did not clear the fragment color"); draw(blend_pipeline(true, true)); Require(name, "bypassed blend output", read_color() == solid_pixels, "blend bypass did not restore the unblended fragment color and coverage"); HW::DepthRenderTarget stencil_target{}; stencil_target.z_info.format = Prospero::DepthFormat::kZ32F; stencil_target.stencil_info.format = Prospero::StencilFormat::k8UInt; stencil_target.stencil_info.htile_stencil_disabled = true; stencil_target.z_read_base_addr = stencil_target.z_write_base_addr = depth_address; stencil_target.stencil_read_base_addr = stencil_target.stencil_write_base_addr = depth_address + 0x20000; stencil_target.size = {static_cast(extent - 1), static_cast(extent - 1), true}; registers.SetDepthRenderTarget(stencil_target); HW::DepthControl stencil_control{}; // Keep the same depth/stencil attachment active when stencil testing is disabled. stencil_control.z_enable = true; stencil_control.zfunc = static_cast(vk::CompareOp::eAlways); stencil_control.stencil_enable = stencil_control.backface_enable = true; stencil_control.stencilfunc = stencil_control.stencilfunc_bf = static_cast(vk::CompareOp::eAlways); registers.SetDepthControl(stencil_control); // PPSA01325 sets bit 7 on front faces and clears it on back faces. registers.SetStencilControl({3, 3, 3, 4, 4, 4}); registers.SetStencilMask({0x80, 0, 0x80, 1, 0x80, 0, 0x80, 1}); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); auto stencil_readback = CreateHostBuffer(name, extent * extent, vk::BufferUsageFlagBits::eTransferDst, {}); const auto read_stencil = [&] { const vk::BufferImageCopy copy{0, 0, 0, {vk::ImageAspectFlagBits::eStencil, 0, 0, 1}, {}, {extent, extent, 1}}; cache.GetImage(depth.image_id).Download(std::span{©, 1}, stencil_readback.buffer, 0, stencil_readback.size); const vk::MemoryBarrier2 barrier{ .srcStageMask = vk::PipelineStageFlagBits2::eCopy, .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, .dstStageMask = vk::PipelineStageFlagBits2::eHost, .dstAccessMask = vk::AccessFlagBits2::eHostRead}; vk::DependencyInfo dependency{}; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; scheduler.Current().Handle().pipelineBarrier2(dependency); scheduler.Finish(); return ReadBuffer(name, stencil_readback, extent * extent / 4); }; const std::array stencil_pipelines{ pipeline(true, 2, 2, false, true).pipeline, pipeline(true, 2, 2, false, false).pipeline}; const auto check_stencil = [&](const char *label, bool back_face, uint8_t initial, uint8_t written) { vk::ClearValue clear{}; clear.depthStencil = vk::ClearDepthStencilValue{0.625f, initial}; TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id, {vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, clear); const auto &selected = pipeline(true, 2, 2, false, !back_face); Require(name, "dynamic stencil pipeline reuse", selected.pipeline == stencil_pipelines[back_face], "stencil enable, operations or references changed the graphics pipeline"); draw(selected); const auto result = read_stencil(); const auto *stencil_bytes = reinterpret_cast(result.data()); for (uint32_t texel = 0; texel < extent * extent; ++texel) { const uint8_t expected = solid_pixels[texel * 4] != 0 ? written : initial; Require(name, label, stencil_bytes[texel] == expected, "captured stencil operations changed masked bits or triangle coverage"); } }; check_stencil("front-face ReplaceTest", false, 0x35, 0xb5); check_stencil("back-face masked ReplaceOp", true, 0xb5, 0x35); stencil_control.stencil_enable = false; registers.SetDepthControl(stencil_control); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); check_stencil("disabled front stencil", false, 0x35, 0x35); check_stencil("disabled back stencil", true, 0xb5, 0xb5); stencil_control.stencil_enable = true; registers.SetDepthControl(stencil_control); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); check_stencil("re-enabled front stencil", false, 0x35, 0xb5); check_stencil("re-enabled back stencil", true, 0xb5, 0x35); // Frame 6115 uses separate test/op replacement values with Always comparisons. stencil_control.z_enable = stencil_control.z_write_enable = true; stencil_control.zfunc = static_cast(vk::CompareOp::eLessOrEqual); registers.SetDepthControl(stencil_control); registers.SetStencilControl({0, 3, 9, 0, 4, 8}); registers.SetStencilMask({8, 0xff, 0xff, 1, 8, 0xff, 0xff, 1}); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); Require(name, "independent stencil references", depth.stencil_front.reference == 8 && depth.stencil_back.reference == 1, "always-pass faces must preserve their distinct replacement values"); check_stencil("front test-value replacement", false, 0x35, 8); check_stencil("back operation-value replacement", true, 0x35, 1); stencil_control.zfunc = static_cast(vk::CompareOp::eNever); registers.SetDepthControl(stencil_control); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); check_stencil("front depth-fail subtract wrap", false, 0, 0xff); check_stencil("back depth-fail add wrap", true, 0xff, 0); // A compare on bit 3 and replacement on bit 0 share one Vulkan reference // without changing either native operation or the untouched stencil bits. stencil_control.z_enable = stencil_control.z_write_enable = false; stencil_control.stencilfunc = stencil_control.stencilfunc_bf = static_cast(vk::CompareOp::eEqual); registers.SetDepthControl(stencil_control); registers.SetStencilControl({0, 4, 0, 0, 4, 0}); registers.SetStencilMask({8, 8, 1, 1, 8, 8, 1, 1}); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); Require(name, "masked compare and replace reference", depth.stencil_front.reference == 9 && depth.stencil_back.reference == 9, "disjoint comparison and replacement bits must combine into one reference"); check_stencil("masked compare passes", false, 0xb8, 0xb9); check_stencil("masked compare fails", false, 0xb0, 0xb0); // SPI_PS_IN_CONTROL must select the native wave width through the actual // program cache. In wave32 a low-word compare preserves scalar VCC_HI. static const auto native_vertex = [] { std::vector code; AppendVMovLiteral(&code, 1, 0xbf800000u); AppendVMovLiteral(&code, 2, 0x40400000u); code.push_back(EncodeVopc(0xc2, InlineU32(1), 5)); code.push_back(EncodeVop2(0x01, 3, Vgpr(1), 2)); code.push_back(EncodeVopc(0xc2, InlineU32(2), 5)); code.push_back(EncodeVop2(0x01, 4, Vgpr(1), 2)); AppendVMovU32(&code, 0, 0); AppendVMovLiteral(&code, 6, 0x3f800000u); code.push_back(EncodeExp0(0x0c, 0xf)); code.push_back(EncodeExp1(3, 4, 0, 6)); for (u32 parameter = 0; parameter < 8; parameter++) { code.push_back(EncodeExp0(0x20 + parameter, 0xf)); code.push_back(EncodeExp1(0, 0, 0, 0)); } AppendEnd(&code); return code; }(); static const auto native_pixel = [] { std::vector code; AppendVMovU32(&code, 0, 0); AppendSMovLiteral(&code, 107, 0x3e800000u); code.push_back(EncodeVopc(0xc2, InlineU32(1), 0)); code.push_back(EncodeVop1(0x01, 1, 107)); AppendVMovLiteral(&code, 2, 0x3f800000u); code.push_back(EncodeExp0(0x00, 0xf)); code.push_back(EncodeExp1(1, 1, 1, 2)); AppendEnd(&code); return code; }(); static ShaderUserData native_user_data{}; const auto vertex_address = reinterpret_cast(native_vertex.data()); const auto pixel_address = reinterpret_cast(native_pixel.data()); ShaderMapUserData(vertex_address, {.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data, .code_size_bytes = static_cast(native_vertex.size() * sizeof(u32))}); ShaderMapUserData(pixel_address, {.type = Prospero::ShaderBinaryType::kPs, .code_size_bytes = static_cast(native_pixel.size() * sizeof(u32))}); HW::VertexShaderInfo native_vertex_regs{}; native_vertex_regs.es_regs.data_addr = vertex_address; HW::PixelShaderInfo native_pixel_regs{}; native_pixel_regs.ps_regs.data_addr = pixel_address; const auto owned_vertex_shader = vertex_shader; const auto owned_pixel_shader = pixel_shader; depth.stencil_test_enable = false; registers.SetViewportTransformControl(0x300); user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList); std::array export_mapping{}; std::array native_vertex_info{}; std::array wave_ids{}; std::array, 2> wave_keys; struct PixelWaveCase { uint32_t control, width, color; }; for (const auto wave : {PixelWaveCase{0x0008, 64, 0}, PixelWaveCase{0x8008, 32, 0x3e800000}, PixelWaveCase{0x0008, 64, 0}}) { registers.SetPsInControl(wave.control); const auto programs = context.GetPipelineCache().GetGraphicsPrograms( native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(), registers, user_config, export_mapping, true, native_vertex_info, pixel); Require(name, "pixel wave metadata propagation", pixel.input_num == 8 && pixel.stage.program->wave_size == wave.width, "SPI_PS_IN_CONTROL width did not reach the compiled pixel program"); const auto index = static_cast(wave.width == 32); std::vector key; BuildStageStaticKey(pixel, key); if (wave_ids[index] != 0) { Require(name, "pixel wave cache reuse", programs.pixel.id == wave_ids[index] && key == wave_keys[index], "restoring wave64 did not reuse its original program"); } wave_ids[index] = programs.pixel.id; wave_keys[index] = std::move(key); vertex_shader = programs.vertex[0]; pixel_shader = programs.pixel; vertex = native_vertex_info[0]; draw(pipeline(true, 2, 2)); const auto wave_pixels = read_color(); for (size_t component = 0; component < wave_pixels.size(); component++) { const auto expected = component % 4 == 3 ? 0x3f800000u : wave.color; Require(name, "pixel wave VCC_HI readback", wave_pixels[component] == expected, "pixel comparison clobbered VCC_HI in wave32 or retained it in wave64"); } } Require(name, "pixel wave cache distinction", wave_ids[0] != wave_ids[1] && wave_keys[0] != wave_keys[1], "wave32 and wave64 pixel programs shared a cache key"); // Captured Playroom strips contain the fullscreen triangle followed by an // out-of-bounds fetch exporting (0,0,0,0). Additive color reveals any extra triangle. static constexpr std::array, 4> fourth_positions{{ {0, 0, 0, 0}, {0x80000000u, 0, 0x80000000u, 0x80000000u}, {0, 0, 0, 0x3f800000u}, {0xbf800000u, 0xbf800000u, 0, 0}}}; static const auto position_shaders = [&] { std::array, fourth_positions.size()> result; for (size_t i = 0; i < result.size(); i++) { auto &code = result[i]; const auto position_export = std::ranges::find(native_vertex, EncodeExp0(0x0c, 0xf)); code.assign(native_vertex.begin(), position_export); code.push_back(EncodeVopc(0xc2, InlineU32(3), 5)); constexpr std::array position_registers{3, 4, 0, 6}; for (size_t component = 0; component < position_registers.size(); component++) { AppendVMovLiteral(&code, 7, fourth_positions[i][component]); const auto reg = position_registers[component]; code.push_back(EncodeVop2(0x01, reg, Vgpr(reg), 7)); } code.insert(code.end(), position_export, native_vertex.end()); } return result; }(); auto additive_blend = registers.GetBlendControl(0); additive_blend.enable = true; additive_blend.color_srcblend = additive_blend.color_destblend = additive_blend.alpha_srcblend = additive_blend.alpha_destblend = static_cast(Prospero::BlendFactor::kOne); registers.SetBlendControl(0, additive_blend); auto target_info = registers.GetRenderTarget(0).info; target_info.blend_bypass = false; registers.SetColorInfo(0, target_info); registers.SetPsInControl(0x8008); user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriStrip); for (size_t i = 0; i < position_shaders.size(); i++) { const auto &code = position_shaders[i]; native_vertex_regs.es_regs.data_addr = reinterpret_cast(code.data()); ShaderMapUserData(native_vertex_regs.es_regs.data_addr, {.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data, .code_size_bytes = static_cast(code.size() * sizeof(u32))}); const auto programs = context.GetPipelineCache().GetGraphicsPrograms( native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(), registers, user_config, export_mapping, true, native_vertex_info, pixel); vertex_shader = programs.vertex[0]; pixel_shader = programs.pixel; vertex = native_vertex_info[0]; draw(pipeline(true, 2, 2, false, false, vk::PrimitiveTopology::eTriangleStrip), 4); const auto pixels = read_color(); bool second_triangle = false; for (size_t component = 0; component < pixels.size(); component += 4) { Require(name, "fullscreen triangle preserved", pixels[component] == 0x3e800000u || pixels[component] == 0x3f000000u, "clipping-error culling removed valid fullscreen coverage"); second_triangle |= pixels[component] == 0x3f000000u; } Require(name, "zero homogeneous position culling", second_triangle == (i >= 2), "zero positions drew an extra triangle, or a valid position was culled"); } // A NULL-export stencil pass must ignore smaller stale color targets, // materialize the whole pending HTile clear, and still execute pixel discard. static const auto null_pixels = [] { std::array, 2> result; for (uint32_t discard = 0; discard < result.size(); discard++) { auto &code = result[discard]; if (discard != 0) { code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); } code.push_back(EncodeExp0(0x09, 0, true, false, true)); code.push_back(EncodeExp1(0, 0, 0, 0)); AppendEnd(&code); } return result; }(); registers.Reset(); registers.SetViewportTransformControl(0x300); registers.SetViewportScaleOffset(0, extent / 2, extent / 2, extent / 2, extent / 2, 1, 0); registers.SetViewportZMax(0, 1); registers.SetScreenScissor(0, 0, extent, extent); registers.SetWindowScissor(0, 0, extent, extent, false); registers.SetGenericScissor(0, 0, extent, extent, false); registers.SetViewportScissor(0, 0, 0, extent, extent, false); registers.SetRenderTargetMask(0xf00f); registers.SetShaderMask(0xf00f); registers.SetPsInControl(0x8000); registers.SetDepthShaderControl({.shader_kill_enable = true}); for (const auto slot : {0u, 3u}) { registers.SetColorBase(slot, {.addr = depth_address + (slot == 0 ? 0x38000 : 0x10000)}); registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k32_32_32_32, .channel_type = Prospero::ChannelType::kFloat, .channel_order = Prospero::ChannelOrder::kStandard}); const auto side = slot == 0 ? extent / 4 : extent; registers.SetColorAttrib2(slot, {.height = side - 1, .width = side - 1}); registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kLinear, .dimension = 1}); registers.SetTargetOutputMode(slot, 4); } stencil_target.z_info.htile_acceleration = true; stencil_target.htile_data_base_addr = depth_address + 0x30000; registers.SetDepthRenderTarget(stencil_target); registers.SetDepthClearValue(1); stencil_control = {}; stencil_control.stencil_enable = true; stencil_control.stencilfunc = static_cast(vk::CompareOp::eAlways); registers.SetDepthControl(stencil_control); registers.SetStencilControl({3, 3, 3, 3, 3, 3}); registers.SetStencilMask({0x80, 0, 0x80, 1, 0x80, 0, 0x80, 1}); user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList); shaders.SetEsShaderBase(vertex_address); for (uint32_t discard = 0; discard < null_pixels.size(); discard++) { const auto &code = null_pixels[discard]; const auto address = reinterpret_cast(code.data()); ShaderMapUserData(address, {.type = Prospero::ShaderBinaryType::kPs, .code_size_bytes = static_cast(code.size() * sizeof(u32))}); shaders.SetPsShaderBase(address); RenderExecutorTestAccess::ResolveRenderDepthTarget(executor, scheduler.Current(), depth); (void)cache.FindDepthTarget(depth.image_id, depth.desc); vk::ClearValue zero{}; TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), depth.image_id, {vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, zero); Require(name, "pending stencil-pass HTile clear", cache.ClearMeta(stencil_target.htile_data_base_addr), "the stencil pass did not retain its HTile metadata"); RenderExecutorTestAccess::DrawAuto( executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1}); const auto depth_pixels = ReadCachedTexel(name, context, depth.image_id, {}, {extent, extent, 1}); Require(name, "NULL-export full depth clear", std::ranges::all_of(depth_pixels, [](u32 v) { return v == 0x3f800000u; }), "an unexported color target clipped the pending depth clear"); const auto stencil = read_stencil(); const auto expected = discard == 0 ? 0x80808080u : 0u; Require(name, "NULL-export stencil coverage and discard", std::ranges::all_of(stencil, [=](u32 v) { return v == expected; }), "a stale color target clipped stencil coverage or the NULL-export shader was skipped"); } // With MRT0 still bound, an MRT3-only export must retain location3 in // rendering attachments, pipeline formats, blend masks and dynamic write enables. static const auto sparse_pixel = [] { auto code = native_pixel; *std::ranges::find(code, EncodeExp0(0x00, 0xf)) = EncodeExp0(0x03, 0xf); return code; }(); const auto sparse_address = reinterpret_cast(sparse_pixel.data()); ShaderMapUserData(sparse_address, {.type = Prospero::ShaderBinaryType::kPs, .code_size_bytes = static_cast(sparse_pixel.size() * sizeof(u32))}); shaders.SetPsShaderBase(sparse_address); registers.SetDepthControl({}); registers.SetDepthShaderControl({}); RenderExecutorTestAccess::DrawAuto( executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1}); RenderColorInfo sparse_color{}; RenderExecutorTestAccess::ResolveRenderColorTarget( executor, scheduler.Current(), sparse_color, 3); const auto sparse_pixels = ReadCachedTexel(name, context, sparse_color.image_id, {}, {extent, extent, 1}); for (size_t component = 0; component < sparse_pixels.size(); component++) { const auto expected = component % 4 == 3 ? 0x3f800000u : 0x3e800000u; Require(name, "sparse MRT3 output", sparse_pixels[component] == expected, "MRT3 was compacted to a different slot or clipped by unused MRT0"); } DestroyBuffer(&stencil_readback); vertex_shader = owned_vertex_shader; pixel_shader = owned_pixel_shader; } resources.UnmapMemory(depth_address, allocation_size); scheduler.Finish(); DestroyBuffer(&buffer); m_device.destroyShaderModule(pixel_shader.module); m_device.destroyShaderModule(vertex_shader.module); Require(name, "release depth memory", Libs::LibKernel::Memory::KernelMunmap(depth_address, allocation_size) == 0 && Libs::LibKernel::Memory::KernelReleaseDirectMemory( direct_offset, allocation_size) == 0, "depth mapping or allocation release failed"); std::printf("[gpu] %-32s ok\n", name); } std::vector RenderFragment(const GraphicsCase &test, const CompiledShader &fragment) { const auto vertex_spirv = TestSpv::MakePassthroughVertexSpirv(test.layers > 1, test.vertex_clip_w); ValidateSpirv(test.name, vertex_spirv); Image target = CreateImageMips(test.name, 1, 1, vk::Format::eR32G32B32A32Sfloat, vk::ImageUsageFlagBits::eColorAttachment, {}, 4, vk::ImageLayout::eGeneral, vk::ImageType::e2D, test.layers > 1 ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, test.layers, 0, 0, test.samples); Image resolved; if (test.samples != vk::SampleCountFlagBits::e1) { resolved = CreateImageMips( test.name, 1, 1, target.format, vk::ImageUsageFlagBits::eColorAttachment, {}, 4, vk::ImageLayout::eGeneral, vk::ImageType::e2D, test.layers > 1 ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, test.layers); } const std::vector default_vertices = { 0xbf800000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u, 0x40400000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u, 0xbf800000u, 0x40400000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u, }; const auto &vertices = test.vertices.empty() ? default_vertices : test.vertices; Require(test.name, "graphics", vertices.size() == 18u, "graphics vertex buffer must contain three pos2/color4 vertices"); auto vertex_buffer = CreateHostBuffer(test.name, vertices.size() * sizeof(u32), vk::BufferUsageFlagBits::eVertexBuffer, vertices); vk::ShaderModule vertex_module = CreateShaderModule(test.name, vertex_spirv); vk::ShaderModule fragment_module = CreateShaderModule(test.name, fragment.spirv); const auto &fragment_bind = fragment.program.bindings; vk::PushConstantRange push_constant_range{}; if (fragment_bind.UsesPushData()) { Require(test.name, "graphics", test.push_constants.size() * sizeof(u32) == fragment_bind.ShaderDataDwords() * sizeof(u32), "fragment push constant data size does not match reflection"); push_constant_range.stageFlags = vk::ShaderStageFlagBits::eFragment; push_constant_range.offset = 0; push_constant_range.size = ShaderRecompiler::IR::NativePushConstantSize; } vk::PipelineLayoutCreateInfo pipeline_layout_info{}; pipeline_layout_info.sType = vk::StructureType::ePipelineLayoutCreateInfo; pipeline_layout_info.pushConstantRangeCount = push_constant_range.size != 0 ? 1u : 0u; pipeline_layout_info.pPushConstantRanges = push_constant_range.size != 0 ? &push_constant_range : nullptr; vk::PipelineLayout pipeline_layout = nullptr; RequireVk(test.name, "graphics", m_device.createPipelineLayout(&pipeline_layout_info, nullptr, &pipeline_layout), "vkCreatePipelineLayout"); vk::PipelineShaderStageCreateInfo stages[2] = {}; stages[0].sType = vk::StructureType::ePipelineShaderStageCreateInfo; stages[0].stage = vk::ShaderStageFlagBits::eVertex; stages[0].module = vertex_module; stages[0].pName = "main"; stages[1].sType = vk::StructureType::ePipelineShaderStageCreateInfo; stages[1].stage = vk::ShaderStageFlagBits::eFragment; stages[1].module = fragment_module; stages[1].pName = "main"; vk::VertexInputBindingDescription vertex_binding{}; vertex_binding.binding = 0; vertex_binding.stride = 6u * sizeof(float); vertex_binding.inputRate = vk::VertexInputRate::eVertex; vk::VertexInputAttributeDescription attributes[2] = {}; attributes[0].location = 0; attributes[0].binding = 0; attributes[0].format = vk::Format::eR32G32Sfloat; attributes[0].offset = 0; attributes[1].location = 1; attributes[1].binding = 0; attributes[1].format = vk::Format::eR32G32B32A32Sfloat; attributes[1].offset = 2u * sizeof(float); vk::PipelineVertexInputStateCreateInfo vertex_input{}; vertex_input.sType = vk::StructureType::ePipelineVertexInputStateCreateInfo; vertex_input.vertexBindingDescriptionCount = 1; vertex_input.pVertexBindingDescriptions = &vertex_binding; vertex_input.vertexAttributeDescriptionCount = 2; vertex_input.pVertexAttributeDescriptions = attributes; vk::PipelineInputAssemblyStateCreateInfo input_assembly{}; input_assembly.sType = vk::StructureType::ePipelineInputAssemblyStateCreateInfo; input_assembly.topology = vk::PrimitiveTopology::eTriangleList; vk::Viewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = 1.0f; viewport.height = 1.0f; viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vk::Rect2D scissor{}; scissor.extent.width = 1; scissor.extent.height = 1; vk::PipelineViewportStateCreateInfo viewport_state{}; viewport_state.sType = vk::StructureType::ePipelineViewportStateCreateInfo; viewport_state.viewportCount = 1; viewport_state.pViewports = &viewport; viewport_state.scissorCount = 1; viewport_state.pScissors = &scissor; vk::PipelineRasterizationStateCreateInfo raster{}; raster.sType = vk::StructureType::ePipelineRasterizationStateCreateInfo; raster.polygonMode = vk::PolygonMode::eFill; raster.cullMode = vk::CullModeFlagBits::eNone; raster.frontFace = vk::FrontFace::eCounterClockwise; raster.lineWidth = 1.0f; vk::PipelineMultisampleStateCreateInfo multisample{}; multisample.sType = vk::StructureType::ePipelineMultisampleStateCreateInfo; multisample.rasterizationSamples = test.samples; vk::PipelineColorBlendAttachmentState color_attachment{}; color_attachment.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; vk::PipelineColorBlendStateCreateInfo color_blend{}; color_blend.sType = vk::StructureType::ePipelineColorBlendStateCreateInfo; color_blend.attachmentCount = 1; color_blend.pAttachments = &color_attachment; vk::GraphicsPipelineCreateInfo pipeline_info{}; const vk::Format color_format = vk::Format::eR32G32B32A32Sfloat; vk::PipelineRenderingCreateInfo rendering_pipeline{}; rendering_pipeline.sType = vk::StructureType::ePipelineRenderingCreateInfo; rendering_pipeline.colorAttachmentCount = 1; rendering_pipeline.pColorAttachmentFormats = &color_format; pipeline_info.sType = vk::StructureType::eGraphicsPipelineCreateInfo; pipeline_info.pNext = &rendering_pipeline; pipeline_info.stageCount = 2; pipeline_info.pStages = stages; pipeline_info.pVertexInputState = &vertex_input; pipeline_info.pInputAssemblyState = &input_assembly; pipeline_info.pViewportState = &viewport_state; pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample; pipeline_info.pColorBlendState = &color_blend; pipeline_info.layout = pipeline_layout; vk::Pipeline pipeline = nullptr; RequireVk(test.name, "graphics", m_device.createGraphicsPipelines(nullptr, 1, &pipeline_info, nullptr, &pipeline), "vkCreateGraphicsPipelines"); vk::CommandBuffer cmd = BeginCommands(test.name, "graphics"); vk::RenderingAttachmentInfo color{}; color.sType = vk::StructureType::eRenderingAttachmentInfo; color.imageView = target.view; color.imageLayout = vk::ImageLayout::eGeneral; color.loadOp = vk::AttachmentLoadOp::eClear; color.storeOp = vk::AttachmentStoreOp::eStore; if (resolved.image != nullptr) { color.resolveMode = vk::ResolveModeFlagBits::eAverage; color.resolveImageView = resolved.view; color.resolveImageLayout = vk::ImageLayout::eGeneral; } vk::RenderingInfo rendering{}; rendering.sType = vk::StructureType::eRenderingInfo; rendering.renderArea.extent = {1, 1}; rendering.layerCount = test.layers; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &color; cmd.beginRendering(rendering); cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); if (push_constant_range.size != 0) { ShaderRecompiler::IR::PushData push_data; std::copy(test.push_constants.begin(), test.push_constants.end(), push_data.dwords.begin() + fragment_bind.push_data_start_dword); cmd.pushConstants(pipeline_layout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(push_data), push_data.dwords.data()); } vk::DeviceSize offset = 0; cmd.bindVertexBuffers(0, 1, &vertex_buffer.buffer, &offset); cmd.draw(3, test.layers, 0, 0); cmd.endRendering(); EndSubmitAndFree(test.name, "graphics", cmd); target.layout = vk::ImageLayout::eGeneral; auto pixel = ReadImage(test.name, resolved.image != nullptr ? &resolved : &target); pixel.resize(4 * test.layers); m_device.destroyPipeline(pipeline, nullptr); m_device.destroyPipelineLayout(pipeline_layout, nullptr); m_device.destroyShaderModule(fragment_module, nullptr); m_device.destroyShaderModule(vertex_module, nullptr); DestroyBuffer(&vertex_buffer); DestroyImage(&resolved); DestroyImage(&target); return pixel; } void CheckGpuTilerCpuParity() { constexpr const char *name = "GpuTilerCpuParity"; EnsureRuntimeContext(); CommandScheduler scheduler(Renderer(), m_runtime_context); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); StreamBuffer parameters(m_runtime_context, scheduler, MemoryUsage::Stream, 1u << 20); TileManager tile_manager(m_runtime_context, scheduler, parameters); constexpr uint64_t conversion_limit = 128ull << 20; Require(name, "D16 conversion chunk boundaries", TileManagerTestAccess::ConversionRows(0, 32ull << 10, 32ull << 10, 4096, 256, conversion_limit, 65535) == 4096 && TileManagerTestAccess::ConversionRows( 0, 32ull << 10, 32ull << 10, 4097, 256, conversion_limit, 65535) == 4096 && TileManagerTestAccess::ConversionRows( 257, 32ull << 10, 32ull << 10, 4097, 256, conversion_limit, 65535) == 4095, "D16 conversion planner exceeds descriptor or dispatch limits"); const auto align_dword = [](uint64_t size) { return (size + sizeof(u32) - 1u) & ~(uint64_t{sizeof(u32) - 1u}); }; const auto to_dwords = [&](const std::vector &bytes, uint64_t padded_size) { std::vector words(static_cast(padded_size / sizeof(u32)), 0); std::memcpy(words.data(), bytes.data(), bytes.size()); return words; }; const auto read_bytes = [&](const Buffer &buffer, uint64_t size) { const auto words = ReadBuffer( name, buffer, static_cast(align_dword(size) / sizeof(u32))); std::vector bytes(static_cast(size)); std::memcpy(bytes.data(), words.data(), bytes.size()); return bytes; }; const auto host_barrier = [&](vk::Buffer buffer, uint64_t size, vk::PipelineStageFlags source_stage, vk::AccessFlags source_access) { vk::BufferMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eBufferMemoryBarrier; barrier.srcAccessMask = source_access; barrier.dstAccessMask = vk::AccessFlagBits::eHostRead; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.buffer = buffer; barrier.offset = 0; barrier.size = size; scheduler.Current().Handle().pipelineBarrier( source_stage, vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr); }; const auto gpu_detile = [&](const std::vector &tiled, std::vector *linear, uint64_t tiled_capacity, uint64_t linear_capacity, std::span infos) { const uint64_t padded_tiled = align_dword(tiled_capacity); const uint64_t padded_linear = align_dword(linear_capacity); auto input = CreateHostBuffer(name, padded_tiled, AllFlags, to_dwords(tiled, padded_tiled)); auto output = CreateHostBuffer( name, padded_linear, AllFlags, std::vector(static_cast(padded_linear / sizeof(u32)), 0xabababab)); const auto result = tile_manager.Detile(input.buffer, 0, padded_tiled, padded_linear, infos); const vk::BufferCopy copy{result.offset, 0, padded_linear}; scheduler.Current().Handle().copyBuffer(result.buffer, output.buffer, 1, ©); host_barrier(output.buffer, padded_linear, vk::PipelineStageFlagBits::eTransfer, vk::AccessFlagBits::eTransferWrite); scheduler.Finish(); *linear = read_bytes(output, linear_capacity); DestroyBuffer(&output); DestroyBuffer(&input); }; const auto gpu_tile = [&](const std::vector &linear, std::vector *tiled, uint64_t tiled_capacity, uint64_t linear_capacity, std::span infos) { const uint64_t padded_tiled = align_dword(tiled_capacity); const uint64_t padded_linear = align_dword(linear_capacity); auto input = CreateHostBuffer(name, padded_linear, AllFlags, to_dwords(linear, padded_linear)); auto output = CreateHostBuffer( name, padded_tiled, AllFlags, std::vector(static_cast(padded_tiled / sizeof(u32)), 0xabababab)); tile_manager.Tile(input.buffer, 0, padded_linear, output.buffer, 0, padded_tiled, infos); host_barrier(output.buffer, padded_tiled, vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eShaderWrite); scheduler.Finish(); *tiled = read_bytes(output, tiled_capacity); DestroyBuffer(&output); DestroyBuffer(&input); }; for (const bool d32 : std::array{true, false}) { constexpr uint32_t width = 5; constexpr uint32_t height = 3; constexpr uint32_t pitch = 8; constexpr uint32_t layers = 2; constexpr uint64_t d16_row = pitch * sizeof(uint16_t); constexpr uint64_t d32_row = pitch * sizeof(uint32_t); constexpr uint64_t d16_slice = d16_row * height; constexpr uint64_t d32_slice = d32_row * height; constexpr uint64_t d16_size = d16_slice * layers; constexpr uint64_t d32_size = d32_slice * layers; StreamBuffer conversion_upload(m_runtime_context, scheduler, MemoryUsage::Upload, 1u << 20); auto [prefix, prefix_offset] = conversion_upload.Map(4096, 256); Require(name, "D16 conversion prefix", prefix != nullptr && prefix_offset == 0, "failed to advance the D16 conversion upload ring"); std::memset(prefix, 0x7b, 4096); conversion_upload.Commit(); auto [source_bytes, source_offset] = conversion_upload.Map(d16_size, 4); Require(name, "D16 conversion source", source_bytes != nullptr && source_offset != 0, "failed to reserve a nonzero D16 conversion source"); std::memset(source_bytes, 0x3c, d16_size); for (uint32_t layer = 0; layer < layers; layer++) { for (uint32_t y = 0; y < height; y++) { auto *row = reinterpret_cast( source_bytes + layer * d16_slice + y * d16_row); for (uint32_t x = 0; x < width; x++) { row[x] = static_cast( 0x1111u * (1u + x + y * width + layer * width * height)); } } } conversion_upload.Commit(); auto promoted = CreateHostBuffer( name, d32_size, AllFlags, std::vector(static_cast(d32_size / sizeof(u32)), 0)); auto round_trip = CreateHostBuffer( name, d16_size, AllFlags, std::vector(static_cast(d16_size / sizeof(u32)), 0xa5a5a5a5u)); const TileManager::D16Layout promote_layout{ .width = width, .height = height, .layers = layers, .source_row_stride = d16_row, .target_row_stride = d32_row, .source_slice_stride = d16_slice, .target_slice_stride = d32_slice, }; tile_manager.ConvertD16({conversion_upload.Handle(), source_offset, conversion_upload.Size() - source_offset}, {promoted.buffer, 0, d32_size}, TileManager::D16Direction::Promote, d32, promote_layout); tile_manager.ConvertD16({promoted.buffer, 0, d32_size}, {round_trip.buffer, 0, d16_size}, TileManager::D16Direction::Demote, d32, {.width = width, .height = height, .layers = layers, .source_row_stride = d32_row, .target_row_stride = d16_row, .source_slice_stride = d32_slice, .target_slice_stride = d16_slice}); host_barrier(round_trip.buffer, d16_size, vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eShaderWrite); scheduler.Finish(); const auto result = read_bytes(round_trip, d16_size); bool conversion_matches = true; for (uint32_t layer = 0; layer < layers; layer++) { for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < pitch; x++) { const auto offset = layer * d16_slice + y * d16_row + x * sizeof(uint16_t); conversion_matches &= x < width ? std::memcmp(result.data() + offset, source_bytes + offset, sizeof(uint16_t)) == 0 : result[offset] == 0xa5 && result[offset + 1] == 0xa5; } } } Require(name, d32 ? "D32 conversion round trip" : "D24 conversion round trip", conversion_matches, "D16 conversion lost rows, layers, active values, or padding"); DestroyBuffer(&round_trip); DestroyBuffer(&promoted); } auto fill = [](std::vector *bytes, u32 salt) { for (size_t i = 0; i < bytes->size(); i++) { uint64_t value = i + static_cast(salt) * 0x9e3779b97f4a7c15ull; value = (value ^ (value >> 30u)) * 0xbf58476d1ce4e5b9ull; value = (value ^ (value >> 27u)) * 0x94d049bb133111ebull; (*bytes)[i] = static_cast((value ^ (value >> 31u)) >> 56u); } }; auto compare = [&](const char *stage, const std::vector &expected, const std::vector &actual) { if (expected != actual) { const auto mismatch = static_cast( std::mismatch(expected.begin(), expected.end(), actual.begin()) .first - expected.begin()); std::ostringstream out; out << "first mismatch at " << mismatch << " of " << expected.size(); Fail(name, stage, out.str()); } }; auto convert_reference = [&](bool to_tiled, std::vector *dst, const std::vector &src, const GpuTileInfo &info) { TileBlockLayout block{}; Require(name, "reference layout", TileGetBlockLayout(info.family, info.bytes_per_element, block), "CPU reference rejected GPU tile info"); const u32 tiled_width = info.tiled_width != 0 ? info.tiled_width : info.pitch; const u32 tiled_height = info.tiled_height != 0 ? info.tiled_height : info.height; const uint64_t columns = (tiled_width + block.block_width - 1u) / block.block_width; const uint64_t rows = (tiled_height + block.block_height - 1u) / block.block_height; const uint64_t slice = info.linear_slice_stride != 0 ? info.linear_slice_stride : static_cast(info.pitch) * info.height * info.bytes_per_element; for (u32 z = 0; z < info.depth; ++z) { for (u32 y = 0; y < info.height; ++y) { for (u32 x = 0; x < info.width; ++x) { const u32 bx = info.tail ? 0 : x / block.block_width; const u32 by = info.tail ? 0 : y / block.block_height; const u32 bz = info.tail ? 0 : z / block.block_depth; const u32 lx = info.tail ? x + info.tail_x : x % block.block_width; const u32 ly = info.tail ? y + info.tail_y : y % block.block_height; const u32 lz = z % block.block_depth; u32 local = 0, block_xor = 0; Require(name, "reference offset", TileGetBlockOffset(block, lx, ly, lz, local) && TileGetBlockXor(block, bx, by, info.surface_z + bz, block_xor), "CPU reference address lookup failed"); const uint64_t block_index = static_cast(bz) * columns * rows + by * columns + bx; const uint64_t tiled = info.tiled_offset + block_index * block.block_size + (local ^ block_xor); const uint64_t linear = info.linear_offset + static_cast(z) * slice + static_cast(y) * info.pitch * info.bytes_per_element + static_cast(x) * info.bytes_per_element; const uint64_t dst_offset = to_tiled ? tiled : linear; const uint64_t src_offset = to_tiled ? linear : tiled; Require(name, "reference range", dst_offset + info.bytes_per_element <= dst->size() && src_offset + info.bytes_per_element <= src.size(), "CPU reference address escaped storage"); std::memcpy(dst->data() + dst_offset, src.data() + src_offset, info.bytes_per_element); } } } }; struct FamilyCase { TileBlockFamily family; u32 max_bpe; }; constexpr FamilyCase families[] = { {TileBlockFamily::Standard256B, 16}, {TileBlockFamily::Standard4KB, 16}, {TileBlockFamily::Standard4KB3D, 16}, {TileBlockFamily::Standard64KB, 16}, {TileBlockFamily::Standard64KB3D, 16}, {TileBlockFamily::Prt64KB, 16}, {TileBlockFamily::Prt64KB3D, 16}, {TileBlockFamily::RenderTarget64KB, 16}, {TileBlockFamily::Depth64KB, 8}, }; { TileBlockLayout standard{}, prt{}, standard_3d{}, prt_3d{}, color{}, depth{}; u32 standard_offset = 0, prt_offset = 0, standard_3d_offset = 0, prt_3d_offset = 0, color_z = 0, depth_z = 0; bool fixed_addresses = TileGetBlockLayout(TileBlockFamily::Standard64KB, 4, standard) && TileGetBlockLayout(TileBlockFamily::Prt64KB, 4, prt) && TileGetBlockLayout(TileBlockFamily::Standard64KB3D, 4, standard_3d) && TileGetBlockLayout(TileBlockFamily::Prt64KB3D, 4, prt_3d) && TileGetBlockLayout(TileBlockFamily::RenderTarget64KB, 4, color) && TileGetBlockLayout(TileBlockFamily::Depth64KB, 4, depth) && TileGetBlockOffset(standard, 64, 0, 0, standard_offset) && TileGetBlockOffset(prt, 64, 0, 0, prt_offset) && TileGetBlockOffset(standard_3d, 16, 0, 0, standard_3d_offset) && TileGetBlockOffset(prt_3d, 16, 0, 0, prt_3d_offset) && TileGetBlockXor(color, 0, 0, 1, color_z) && TileGetBlockXor(depth, 0, 0, 15, depth_z); Require(name, "fixed address vectors", fixed_addresses && standard_offset == 0x8000 && prt_offset == 0x8100 && standard_3d_offset == 0x8000 && prt_3d_offset == 0x8400 && standard_3d.block_width == 32 && standard_3d.block_height == 32 && standard_3d.block_depth == 16 && color_z == 0x800 && depth_z == 0xf00, "a fixed block address changed"); constexpr auto format = Prospero::BufferFormat::k32Float; constexpr u32 levels = 7; TileSizeAlign total{}; TileSizeOffset mip[levels]{}; TilePaddedSize padded[levels]{}; TileGetTextureSize(format, 65, 33, levels, Prospero::TileMode::kStandard256B, &total, mip, padded); constexpr u32 offsets[levels] = {0x1a00, 0xb00, 0x500, 0x300, 0x200, 0x100, 0}; constexpr u32 sizes[levels] = {0x2d00, 0xf00, 0x600, 0x200, 0x100, 0x100, 0x100}; bool layout_matches = total.size == 0x4700 && total.align == 0x100 && padded[0].width == 72 && padded[0].height == 40; for (u32 level = 0; level < levels; ++level) { layout_matches &= mip[level].offset == offsets[level] && mip[level].size == sizes[level]; } Require(name, "fixed mip vector", layout_matches, "the reverse-packed small-block mip layout changed"); } u32 case_index = 0; auto check_round_trip = [&](const char *stage, uint64_t tiled_size, std::span infos) { uint64_t linear_size = 0; for (const auto &info : infos) { linear_size = std::max(linear_size, info.linear_offset + info.linear_size); } std::vector tiled(tiled_size); std::vector cpu(linear_size, 0); std::vector gpu(linear_size, 0xab); fill(&tiled, ++case_index); for (const auto &info : infos) { convert_reference(false, &cpu, tiled, info); } gpu_detile(tiled, &gpu, tiled_size, linear_size, infos); compare((std::string(stage) + " detile bytes").c_str(), cpu, gpu); std::vector linear(linear_size); std::vector cpu_tiled(tiled_size, 0xab); std::vector gpu_tiled(tiled_size, 0xab); fill(&linear, 0x280u + case_index); for (const auto &info : infos) { convert_reference(true, &cpu_tiled, linear, info); } gpu_tile(linear, &gpu_tiled, tiled_size, linear_size, infos); compare((std::string(stage) + " tile bytes").c_str(), cpu_tiled, gpu_tiled); }; for (const auto family : families) { for (u32 bpe = 1; bpe <= family.max_bpe; bpe <<= 1u) { TileBlockLayout block{}; Require(name, "block layout", TileGetBlockLayout(family.family, bpe, block), "admitted family/BPE has no block layout"); const bool volume = block.block_depth > 1; const u32 width = block.block_width * 3u + std::min(block.block_width, 3u); const u32 height = block.block_height * 3u + std::min(block.block_height, 3u); const u32 depth = volume ? block.block_depth + 1u : 1u; const u32 pitch = block.block_width * 4u; const uint64_t block_columns = (pitch + block.block_width - 1u) / block.block_width; const uint64_t block_rows = (height + block.block_height - 1u) / block.block_height; const uint64_t block_slices = (depth + block.block_depth - 1u) / block.block_depth; const uint64_t storage_size = block_columns * block_rows * block_slices * block.block_size; const uint64_t slice_stride = static_cast(pitch) * height * bpe; std::vector tiled(storage_size); std::vector cpu(storage_size, 0); std::vector gpu(storage_size, 0xab); fill(&tiled, ++case_index); GpuTileInfo info{}; info.family = block.family; info.bytes_per_element = block.bytes_per_element; info.linear_size = storage_size; info.tiled_size = storage_size; info.linear_slice_stride = volume ? slice_stride : 0; info.width = width; info.height = height; info.depth = depth; info.pitch = pitch; info.surface_z = family.family == TileBlockFamily::RenderTarget64KB || family.family == TileBlockFamily::Depth64KB ? 3 : 0; convert_reference(false, &cpu, tiled, info); gpu_detile(tiled, &gpu, storage_size, storage_size, std::span(&info, 1)); const auto family_label = [&](const char *operation) { std::ostringstream out; out << operation << " family=" << static_cast(family.family) << " bpe=" << bpe; return out.str(); }; compare(family_label("detile bytes").c_str(), cpu, gpu); { std::vector linear(storage_size); std::vector cpu_tiled(storage_size, 0xab); std::vector gpu_tiled(storage_size, 0xab); fill(&linear, 0x80u + case_index); convert_reference(true, &cpu_tiled, linear, info); gpu_tile(linear, &gpu_tiled, storage_size, storage_size, std::span(&info, 1)); compare(family_label("tile bytes").c_str(), cpu_tiled, gpu_tiled); } } } // Exercise the real texture-layout/info-building seam for every format // admitted by each standard tile mode. Formats sharing a byte/block width // intentionally share a shader, but this loop still validates their // texel-to-element conversion (notably every BCn format). struct StandardMode { Prospero::TileMode tile; TileBlockFamily family; }; constexpr StandardMode standard_modes[] = { {Prospero::TileMode::kStandard256B, TileBlockFamily::Standard256B}, {Prospero::TileMode::kStandard4KB, TileBlockFamily::Standard4KB}, {Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB}, {Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB}, }; for (const auto format : {Prospero::BufferFormat::k8Srgb, Prospero::BufferFormat::k8_8Srgb, Prospero::BufferFormat::k9_9_9_5Float}) { for (const auto tile : {Prospero::TileMode::kDepth, Prospero::TileMode::kRenderTarget}) { TileTextureBlockLayout texture{}; Require(name, "RT format policy", !TileGetTextureBlockLayout(format, tile, false, texture), "non-render-target format admitted by an RT/depth tile family"); } } { TileSurfaceLayout invalid{}; const TileSurfaceDescription description{ Prospero::BufferFormat::k32Float, Prospero::TileMode::kStandard256B, TileSurfaceDimension::Dim3D, 16, 16, 16, 1, 1, }; Require(name, "3D tile policy", !TileGetTiledTextureLayout(description, invalid), "Standard256B volume silently fell back to a 2D array layout"); } { constexpr auto format = Prospero::BufferFormat::kFmask8_S4_F4; constexpr auto tile = Prospero::TileMode::kStandard64KB; TileSizeAlign total{}; TileGetTextureSize(format, 128, 128, 1, tile, &total, nullptr, nullptr); const auto layout = TextureCalcUploadLayout( format, 128, 128, 1, 1, tile, total.size, false, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; Require(name, "FMASK policy", !TextureBuildGpuTileInfos(total.size, regions, layout, 1, infos), "FMASK entered the texel tiler through a non-depth family"); } u32 format_cases = 0; for (u32 raw_format = 1; raw_format <= static_cast(Prospero::BufferFormat::kBc7Srgb); ++raw_format) { const auto format = static_cast(raw_format); // FMASK contains sample-to-fragment metadata, not texels for the GPU tiler. if (Prospero::IsFmaskTextureFormat(format)) { continue; } for (const auto &mode : standard_modes) { TileTextureBlockLayout texture{}; if (!TileGetTextureBlockLayout(format, mode.tile, false, texture)) { continue; } const u32 bpe = texture.block.bytes_per_element; Require(name, "format block", texture.block.family == mode.family, "CPU-supported format selected the wrong block family"); constexpr u32 width = 67; constexpr u32 height = 51; const u32 pitch = TileGetTexturePitch(format, width, mode.tile); TileSizeAlign total{}; TileGetTextureSize(format, width, height, 1, mode.tile, &total, nullptr, nullptr); Require(name, "format size", total.size != 0, "supported format has an empty layout"); const auto layout = TextureCalcUploadLayout(format, width, height, 1, 1, mode.tile, total.size, false, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; if (!TextureBuildGpuTileInfos(total.size, regions, layout, 1, infos)) { std::ostringstream out; out << "format=" << raw_format << " tile=" << static_cast(mode.tile) << " size=" << total.size << " pitch=" << pitch; Fail(name, "format infos", out.str()); } Require(name, "format family", infos.size() == 1 && infos[0].family == mode.family && infos[0].bytes_per_element == bpe, "texture info selected the wrong shader family"); std::vector tiled(total.size); std::vector cpu(total.size, 0); std::vector gpu(total.size, 0xab); fill(&tiled, ++case_index); convert_reference(false, &cpu, tiled, infos[0]); gpu_detile(tiled, &gpu, total.size, total.size, infos); compare("format bytes", cpu, gpu); ++format_cases; } } Require(name, "format coverage", format_cases != 0, "no CPU-supported standard formats were tested"); { constexpr auto format = Prospero::BufferFormat::k11_11_10UInt; constexpr auto tile = Prospero::TileMode::kStandard64KB; constexpr u32 width = 128, height = 128, levels = 8; TileSurfaceLayout surface{}; const TileSurfaceDescription description{ format, tile, TileSurfaceDimension::Dim2D, width, height, 1, levels, 1}; TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, 1, levels, tile, false, total); const bool built = TileGetTiledTextureLayout(description, surface); const auto surface_format = TextureGetSurfaceFormatInfo(format); constexpr u32 tail_xy[7][2] = {{64, 0}, {0, 64}, {32, 0}, {0, 32}, {16, 0}, {8, 16}, {0, 24}}; bool valid = built && Prospero::NumBytesPerElement(format) == 4 && Prospero::BlockCompressedBytesPerBlock(format) == 0 && Prospero::SampledTextureNumericClass(format) == Prospero::TextureNumericClass::Uint && Prospero::RemapTextureFormat(format) == Prospero::BufferFormat::k32UInt && surface_format.vk_format == vk::Format::eR32Uint && surface_format.conversion_format == format && VulkanFormat(format) == vk::Format::eUndefined && surface.texture.block.block_width == 128 && surface.texture.block.block_height == 128 && surface.texture.block.block_depth == 1 && surface.first_tail_level == 1 && surface.mips[0].offset == 0x10000 && total.size == 0x20000 && total.align == 0x10000; for (u32 level = 1; level < levels && valid; level++) { valid &= surface.mips[level].offset == 0 && surface.mips[level].tail_x == tail_xy[level - 1][0] && surface.mips[level].tail_y == tail_xy[level - 1][1]; } Require(name, "11_11_10 uint layout", valid, "PS5 packed integer texture metadata or Standard64KB mip tail " "changed"); const auto signed_format = Prospero::BufferFormat::k32SInt; const auto signed_surface = TextureGetSurfaceFormatInfo(signed_format); Require(name, "signed sampled surface mapping", Prospero::SampledTextureNumericClass(signed_format) == Prospero::TextureNumericClass::Sint && Prospero::RemapTextureFormat(signed_format) == signed_format && signed_surface.vk_format == vk::Format::eR32Sint && signed_surface.conversion_format == Prospero::BufferFormat::kInvalid, "signed sampled texture class or native backing changed"); } { constexpr auto format = Prospero::BufferFormat::k32_32_32Float; TileTextureElementLayout element{}; Require(name, "RGB32 texture policy", !TileGetTextureElementLayout(format, element), "12-byte texel layouts must be rejected"); } { constexpr auto format = Prospero::BufferFormat::kBc1UNorm; constexpr auto tile = Prospero::TileMode::kStandard64KB; constexpr u32 width = 256, height = 256, levels = 9; TileSizeAlign total{}; TileGetTextureSize(format, width, height, levels, tile, &total, nullptr, nullptr); const auto layout = TextureCalcUploadLayout(format, width, height, levels, 1, tile, total.size, false, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; const bool built = TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos); uint64_t linear_size = 0; for (const auto &info : infos) { linear_size = std::max(linear_size, info.linear_offset + info.linear_size); } Require(name, "BC1 mip-tail capacities", built && total.size == 0x10000 && layout.surface.first_tail_level == 0 && linear_size == 0x15560 && linear_size > total.size, "BC1 mip tail conflated tiled and linear capacities"); check_round_trip("BC1 mip tail", total.size, infos); } { constexpr auto format = Prospero::BufferFormat::k32Float; constexpr auto tile = Prospero::TileMode::kRenderTarget; constexpr u32 width = 129, height = 65, layers = 3; TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, layers, 1, tile, false, total); const auto layout = TextureCalcUploadLayout(format, width, height, 1, layers, tile, total.size, true, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; const bool built = TextureBuildGpuTileInfos(total.size, regions, layout, 1, infos); Require(name, "array infos", built && infos.size() == layers && infos[0].surface_z == 0 && infos[1].surface_z == 1 && infos[2].surface_z == 2, "array slices lost their absolute surface Z"); check_round_trip("array", total.size, infos); } for (const auto &mode : standard_modes) { constexpr auto format = Prospero::BufferFormat::k32Float; constexpr u32 levels = 2; TileBlockLayout block{}; Require(name, "odd mip block", TileGetBlockLayout(mode.family, 4, block), "odd multi-mip format has no block layout"); const u32 width = block.block_width * 2u + 1u; const u32 height = block.block_height * 2u + 1u; TileSizeAlign total{}; TileGetTextureSize(format, width, height, levels, mode.tile, &total, nullptr, nullptr); const auto layout = TextureCalcUploadLayout(format, width, height, levels, 1, mode.tile, total.size, false, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; Require(name, "odd mip infos", TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos) && infos.size() == 2 && infos[1].tiled_width >= infos[1].pitch && infos[1].tiled_height >= infos[1].height && (mode.family == TileBlockFamily::Standard256B || infos[1].tiled_height > infos[1].height), "odd multi-mip physical stride collapsed to the active linear " "extent"); check_round_trip("odd multi-mip", total.size, infos); } struct TailMode { Prospero::TileMode tile; TileBlockFamily family; }; constexpr TailMode tail_modes[] = { {Prospero::TileMode::kStandard4KB, TileBlockFamily::Standard4KB}, {Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB}, {Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB}, {Prospero::TileMode::kRenderTarget, TileBlockFamily::RenderTarget64KB}, {Prospero::TileMode::kDepth, TileBlockFamily::Depth64KB}, }; for (const auto &mode : tail_modes) { constexpr auto format = Prospero::BufferFormat::k32Float; constexpr u32 levels = 7; TileBlockLayout block{}; Require(name, "2D tail block", TileGetBlockLayout(mode.family, 4, block), "2D tail mode has no block layout"); const u32 width = block.block_width * 2u; const u32 height = block.block_height * 2u; TileSizeAlign total{}; TileGetTextureSize(format, width, height, levels, mode.tile, &total, nullptr, nullptr); const auto layout = TextureCalcUploadLayout(format, width, height, levels, 1, mode.tile, total.size, true, false, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; Require(name, "2D mip tail seam", layout.surface.first_tail_level == 2 && TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos) && infos.size() == levels && !infos[0].tail && !infos[1].tail && std::all_of(infos.begin() + 2, infos.end(), [](const auto &info) { return info.tail; }), "2D mip chain lost its linear/tiled tail boundary"); check_round_trip("2D mip tail seam", total.size, infos); } { constexpr auto format = Prospero::BufferFormat::k32Float; constexpr auto tile = Prospero::TileMode::kStandard4KB; constexpr u32 width = 65; constexpr u32 height = 129; constexpr u32 depth = 17; constexpr u32 levels = 2; TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, depth, levels, tile, true, total); const auto layout = TextureCalcUploadLayout(format, width, height, levels, depth, tile, total.size, false, true, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; const bool built = TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos); Require(name, "3D mip infos", regions.size() == depth + (depth >> 1u) && built && infos.size() == 4 && infos[0].depth == 8 && infos[1].depth == 8 && infos[2].depth == 1 && infos[3].depth == 8 && infos[0].tiled_offset == 0x19000 && infos[1].tiled_offset == 0x83000 && infos[2].tiled_offset == 0xed000 && infos[3].tiled_offset == 0 && infos[3].pitch == 32 && infos[3].height == 64 && infos[3].tiled_width == 40 && infos[3].tiled_height == 80, "Standard4KB3D mip depth, packing, or physical stride changed"); check_round_trip("3D mip", total.size, infos); } { constexpr auto format = Prospero::BufferFormat::kBc1UNorm; constexpr auto tile = Prospero::TileMode::kStandard4KB; constexpr u32 width = 65; constexpr u32 height = 129; constexpr u32 depth = 17; constexpr u32 levels = 2; TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, depth, levels, tile, true, total); const auto layout = TextureCalcUploadLayout(format, width, height, levels, depth, tile, total.size, false, true, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; Require(name, "3D BC mip infos", TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos) && infos.size() == 4 && infos[3].pitch == 8 && infos[3].height == 16 && infos[3].tiled_width == 16 && infos[3].tiled_height == 24, "block-compressed 3D mip lost its physical row or slice stride"); check_round_trip("3D BC mip", total.size, infos); } { constexpr auto format = Prospero::BufferFormat::kBc3UNorm; constexpr auto tile = Prospero::TileMode::kLinear; constexpr u32 width = 8, height = 8, levels = 4; TileSizeAlign total{}; TileGetTextureSize(format, width, height, levels, tile, &total, nullptr, nullptr); const auto layout = TextureCalcUploadLayout(format, width, height, levels, 1, tile, total.size, false, false, name); const auto regions = TextureBuildImageCopies(layout); Require(name, "linear BC native regions", regions.size() == levels && std::all_of(regions.begin(), regions.end(), [](const auto ®ion) { return region.bufferRowLength >= region.imageExtent.width && region.bufferImageHeight >= region.imageExtent.height && region.bufferRowLength % 4 == 0 && region.bufferImageHeight % 4 == 0; }), "linear BC mip tails emitted invalid Vulkan copy strides"); } struct VolumeModeCase { Prospero::TileMode tile; TileBlockFamily family; Prospero::BufferFormat format; }; constexpr VolumeModeCase volume_modes[] = { {Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB3D, Prospero::BufferFormat::k32Float}, {Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB3D, Prospero::BufferFormat::k32Float}, {Prospero::TileMode::kRenderTarget, TileBlockFamily::RenderTarget64KB, Prospero::BufferFormat::k32Float}, {Prospero::TileMode::kDepth, TileBlockFamily::Depth64KB, Prospero::BufferFormat::k32Float}, {Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB3D, Prospero::BufferFormat::kBc1UNorm}, {Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB3D, Prospero::BufferFormat::kBc3UNorm}, }; for (const auto test : volume_modes) { constexpr u32 width = 65, height = 33, depth = 37, levels = 6; TileSizeAlign total{}; TileGetTextureTotalSize(test.format, width, height, depth, levels, test.tile, true, total); const auto layout = TextureCalcUploadLayout(test.format, width, height, levels, depth, test.tile, total.size, true, true, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; const bool built = TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos); const bool uses_z = test.family == TileBlockFamily::RenderTarget64KB || test.family == TileBlockFamily::Depth64KB; Require(name, "volume family infos", built && !infos.empty() && std::all_of(infos.begin(), infos.end(), [&](const auto &info) { return info.family == test.family; }) && (!uses_z || std::any_of(infos.begin(), infos.end(), [](const auto &info) { return info.surface_z != 0; })), "volume mode selected the wrong family or lost its surface Z"); check_round_trip("volume family", total.size, infos); } struct VolumeTailCase { Prospero::BufferFormat format; u32 bytes; }; constexpr VolumeTailCase volume_tails[] = { {Prospero::BufferFormat::k8UNorm, 1}, {Prospero::BufferFormat::k16UNorm, 2}, {Prospero::BufferFormat::k32Float, 4}, {Prospero::BufferFormat::k16_16_16_16Float, 8}, {Prospero::BufferFormat::k32_32_32_32Float, 16}, {Prospero::BufferFormat::kBc1UNorm, 8}, {Prospero::BufferFormat::kBc3UNorm, 16}, }; constexpr u32 volume_tail_xy[5][5][2] = { {{0, 8}, {8, 4}, {8, 0}, {0, 4}, {0, 0}}, {{0, 8}, {4, 4}, {4, 0}, {0, 4}, {0, 0}}, {{0, 8}, {4, 4}, {4, 0}, {0, 4}, {0, 0}}, {{0, 4}, {4, 2}, {4, 0}, {0, 2}, {0, 0}}, {{0, 4}, {2, 2}, {2, 0}, {0, 2}, {0, 0}}, }; for (const auto &tail : volume_tails) { constexpr auto tile = Prospero::TileMode::kStandard4KB; constexpr u32 levels = 5; TileBlockLayout block{}; Require( name, "3D tail block", TileGetBlockLayout(TileBlockFamily::Standard4KB3D, tail.bytes, block), "Standard4KB3D tail format has no block layout"); const bool compressed = Prospero::BlockCompressedBytesPerBlock(tail.format) != 0; const u32 width = block.block_width * (compressed ? 4u : 1u); const u32 height = block.block_height / 2u * (compressed ? 4u : 1u); const u32 depth = block.block_depth + 1u; TileSizeAlign total{}; TileGetTextureTotalSize(tail.format, width, height, depth, levels, tile, true, total); const auto layout = TextureCalcUploadLayout(tail.format, width, height, levels, depth, tile, total.size, false, true, name); const auto regions = TextureBuildImageCopies(layout); std::vector infos; bool valid = total.size == 8192 && regions.size() == depth + std::max(depth >> 1u, 1u) + std::max(depth >> 2u, 1u) + std::max(depth >> 3u, 1u) + std::max(depth >> 4u, 1u) && TextureBuildGpuTileInfos(total.size, regions, layout, levels, infos) && infos.size() == 6; const u32 table = std::countr_zero(tail.bytes); for (u32 level = 0; level < levels && valid; level++) { const auto &info = infos[level == 0 ? 0 : level + 1]; valid &= info.tail && info.tail_x == volume_tail_xy[table][level][0] && info.tail_y == volume_tail_xy[table][level][1] && info.tiled_offset == 0; } if (valid) { valid = infos[1].tail && infos[1].tail_x == volume_tail_xy[table][0][0] && infos[1].tail_y == volume_tail_xy[table][0][1] && infos[1].tiled_offset == 4096; } Require( name, "3D mip tail infos", valid, "Standard4KB3D mip tail coordinates or block-slice packing changed"); check_round_trip("3D mip tail", total.size, infos); } for (const auto family : {TileBlockFamily::Standard4KB, TileBlockFamily::Standard64KB, TileBlockFamily::Prt64KB, TileBlockFamily::RenderTarget64KB, TileBlockFamily::Depth64KB}) { for (u32 bpe = 1; bpe <= 16; bpe <<= 1u) { if (family == TileBlockFamily::Depth64KB && bpe == 16) continue; TileBlockLayout block{}; Require(name, "tail layout", TileGetBlockLayout(family, bpe, block), "tail family/BPE has no block layout"); const u32 width = std::max(block.block_width / 4u, 1u); const u32 height = std::max(block.block_height / 4u, 1u); const u32 x = block.block_width / 2u; const u32 y = block.block_height / 2u; const u32 pitch = width; const uint64_t linear_size = static_cast(pitch) * height * bpe; std::vector tiled(block.block_size); std::vector cpu(linear_size, 0xcd); std::vector gpu(linear_size, 0xab); fill(&tiled, ++case_index); GpuTileInfo info{}; info.family = block.family; info.bytes_per_element = block.bytes_per_element; info.linear_size = linear_size; info.tiled_size = block.block_size; info.width = width; info.height = height; info.pitch = pitch; info.tail = true; info.tail_x = x; info.tail_y = y; info.surface_z = family == TileBlockFamily::RenderTarget64KB || family == TileBlockFamily::Depth64KB ? 2 : 0; convert_reference(false, &cpu, tiled, info); gpu_detile(tiled, &gpu, block.block_size, linear_size, std::span(&info, 1)); compare("tail bytes", cpu, gpu); std::vector linear(linear_size); std::vector cpu_tiled(block.block_size, 0xab); std::vector gpu_tiled(block.block_size, 0xab); fill(&linear, 0x400u + case_index); convert_reference(true, &cpu_tiled, linear, info); gpu_tile(linear, &gpu_tiled, block.block_size, linear_size, std::span(&info, 1)); compare("tail tile bytes", cpu_tiled, gpu_tiled); } } TileBlockLayout small_block{}; Require(name, "small layout", TileGetBlockLayout(TileBlockFamily::Standard256B, 4, small_block), "small fixture layout is unavailable"); std::vector small_input(small_block.block_size); std::vector small_expected(small_block.block_size, 0); std::vector small_output(small_block.block_size, 0xab); fill(&small_input, 0xee); GpuTileInfo small_info{}; small_info.family = small_block.family; small_info.bytes_per_element = small_block.bytes_per_element; small_info.linear_size = small_output.size(); small_info.tiled_size = small_input.size(); small_info.width = 1; small_info.height = 1; small_info.pitch = small_block.block_width; convert_reference(false, &small_expected, small_input, small_info); gpu_detile(small_input, &small_output, small_input.size(), small_output.size(), std::span(&small_info, 1)); compare("small detile", small_expected, small_output); std::fill(small_output.begin(), small_output.end(), 0xab); gpu_detile(small_input, &small_output, small_input.size(), small_output.size(), std::span(&small_info, 1)); compare("scheduler-owned reuse", small_expected, small_output); constexpr auto volume_format = Prospero::BufferFormat::k32UInt; constexpr auto volume_tile = Prospero::TileMode::kLinear; constexpr u32 volume_width = 8, volume_height = 4, volume_depth = 5; constexpr u32 volume_levels = 3; const u32 volume_pitch = TileGetTexturePitch(volume_format, volume_width, volume_tile); TileSizeAlign volume_size{}; TileGetTextureTotalSize(volume_format, volume_width, volume_height, volume_depth, volume_levels, volume_tile, true, volume_size); const auto volume_layout = TextureCalcUploadLayout( volume_format, volume_width, volume_height, volume_levels, volume_depth, volume_tile, volume_size.size, true, true, name); const auto volume_copies = TextureBuildImageCopies(volume_layout); std::vector volume_source(volume_size.size / sizeof(u32), 0); std::vector> volume_probes; for (const auto © : volume_copies) { for (u32 y = 0; y < copy.imageExtent.height; y++) { for (u32 x = 0; x < copy.imageExtent.width; x++) { const auto index = (copy.bufferOffset + (static_cast(y) * copy.bufferRowLength + x) * sizeof(u32)) / sizeof(u32); const u32 value = 0xa0000000u | (copy.imageSubresource.mipLevel << 24u) | (static_cast(copy.imageOffset.z) << 16u) | (y << 8u) | x; Require(name, "linear volume native bounds", index < volume_source.size(), "linear volume copy escaped its padded guest layout"); volume_source[index] = value; volume_probes.emplace_back(static_cast(index), value); } } } auto volume_upload = CreateHostBuffer(name, volume_size.size, vk::BufferUsageFlagBits::eTransferSrc, volume_source); auto volume_download = CreateHostBuffer( name, volume_size.size, vk::BufferUsageFlagBits::eTransferDst, std::vector(volume_source.size(), 0)); ImageInfo volume_info{}; volume_info.data = {0x10000, volume_size.size}; volume_info.pixel_format = vk::Format::eR32Uint; volume_info.guest_format = volume_format; volume_info.type = Prospero::ImageType::kColor3D; volume_info.extent = {volume_width, volume_height, volume_depth}; volume_info.resources = {volume_levels, 1}; volume_info.pitch = volume_pitch; volume_info.bytes_per_block = sizeof(u32); volume_info.tile_mode = volume_tile; for (u32 level = 0; level < volume_levels; level++) { volume_info.mip_layout[level] = {volume_layout.mips[level].offset, volume_layout.mips[level].size, volume_layout.mips[level].row_length, volume_layout.mips[level].image_height}; } Libs::Graphics::Image volume_image(m_runtime_context, scheduler, volume_info); volume_image.Upload(volume_copies, volume_upload.buffer, 0, volume_size.size); volume_image.Download(volume_copies, volume_download.buffer, 0, volume_size.size); scheduler.Finish(); const auto volume_observed = ReadBuffer(name, volume_download, volume_source.size()); Require(name, "linear volume native content", std::ranges::all_of(volume_probes, [&](const auto &probe) { return volume_observed[probe.first] == probe.second; }), "native upload/download did not preserve padded 3D mip rows"); DestroyBuffer(&volume_download); DestroyBuffer(&volume_upload); std::printf("[gpu] %-32s ok (%u cases, %u format/mode pairs)\n", name, case_index, format_cases); } private: RenderContext &Renderer() { EXIT_IF(m_renderer == nullptr); return *m_renderer; } void EnsureRuntimeContext() { if (m_runtime_context.allocator != nullptr) { return; } namespace Exception = Common::HostException; Require("VulkanHarness", "guest fault handler", Exception::InstallHandler([](const Exception::ExceptionInfo &info) { if (info.type != Exception::ExceptionType::AccessViolation || (info.access_violation_type != Exception::AccessViolationType::Read && info.access_violation_type != Exception::AccessViolationType::Write)) { return false; } const auto access = info.access_violation_type == Exception::AccessViolationType::Write ? PageFaultAccess::Write : PageFaultAccess::Read; return LibKernel::Memory::HandleGpuFault(access, info.access_violation_vaddr); }), "failed to install the production guest-memory fault route"); m_runtime_context.instance = m_instance; m_runtime_context.physical_device = m_physical_device; m_runtime_context.device = m_device; m_physical_device.getProperties( &m_runtime_context.physical_device_properties); m_runtime_context.physical_device_memory_properties = m_memory_properties; m_runtime_context.queue_family = m_queue_family; m_runtime_context.queue = m_queue; m_runtime_context.attachment_feedback_loop_enabled = true; m_runtime_context.provoking_vertex_last_enabled = true; const vk::PhysicalDeviceImageFormatInfo2 block_texel_view_info{ .format = vk::Format::eBc1RgbaUnormBlock, .type = vk::ImageType::e2D, .tiling = vk::ImageTiling::eOptimal, .usage = vk::ImageUsageFlagBits::eSampled, .flags = vk::ImageCreateFlagBits::eBlockTexelViewCompatible, }; const auto block_texel_view_props = m_physical_device.getImageFormatProperties2(block_texel_view_info); m_runtime_context.supports_block_texel_view = block_texel_view_props.result == vk::Result::eSuccess; VmaVulkanFunctions functions{}; functions.vkGetInstanceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr; functions.vkGetDeviceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr; VmaAllocatorCreateInfo allocator_info{}; allocator_info.instance = m_instance; allocator_info.physicalDevice = m_physical_device; allocator_info.device = m_device; allocator_info.pVulkanFunctions = &functions; allocator_info.vulkanApiVersion = VK_API_VERSION_1_3; allocator_info.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; RequireVk("VulkanHarness", "runtime context", static_cast(vmaCreateAllocator( &allocator_info, &m_runtime_context.allocator)), "vmaCreateAllocator"); m_renderer = std::make_unique(m_runtime_context); } void Init() { static vk::detail::DynamicLoader loader; const auto get_instance_proc_addr = loader.getProcAddress( "vkGetInstanceProcAddr"); Require("VulkanHarness", "dispatch", get_instance_proc_addr != nullptr, "could not load the Vulkan loader"); VULKAN_HPP_DEFAULT_DISPATCHER.init(get_instance_proc_addr); vk::ApplicationInfo app{}; app.sType = vk::StructureType::eApplicationInfo; app.pApplicationName = "ShaderRecompilerComputeTests"; app.apiVersion = VK_API_VERSION_1_3; vk::InstanceCreateInfo instance_info{}; instance_info.sType = vk::StructureType::eInstanceCreateInfo; instance_info.pApplicationInfo = &app; RequireVk("VulkanHarness", "dispatch", vk::createInstance(&instance_info, nullptr, &m_instance), "vkCreateInstance"); VULKAN_HPP_DEFAULT_DISPATCHER.init(m_instance); u32 physical_count = 0; RequireVk("VulkanHarness", "dispatch", m_instance.enumeratePhysicalDevices(&physical_count, nullptr), "vkEnumeratePhysicalDevices"); Require("VulkanHarness", "dispatch", physical_count != 0, "no Vulkan physical devices"); std::vector physical_devices(physical_count); RequireVk("VulkanHarness", "dispatch", m_instance.enumeratePhysicalDevices(&physical_count, physical_devices.data()), "vkEnumeratePhysicalDevices"); for (auto physical : physical_devices) { u32 queue_count = 0; physical.getQueueFamilyProperties(&queue_count, nullptr); std::vector queues(queue_count); physical.getQueueFamilyProperties(&queue_count, queues.data()); for (u32 i = 0; i < queue_count; i++) { if ((queues[i].queueFlags & (vk::QueueFlagBits::eCompute | vk::QueueFlagBits::eGraphics)) == (vk::QueueFlagBits::eCompute | vk::QueueFlagBits::eGraphics)) { vk::PhysicalDeviceVulkan12Features features12{}; features12.sType = vk::StructureType::ePhysicalDeviceVulkan12Features; vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric{}; barycentric.sType = vk::StructureType::ePhysicalDeviceFragmentShaderBarycentricFeaturesKHR; barycentric.pNext = &features12; vk::PhysicalDeviceFeatures2 features{}; features.sType = vk::StructureType::ePhysicalDeviceFeatures2; features.pNext = &barycentric; physical.getFeatures2(&features); if (barycentric.fragmentShaderBarycentric != true || features.features.shaderInt64 != true || features12.bufferDeviceAddress != true) { continue; } m_physical_device = physical; m_queue_family = i; break; } } if (m_physical_device != nullptr) { break; } } Require("VulkanHarness", "dispatch", m_physical_device != nullptr, "no Vulkan graphics+compute device with fragment barycentrics"); m_physical_device.getMemoryProperties(&m_memory_properties); vk::PhysicalDeviceFeatures available_features{}; m_physical_device.getFeatures(&available_features); vk::PhysicalDeviceVulkan12Features available_features12{}; available_features12.sType = vk::StructureType::ePhysicalDeviceVulkan12Features; vk::PhysicalDeviceVulkan13Features available_features13{}; available_features13.sType = vk::StructureType::ePhysicalDeviceVulkan13Features; available_features13.pNext = &available_features12; vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR available_derivatives{}; available_derivatives.pNext = &available_features13; vk::PhysicalDeviceDepthClipEnableFeaturesEXT available_depth_clip{}; available_depth_clip.pNext = &available_derivatives; vk::PhysicalDeviceDepthClipControlFeaturesEXT available_clip_control{}; available_clip_control.pNext = &available_depth_clip; vk::PhysicalDeviceColorWriteEnableFeaturesEXT available_color_write{}; available_color_write.pNext = &available_clip_control; vk::PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT available_feedback_layout{}; available_feedback_layout.pNext = &available_color_write; vk::PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT available_feedback_dynamic{}; available_feedback_dynamic.pNext = &available_feedback_layout; vk::PhysicalDeviceProvokingVertexFeaturesEXT available_provoking_vertex{}; available_provoking_vertex.pNext = &available_feedback_dynamic; vk::PhysicalDeviceImageViewMinLodFeaturesEXT available_min_lod{}; available_min_lod.pNext = &available_provoking_vertex; vk::PhysicalDeviceFeatures2 available_features2{}; available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2; available_features2.pNext = &available_min_lod; m_physical_device.getFeatures2(&available_features2); Require("VulkanHarness", "dispatch", available_features.shaderStorageImageWriteWithoutFormat == true, "shaderStorageImageWriteWithoutFormat is not supported"); Require("VulkanHarness", "dispatch", available_features.shaderImageGatherExtended == true && available_derivatives.computeDerivativeGroupQuads == true, "image gather or compute derivative quads are not supported"); Require("VulkanHarness", "dispatch", available_features12.timelineSemaphore == true, "timeline semaphores are not supported"); Require("VulkanHarness", "dispatch", available_features13.dynamicRendering == true, "dynamic rendering is not supported"); Require("VulkanHarness", "dispatch", available_features13.synchronization2 == true, "synchronization2 is not supported"); Require("VulkanHarness", "dispatch", available_features.sampleRateShading == true, "sample-rate shading is not supported"); Require("VulkanHarness", "dispatch", available_features.shaderInt64 == true, "shaderInt64 is not supported"); Require("VulkanHarness", "dispatch", available_features12.bufferDeviceAddress == true, "bufferDeviceAddress is not supported"); Require("VulkanHarness", "dispatch", available_min_lod.minLod == true, "image view minimum LOD is not supported"); Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true, "vertex layer output is not supported"); Require("VulkanHarness", "graphics", available_features.fillModeNonSolid && available_depth_clip.depthClipEnable && available_clip_control.depthClipControl && available_color_write.colorWriteEnable && available_feedback_layout.attachmentFeedbackLoopLayout && available_feedback_dynamic.attachmentFeedbackLoopDynamicState && available_provoking_vertex.provokingVertexLast, "production rasterization features are not supported"); float priority = 1.0f; vk::DeviceQueueCreateInfo queue_info{}; queue_info.sType = vk::StructureType::eDeviceQueueCreateInfo; queue_info.queueFamilyIndex = m_queue_family; queue_info.queueCount = 1; queue_info.pQueuePriorities = &priority; vk::DeviceCreateInfo device_info{}; device_info.sType = vk::StructureType::eDeviceCreateInfo; device_info.queueCreateInfoCount = 1; device_info.pQueueCreateInfos = &queue_info; vk::PhysicalDeviceVulkan12Features device_features12{}; device_features12.sType = vk::StructureType::ePhysicalDeviceVulkan12Features; device_features12.timelineSemaphore = true; device_features12.bufferDeviceAddress = true; device_features12.shaderOutputLayer = true; vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric{}; barycentric.sType = vk::StructureType::ePhysicalDeviceFragmentShaderBarycentricFeaturesKHR; barycentric.pNext = &device_features12; barycentric.fragmentShaderBarycentric = true; vk::PhysicalDeviceVulkan13Features device_features13{}; device_features13.sType = vk::StructureType::ePhysicalDeviceVulkan13Features; device_features13.pNext = &barycentric; device_features13.dynamicRendering = true; device_features13.synchronization2 = true; vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR derivatives{}; derivatives.pNext = &device_features13; derivatives.computeDerivativeGroupQuads = true; vk::PhysicalDeviceDepthClipEnableFeaturesEXT depth_clip{}; depth_clip.pNext = &derivatives; depth_clip.depthClipEnable = true; vk::PhysicalDeviceDepthClipControlFeaturesEXT clip_control{}; clip_control.pNext = &depth_clip; clip_control.depthClipControl = true; vk::PhysicalDeviceColorWriteEnableFeaturesEXT color_write{}; color_write.pNext = &clip_control; color_write.colorWriteEnable = true; vk::PhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT feedback_layout{}; feedback_layout.pNext = &color_write; feedback_layout.attachmentFeedbackLoopLayout = true; vk::PhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT feedback_dynamic{}; feedback_dynamic.pNext = &feedback_layout; feedback_dynamic.attachmentFeedbackLoopDynamicState = true; vk::PhysicalDeviceProvokingVertexFeaturesEXT provoking_vertex{}; provoking_vertex.pNext = &feedback_dynamic; provoking_vertex.provokingVertexLast = available_provoking_vertex.provokingVertexLast; vk::PhysicalDeviceImageViewMinLodFeaturesEXT min_lod{}; min_lod.pNext = &provoking_vertex; min_lod.minLod = true; device_info.pNext = &min_lod; vk::PhysicalDeviceFeatures device_features{}; device_features.shaderStorageImageWriteWithoutFormat = true; device_features.shaderImageGatherExtended = true; device_features.sampleRateShading = true; device_features.shaderInt64 = true; device_features.fillModeNonSolid = true; device_info.pEnabledFeatures = &device_features; constexpr const char *device_extensions[] = { VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, VK_KHR_COMPUTE_SHADER_DERIVATIVES_EXTENSION_NAME, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME, VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_EXTENSION_NAME, VK_EXT_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_EXTENSION_NAME, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME}; device_info.enabledExtensionCount = std::size(device_extensions); device_info.ppEnabledExtensionNames = device_extensions; RequireVk("VulkanHarness", "dispatch", m_physical_device.createDevice(&device_info, nullptr, &m_device), "vkCreateDevice"); VULKAN_HPP_DEFAULT_DISPATCHER.init(m_device); m_device.getQueue(m_queue_family, 0, &m_queue); vk::CommandPoolCreateInfo pool_info{}; pool_info.sType = vk::StructureType::eCommandPoolCreateInfo; pool_info.queueFamilyIndex = m_queue_family; pool_info.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer; RequireVk("VulkanHarness", "dispatch", m_device.createCommandPool(&pool_info, nullptr, &m_command_pool), "vkCreateCommandPool"); } void Destroy() { if (m_device != nullptr) { RequireVulkanSuccess(m_device.waitIdle(), "vkDeviceWaitIdle"); DestroyBuffer(&m_fault_buffer); DestroyBuffer(&m_bda_pagetable_buffer); if (m_runtime_context.allocator != nullptr) { m_renderer.reset(); vmaDestroyAllocator(m_runtime_context.allocator); m_runtime_context.allocator = nullptr; } if (m_command_pool != nullptr) { m_device.destroyCommandPool(m_command_pool, nullptr); } m_device.destroy(nullptr); } if (m_instance != nullptr) { m_instance.destroy(nullptr); } } bool FindMemoryType(u32 type_bits, vk::MemoryPropertyFlags required, u32 *index) const { for (u32 i = 0; i < m_memory_properties.memoryTypeCount; i++) { if ((type_bits & (1u << i)) == 0) { continue; } if ((m_memory_properties.memoryTypes[i].propertyFlags & required) == required) { *index = i; return true; } } return false; } static vk::AccessFlags AccessForLayout(vk::ImageLayout layout) { switch (layout) { case vk::ImageLayout::eTransferDstOptimal: return vk::AccessFlagBits::eTransferWrite; case vk::ImageLayout::eTransferSrcOptimal: return vk::AccessFlagBits::eTransferRead; case vk::ImageLayout::eShaderReadOnlyOptimal: return vk::AccessFlagBits::eShaderRead; case vk::ImageLayout::eGeneral: return vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; default: return {}; } } vk::ShaderModule CreateShaderModule(const char *shader_name, const std::vector &spirv) { vk::ShaderModuleCreateInfo info{}; info.codeSize = spirv.size() * sizeof(u32); info.pCode = spirv.data(); vk::ShaderModule module{}; RequireVk(shader_name, "graphics", m_device.createShaderModule(&info, nullptr, &module), "vkCreateShaderModule"); return module; } vk::CommandBuffer BeginCommands(const char *shader_name, const char *stage) { vk::CommandBufferAllocateInfo cmd_alloc{}; cmd_alloc.sType = vk::StructureType::eCommandBufferAllocateInfo; cmd_alloc.commandPool = m_command_pool; cmd_alloc.level = vk::CommandBufferLevel::ePrimary; cmd_alloc.commandBufferCount = 1; vk::CommandBuffer cmd = nullptr; RequireVk(shader_name, stage, m_device.allocateCommandBuffers(&cmd_alloc, &cmd), "vkAllocateCommandBuffers"); vk::CommandBufferBeginInfo begin{}; begin.sType = vk::StructureType::eCommandBufferBeginInfo; begin.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit; RequireVk(shader_name, stage, cmd.begin(&begin), "vkBeginCommandBuffer"); return cmd; } void EndSubmitAndFree(const char *shader_name, const char *stage, vk::CommandBuffer cmd) { RequireVk(shader_name, stage, cmd.end(), "vkEndCommandBuffer"); vk::FenceCreateInfo fence_info{}; fence_info.sType = vk::StructureType::eFenceCreateInfo; vk::Fence fence = nullptr; RequireVk(shader_name, stage, m_device.createFence(&fence_info, nullptr, &fence), "vkCreateFence"); vk::SubmitInfo submit{}; submit.sType = vk::StructureType::eSubmitInfo; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; RequireVk(shader_name, stage, m_queue.submit(1, &submit, fence), "vkQueueSubmit"); RequireVk(shader_name, stage, m_device.waitForFences(1, &fence, true, UINT64_MAX), "vkWaitForFences"); m_device.destroyFence(fence, nullptr); m_device.freeCommandBuffers(m_command_pool, 1, &cmd); } void AddImageBarrier(vk::CommandBuffer cmd, vk::Image image, vk::ImageLayout old_layout, vk::ImageLayout new_layout, vk::PipelineStageFlags src_stage, vk::PipelineStageFlags dst_stage, vk::AccessFlags src_access, vk::AccessFlags dst_access, u32 mip_levels = 1, u32 layers = 1) { vk::ImageMemoryBarrier barrier{}; barrier.sType = vk::StructureType::eImageMemoryBarrier; barrier.srcAccessMask = src_access; barrier.dstAccessMask = dst_access; barrier.oldLayout = old_layout; barrier.newLayout = new_layout; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; barrier.subresourceRange.baseMipLevel = 0; barrier.subresourceRange.levelCount = mip_levels; barrier.subresourceRange.baseArrayLayer = 0; barrier.subresourceRange.layerCount = layers; cmd.pipelineBarrier(src_stage, dst_stage, {}, 0, nullptr, 0, nullptr, 1, &barrier); } void TransitionImage(const char *shader_name, Image *image, vk::ImageLayout new_layout, vk::PipelineStageFlags src_stage, vk::PipelineStageFlags dst_stage, vk::AccessFlags src_access, vk::AccessFlags dst_access) { vk::CommandBuffer cmd = BeginCommands(shader_name, "dispatch"); AddImageBarrier(cmd, image->image, image->layout, new_layout, src_stage, dst_stage, src_access, dst_access, image->mip_levels, image->layers); EndSubmitAndFree(shader_name, "dispatch", cmd); image->layout = new_layout; } void UploadImage(const char *shader_name, Image *image, vk::Buffer staging, vk::ImageLayout final_layout) { UploadImageMips(shader_name, image, staging, final_layout); } void UploadImageMips(const char *shader_name, Image *image, vk::Buffer staging, vk::ImageLayout final_layout) { vk::CommandBuffer cmd = BeginCommands(shader_name, "dispatch"); AddImageBarrier(cmd, image->image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, vk::AccessFlagBits::eTransferWrite, image->mip_levels, image->layers); std::vector copies; copies.reserve(image->mip_levels); vk::DeviceSize offset = 0; for (u32 level = 0; level < image->mip_levels; level++) { vk::BufferImageCopy copy{}; copy.bufferOffset = offset; copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; copy.imageSubresource.mipLevel = level; copy.imageSubresource.baseArrayLayer = 0; copy.imageSubresource.layerCount = image->layers; copy.imageExtent.width = MipExtent(image->width, level); copy.imageExtent.height = MipExtent(image->height, level); copy.imageExtent.depth = 1; copies.push_back(copy); offset += static_cast( ImageMipDwordCount(image->width, image->height, image->dwords_per_pixel, level, image->layers) * sizeof(u32)); } cmd.copyBufferToImage(staging, image->image, vk::ImageLayout::eTransferDstOptimal, static_cast(copies.size()), copies.data()); AddImageBarrier(cmd, image->image, vk::ImageLayout::eTransferDstOptimal, final_layout, vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eTransferWrite, AccessForLayout(final_layout), image->mip_levels, image->layers); EndSubmitAndFree(shader_name, "dispatch", cmd); image->layout = final_layout; } Buffer CreateDeviceBuffer(const char *shader_name, vk::DeviceSize size, vk::BufferUsageFlags usage) { Buffer ret; ret.size = size; vk::BufferCreateInfo buffer_info{}; buffer_info.sType = vk::StructureType::eBufferCreateInfo; buffer_info.size = size; buffer_info.usage = usage; buffer_info.sharingMode = vk::SharingMode::eExclusive; RequireVk(shader_name, "dispatch", m_device.createBuffer(&buffer_info, nullptr, &ret.buffer), "vkCreateBuffer"); vk::MemoryRequirements requirements{}; m_device.getBufferMemoryRequirements(ret.buffer, &requirements); u32 memory_type = 0; Require(shader_name, "dispatch", FindMemoryType(requirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal, &memory_type) || FindMemoryType(requirements.memoryTypeBits, {}, &memory_type), "no memory type for BDA support buffer"); vk::MemoryAllocateInfo allocation{}; allocation.sType = vk::StructureType::eMemoryAllocateInfo; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = memory_type; RequireVk(shader_name, "dispatch", m_device.allocateMemory(&allocation, nullptr, &ret.memory), "vkAllocateMemory"); RequireVk(shader_name, "dispatch", m_device.bindBufferMemory(ret.buffer, ret.memory, 0), "vkBindBufferMemory"); return ret; } void EnsureBdaBuffers(const char *shader_name) { if (m_bda_pagetable_buffer.buffer != nullptr) { return; } const auto usage = vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst; m_bda_pagetable_buffer = CreateDeviceBuffer(shader_name, BufferCache::BDA_PAGETABLE_SIZE, usage); m_fault_buffer = CreateDeviceBuffer( shader_name, BufferCache::CACHING_NUMPAGES / 8, usage); } Buffer CreateHostBuffer(const char *shader_name, vk::DeviceSize size, vk::BufferUsageFlags usage, const std::vector &contents) { Buffer ret; ret.size = std::max(size, sizeof(u32)); vk::BufferCreateInfo buffer_info{}; buffer_info.sType = vk::StructureType::eBufferCreateInfo; buffer_info.size = ret.size; buffer_info.usage = usage; buffer_info.sharingMode = vk::SharingMode::eExclusive; RequireVk(shader_name, "dispatch", m_device.createBuffer(&buffer_info, nullptr, &ret.buffer), "vkCreateBuffer"); vk::MemoryRequirements req{}; m_device.getBufferMemoryRequirements(ret.buffer, &req); u32 memory_type = 0; ret.coherent = FindMemoryType(req.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, &memory_type); if (!ret.coherent) { Require(shader_name, "dispatch", FindMemoryType(req.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible, &memory_type), "no host-visible memory type for staging buffer"); } vk::MemoryAllocateInfo alloc{}; alloc.sType = vk::StructureType::eMemoryAllocateInfo; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type; RequireVk(shader_name, "dispatch", m_device.allocateMemory(&alloc, nullptr, &ret.memory), "vkAllocateMemory"); RequireVk(shader_name, "dispatch", m_device.bindBufferMemory(ret.buffer, ret.memory, 0), "vkBindBufferMemory"); if (!contents.empty()) { WriteBuffer(shader_name, ret, contents); } return ret; } void WriteBuffer(const char *shader_name, const Buffer &buffer, const std::vector &contents) { void *data = nullptr; RequireVk(shader_name, "dispatch", m_device.mapMemory(buffer.memory, 0, buffer.size, {}, &data), "vkMapMemory"); std::memcpy(data, contents.data(), contents.size() * sizeof(u32)); if (!buffer.coherent) { vk::MappedMemoryRange range{}; range.sType = vk::StructureType::eMappedMemoryRange; range.memory = buffer.memory; range.offset = 0; range.size = VK_WHOLE_SIZE; RequireVk(shader_name, "dispatch", m_device.flushMappedMemoryRanges(1, &range), "vkFlushMappedMemoryRanges"); } m_device.unmapMemory(buffer.memory); } vk::Instance m_instance = nullptr; vk::PhysicalDevice m_physical_device = nullptr; vk::Device m_device = nullptr; vk::Queue m_queue = nullptr; vk::CommandPool m_command_pool = nullptr; u32 m_queue_family = 0; vk::PhysicalDeviceMemoryProperties m_memory_properties{}; Buffer m_bda_pagetable_buffer; Buffer m_fault_buffer; GraphicContext m_runtime_context{}; std::unique_ptr m_renderer; }; void CompareWords(const TestCase &test, const char *stage, const std::vector &expected, const std::vector &actual) { if (actual == expected) { return; } std::ostringstream out; out << "expected ["; for (size_t i = 0; i < expected.size(); i++) { out << (i == 0 ? "" : ", ") << Hex(expected[i]); } out << "] actual ["; for (size_t i = 0; i < actual.size(); i++) { out << (i == 0 ? "" : ", ") << Hex(actual[i]); } out << "]"; Fail(test.name, stage, out.str()); } void CompareGraphicsWords(const GraphicsCase &test, const std::vector &actual) { if (actual == test.expected_pixel) { return; } std::ostringstream out; out << "expected ["; for (size_t i = 0; i < test.expected_pixel.size(); i++) { out << (i == 0 ? "" : ", ") << Hex(test.expected_pixel[i]); } out << "] actual ["; for (size_t i = 0; i < actual.size(); i++) { out << (i == 0 ? "" : ", ") << Hex(actual[i]); } out << "]"; Fail(test.name, "graphics readback", out.str()); } void RunCase(VulkanHarness *vulkan, const TestCase &test) { auto compiled = CompileCase(test, vulkan != nullptr ? vulkan->SubgroupSize() : 64u); if (test.image_descriptor_swizzle != DstSel(4, 5, 6, 7) && !compiled.program.info.images.empty()) { Require(test.name, "resource specialization", compiled.program.info.images[0].shader_swizzle == test.image_descriptor_swizzle, "storage image descriptor swizzle did not reach the specialized " "program"); } if (test.expected_force_point_sampler) { Require(test.name, "sampler specialization", compiled.program.info.samplers.size() == 1u && compiled.program.info.samplers[0].force_point_filtering, "bit-packed sampled image did not force point filtering"); } if (test.compile_only) { std::printf("[compute] %-32s ok\n", test.name); return; } using Kind = ShaderRecompiler::IR::DescriptorBindingKind; auto Has = [&](Kind kind) { return ShaderRecompiler::IR::FindBinding(compiled.program.bindings, kind) != nullptr; }; const auto dwords = std::max( {test.initial.size(), test.expected.size(), static_cast(1)}); auto buffer = vulkan->CreateStorageBuffer( test.name, test.initial, dwords, Has(Kind::BdaPagetable)); VulkanHarness::Image sampled_image; VulkanHarness::Image storage_image; VulkanHarness::Image storage_image_uint; VulkanHarness::Buffer gds_buffer; vk::Sampler sampler = nullptr; bool needs_sampled_image = false; bool needs_storage_image = false; for (const auto &binding : compiled.program.bindings.descriptors) { const auto resource_class = ShaderRecompiler::IR::ImageBindingResourceClass(binding.kind); if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) { needs_sampled_image = true; } else if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Storage) { needs_storage_image = true; } } const bool needs_sampler = Has(Kind::Samplers); const bool needs_gds = Has(Kind::Gds); if (needs_gds) { const auto gds_dwords = std::max( {test.gds_initial.size(), test.expected_gds.size(), 1u}); gds_buffer = vulkan->CreateStorageBuffer(test.name, test.gds_initial, gds_dwords); } if (needs_sampled_image) { auto sampled_mips = test.sampled_image_rgba_mips; auto sampled_format = test.sampled_image_format; auto sampled_dwords_per_pixel = test.sampled_image_dwords_per_pixel; if (!test.sampled_image_rgba_mips.empty()) { sampled_format = vk::Format::eR32G32B32A32Sfloat; sampled_dwords_per_pixel = 4; } else if (!test.sampled_image_rgba.empty()) { sampled_mips.push_back(test.sampled_image_rgba); } sampled_image = vulkan->CreateImageMips( test.name, test.image_width, test.image_height, sampled_format, vk::ImageUsageFlagBits::eSampled, sampled_mips, sampled_dwords_per_pixel, vk::ImageLayout::eShaderReadOnlyOptimal, test.sampled_image_type, test.sampled_image_view_type, test.sampled_image_layers, test.sampled_image_view_base_layer, test.sampled_image_view_layers); } if (needs_storage_image) { storage_image = vulkan->CreateImage2D( test.name, test.image_width, test.image_height, test.storage_image_format, vk::ImageUsageFlagBits::eStorage, test.storage_image_rgba, test.storage_image_dwords_per_pixel, vk::ImageLayout::eGeneral); storage_image_uint = vulkan->CreateImage2D( test.name, test.image_width, test.image_height, vk::Format::eR32Uint, vk::ImageUsageFlagBits::eStorage, test.storage_image_r32ui, 1, vk::ImageLayout::eGeneral); } if (needs_sampler) { sampler = vulkan->CreateNearestSampler(test.name, sampled_image.mip_levels); } vulkan->Dispatch(test, compiled, buffer, needs_gds ? &gds_buffer : nullptr, needs_sampled_image ? &sampled_image : nullptr, needs_storage_image ? &storage_image : nullptr, needs_storage_image ? &storage_image_uint : nullptr, sampler); auto actual = vulkan->ReadBuffer(test.name, buffer, test.expected.size()); if (!test.expected_gds.empty()) { const auto gds_actual = vulkan->ReadBuffer(test.name, gds_buffer, test.expected_gds.size()); CompareWords(test, "GDS readback", test.expected_gds, gds_actual); } if (!test.expected_storage_image_rgba.empty()) { auto image_actual = vulkan->ReadImage(test.name, &storage_image); image_actual.resize(test.expected_storage_image_rgba.size()); CompareWords(test, "storage image readback", test.expected_storage_image_rgba, image_actual); } if (!test.expected_storage_image_r32ui.empty()) { auto image_actual = vulkan->ReadImage(test.name, &storage_image_uint); image_actual.resize(test.expected_storage_image_r32ui.size()); CompareWords(test, "uint storage image readback", test.expected_storage_image_r32ui, image_actual); } if (sampler != nullptr) { vulkan->Device().destroySampler(sampler, nullptr); } vulkan->DestroyImage(&sampled_image); vulkan->DestroyImage(&storage_image); vulkan->DestroyImage(&storage_image_uint); vulkan->DestroyBuffer(&gds_buffer); vulkan->DestroyBuffer(&buffer); CompareWords(test, "readback", test.expected, actual); std::printf("[compute] %-32s ok\n", test.name); } void RunGraphicsCase(VulkanHarness *vulkan, const GraphicsCase &test) { auto compiled = CompileFragmentCase(test); auto actual = vulkan->RenderFragment(test, compiled); CompareGraphicsWords(test, actual); std::printf("[graphics] %-31s ok\n", test.name); } enum class CoverageClass { Covered, ControlOrMarker, NeedsAluCase, NeedsFloatCase, NeedsMemoryCase, NeedsImageCase, NeedsGraphicsStageCase, }; CoverageClass ClassifyOpcode(ShaderOpcode opcode, const std::set &covered) { using ShaderRecompiler::Decoder::Opcode; if (covered.contains(opcode)) { return CoverageClass::Covered; } switch (opcode) { case Opcode::S_GETPC_B64: case Opcode::S_SETPC_B64: case Opcode::S_NOP: case Opcode::S_WAITCNT: case Opcode::S_BARRIER: case Opcode::S_BRANCH: case Opcode::S_CBRANCH_SCC0: case Opcode::S_CBRANCH_SCC1: case Opcode::S_CBRANCH_VCCZ: case Opcode::S_CBRANCH_VCCNZ: case Opcode::S_CBRANCH_EXECZ: case Opcode::S_CBRANCH_EXECNZ: case Opcode::S_SENDMSG: case Opcode::S_SETREG_B32: case Opcode::S_SLEEP: case Opcode::S_TTRACEDATA: case Opcode::S_INST_PREFETCH: case Opcode::S_ENDPGM: return CoverageClass::ControlOrMarker; case Opcode::V_ADD_F32: case Opcode::V_SUB_F32: case Opcode::V_SUBREV_F32: case Opcode::V_MUL_F32: case Opcode::V_MAC_F32: case Opcode::V_MADMK_F32: case Opcode::V_MADAK_F32: case Opcode::V_MIN_F32: case Opcode::V_MAX_F32: case Opcode::V_MAD_F32: case Opcode::V_FMA_F32: case Opcode::V_MIN3_F32: case Opcode::V_MAX3_F32: case Opcode::V_MED3_F32: case Opcode::V_DOT2C_F32_F16: case Opcode::V_CVT_F32_I32: case Opcode::V_CVT_F32_U32: case Opcode::V_CVT_U32_F32: case Opcode::V_CVT_I32_F32: case Opcode::V_CVT_F16_F32: case Opcode::V_CVT_F32_F16: case Opcode::V_CVT_U16_F16: case Opcode::V_CVT_RPI_I32_F32: case Opcode::V_CVT_FLR_I32_F32: case Opcode::V_CVT_OFF_F32_I4: case Opcode::V_CVT_F32_UBYTE0: case Opcode::V_CVT_F32_UBYTE1: case Opcode::V_CVT_F32_UBYTE2: case Opcode::V_CVT_F32_UBYTE3: case Opcode::V_RCP_F32: case Opcode::V_FRACT_F32: case Opcode::V_TRUNC_F32: case Opcode::V_CEIL_F32: case Opcode::V_RNDNE_F32: case Opcode::V_FLOOR_F32: case Opcode::V_EXP_F32: case Opcode::V_LOG_F32: case Opcode::V_RSQ_F32: case Opcode::V_SQRT_F32: case Opcode::V_SIN_F32: case Opcode::V_COS_F32: case Opcode::V_CUBEID_F32: case Opcode::V_CUBESC_F32: case Opcode::V_CUBETC_F32: case Opcode::V_CUBEMA_F32: case Opcode::V_LDEXP_F32: case Opcode::V_CVT_PK_U8_F32: case Opcode::V_CVT_PKNORM_I16_F32: case Opcode::V_CVT_PKNORM_U16_F32: case Opcode::V_CVT_PKRTZ_F16_F32: case Opcode::V_PK_ADD_F16: case Opcode::V_PK_MUL_F16: case Opcode::V_PK_MIN_F16: case Opcode::V_PK_MAX_F16: case Opcode::V_PK_FMA_F16: case Opcode::V_ADD_F16: case Opcode::V_SUB_F16: case Opcode::V_SUBREV_F16: case Opcode::V_MUL_F16: case Opcode::V_MAX_F16: case Opcode::V_MIN_F16: case Opcode::V_MIN3_F16: case Opcode::V_MAX3_F16: case Opcode::V_MED3_F16: case Opcode::V_RCP_F16: case Opcode::V_RSQ_F16: case Opcode::V_LOG_F16: case Opcode::V_EXP_F16: case Opcode::V_SIN_F16: case Opcode::V_COS_F16: case Opcode::V_MAD_MIXLO_F16: case Opcode::V_MAD_MIXHI_F16: case Opcode::DS_MIN_F32: case Opcode::DS_MAX_F32: case Opcode::V_CMP_F_F32: case Opcode::V_CMP_LT_F32: case Opcode::V_CMP_EQ_F32: case Opcode::V_CMP_LE_F32: case Opcode::V_CMP_GT_F32: case Opcode::V_CMP_LG_F32: case Opcode::V_CMP_GE_F32: case Opcode::V_CMP_O_F32: case Opcode::V_CMP_U_F32: case Opcode::V_CMP_NGE_F32: case Opcode::V_CMP_NLG_F32: case Opcode::V_CMP_NGT_F32: case Opcode::V_CMP_NLE_F32: case Opcode::V_CMP_NEQ_F32: case Opcode::V_CMP_NLT_F32: case Opcode::V_CMP_TRU_F32: case Opcode::V_CMPX_LT_F32: case Opcode::V_CMPX_EQ_F32: case Opcode::V_CMPX_LE_F32: case Opcode::V_CMPX_GT_F32: case Opcode::V_CMPX_LG_F32: case Opcode::V_CMPX_GE_F32: case Opcode::V_CMPX_NGE_F32: case Opcode::V_CMPX_NLG_F32: case Opcode::V_CMPX_NGT_F32: case Opcode::V_CMPX_NLE_F32: case Opcode::V_CMPX_NEQ_F32: case Opcode::V_CMPX_NLT_F32: case Opcode::V_CMP_CLASS_F32: case Opcode::V_CMPX_CLASS_F32: case Opcode::V_CMP_LT_F16: case Opcode::V_CMP_EQ_F16: case Opcode::V_CMP_LE_F16: case Opcode::V_CMP_GT_F16: case Opcode::V_CMP_LG_F16: case Opcode::V_CMP_GE_F16: case Opcode::V_CMP_NGT_F16: case Opcode::V_CMP_NEQ_F16: case Opcode::V_CMP_NLT_F16: case Opcode::V_CMPX_LT_F16: case Opcode::V_CMPX_EQ_F16: case Opcode::V_CMPX_LE_F16: case Opcode::V_CMPX_GT_F16: case Opcode::V_CMPX_GE_F16: case Opcode::V_CMPX_NEQ_F16: case Opcode::V_CMPX_NLT_F16: return CoverageClass::NeedsFloatCase; case Opcode::S_LOAD_DWORD: case Opcode::S_LOAD_DWORDX2: case Opcode::S_LOAD_DWORDX4: case Opcode::S_LOAD_DWORDX8: case Opcode::S_LOAD_DWORDX16: case Opcode::S_BUFFER_LOAD_DWORD: case Opcode::S_BUFFER_LOAD_DWORDX2: case Opcode::S_BUFFER_LOAD_DWORDX4: case Opcode::S_BUFFER_LOAD_DWORDX8: case Opcode::S_BUFFER_LOAD_DWORDX16: case Opcode::BUFFER_LOAD_FORMAT_X: case Opcode::BUFFER_LOAD_FORMAT_XY: case Opcode::BUFFER_LOAD_FORMAT_XYZ: case Opcode::BUFFER_LOAD_FORMAT_XYZW: case Opcode::BUFFER_STORE_FORMAT_X: case Opcode::BUFFER_STORE_FORMAT_XY: case Opcode::BUFFER_STORE_FORMAT_XYZ: case Opcode::BUFFER_STORE_FORMAT_XYZW: case Opcode::BUFFER_LOAD_UBYTE: case Opcode::BUFFER_LOAD_SBYTE: case Opcode::BUFFER_LOAD_USHORT: case Opcode::BUFFER_LOAD_SSHORT: case Opcode::BUFFER_LOAD_DWORDX2: case Opcode::BUFFER_LOAD_DWORDX3: case Opcode::BUFFER_LOAD_DWORDX4: case Opcode::BUFFER_STORE_BYTE: case Opcode::BUFFER_STORE_SHORT: case Opcode::BUFFER_STORE_DWORDX2: case Opcode::BUFFER_STORE_DWORDX3: case Opcode::BUFFER_STORE_DWORDX4: case Opcode::TBUFFER_LOAD_FORMAT_X: case Opcode::TBUFFER_LOAD_FORMAT_XY: case Opcode::TBUFFER_LOAD_FORMAT_XYZ: case Opcode::TBUFFER_LOAD_FORMAT_XYZW: case Opcode::TBUFFER_STORE_FORMAT_X: case Opcode::TBUFFER_STORE_FORMAT_XY: case Opcode::TBUFFER_STORE_FORMAT_XYZ: case Opcode::TBUFFER_STORE_FORMAT_XYZW: case Opcode::BUFFER_ATOMIC_SWAP: case Opcode::BUFFER_ATOMIC_CMPSWAP: case Opcode::BUFFER_ATOMIC_ADD: case Opcode::BUFFER_ATOMIC_SUB: case Opcode::BUFFER_ATOMIC_SMIN: case Opcode::BUFFER_ATOMIC_UMIN: case Opcode::BUFFER_ATOMIC_SMAX: case Opcode::BUFFER_ATOMIC_UMAX: case Opcode::BUFFER_ATOMIC_AND: case Opcode::BUFFER_ATOMIC_OR: case Opcode::BUFFER_ATOMIC_XOR: case Opcode::BUFFER_ATOMIC_FMIN: case Opcode::BUFFER_ATOMIC_FMAX: case Opcode::FLAT_LOAD_UBYTE: case Opcode::FLAT_LOAD_SBYTE: case Opcode::FLAT_LOAD_USHORT: case Opcode::FLAT_LOAD_SSHORT: case Opcode::FLAT_LOAD_DWORD: case Opcode::FLAT_LOAD_DWORDX2: case Opcode::FLAT_LOAD_DWORDX3: case Opcode::FLAT_LOAD_DWORDX4: case Opcode::FLAT_STORE_BYTE: case Opcode::FLAT_STORE_SHORT: case Opcode::FLAT_STORE_DWORD: case Opcode::FLAT_STORE_DWORDX2: case Opcode::FLAT_STORE_DWORDX3: case Opcode::FLAT_STORE_DWORDX4: case Opcode::DS_ADD_U32: case Opcode::DS_ADD_RTN_U32: case Opcode::DS_SUB_U32: case Opcode::DS_SUB_RTN_U32: case Opcode::DS_INC_RTN_U32: case Opcode::DS_DEC_RTN_U32: case Opcode::DS_MIN_I32: case Opcode::DS_MIN_RTN_I32: case Opcode::DS_MAX_I32: case Opcode::DS_MAX_RTN_I32: case Opcode::DS_MIN_U32: case Opcode::DS_MIN_RTN_U32: case Opcode::DS_MAX_U32: case Opcode::DS_MAX_RTN_U32: case Opcode::DS_AND_B32: case Opcode::DS_AND_RTN_B32: case Opcode::DS_OR_B32: case Opcode::DS_OR_RTN_B32: case Opcode::DS_XOR_B32: case Opcode::DS_XOR_RTN_B32: case Opcode::DS_WRXCHG_RTN_B32: case Opcode::DS_SWIZZLE_B32: case Opcode::DS_BPERMUTE_B32: case Opcode::DS_READ_I8: case Opcode::DS_READ_U8: case Opcode::DS_READ_I16: case Opcode::DS_READ_U16: case Opcode::DS_READ_U16_D16: case Opcode::DS_READ_U16_D16_HI: case Opcode::DS_READ2_B32: case Opcode::DS_READ_B32: case Opcode::DS_READ_B64: case Opcode::DS_READ2_B64: case Opcode::DS_READ_B96: case Opcode::DS_READ_B128: case Opcode::DS_WRITE_B8: case Opcode::DS_WRITE_B16: case Opcode::DS_WRITE_B16_D16_HI: case Opcode::DS_WRITE2_B32: case Opcode::DS_WRITE2ST64_B32: case Opcode::DS_WRITE_B32: case Opcode::DS_WRITE_B64: case Opcode::DS_WRITE_B96: case Opcode::DS_WRITE_B128: case Opcode::DS_WRITE_ADDTID_B32: case Opcode::DS_READ_ADDTID_B32: return CoverageClass::NeedsMemoryCase; case Opcode::IMAGE_GET_RESINFO: case Opcode::IMAGE_GET_LOD: case Opcode::IMAGE_LOAD: case Opcode::IMAGE_LOAD_MIP: case Opcode::IMAGE_STORE: case Opcode::IMAGE_STORE_MIP: case Opcode::IMAGE_ATOMIC_SWAP: case Opcode::IMAGE_ATOMIC_ADD: case Opcode::IMAGE_ATOMIC_UMIN: case Opcode::IMAGE_ATOMIC_AND: case Opcode::IMAGE_ATOMIC_OR: case Opcode::IMAGE_ATOMIC_XOR: case Opcode::IMAGE_SAMPLE: case Opcode::IMAGE_GATHER4_LZ: case Opcode::IMAGE_GATHER4_C: case Opcode::IMAGE_GATHER4_C_LZ: case Opcode::IMAGE_GATHER4_LZ_O: case Opcode::IMAGE_GATHER4_C_O: case Opcode::IMAGE_GATHER4_C_LZ_O: return CoverageClass::NeedsImageCase; case Opcode::V_INTERP_P1_F32: case Opcode::V_INTERP_P2_F32: case Opcode::V_INTERP_MOV_F32: case Opcode::EXP: return CoverageClass::NeedsGraphicsStageCase; default: return CoverageClass::NeedsAluCase; } } const char *CoverageClassName(CoverageClass status) { switch (status) { case CoverageClass::Covered: return "covered"; case CoverageClass::ControlOrMarker: return "control"; case CoverageClass::NeedsAluCase: return "alu"; case CoverageClass::NeedsFloatCase: return "float"; case CoverageClass::NeedsMemoryCase: return "memory"; case CoverageClass::NeedsImageCase: return "image"; case CoverageClass::NeedsGraphicsStageCase: return "graphics"; default: return "unknown"; } } void PrintPendingOpcodes(CoverageClass status, const std::vector &opcodes) { if (opcodes.empty()) { return; } std::printf("[coverage] pending_%s:", CoverageClassName(status)); for (auto opcode : opcodes) { const auto name = magic_enum::enum_name(opcode); std::printf(" %.*s", static_cast(name.size()), name.data()); } std::printf("\n"); } void CheckOpcodeCoverage(const std::vector &tests, const std::vector &graphics_tests) { using ShaderRecompiler::Decoder::Opcode; std::set covered; for (const auto &test : tests) { for (auto opcode : test.opcodes) { covered.insert(opcode); } } for (const auto &test : graphics_tests) { for (auto opcode : test.opcodes) { covered.insert(opcode); } } uint32_t counts[7] = {}; std::vector pending[7]; for (auto value = static_cast(Opcode::S_MOV_B32); value < static_cast(Opcode::COUNT); value++) { const auto opcode = static_cast(value); const auto status = ClassifyOpcode(opcode, covered); counts[static_cast(status)]++; if (status != CoverageClass::Covered && status != CoverageClass::ControlOrMarker) { pending[static_cast(status)].push_back(opcode); } } std::printf( "[coverage] decoder opcodes: covered=%u control=%u alu_pending=%u " "float_pending=%u memory_pending=%u image_pending=%u " "graphics_pending=%u\n", counts[static_cast(CoverageClass::Covered)], counts[static_cast(CoverageClass::ControlOrMarker)], counts[static_cast(CoverageClass::NeedsAluCase)], counts[static_cast(CoverageClass::NeedsFloatCase)], counts[static_cast(CoverageClass::NeedsMemoryCase)], counts[static_cast(CoverageClass::NeedsImageCase)], counts[static_cast(CoverageClass::NeedsGraphicsStageCase)]); PrintPendingOpcodes( CoverageClass::NeedsAluCase, pending[static_cast(CoverageClass::NeedsAluCase)]); PrintPendingOpcodes( CoverageClass::NeedsFloatCase, pending[static_cast(CoverageClass::NeedsFloatCase)]); PrintPendingOpcodes( CoverageClass::NeedsMemoryCase, pending[static_cast(CoverageClass::NeedsMemoryCase)]); PrintPendingOpcodes( CoverageClass::NeedsImageCase, pending[static_cast(CoverageClass::NeedsImageCase)]); PrintPendingOpcodes( CoverageClass::NeedsGraphicsStageCase, pending[static_cast(CoverageClass::NeedsGraphicsStageCase)]); } TestCase IntegerAddSubMul() { using O = ShaderOpcode; std::vector code = { EncodeSMovB32(0, InlineU32(7)), EncodeSMovB32(1, InlineU32(3)), EncodeSop2(0x00, 2, 0, 1), EncodeSop2(0x01, 3, 2, InlineU32(1)), EncodeSop2(0x26, 4, 3, InlineU32(2)), EncodeVop1(0x01, 0, 4), }; AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"IntegerAddSubMul", code, {}, {18}, {O::S_MOV_B32, O::S_ADD_U32, O::S_SUB_U32, O::S_MUL_I32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BitwiseOps() { using O = ShaderOpcode; std::vector code = { EncodeSMovB32(0, InlineU32(60)), EncodeSMovB32(1, InlineU32(15)), EncodeSop2(0x0e, 2, 0, 1), EncodeSop2(0x10, 3, 0, 1), EncodeSop2(0x12, 4, 3, 2), EncodeVop1(0x37, 0, 4), }; AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"BitwiseAndOrXorNot", code, {}, {~0x33u}, {O::S_MOV_B32, O::S_AND_B32, O::S_OR_B32, O::S_XOR_B32, O::V_NOT_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Shifts() { using O = ShaderOpcode; std::vector code = { EncodeSMovB32(0, InlineU32(3)), EncodeSop2(0x1e, 1, 0, InlineU32(2)), EncodeSop2(0x20, 2, 1, InlineU32(1)), EncodeVop1(0x01, 0, 2), }; AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"Shifts", code, {}, {6}, {O::S_MOV_B32, O::S_LSHL_B32, O::S_LSHR_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ExactPushConstantExtent() { using O = ShaderOpcode; constexpr std::array values{ 0x10203040u, 0x21314151u, 0x32425262u, 0x43536373u, 0x54647484u, 0x65758595u, 0x768696a6u, 0x8797a7b7u, 0x98a8b8c8u, 0xa9b9c9d9u}; std::vector code; for (u32 i = 0; i < values.size(); i++) { AppendStoreSgpr(&code, 4u + i, i); } AppendEnd(&code); TestCase test; test.name = "ExactPushConstantExtent"; test.code = std::move(code); test.initial.resize(values.size()); test.expected.assign(values.begin(), values.end()); test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, values.size() * sizeof(u32)); std::copy(values.begin(), values.end(), test.user_data.begin() + 4); test.has_user_data = true; return test; } TestCase ScalarShiftCountsMaskLowBits() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(1))); AppendSMovLiteral(&code, 1, 0x80000000u); code.push_back(EncodeSMovB32(2, InlineU32(32))); code.push_back(EncodeSMovB32(3, InlineU32(33))); code.push_back(EncodeSop2(0x1e, 10, 0, 2)); code.push_back(EncodeSop2(0x1e, 11, 0, 3)); code.push_back(EncodeSop2(0x20, 12, 1, 2)); code.push_back(EncodeSop2(0x20, 13, 1, 3)); code.push_back(EncodeSop2(0x22, 14, 1, 2)); code.push_back(EncodeSop2(0x22, 15, 1, 3)); for (u32 i = 0; i < 6u; i++) { AppendStoreSgpr(&code, 10u + i, i); } AppendEnd(&code); return {"ScalarShiftCountsMaskLowBits", code, {}, {1, 2, 0x80000000u, 0x40000000u, 0x80000000u, 0xc0000000u}, {O::S_MOV_B32, O::S_LSHL_B32, O::S_LSHR_B32, O::S_ASHR_I32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Rdna2ScalarOpcodes() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(2, InlineU32(7))); code.push_back(EncodeSMovB32(106, InlineU32(16))); code.push_back(EncodeSop1(0x1d, 106, InlineU32(0))); AppendStoreSgpr(&code, 106, 0); code.push_back(EncodeSopk(0x13, 106, 0x1019u)); AppendStoreSgpr(&code, 106, 1); code.push_back(EncodeSop2(0x02, 106, 2, 239u)); AppendStoreSgpr(&code, 106, 2); code.push_back(EncodeSopp(0x0e, 0)); code.push_back(EncodeSop2(0x02, 106, 239u, 2)); AppendStoreSgpr(&code, 106, 3); AppendEnd(&code); return {"Rdna2ScalarOpcodes", code, {}, {0x11u, 0x11u, 7u, 7u}, {O::S_MOV_B32, O::S_BITSET1_B32, O::S_SETREG_B32, O::S_ADD_I32, O::S_SLEEP, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarTrapDisabledFallsThroughExactRaw() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 7); code.push_back(0xbf920001u); // s_trap 1 from the game shader AppendVMovU32(&code, 0, 42); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ScalarTrapDisabledFallsThroughExactRaw"; test.code = code; test.expected = {42}; test.opcodes = {O::V_MOV_B32, O::S_TRAP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_TRAP 0x00000001", 1}}; test.compute_info.threads_num[0] = 1; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.wave_size = 32; test.has_compute_info = true; return test; } TestCase ScalarWaitcntDepctrCapturedVmVsrc() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 7, 7); AppendVMovU32(&code, 31, 0); AppendBufferStoreDword(&code, 7, 31); code.push_back(0xbfa3ffe3u); // s_waitcnt_depctr vm_vsrc(0) AppendVMovU32(&code, 7, 42); AppendStoreVgpr(&code, 7, 1); AppendEnd(&code); TestCase test; test.name = "ScalarWaitcntDepctrCapturedVmVsrc"; test.code = std::move(code); test.initial = {0, 0}; test.expected = {7, 42}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_WAITCNT_DEPCTR, O::S_ENDPGM}; test.decoded_counts = {{"0x00000010: S_WAITCNT_DEPCTR 0x0000ffe3\n", 1}}; test.forbidden_spirv = {"OpControlBarrier", "OpMemoryBarrier"}; return test; } TestCase ScalarExtendedArithmetic() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0xffffffffu); code.push_back(EncodeSMovB32(1, InlineU32(1))); code.push_back(EncodeSop2(0x00, 2, 0, 1)); code.push_back(EncodeSMovB32(3, InlineU32(5))); code.push_back(EncodeSMovB32(4, InlineU32(6))); code.push_back(EncodeSop2(0x04, 5, 3, 4)); code.push_back(EncodeSop2(0x02, 6, InlineU32(7), InlineU32(8))); code.push_back(EncodeSop2(0x03, 7, InlineU32(7), InlineU32(9))); AppendSMovLiteral(&code, 8, 0xfffffffbu); code.push_back(EncodeSMovB32(9, InlineU32(3))); code.push_back(EncodeSop2(0x06, 10, 8, 9)); code.push_back(EncodeSop2(0x08, 11, 8, 9)); AppendSMovLiteral(&code, 12, 0xfffffffeu); code.push_back(EncodeSMovB32(13, InlineU32(3))); code.push_back(EncodeSop2(0x07, 14, 12, 13)); code.push_back(EncodeSop2(0x09, 15, 12, 13)); code.push_back(EncodeSMovB32(16, 8)); code.push_back(EncodeSop1(0x34, 16, 16)); code.push_back(EncodeSopk(0x00, 17, 0xfff5u)); code.push_back(EncodeSopk(0x10, 17, 0xfffdu)); const u32 results[] = {2, 5, 6, 7, 10, 11, 14, 15, 16, 17}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreSgpr(&code, results[i], i); } AppendEnd(&code); return {"ScalarExtendedArithmetic", code, {}, {0, 12, 15, 0xfffffffeu, 0xfffffffbu, 3, 3, 0xfffffffeu, 5, 33}, {O::S_MOV_B32, O::S_ADD_U32, O::S_ADDC_U32, O::S_ADD_I32, O::S_SUB_I32, O::S_MIN_I32, O::S_MAX_I32, O::S_MIN_U32, O::S_MAX_U32, O::S_ABS_I32, O::S_MOVK_I32, O::S_MULK_I32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarArithmeticSccCarryBorrowOverflow() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; auto append_case = [&](bool prior_scc, u32 instruction, u32 out_sgpr) { set_scc(prior_scc); code.push_back(instruction); capture_scc(out_sgpr); }; code.push_back(EncodeSMovB32(0, InlineU32(0))); code.push_back(EncodeSMovB32(1, InlineU32(1))); append_case(false, EncodeSop2(0x01, 10, 0, 1), 20); code.push_back(EncodeSMovB32(2, InlineU32(5))); code.push_back(EncodeSMovB32(3, InlineU32(3))); append_case(true, EncodeSop2(0x01, 11, 2, 3), 21); AppendSMovLiteral(&code, 4, 0x7fffffffu); code.push_back(EncodeSMovB32(5, InlineU32(1))); append_case(false, EncodeSop2(0x02, 12, 4, 5), 22); code.push_back(EncodeSMovB32(6, InlineU32(1))); code.push_back(EncodeSMovB32(7, InlineU32(2))); append_case(true, EncodeSop2(0x02, 13, 6, 7), 23); AppendSMovLiteral(&code, 8, 0x80000000u); code.push_back(EncodeSMovB32(9, InlineU32(1))); append_case(false, EncodeSop2(0x03, 14, 8, 9), 24); code.push_back(EncodeSMovB32(15, InlineU32(5))); code.push_back(EncodeSMovB32(16, InlineU32(3))); append_case(true, EncodeSop2(0x03, 17, 15, 16), 25); AppendSMovLiteral(&code, 18, 0x7fffffffu); set_scc(false); code.push_back(EncodeSopk(0x0f, 18, 1)); capture_scc(26); code.push_back(EncodeSMovB32(19, InlineU32(4))); set_scc(true); code.push_back(EncodeSopk(0x0f, 19, 0xfffeu)); capture_scc(27); for (u32 i = 0; i < 8u; i++) { AppendStoreSgpr(&code, 20u + i, i); } AppendEnd(&code); return {"ScalarArithmeticSccCarryBorrowOverflow", code, {}, {1, 0, 1, 0, 1, 0, 1, 0}, {O::S_MOV_B32, O::S_SUB_U32, O::S_ADD_I32, O::S_SUB_I32, O::S_CMP_EQ_U32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarMinMaxSccComparisonEdges() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; auto append_case = [&](bool prior_scc, u32 instruction, u32 out_sgpr) { set_scc(prior_scc); code.push_back(instruction); capture_scc(out_sgpr); }; AppendSMovLiteral(&code, 0, 0xfffffffeu); code.push_back(EncodeSMovB32(1, InlineU32(3))); append_case(false, EncodeSop2(0x06, 10, 0, 1), 20); append_case(true, EncodeSop2(0x06, 11, 1, 0), 21); append_case(false, EncodeSop2(0x08, 12, 1, 0), 22); append_case(true, EncodeSop2(0x08, 13, 0, 1), 23); code.push_back(EncodeSMovB32(2, InlineU32(2))); code.push_back(EncodeSMovB32(3, InlineU32(3))); append_case(false, EncodeSop2(0x07, 14, 2, 3), 24); append_case(true, EncodeSop2(0x07, 15, 3, 2), 25); append_case(false, EncodeSop2(0x09, 16, 3, 2), 26); append_case(true, EncodeSop2(0x09, 17, 2, 3), 27); for (u32 i = 0; i < 8u; i++) { AppendStoreSgpr(&code, 20u + i, i); } AppendEnd(&code); return {"ScalarMinMaxSccComparisonEdges", code, {}, {1, 0, 1, 0, 1, 0, 1, 0}, {O::S_MOV_B32, O::S_MIN_I32, O::S_MAX_I32, O::S_MIN_U32, O::S_MAX_U32, O::S_CMP_EQ_U32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarAbsI32UpdatesScc() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; code.push_back(EncodeSMovB32(0, InlineU32(0))); set_scc(true); code.push_back(EncodeSop1(0x34, 1, 0)); capture_scc(2); AppendSMovLiteral(&code, 3, 0xfffffffbu); set_scc(false); code.push_back(EncodeSop1(0x34, 4, 3)); capture_scc(5); AppendStoreSgpr(&code, 1, 0); AppendStoreSgpr(&code, 2, 1); AppendStoreSgpr(&code, 4, 2); AppendStoreSgpr(&code, 5, 3); AppendEnd(&code); return {"ScalarAbsI32UpdatesScc", code, {}, {0, 0, 5, 1}, {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_ABS_I32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarAbsdiffBitset64AndSetprio() { using O = ShaderOpcode; std::vector code; auto capture_scc = [&](u32 dst_sgpr) { code.push_back( EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; code.push_back(EncodeSMovB32(0, InlineU32(2))); code.push_back(EncodeSMovB32(1, InlineU32(5))); code.push_back(EncodeSop2(0x2c, 10, 0, 1)); capture_scc(20); AppendSMovLiteral(&code, 0, 0xffffffffu); code.push_back(EncodeSMovB32(1, InlineU32(0))); code.push_back(EncodeSop2(0x2c, 11, 0, 1)); capture_scc(21); AppendSMovLiteral(&code, 0, 0x80000000u); code.push_back(EncodeSop2(0x2c, 12, 0, 1)); capture_scc(22); code.push_back(EncodeSMovB32(1, InlineU32(1))); code.push_back(EncodeSop2(0x2c, 13, 0, 1)); capture_scc(23); code.push_back(EncodeSop2(0x2c, 14, 0, 0)); capture_scc(24); AppendSMovLiteral(&code, 2, 65u); AppendSMovLiteral(&code, 3, 101u); code.push_back(EncodeSMovB32(4, InlineU32(0))); code.push_back(EncodeSMovB32(5, InlineU32(0))); code.push_back(EncodeSop1(0x1e, 4, 2)); AppendStoreSgprPair(&code, 4, 10); code.push_back(EncodeSop1(0x1e, 4, 3)); AppendStoreSgprPair(&code, 4, 12); code.push_back(EncodeSop1(0x1c, 4, 2)); AppendStoreSgprPair(&code, 4, 14); code.push_back(EncodeSop1(0x1c, 4, 3)); AppendStoreSgprPair(&code, 4, 16); code.push_back(EncodeSopp(0x0f, 3)); AppendStoreSgpr(&code, 10, 0); AppendStoreSgpr(&code, 11, 1); AppendStoreSgpr(&code, 12, 2); AppendStoreSgpr(&code, 13, 3); AppendStoreSgpr(&code, 14, 4); AppendStoreSgpr(&code, 20, 5); AppendStoreSgpr(&code, 21, 6); AppendStoreSgpr(&code, 22, 7); AppendStoreSgpr(&code, 23, 8); AppendStoreSgpr(&code, 24, 9); AppendEnd(&code); TestCase test; test.name = "ScalarAbsdiffBitset64AndSetprio"; test.code = std::move(code); test.expected = {3u, 1u, 0x80000000u, 0x7fffffffu, 0u, 1u, 1u, 1u, 1u, 0u, 2u, 0u, 2u, 32u, 0u, 32u, 0u, 0u}; test.opcodes = {O::S_MOV_B32, O::S_ABSDIFF_I32, O::S_CSELECT_B32, O::S_BITSET1_B64, O::S_BITSET0_B64, O::S_SETPRIO, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_ABSDIFF_I32", 5}, {"S_BITSET1_B64", 2}, {"S_BITSET0_B64", 2}, {"S_SETPRIO 0x00000003", 1}}; return test; } TestCase ScalarShiftLeftAddSccCarryEdges() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; auto append_case = [&](bool prior_scc, u32 instruction, u32 out_sgpr) { set_scc(prior_scc); code.push_back(instruction); capture_scc(out_sgpr); }; AppendSMovLiteral(&code, 0, 0x80000000u); AppendSMovLiteral(&code, 1, 0x40000000u); AppendSMovLiteral(&code, 2, 0x20000000u); AppendSMovLiteral(&code, 3, 0x10000000u); code.push_back(EncodeSMovB32(4, InlineU32(1))); code.push_back(EncodeSMovB32(5, InlineU32(0))); append_case(false, EncodeSop2(0x2e, 10, 0, 5), 20); append_case(true, EncodeSop2(0x2e, 11, 4, 4), 21); append_case(false, EncodeSop2(0x2f, 12, 1, 5), 22); append_case(true, EncodeSop2(0x2f, 13, 4, 4), 23); append_case(false, EncodeSop2(0x30, 14, 2, 5), 24); append_case(true, EncodeSop2(0x30, 15, 4, 4), 25); append_case(false, EncodeSop2(0x31, 16, 3, 5), 26); append_case(true, EncodeSop2(0x31, 17, 4, 4), 27); for (u32 i = 0; i < 8u; i++) { AppendStoreSgpr(&code, 20u + i, i); } AppendEnd(&code); return {"ScalarShiftLeftAddSccCarryEdges", code, {}, {1, 0, 1, 0, 1, 0, 1, 0}, {O::S_MOV_B32, O::S_LSHL1_ADD_U32, O::S_LSHL2_ADD_U32, O::S_LSHL3_ADD_U32, O::S_LSHL4_ADD_U32, O::S_CMP_EQ_U32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarCompareOps() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0xfffffffeu); code.push_back(EncodeSMovB32(1, InlineU32(1))); code.push_back(EncodeSMovB32(2, InlineU32(5))); code.push_back(EncodeSMovB32(3, InlineU32(2))); code.push_back(EncodeSMovB32(4, InlineU32(3))); AppendSMovLiteral(&code, 10, 0x00000001u); code.push_back(EncodeSMovB32(11, InlineU32(0))); code.push_back(EncodeSMovB32(12, InlineU32(2))); code.push_back(EncodeSMovB32(13, InlineU32(0))); code.push_back(EncodeSMovB32(14, InlineU32(1))); code.push_back(EncodeSMovB32(15, InlineU32(1))); u32 dst = 20; auto append_compare = [&](u32 opcode, u32 src0, u32 src1) { code.push_back(EncodeSopc(opcode, src0, src1)); code.push_back(EncodeSop2(0x0a, dst++, InlineU32(1), InlineU32(0))); }; append_compare(0x00, 2, 2); append_compare(0x01, 0, 1); append_compare(0x02, 1, 0); append_compare(0x03, 1, 1); append_compare(0x04, 0, 1); append_compare(0x05, 0, 0); append_compare(0x07, 3, 4); append_compare(0x08, 4, 3); append_compare(0x09, 4, 4); append_compare(0x0b, 3, 4); append_compare(0x12, 10, 10); append_compare(0x12, 10, 12); append_compare(0x12, 10, 14); append_compare(0x13, 10, 12); append_compare(0x13, 10, 14); for (u32 i = 0; i < 15u; i++) { AppendStoreSgpr(&code, 20u + i, i); } AppendEnd(&code); return {"ScalarCompareOps", code, {}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1}, {O::S_MOV_B32, O::S_CMP_EQ_I32, O::S_CMP_LG_I32, O::S_CMP_GT_I32, O::S_CMP_GE_I32, O::S_CMP_LT_I32, O::S_CMP_LE_I32, O::S_CMP_LG_U32, O::S_CMP_GT_U32, O::S_CMP_GE_U32, O::S_CMP_LE_U32, O::S_CMP_EQ_U64, O::S_CMP_LG_U64, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarShiftAddAndMaskOps() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(2))); code.push_back(EncodeSMovB32(1, InlineU32(3))); code.push_back(EncodeSop2(0x2e, 2, 0, 1)); code.push_back(EncodeSop2(0x2f, 3, 0, 1)); code.push_back(EncodeSop2(0x30, 4, 0, 1)); code.push_back(EncodeSop2(0x31, 5, 0, 1)); AppendSMovLiteral(&code, 6, 0xfffffff8u); code.push_back(EncodeSop2(0x22, 7, 6, InlineU32(2))); AppendSMovLiteral(&code, 8, 0xffffffffu); code.push_back(EncodeSMovB32(9, InlineU32(2))); code.push_back(EncodeSop2(0x35, 10, 8, 9)); AppendSMovLiteral(&code, 12, 0x80000011u); AppendSMovLiteral(&code, 13, 0x80000001u); code.push_back(EncodeSop1(0x08, 14, 12)); code.push_back(EncodeSop1(0x0a, 16, 12)); AppendStoreSgpr(&code, 2, 0); AppendStoreSgpr(&code, 3, 1); AppendStoreSgpr(&code, 4, 2); AppendStoreSgpr(&code, 5, 3); AppendStoreSgpr(&code, 7, 4); AppendStoreSgpr(&code, 10, 5); AppendStoreSgprPair(&code, 14, 6); AppendStoreSgprPair(&code, 16, 8); AppendEnd(&code); return {"ScalarShiftAddAndMaskOps", code, {}, {7, 11, 19, 35, 0xfffffffeu, 1, 0x7fffffeeu, 0x7ffffffeu, 0xf00000ffu, 0xf000000fu}, {O::S_MOV_B32, O::S_LSHL1_ADD_U32, O::S_LSHL2_ADD_U32, O::S_LSHL3_ADD_U32, O::S_LSHL4_ADD_U32, O::S_ASHR_I32, O::S_MUL_HI_U32, O::S_NOT_B64, O::S_WQM_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarNotB64UpdatesScc() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; AppendSMovLiteral(&code, 0, 0xffffffffu); AppendSMovLiteral(&code, 1, 0xffffffffu); set_scc(true); code.push_back(EncodeSop1(0x08, 2, 0)); capture_scc(4); AppendSMovLiteral(&code, 6, 0xfffffffeu); AppendSMovLiteral(&code, 7, 0xffffffffu); set_scc(false); code.push_back(EncodeSop1(0x08, 8, 6)); capture_scc(10); code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 12, 106)); code.push_back(EncodeSop1(0x08, 14, 12)); capture_scc(16); AppendStoreSgpr(&code, 4, 0); AppendStoreSgpr(&code, 10, 1); AppendStoreSgprPair(&code, 2, 2); AppendStoreSgprPair(&code, 8, 4); AppendStoreSgpr(&code, 16, 6); AppendStoreSgprPair(&code, 14, 7); AppendEnd(&code); return {"ScalarNotB64UpdatesScc", code, {}, {0, 1, 0, 0, 1, 0, 1, 0xfffffffeu, 0xffffffffu}, {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_NOT_B64, O::S_CSELECT_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarQuadmaskB64() { using O = ShaderOpcode; std::vector code; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; AppendSMovLiteral(&code, 106, 0x80000011u); AppendSMovLiteral(&code, 107, 0x000000f0u); code.push_back(EncodeSop1(0x2d, 106, 106)); capture_scc(0); AppendStoreSgprPair(&code, 106, 0); AppendStoreSgpr(&code, 0, 2); code.push_back(EncodeSMovB32(106, InlineU32(0))); code.push_back(EncodeSMovB32(107, InlineU32(0))); code.push_back(EncodeSop1(0x2d, 106, 106)); capture_scc(0); AppendStoreSgprPair(&code, 106, 3); AppendStoreSgpr(&code, 0, 5); AppendEnd(&code); return {"ScalarQuadmaskB64", code, {}, {0x00000283u, 0, 1, 0, 0, 0}, {O::S_MOV_B32, O::S_QUADMASK_B64, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarFlbitI32B64Gpu() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0x00000000u); AppendSMovLiteral(&code, 1, 0x80000000u); code.push_back(EncodeSop1(0x16, 20, 0)); AppendSMovLiteral(&code, 2, 0x00008000u); AppendSMovLiteral(&code, 3, 0x00000000u); code.push_back(EncodeSop1(0x16, 21, 2)); AppendSMovLiteral(&code, 4, 0x00000000u); AppendSMovLiteral(&code, 5, 0x00000000u); code.push_back(EncodeSop1(0x16, 22, 4)); AppendSMovLiteral(&code, 14, 0x00000008u); AppendSMovLiteral(&code, 15, 0x00000000u); code.push_back(EncodeSop1(0x16, 106, 14)); AppendStoreSgpr(&code, 20, 0); AppendStoreSgpr(&code, 21, 1); AppendStoreSgpr(&code, 22, 2); AppendStoreSgpr(&code, 106, 3); AppendEnd(&code); return {"ScalarFlbitI32B64Gpu", code, {}, {0, 48, 0xffffffffu, 60}, {O::S_MOV_B32, O::S_FLBIT_I32_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarFlbitI32B32Gpu() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0x00000000u); code.push_back(EncodeSop1(0x15, 20, 0)); AppendSMovLiteral(&code, 0, 0x0000ccccu); code.push_back(EncodeSop1(0x15, 21, 0)); AppendSMovLiteral(&code, 0, 0x80000000u); code.push_back(EncodeSop1(0x15, 22, 0)); AppendSMovLiteral(&code, 0, 0x7fffffffu); code.push_back(EncodeSop1(0x15, 23, 0)); AppendStoreSgpr(&code, 20, 0); AppendStoreSgpr(&code, 21, 1); AppendStoreSgpr(&code, 22, 2); AppendStoreSgpr(&code, 23, 3); AppendEnd(&code); return {"ScalarFlbitI32B32Gpu", code, {}, {0xffffffffu, 16, 0, 1}, {O::S_MOV_B32, O::S_FLBIT_I32_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarSaveExecOps() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 106, 0xffffffffu); AppendSMovLiteral(&code, 107, 0xffffffffu); code.push_back(0xbe80246au); code.push_back(EncodeSop1(0x28, 2, 106)); code.push_back(EncodeSMovB32(106, InlineU32(0))); code.push_back(EncodeSMovB32(107, InlineU32(0))); code.push_back(EncodeSop1(0x37, 4, 106)); AppendSMovLiteral(&code, 106, 0x00000003u); code.push_back(EncodeSop1(0x3c, 8, 106)); code.push_back(EncodeSop1(0x04, 10, 126)); code.push_back(EncodeSMovB32(106, InlineU32(1))); code.push_back(EncodeSop1(0x44, 12, 106)); code.push_back(EncodeSop1(0x04, 14, 126)); AppendSMovLiteral(&code, 126, 0xffffffffu); AppendSMovLiteral(&code, 127, 0xffffffffu); AppendStoreSgprPair(&code, 0, 0); AppendStoreSgprPair(&code, 2, 2); AppendStoreSgprPair(&code, 4, 4); AppendStoreSgpr(&code, 8, 6); AppendStoreSgprPair(&code, 10, 7); AppendStoreSgpr(&code, 12, 9); AppendStoreSgprPair(&code, 14, 10); AppendStoreSgpr(&code, 253, 12); AppendEnd(&code); return {"ScalarSaveExecOps", code, {}, {1, 0, 1, 0, 0xffffffffu, 0xffffffffu, 0xffffffffu, 3, 0xffffffffu, 3, 2, 0xffffffffu, 1}, {O::S_MOV_B32, O::S_AND_SAVEEXEC_B64, O::S_ORN2_SAVEEXEC_B64, O::S_ANDN1_SAVEEXEC_B64, O::S_AND_SAVEEXEC_B32, O::S_ANDN1_SAVEEXEC_B32, O::S_MOV_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarOrn2SaveexecUsesSourceOrNotExec() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 126, 0x0000000cu); AppendSMovLiteral(&code, 127, 0x80000000u); AppendSMovLiteral(&code, 0, 0x00000001u); AppendSMovLiteral(&code, 1, 0x00000001u); code.push_back(EncodeSop1(0x28, 2, 0)); code.push_back(EncodeSop1(0x04, 4, 126)); code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeSMovB32(127, InlineU32(0))); AppendStoreSgprPair(&code, 2, 0); AppendStoreSgprPair(&code, 4, 2); AppendStoreSgpr(&code, 253, 4); AppendEnd(&code); return {"ScalarOrn2SaveexecUsesSourceOrNotExec", code, {}, {0x0000000cu, 0x80000000u, 0xfffffff3u, 0x7fffffffu, 1}, {O::S_MOV_B32, O::S_ORN2_SAVEEXEC_B64, O::S_MOV_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarOrn2SaveexecB32(u32 wave_size, u32 threads) { using O = ShaderOpcode; constexpr u32 exec_hi = 0xa5a55a5au; constexpr u32 vcc_hi = 0x13579bdfu; struct Case { u32 exec_lo, source, destination, result; }; constexpr Case cases[] = { {0x8000000cu, 126, 106, 0xffffffffu}, // Captured vcc_lo, exec_lo. {0x0000000fu, 6, 6, 0xfffffff3u}, // Aliased SGPR source/destination. {0xffffffffu, 128, 8, 0}, // SCC ignores the nonzero high half. {0xffffffffu, 107, 8, vcc_hi}, {0xffffffffu, 127, 8, exec_hi}, {0xffffffffu, 253, 8, 1}, // SCC is a scalar value, not a mask. {0xffffffffu, 251, 8, 0}, // VCCZ summarizes the whole mask. {0x00000001u, 252, 8, 0xfffffffeu}, // EXECZ is uniform across lanes. }; TestCase test; test.name = wave_size == 64 ? "ScalarOrn2SaveexecB32Wave64" : threads == 32 ? "ScalarOrn2SaveexecB32Wave32" : "ScalarOrn2SaveexecB32PartialWave"; auto &code = test.code; // Zero flags ignore high bits only in wave32 and include inactive low bits. constexpr u32 masks[][2] = {{0, 0}, {0x80000000u, 0}, {0, 0x80000000u}}; for (const auto &mask : masks) { AppendSMovLiteral(&code, 106, mask[0]); AppendSMovLiteral(&code, 107, mask[1]); AppendSMovLiteral(&code, 126, mask[0]); AppendSMovLiteral(&code, 127, mask[1]); code.push_back(EncodeSMovB32(20, 251)); code.push_back(EncodeSMovB32(21, 252)); code.push_back(EncodeSop1(0x04, 126, 193u)); const u32 zero = mask[0] == 0 && (wave_size == 32 || mask[1] == 0); for (u32 reg = 20; reg <= 21; ++reg) { AppendStoreSgprAtLaneDwordOffset(&code, reg, 0, static_cast(test.expected.size())); test.expected.insert(test.expected.end(), threads, zero); } } for (const auto &item : cases) { AppendVMovU32(&code, 2, 0); AppendSMovLiteral(&code, 126, item.exec_lo); AppendSMovLiteral(&code, 127, exec_hi); AppendSMovLiteral(&code, 106, 1); AppendSMovLiteral(&code, 107, vcc_hi); AppendSMovLiteral(&code, 6, 3); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(0))); code.push_back(item.source == 126 ? 0xbeea407eu : EncodeSop1(0x40, item.destination, item.source)); const u32 snapshots[] = {item.destination, 126, 127, 107, 253}; for (u32 i = 0; i < std::size(snapshots); ++i) { code.push_back(EncodeSMovB32(20 + i, snapshots[i])); } AppendVMovU32(&code, 2, 1); code.push_back(EncodeSop1(0x04, 126, 193u)); // Restore EXEC for readback. const u32 expected[] = {item.exec_lo, item.result, exec_hi, vcc_hi, item.result != 0 ? 1u : 0u}; for (u32 i = 0; i < std::size(expected); ++i) { AppendStoreSgprAtLaneDwordOffset(&code, 20 + i, 0, static_cast(test.expected.size())); test.expected.insert(test.expected.end(), threads, expected[i]); } AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, static_cast(test.expected.size())); for (u32 lane = 0; lane < threads; ++lane) { const u32 mask = lane < 32 ? item.result : exec_hi; test.expected.push_back((mask >> (lane % 32)) & 1u); } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_ORN2_SAVEEXEC_B32, O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_ORN2_SAVEEXEC_B32 vcc_lo, exec_lo", 1}}; test.compute_info.threads_num[0] = threads; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = wave_size; test.has_compute_info = true; return test; } TestCase ScalarSubvectorLoops(u32 wave_size) { using O = ShaderOpcode; using namespace ShaderRecompiler::Decoder; TestCase test; test.name = wave_size == 64 ? "ScalarSubvectorLoopsWave64" : "ScalarSubvectorLoopsWave32"; // The captured mesh shader's signed SOPK targets are relative to PC + 4. std::array captured{}; captured[0x30 / 4] = 0xbda60022u; captured[0xb8 / 4] = 0xbe26ffdeu; Instruction begin, end; DecodeInstruction(captured, 0x30 / 4, begin); DecodeInstruction(captured, 0xb8 / 4, end); Require(test.name, "captured branch decode", begin.opcode == O::S_SUBVECTOR_LOOP_BEGIN && begin.dst.reg == 38 && begin.dst.kind == OperandKind::Sgpr && begin.branch_target == 0xbc && end.opcode == O::S_SUBVECTOR_LOOP_END && end.dst.reg == 38 && end.dst.kind == OperandKind::Sgpr && end.branch_target == 0x34 && begin.word_count == 1 && end.word_count == 1, "subvector SDST or signed PC-relative target was decoded incorrectly"); constexpr u32 sentinel = 0x5a13abcdu; constexpr u32 low = 0x80000005u, high = 0x42000002u; struct Case { u32 lo, hi, body_mode, saved, final_lo, final_hi, passes; }; constexpr Case cases[] = { {0, 0, 0, sentinel, 0, 0, 0}, {low, 0, 0, 0, low, 0, 1}, {0, high, 0, 0, 0, high, 1}, {low, high, 0, low, low, high, 2}, {~0u, ~0u, 0, ~0u, ~0u, ~0u, 2}, // The game resets the full EXEC mask before END, selecting its HI != 0 arm. {low, high, 1, high, high, ~0u, 1}, // END must restore the low mask saved after the body has changed it. {low, high, 2, 0, 0, high, 2}, // BEGIN EXEC_LO first copies HI to its aliased destination, then clears HI. {low, high, 3, 0, 0, high, 2}, }; auto &code = test.code; for (u32 index = 0; index < std::size(cases); ++index) { const auto &item = cases[index]; const u32 scc = index & 1u; const u32 saved_reg = item.body_mode == 3 ? 126u : 38u; AppendVMovU32(&code, 2, 0); AppendVMovU32(&code, 3, 0); AppendSMovLiteral(&code, 38, sentinel); AppendSMovLiteral(&code, 32, 1); // Scalar 2^passes; MULK preserves SCC. AppendSMovLiteral(&code, 33, 0xff); AppendSMovLiteral(&code, 126, item.lo); AppendSMovLiteral(&code, 127, item.hi); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(scc == 0 ? 1 : 0))); const auto begin_word = static_cast(code.size()); code.push_back(0); const auto body_word = static_cast(code.size()); code.push_back(EncodeSMovB32(33, 253)); // Observe SCC immediately after BEGIN. code.push_back(EncodeSopk(0x10, 32, 2)); code.push_back(EncodeVop2(0x25, 2, InlineU32(1), 2)); code.push_back(EncodeVop1(0x01, 3, 32)); if (item.body_mode == 1) { code.push_back(EncodeSop1(0x04, 126, 193)); // S_MOV_B64 EXEC, -1. } else if (item.body_mode == 2) { code.push_back(EncodeSMovB32(126, InlineU32(0))); } const auto end_word = static_cast(code.size()); code.push_back(EncodeSopk(0x1c, saved_reg, body_word - end_word - 1u)); code[begin_word] = EncodeSopk(0x1b, saved_reg, end_word - begin_word); const u32 sources[] = {saved_reg, 126, 127, 253, 32, 33}; for (u32 i = 0; i < std::size(sources); ++i) { code.push_back(EncodeSMovB32(20 + i, sources[i])); } code.push_back(EncodeSop1(0x04, 126, 193)); // Restore all lanes for readback. const u32 expected[] = {item.saved, item.final_lo, item.final_hi, scc, 1u << item.passes, item.passes == 0 ? 0xff : scc}; for (u32 i = 0; i < std::size(expected); ++i) { AppendStoreSgprAtLaneDwordOffset(&code, 20 + i, 0, static_cast(test.expected.size())); test.expected.insert(test.expected.end(), wave_size, expected[i]); } for (u32 reg : {2u, 3u}) { AppendStoreVgprAtLaneDwordOffset(&code, reg, 0, static_cast(test.expected.size())); for (u32 lane = 0; lane < wave_size; ++lane) { const bool upper = lane >= 32; const u32 mask = upper || item.body_mode == 3 ? item.hi : item.lo; const bool active = ((mask >> (lane % 32)) & 1u) != 0 && !(upper && item.body_mode == 1); const u32 pass_value = upper && item.lo != 0 ? 4u : 2u; test.expected.push_back(active ? (reg == 2 ? 1u : pass_value) : 0u); } } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::S_SUBVECTOR_LOOP_BEGIN, O::S_SUBVECTOR_LOOP_END, O::S_MOV_B32, O::S_MOV_B64, O::S_MULK_I32, O::S_CMP_EQ_U32, O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = wave_size; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = wave_size; test.has_compute_info = true; return test; } TestCase ScalarGetpcWritesNextInstructionPc() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSop1(0x1f, 0, 0)); AppendStoreSgprPair(&code, 0, 0); AppendEnd(&code); return {"ScalarGetpcWritesNextInstructionPc", code, {}, {4, 0}, {O::S_GETPC_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarBitfieldPack() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(15))); code.push_back(EncodeSop1(0x0b, 1, 0)); AppendSMovLiteral(&code, 2, 0xf0000000u); code.push_back(EncodeSop1(0x10, 3, 0)); AppendSMovLiteral(&code, 19, 0xf0f00001u); code.push_back(EncodeSop1(0x0f, 20, 19)); code.push_back(EncodeSop2(0x0a, 21, InlineU32(1), InlineU32(0))); code.push_back(EncodeSMovB32(22, InlineU32(0))); code.push_back(EncodeSop1(0x0f, 23, 22)); code.push_back(EncodeSop2(0x0a, 24, InlineU32(1), InlineU32(0))); code.push_back(EncodeSMovB32(4, InlineU32(3))); code.push_back(EncodeSop1(0x3b, 5, 4)); code.push_back(EncodeSop2(0x24, 7, InlineU32(4), InlineU32(8))); AppendSMovLiteral(&code, 8, 0x00f00000u); AppendSMovLiteral(&code, 9, 0x00040014u); code.push_back(EncodeSop2(0x27, 10, 8, 9)); AppendSMovLiteral(&code, 11, 0xaaaabbbbu); AppendSMovLiteral(&code, 12, 0xccccddddu); code.push_back(EncodeSop2(0x32, 13, 11, 12)); code.push_back(EncodeSop2(0x33, 14, 11, 12)); code.push_back(EncodeSop2(0x34, 15, 11, 12)); code.push_back(EncodeSopc(0x0d, InlineU32(4), InlineU32(2))); code.push_back(EncodeSop2(0x0a, 17, InlineU32(1), InlineU32(0))); code.push_back(EncodeSopc(0x0c, InlineU32(4), InlineU32(1))); code.push_back(EncodeSop2(0x0a, 18, InlineU32(1), InlineU32(0))); AppendSMovLiteral(&code, 25, 0u); AppendSMovLiteral(&code, 26, 0x00000100u); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(1))); code.push_back(EncodeSop1(0x14, 31, 25)); code.push_back(EncodeSop2(0x0a, 34, InlineU32(1), InlineU32(0))); AppendSMovLiteral(&code, 27, 0x00000020u); AppendSMovLiteral(&code, 28, 0x00000100u); code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(1))); code.push_back(EncodeSop1(0x14, 32, 27)); code.push_back(EncodeSop2(0x0a, 35, InlineU32(1), InlineU32(0))); AppendSMovLiteral(&code, 29, 0u); AppendSMovLiteral(&code, 30, 0u); code.push_back(EncodeSop1(0x14, 33, 29)); AppendStoreSgpr(&code, 1, 0); AppendStoreSgpr(&code, 3, 1); AppendStoreSgpr(&code, 20, 2); AppendStoreSgpr(&code, 21, 3); AppendStoreSgpr(&code, 23, 4); AppendStoreSgpr(&code, 24, 5); AppendStoreSgprPair(&code, 5, 6); AppendStoreSgpr(&code, 7, 8); AppendStoreSgpr(&code, 10, 9); AppendStoreSgpr(&code, 13, 10); AppendStoreSgpr(&code, 14, 11); AppendStoreSgpr(&code, 15, 12); AppendStoreSgpr(&code, 17, 13); AppendStoreSgpr(&code, 18, 14); AppendStoreSgpr(&code, 31, 15); AppendStoreSgpr(&code, 32, 16); AppendStoreSgpr(&code, 33, 17); AppendStoreSgpr(&code, 34, 18); AppendStoreSgpr(&code, 35, 19); AppendEnd(&code); return {"ScalarBitfieldPack", code, {}, {0xf0000000u, 8, 9, 1, 0, 0, 0x0000000fu, 0, 0x00000f00u, 0x0000000fu, 0xddddbbbbu, 0xccccbbbbu, 0xccccaaaau, 1, 1, 40, 5, 0xffffffffu, 0, 1}, {O::S_MOV_B32, O::S_BREV_B32, O::S_BCNT1_I32_B32, O::S_BCNT1_I32_B64, O::S_FF1_I32_B64, O::S_BITREPLICATE_B64_B32, O::S_BFM_B32, O::S_BFE_U32, O::S_PACK_LL_B32_B16, O::S_PACK_LH_B32_B16, O::S_PACK_HH_B32_B16, O::S_BITCMP0_B32, O::S_BITCMP1_B32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarBrevB32PreservesScc() { using O = ShaderOpcode; std::vector code; auto set_scc = [&](bool value) { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(value ? 1u : 0u))); }; auto capture_scc = [&](u32 dst_sgpr) { code.push_back(EncodeSop2(0x0a, dst_sgpr, InlineU32(1), InlineU32(0))); }; code.push_back(EncodeSMovB32(0, InlineU32(1))); set_scc(false); code.push_back(EncodeSop1(0x0b, 1, 0)); capture_scc(2); code.push_back(EncodeSMovB32(3, InlineU32(0))); set_scc(true); code.push_back(EncodeSop1(0x0b, 4, 3)); capture_scc(5); AppendStoreSgpr(&code, 1, 0); AppendStoreSgpr(&code, 2, 1); AppendStoreSgpr(&code, 4, 2); AppendStoreSgpr(&code, 5, 3); AppendEnd(&code); return {"ScalarBrevB32PreservesScc", code, {}, {0x80000000u, 0, 0, 1}, {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_BREV_B32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarBfeI32CapturedRawSignExtends() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 0, 30); code.push_back(EncodeVop1(0x02, 20, Vgpr(0))); code.push_back(0x946aff14u); // s_bfe_i32 vcc_lo, s20, literal code.push_back(0x0004001cu); // offset 28, width 4 code.push_back(EncodeSop2(0x0a, 21, InlineU32(1), InlineU32(0))); code.push_back(EncodeSop2(0x28, 22, 20, 255u)); code.push_back(0u); // zero width produces zero and clears SCC code.push_back(EncodeSop2(0x0a, 23, InlineU32(1), InlineU32(0))); AppendStoreSgpr(&code, 106, 1); AppendStoreSgpr(&code, 21, 2); AppendStoreSgpr(&code, 22, 3); AppendStoreSgpr(&code, 23, 4); AppendEnd(&code); TestCase test; test.name = "ScalarBfeI32CapturedRawSignExtends"; test.code = std::move(code); test.initial = {0xf0000000u, 0, 0, 0, 0}; test.expected = {0xf0000000u, 0xffffffffu, 1, 0, 0}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_BFE_I32, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpBitFieldSExtract"}; return test; } TestCase BitfieldExtractWidthPastEndEdges() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0xf0000000u); AppendSMovLiteral(&code, 1, (20u << 16u) | 20u); code.push_back(EncodeSop2(0x27, 2, 0, 1)); AppendSMovLiteral(&code, 3, 0x80000000u); AppendSMovLiteral(&code, 4, (7u << 16u) | 31u); code.push_back(EncodeSop2(0x27, 5, 3, 4)); AppendVMovLiteral(&code, 6, 0xf0000000u); AppendVMovU32(&code, 7, 28); AppendVMovU32(&code, 8, 8); AppendVop3(&code, 0x148, 9, Vgpr(6), Vgpr(7), Vgpr(8)); AppendStoreSgpr(&code, 2, 0); AppendStoreSgpr(&code, 5, 1); AppendStoreVgpr(&code, 9, 2); AppendEnd(&code); return {"BitfieldExtractWidthPastEndEdges", code, {}, {0x00000f00u, 1, 0x0000000fu}, {O::S_MOV_B32, O::S_BFE_U32, O::V_MOV_B32, O::V_BFE_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Scalar64BitOps() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0x0f0f0f0fu); AppendSMovLiteral(&code, 1, 0x00ff00ffu); AppendSMovLiteral(&code, 2, 0x33333333u); AppendSMovLiteral(&code, 3, 0x0f0f0f0fu); code.push_back(EncodeSop2(0x0f, 4, 0, 2)); code.push_back(EncodeSop2(0x15, 6, 0, 2)); code.push_back(EncodeSop2(0x11, 8, 0, 2)); code.push_back(EncodeSop2(0x17, 10, 0, 2)); code.push_back(EncodeSop2(0x13, 12, 0, 2)); code.push_back(EncodeSop2(0x19, 14, 0, 2)); code.push_back(EncodeSop2(0x1b, 16, 0, 2)); code.push_back(EncodeSop2(0x1d, 18, 0, 2)); code.push_back(EncodeSop1(0x04, 20, 0)); code.push_back(EncodeSop2(0x1f, 22, 0, InlineU32(4))); code.push_back(EncodeSop2(0x21, 24, 0, InlineU32(8))); code.push_back(EncodeSop2(0x25, 26, InlineU32(36), InlineU32(4))); code.push_back(EncodeSop2(0x21, 34, 193u, InlineU32(1))); AppendSMovLiteral(&code, 28, 0x000c0004u); code.push_back(EncodeSop2(0x29, 30, 0, 28)); code.push_back(EncodeSopc(0x06, 0, 0)); code.push_back(EncodeSop2(0x0b, 32, 0, 2)); const u32 result_pairs[] = {4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 34, 30, 32}; u32 out = 0; for (auto sgpr : result_pairs) { AppendStoreSgprPair(&code, sgpr, out); out += 2; } AppendEnd(&code); return {"Scalar64BitOps", code, {}, {0x03030303u, 0x000f000fu, 0x0c0c0c0cu, 0x00f000f0u, 0x3f3f3f3fu, 0x0fff0fffu, 0xcfcfcfcfu, 0xf0fff0ffu, 0x3c3c3c3cu, 0x0ff00ff0u, 0xfcfcfcfcu, 0xfff0fff0u, 0xc0c0c0c0u, 0xf000f000u, 0xc3c3c3c3u, 0xf00ff00fu, 0x0f0f0f0fu, 0x00ff00ffu, 0xf0f0f0f0u, 0x0ff00ff0u, 0xff0f0f0fu, 0x0000ff00u, 0xfffffff0u, 0x000000ffu, 0xffffffffu, 0x7fffffffu, 0x000000f0u, 0, 0x0f0f0f0fu, 0x00ff00ffu}, {O::S_MOV_B32, O::S_MOV_B64, O::S_AND_B64, O::S_ANDN2_B64, O::S_OR_B64, O::S_ORN2_B64, O::S_XOR_B64, O::S_NAND_B64, O::S_NOR_B64, O::S_XNOR_B64, O::S_LSHL_B64, O::S_LSHR_B64, O::S_BFM_B64, O::S_BFE_U64, O::S_CMP_EQ_U32, O::S_CSELECT_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } uint64_t ReferenceAshrI64(uint64_t bits, u32 count) { const auto shift = count & 63u; // Complementing negative values expresses sign fill using unsigned shifts. return (bits & (uint64_t{1} << 63u)) != 0 ? ~(~bits >> shift) : bits >> shift; } TestCase ScalarAshrI64Edges(bool dynamic) { using O = ShaderOpcode; TestCase test; test.name = dynamic ? "ScalarAshrI64DynamicEdges" : "ScalarAshrI64ConstantEdges"; constexpr uint64_t sources[] = {0, 1, 0x80000000ull, 0x100000000ull, 0x7fffffffffffffffull, 0x8000000000000000ull, 0xffffffffffffffffull, 0x0123456789abcdefull, 0xfedcba9876543210ull}; constexpr u32 counts[] = {0, 1, 7, 31, 32, 33, 63, 64, 65, 95, 96, 127, 0xffffffffu}; test.initial.assign(std::begin(counts), std::end(counts)); for (const auto source : sources) { test.initial.push_back(static_cast(source)); test.initial.push_back(static_cast(source >> 32u)); } test.expected = test.initial; auto &code = test.code; const auto load_scalar = [&](u32 reg, u32 index) { AppendVMovU32(&code, 30, index * sizeof(u32)); AppendBufferLoadDword(&code, 1, 30); code.push_back(EncodeVop1(0x02, reg, Vgpr(1))); }; for (u32 source_index = 0; source_index < std::size(sources); ++source_index) { const auto source = sources[source_index]; // Runtime sources exercise both constant-count and dynamic-count emission. load_scalar(20, static_cast(std::size(counts)) + source_index * 2u); load_scalar(21, static_cast(std::size(counts)) + source_index * 2u + 1u); for (u32 index = 0; index < std::size(counts); ++index) { const auto expected = ReferenceAshrI64(source, counts[index]); if (dynamic) { load_scalar(22, index); } // Start SCC at the opposite value so every result must update it. code.push_back( EncodeSopc(0x06, InlineU32(0), InlineU32(expected != 0 ? 1u : 0u))); const auto count_src = dynamic ? 22u : counts[index] <= 64u ? InlineU32(counts[index]) : 255u; code.push_back(EncodeSop2(0x23, 24, 20, count_src)); if (!dynamic && count_src == 255u) { code.push_back(counts[index]); } code.push_back(EncodeSMovB32(26, 253)); const auto output = static_cast(test.expected.size()); AppendStoreSgprPair(&code, 24, output); AppendStoreSgpr(&code, 26, output + 2u); test.expected.push_back(static_cast(expected)); test.expected.push_back(static_cast(expected >> 32u)); test.expected.push_back(expected != 0 ? 1u : 0u); } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_ASHR_I64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = { {"S_ASHR_I64", std::size(sources) * std::size(counts)}}; test.required_spirv = {"OpShiftRightArithmetic"}; test.forbidden_spirv = {"OpTypeInt 64"}; return test; } TestCase ScalarAshrI64OperandsAndAliases() { using O = ShaderOpcode; TestCase test; test.name = "ScalarAshrI64OperandsAndAliases"; test.initial = {33}; // Runtime shift for signed literal source tests. test.expected = test.initial; auto &code = test.code; AppendVMovU32(&code, 30, 0); AppendBufferLoadDword(&code, 1, 30); code.push_back(EncodeVop1(0x02, 22, Vgpr(1))); size_t instructions = 0; const auto check_result = [&](u32 dst, u32 src0, u32 src1, uint64_t value, u32 count, std::optional literal = {}) { code.push_back(EncodeSop2(0x23, dst, src0, src1)); if (literal) { code.push_back(*literal); } code.push_back(EncodeSMovB32(26, 253)); const auto output = static_cast(test.expected.size()); AppendStoreSgprPair(&code, dst, output); AppendStoreSgpr(&code, 26, output + 2u); const auto expected = ReferenceAshrI64(value, count); test.expected.push_back(static_cast(expected)); test.expected.push_back(static_cast(expected >> 32u)); test.expected.push_back(expected != 0 ? 1u : 0u); ++instructions; }; for (const u32 literal : {0u, 0x7fffffffu, 0x80000000u, 0xffffffffu}) { const uint64_t value = literal | ((literal & 0x80000000u) != 0 ? 0xffffffff00000000ull : 0ull); for (const u32 count : {0u, 1u, 31u, 32u, 63u, 64u}) { check_result(24, 255, InlineU32(count), value, count, literal); } check_result(24, 255, 22, value, 33, literal); } for (const u32 source : {128u, 129u, 192u, 193u, 208u, 125u}) { const uint64_t value = source == 125u ? 0ull : source <= 192u ? source - 128u : uint64_t{192} - source; for (const u32 count : {0u, 1u, 32u, 63u}) { check_result(24, source, InlineU32(count), value, count); } } for (const u32 scc : {0u, 1u}) { // SCC may be either source and must be read before it is overwritten. code.push_back(EncodeSopc(0x06, InlineU32(scc), InlineU32(1))); check_result(24, 253, InlineU32(1), scc, 1); AppendSMovLiteral(&code, 20, 1); AppendSMovLiteral(&code, 21, 0); code.push_back(EncodeSopc(0x06, InlineU32(scc), InlineU32(1))); check_result(24, 20, 253, 1, scc); } constexpr uint64_t value = 0xfedcba9889abcdefull; for (const u32 count : {0u, 1u, 31u, 32u, 33u, 63u, 65u}) { AppendSMovLiteral(&code, 20, static_cast(value)); AppendSMovLiteral(&code, 21, static_cast(value >> 32u)); AppendSMovLiteral(&code, 23, count); check_result(20, 20, 23, value, count); // In-place source/destination pair. for (const u32 count_reg : {24u, 25u}) { AppendSMovLiteral(&code, 20, static_cast(value)); AppendSMovLiteral(&code, 21, static_cast(value >> 32u)); AppendSMovLiteral(&code, count_reg, count); check_result(24, 20, count_reg, value, count); // Count aliases destination. } } for (const u32 count_reg : {20u, 21u}) { const uint64_t alias_value = count_reg == 20u ? 0x8000000000000021ull : 0x0000002100000001ull; AppendSMovLiteral(&code, 20, static_cast(alias_value)); AppendSMovLiteral(&code, 21, static_cast(alias_value >> 32u)); check_result(20, 20, count_reg, alias_value, 33); } // Both operands may refer to the single literal word in the instruction. check_result(24, 255, 255, 0xffffffffffffffffull, 0xffffffffu, 0xffffffffu); for (const u32 source : {0u, 1u}) { // Discarding the destination must still update SCC and leave M0 untouched. AppendSMovLiteral(&code, 124, 0x5a13abcdu); AppendSMovLiteral(&code, 20, 0); AppendSMovLiteral(&code, 21, source); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(source))); code.push_back(EncodeSop2(0x23, 125, 20, InlineU32(0))); code.push_back(EncodeSMovB32(26, 253)); const auto output = static_cast(test.expected.size()); AppendStoreSgpr(&code, 26, output); AppendStoreSgpr(&code, 124, output + 1u); AppendStoreSgprPair(&code, 20, output + 2u); test.expected.insert(test.expected.end(), {source, 0x5a13abcdu, 0, source}); ++instructions; } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_ASHR_I64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_ASHR_I64", instructions}}; test.forbidden_spirv = {"OpTypeInt 64"}; return test; } TestCase ScalarAshrI64Masks(u32 wave_size) { using O = ShaderOpcode; TestCase test; test.name = wave_size == 64 ? "ScalarAshrI64MasksWave64" : "ScalarAshrI64MasksWave32"; auto &code = test.code; struct Case { uint64_t source; u32 count; }; constexpr Case cases[] = { {0x8000000100000001ull, 1}, {0x8000000100000001ull, 32}, {0x8000000100000001ull, 63}, {0x0000000100000000ull, 0}, {0x0000000100000000ull, 32}, {1, 1}}; for (const u32 mask_reg : {106u, 126u}) { for (const auto &item : cases) { const auto result = ReferenceAshrI64(item.source, item.count); AppendVMovU32(&code, 1, 1); AppendVMovU32(&code, 2, 0); AppendSMovLiteral(&code, mask_reg, static_cast(item.source)); AppendSMovLiteral(&code, mask_reg + 1u, static_cast(item.source >> 32u)); code.push_back( EncodeSopc(0x06, InlineU32(0), InlineU32(result != 0 ? 1u : 0u))); code.push_back( EncodeSop2(0x23, mask_reg, mask_reg, InlineU32(item.count))); code.push_back(EncodeSMovB32(20, mask_reg)); code.push_back(EncodeSMovB32(21, mask_reg + 1u)); code.push_back(EncodeSMovB32(22, 253)); if (mask_reg == 106u) { code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1)); } else { code.push_back(EncodeVop1(0x01, 2, InlineU32(1))); } code.push_back(EncodeSop1(0x04, 126, 193)); // Restore EXEC for readback. const u32 expected[] = {static_cast(result), static_cast(result >> 32u), result != 0 ? 1u : 0u}; for (u32 i = 0; i < std::size(expected); ++i) { AppendStoreSgprAtLaneDwordOffset( &code, 20 + i, 0, static_cast(test.expected.size())); test.expected.insert(test.expected.end(), wave_size, expected[i]); } AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, static_cast(test.expected.size())); for (u32 lane = 0; lane < wave_size; ++lane) { test.expected.push_back(static_cast((result >> lane) & 1u)); } } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_ASHR_I64, O::V_MOV_B32, O::V_CNDMASK_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_ASHR_I64 vcc_lo, vcc_lo", std::size(cases)}, {"S_ASHR_I64 exec_lo, exec_lo", std::size(cases)}}; test.forbidden_spirv = {"OpTypeInt 64"}; test.compute_info.threads_num[0] = wave_size; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = wave_size; test.has_compute_info = true; return test; } TestCase ScalarConditionalMoveB64() { using O = ShaderOpcode; TestCase test; test.name = "ScalarConditionalMoveB64"; auto &code = test.code; for (u32 scc : {0u, 1u}) { AppendSMovLiteral(&code, 0, 0x13579bdfu); AppendSMovLiteral(&code, 1, 0x2468ace0u); AppendSMovLiteral(&code, 2, 0xabcdef01u); AppendSMovLiteral(&code, 3, 0xfedcba98u); code.push_back(EncodeSopc(0x06, InlineU32(scc), InlineU32(1))); code.push_back(EncodeSop1(0x06, 0, 2)); code.push_back(EncodeSop2(0x0a, 4, InlineU32(1), InlineU32(0))); AppendStoreSgprPair(&code, 0, scc * 3); AppendStoreSgpr(&code, 4, scc * 3 + 2); } AppendEnd(&code); test.expected = {0x13579bdfu, 0x2468ace0u, 0, 0xabcdef01u, 0xfedcba98u, 1}; test.opcodes = {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_CMOV_B64, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase ScalarConditionalMoveB64PreservesMasks() { using O = ShaderOpcode; TestCase test; test.name = "ScalarConditionalMoveB64PreservesMasks"; auto &code = test.code; code.push_back(EncodeSop1(0x04, 16, 126)); // Save full EXEC. AppendVMovU32(&code, 1, 3); AppendVMovU32(&code, 2, 9); u32 offset = 0; for (u32 destination : {126u, 106u, 8u}) { for (u32 scc : {0u, 1u}) { code.push_back(EncodeVopc(0xc2, Vgpr(0), 1)); // Only lane 3. code.push_back(EncodeSop1(0x04, destination, 106)); code.push_back(EncodeSopc(0x06, InlineU32(scc), InlineU32(1))); // EXEC encoding is the captured Pathless instruction, 0xbefe06c1. code.push_back(EncodeSop1(0x06, destination, 193u)); code.push_back(EncodeSop2(0x0a, 4, InlineU32(1), InlineU32(0))); code.push_back(EncodeSop1(0x04, 126, destination)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, offset); code.push_back(EncodeSop1(0x04, 126, 16)); AppendStoreSgprAtLaneDwordOffset(&code, 4, 0, offset + 64); for (u32 lane = 0; lane < 64; ++lane) { test.expected.push_back(scc || lane == 3 ? 9u : 0xdeadbeefu); } test.expected.insert(test.expected.end(), 64, scc); offset += 128; } } AppendEnd(&code); test.initial.assign(test.expected.size(), 0xdeadbeefu); test.opcodes = {O::S_MOV_B64, O::V_MOV_B32, O::V_CMP_EQ_U32, O::S_CMP_EQ_U32, O::S_CMOV_B64, O::S_CSELECT_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = 64; test.has_compute_info = true; return test; } TestCase ScalarAndn2B64SccUsesMaskShadow() { using O = ShaderOpcode; std::vector code = { EncodeVopc(0xc2, InlineU32(0), 0), EncodeSop1(0x04, 0, 106), EncodeSop2(0x15, 4, 0, InlineU32(0)), EncodeVop1(0x01, 0, InlineU32(1)), EncodeSopp(0x04, 1), EncodeVop1(0x01, 0, InlineU32(7)), }; AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"ScalarAndn2B64SccUsesMaskShadow", code, {}, {7}, {O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_ANDN2_B64, O::V_MOV_B32, O::S_CBRANCH_SCC0, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarMaskHighWriteInvalidatesProvenance() { using O = ShaderOpcode; std::vector code = { EncodeVopc(0xc2, Vgpr(0), 0), EncodeSop1(0x04, 0, 106), EncodeSMovB32(1, InlineU32(1)), EncodeSop1(0x08, 2, 0), EncodeVop1(0x01, 0, InlineU32(1)), EncodeSopp(0x04, 1), EncodeVop1(0x01, 0, InlineU32(7)), }; AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"ScalarMaskHighWriteInvalidatesProvenance", code, {}, {7}, {O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_MOV_B32, O::S_NOT_B64, O::V_MOV_B32, O::S_CBRANCH_SCC0, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarSelectB64PreservesMaskProvenance() { using O = ShaderOpcode; std::vector code; const auto set_scc = [&] { code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(1))); }; const auto capture_scc = [&](u32 dst) { code.push_back(EncodeSop2(0x0a, dst, InlineU32(1), InlineU32(0))); }; AppendSMovLiteral(&code, 0, 0xfffffffeu); AppendSMovLiteral(&code, 1, 0xffffffffu); code.push_back(EncodeSMovB32(2, InlineU32(0))); code.push_back(EncodeSMovB32(3, InlineU32(0))); set_scc(); code.push_back(EncodeSop2(0x0b, 4, 0, 2)); code.push_back(EncodeSop1(0x08, 6, 4)); capture_scc(8); code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 10, 106)); set_scc(); code.push_back(EncodeSop2(0x0b, 12, 10, 2)); code.push_back(EncodeSop1(0x08, 14, 12)); capture_scc(16); AppendStoreSgpr(&code, 8, 0); AppendStoreSgpr(&code, 16, 1); AppendEnd(&code); return {"ScalarSelectB64PreservesMaskProvenance", code, {}, {1, 1}, {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_CSELECT_B64, O::S_NOT_B64, O::S_CSELECT_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarWqmB32Masks(u32 wave_size) { using O = ShaderOpcode; TestCase test; test.name = wave_size == 64 ? "ScalarWqmB32MasksWave64" : "ScalarWqmB32MasksWave32"; auto &code = test.code; const auto store_scalar = [&](u32 reg, u32 expected) { AppendStoreSgprAtLaneDwordOffset(&code, reg, 0, static_cast(test.expected.size())); test.expected.insert(test.expected.end(), wave_size, expected); }; const auto store_marker = [&](u32 lo, u32 hi) { AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, static_cast(test.expected.size())); for (u32 lane = 0; lane < wave_size; ++lane) { const u32 mask = lane < 32 ? lo : hi; test.expected.push_back(((mask >> (lane % 32)) & 1u) != 0 ? 9u : 0u); } }; constexpr u32 other_half = 0x2468ace0u; constexpr u32 masks[] = {0, 1, 8, 0x10, 0x100, 0x80000000u, 0x12481248u, ~0u}; for (u32 mask : masks) { u32 expanded = 0; for (u32 bit = 0; bit < 32; bit += 4) { if ((mask & (0xfu << bit)) != 0) expanded |= 0xfu << bit; } AppendVMovU32(&code, 2, 0); AppendSMovLiteral(&code, 126, mask); AppendSMovLiteral(&code, 127, other_half); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(mask == 0 ? 0 : 1))); code.push_back(0xbefe097eu); // Captured S_WQM_B32 EXEC_LO, EXEC_LO. code.push_back(EncodeSMovB32(20, 126)); code.push_back(EncodeSMovB32(21, 127)); code.push_back(EncodeSMovB32(22, 253)); AppendVMovU32(&code, 2, 9); code.push_back(EncodeSop1(0x04, 126, 193)); store_scalar(20, expanded); store_scalar(21, other_half); store_scalar(22, mask != 0 ? 1u : 0u); store_marker(expanded, other_half); } // VALU-produced masks keep WQM dynamic and carry provenance through S_MOV_B64. // Updating either word must invalidate an old SGPR-pair predicate and preserve // the other word. Wave32 still reads/writes the scalar high half independently. for (u32 base : {8u, 106u, 126u}) { for (u32 half : {0u, 1u}) { AppendVMovU32(&code, 2, 0); code.push_back(EncodeVop2(0x1b, 1, InlineU32(31), 0)); code.push_back(EncodeVopc(0xc2, InlineU32(3), 1)); // Lanes 3 and 35. code.push_back(EncodeSop1(0x04, base, 106)); code.push_back(EncodeSop1(0x09, base + half, base + half)); code.push_back(EncodeSMovB32(20, base)); code.push_back(EncodeSMovB32(21, base + 1)); code.push_back(EncodeSMovB32(22, 253)); code.push_back(EncodeSop1(0x04, 126, base)); AppendVMovU32(&code, 2, 9); code.push_back(EncodeSop1(0x04, 126, 193)); const u32 lo = half == 0 ? 0xfu : 8u; const u32 hi = wave_size == 32 ? 0u : half == 1 ? 0xfu : 8u; store_scalar(20, lo); store_scalar(21, hi); store_scalar(22, wave_size == 32 && half == 1 ? 0u : 1u); store_marker(lo, hi); } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::S_WQM_B32, O::S_MOV_B32, O::S_MOV_B64, O::S_CMP_EQ_U32, O::V_CMP_EQ_U32, O::V_MOV_B32, O::V_AND_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"S_WQM_B32 exec_lo, exec_lo", 9}}; test.compute_info.threads_num[0] = wave_size; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = wave_size; test.has_compute_info = true; return test; } TestCase ScalarWqmB64SelectsSccDomain() { using O = ShaderOpcode; std::vector code; const auto capture_scc = [&](u32 dst) { code.push_back(EncodeSop2(0x0a, dst, InlineU32(1), InlineU32(0))); }; code.push_back(EncodeSMovB32(0, InlineU32(0))); code.push_back(EncodeSMovB32(1, InlineU32(1))); code.push_back(EncodeSop1(0x0a, 2, 0)); capture_scc(4); code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 6, 106)); code.push_back(EncodeSop1(0x08, 8, 6)); code.push_back(EncodeSop1(0x0a, 10, 8)); capture_scc(12); AppendStoreSgpr(&code, 4, 0); AppendStoreSgpr(&code, 12, 1); AppendEnd(&code); return {"ScalarWqmB64SelectsSccDomain", code, {}, {1, 1}, {O::S_MOV_B32, O::S_WQM_B64, O::S_CSELECT_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_NOT_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarWqmB64PreservesPartialMasks() { using O = ShaderOpcode; TestCase test; test.name = "ScalarWqmB64PreservesPartialMasks"; auto &code = test.code; code.push_back(EncodeSop1(0x04, 16, 126)); // Save full EXEC. AppendVMovU32(&code, 1, 3); AppendVMovU32(&code, 2, 9); u32 offset = 0; for (const u32 destination : {126u, 106u, 8u}) { code.push_back(EncodeVopc(0xc2, Vgpr(0), 1)); // Only lane 3. code.push_back(EncodeSop1(0x04, destination, 106)); code.push_back(EncodeSop1(0x0a, destination, destination)); code.push_back(EncodeSop1(0x04, 126, destination)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, offset); code.push_back(EncodeSop1(0x04, 126, 16)); offset += 64; } code.push_back(EncodeSMovB32(8, InlineU32(32))); // Raw bits 5 and 33. code.push_back(EncodeSMovB32(9, InlineU32(2))); code.push_back(EncodeSop1(0x0a, 10, 8)); code.push_back(EncodeSop1(0x04, 126, 10)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, offset); AppendEnd(&code); test.initial.assign(256, 0xdeadbeef); test.expected = test.initial; for (u32 lane = 0; lane < 64; ++lane) { for (u32 region = 0; region < 3; ++region) { if (lane < 4) test.expected[region * 64 + lane] = 9; } if ((lane >= 4 && lane < 8) || (lane >= 32 && lane < 36)) { test.expected[192 + lane] = 9; } } test.opcodes = {O::S_MOV_B64, O::S_MOV_B32, O::S_WQM_B64, O::V_MOV_B32, O::V_CMP_EQ_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase ScalarMaskProvenanceOverlapAndMixedBinary() { using O = ShaderOpcode; std::vector code; const auto capture_scc = [&](u32 dst) { code.push_back(EncodeSop2(0x0a, dst, InlineU32(1), InlineU32(0))); }; code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 0, 106)); code.push_back(EncodeSop1(0x04, 1, 0)); code.push_back(EncodeSop1(0x08, 3, 1)); capture_scc(5); code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 8, 106)); code.push_back(EncodeSMovB32(10, InlineU32(1))); code.push_back(EncodeSMovB32(11, InlineU32(0))); code.push_back(EncodeSop2(0x19, 12, 8, 10)); capture_scc(14); AppendStoreSgpr(&code, 5, 0); AppendStoreSgpr(&code, 14, 1); AppendEnd(&code); return {"ScalarMaskProvenanceOverlapAndMixedBinary", code, {}, {1, 1}, {O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_NOT_B64, O::S_CSELECT_B32, O::S_MOV_B32, O::S_NAND_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarLiteral() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeVop1(0x01, 0, 0)); AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"ScalarLiteral", code, {}, {0x12345678u}, {O::S_MOV_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMoves() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x00, 0, 0), EncodeVop1(0x01, 0, InlineU32(5)), EncodeVop1(0x01, 2, Vgpr(0)), }; AppendBufferStoreDword(&code, 2, 30); AppendEnd(&code); return {"VectorRegisterMoves", code, {}, {5}, {O::V_NOP, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3MoveAppliesFloatSourceModifiers() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 24, 0x40000000u); AppendVop3(&code, 0x181, 1, 24, 0, 0, 0, 0, false, 0, 0x1); AppendStoreVgpr(&code, 1, 0); AppendEnd(&code); TestCase test; test.name = "VectorVop3MoveAppliesFloatSourceModifiers"; test.code = code; test.expected = {0xc0000000u}; test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpFNegate"}; return test; } TestCase VectorIntegerOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xfffffff8u); code.push_back(EncodeVop1(0x01, 1, InlineU32(5))); code.push_back(EncodeVop1(0x01, 4, InlineU32(7))); AppendVMovLiteral(&code, 10, 0x01000001u); code.push_back(EncodeVop1(0x01, 11, InlineU32(2))); AppendVMovLiteral(&code, 12, 0x0f0f0f0fu); AppendVMovLiteral(&code, 13, 0x33333333u); AppendVMovLiteral(&code, 14, 0xfffffff0u); code.push_back(EncodeVop1(0x01, 15, InlineU32(4))); code.push_back(EncodeVop1(0x01, 29, InlineU32(15))); code.push_back(EncodeVop2(0x11, 2, Vgpr(0), 1)); code.push_back(EncodeVop2(0x12, 3, Vgpr(0), 1)); code.push_back(EncodeVop2(0x13, 5, Vgpr(0), 1)); code.push_back(EncodeVop2(0x14, 6, Vgpr(0), 1)); code.push_back(EncodeVop2(0x25, 7, Vgpr(4), 1)); code.push_back(EncodeVop2(0x26, 8, Vgpr(4), 1)); code.push_back(EncodeVop2(0x27, 9, InlineU32(3), 4)); code.push_back(EncodeVop2(0x26, 35, 249, 11)); code.push_back(EncodeVop2Sdwa(12, 6, 0, 0, 6)); code.push_back(EncodeVop2(0x27, 36, 249, 11)); code.push_back(EncodeVop2Sdwa(12, 6, 0, 0, 6)); code.push_back(EncodeVop2(0x0b, 16, Vgpr(10), 11)); code.push_back(EncodeVop2(0x09, 37, Vgpr(0), 1)); code.push_back(EncodeVop2(0x1b, 17, Vgpr(12), 13)); code.push_back(EncodeVop2(0x1c, 18, Vgpr(12), 13)); code.push_back(EncodeVop2(0x1d, 19, Vgpr(12), 13)); code.push_back(EncodeVop2(0x1e, 20, Vgpr(12), 13)); code.push_back(EncodeVop2(0x19, 21, Vgpr(11), 15)); code.push_back(EncodeVop2(0x1a, 22, InlineU32(4), 11)); code.push_back(EncodeVop2(0x15, 23, Vgpr(14), 15)); code.push_back(EncodeVop2(0x16, 24, InlineU32(4), 14)); code.push_back(EncodeVop2(0x17, 25, Vgpr(14), 15)); code.push_back(EncodeVop2(0x18, 26, InlineU32(4), 14)); code.push_back(EncodeVop1(0x37, 27, Vgpr(12))); code.push_back(EncodeVop1(0x38, 28, Vgpr(29))); code.push_back(EncodeVop1(0x3a, 30, Vgpr(11))); code.push_back(EncodeVop1(0x39, 32, Vgpr(11))); code.push_back(EncodeVopc(0xc2, Vgpr(4), 4)); code.push_back(EncodeVop2(0x01, 33, InlineU32(3), 4)); code.push_back(EncodeVop2(0x11, 34, 249, 4)); code.push_back(EncodeVop2Sdwa(0)); const u32 results[] = {2, 3, 5, 6, 7, 8, 9, 35, 36, 16, 37, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30, 32, 33, 34}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"VectorIntegerOps", code, {}, {0xfffffff8u, 5, 5, 0xfffffff8u, 12, 2, 4, 13, 0xfffffff3u, 2, 0xffffffd8u, 0x03030303u, 0x3f3f3f3fu, 0x3c3c3c3cu, 0xc3c3c3c3u, 32, 32, 0x0fffffffu, 0x0fffffffu, 0xffffffffu, 0xffffffffu, 0xf0f0f0f0u, 0xf0000000u, 1, 30, 7, 0xfffffff8u}, {O::V_MOV_B32, O::V_MIN_I32, O::V_MAX_I32, O::V_MIN_U32, O::V_MAX_U32, O::V_ADD_NC_U32, O::V_SUB_NC_U32, O::V_SUBREV_NC_U32, O::V_MUL_U32_U24, O::V_MUL_I32_I24, O::V_AND_B32, O::V_OR_B32, O::V_XOR_B32, O::V_XNOR_B32, O::V_LSHL_B32, O::V_LSHLREV_B32, O::V_LSHR_B32, O::V_LSHRREV_B32, O::V_ASHR_I32, O::V_ASHRREV_I32, O::V_NOT_B32, O::V_BFREV_B32, O::V_FFBL_B32, O::V_FFBH_U32, O::V_CMP_EQ_U32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorFfbhI32NativeAndVop3OnGpu() { using O = ShaderOpcode; constexpr std::array inputs = {0x00000000u, 0x40000000u, 0x80000000u, 0x0fffffffu, 0xffff0000u, 0xfffffffeu, 0xffffffffu, 0x80000000u}; std::vector code; for (u32 i = 0; i < inputs.size() - 1u; i++) { AppendVMovU32(&code, 30, i * 4u); AppendBufferLoadDword(&code, 14, 30); code.push_back(0x7e1c770eu); // v_ffbh_i32 v14, v14 AppendStoreVgpr(&code, 14, i); } AppendVMovU32(&code, 30, (inputs.size() - 1u) * 4u); AppendBufferLoadDword(&code, 14, 30); AppendVop3(&code, 0x1bbu, 15, Vgpr(14), 0); AppendStoreVgpr(&code, 15, inputs.size() - 1u); AppendEnd(&code); TestCase test; test.name = "VectorFfbhI32NativeAndVop3OnGpu"; test.code = std::move(code); test.initial.assign(inputs.begin(), inputs.end()); test.expected = {0xffffffffu, 1u, 1u, 4u, 16u, 31u, 0xffffffffu, 1u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_FFBH_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_FFBH_I32 v14, v14", inputs.size() - 1u}, {"V_FFBH_I32 v15, v14", 1u}}; return test; } TestCase Vop1SdwaFfblCapturedHighWordSource() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 2, 30); code.push_back(0x7e2074f9u); code.push_back(0x00050602u); // v_ffbl_b32 v16, v2.word1 AppendStoreVgpr(&code, 16, 0); AppendEnd(&code); TestCase test; test.name = "Vop1SdwaFfblCapturedHighWordSource"; test.code = std::move(code); test.initial = {0x00400000u}; test.expected = {6u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_FFBL_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_FFBL_B32 v16, v2.sdwa(sel=5,sext=0)", 1}}; test.ir_counts = {{" = BitFieldUExtract ", 1}, {" = FindILsb32 ", 1}}; test.required_spirv = {"OpBitFieldUExtract"}; return test; } TestCase Vop1SdwaNotCapturedByte0Source() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 2, 30); code.push_back(0x7e046ef9u); code.push_back(0x00000602u); // v_not_b32 v2, v2.byte0 AppendStoreVgpr(&code, 2, 0); AppendEnd(&code); TestCase test; test.name = "Vop1SdwaNotCapturedByte0Source"; test.code = std::move(code); test.initial = {0x12345678u}; test.expected = {0xffffff87u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_NOT_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_NOT_B32 v2, v2.sdwa(sel=0,sext=0)", 1}}; test.ir_counts = {{" = BitFieldUExtract ", 1}, {" = BitwiseNot32 ", 1}}; test.required_spirv = {"OpBitFieldUExtract", "OpNot"}; return test; } TestCase Vop1SdwaMovByteDestinations() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 3, 30); AppendVMovU32(&code, 30, 4); AppendBufferLoadDword(&code, 2, 30); code.push_back(EncodeVop1(0x01, 1, Vgpr(3))); code.push_back(0x7e0202f9u); code.push_back(0x00861280u); // Captured v_mov_b32 v1.byte2, 0, preserve. AppendStoreVgpr(&code, 1, 2); // RDNA2 table 88: select any byte, then pad, sign extend, or preserve. for (u32 dst_u = 0; dst_u < 3; dst_u++) { for (u32 dst_sel = 0; dst_sel < 4; dst_sel++) { code.push_back(EncodeVop1(0x01, 1, Vgpr(3))); code.push_back(EncodeVop1(0x01, 1, 249)); code.push_back(EncodeVop1Sdwa(2, dst_sel, dst_u)); AppendStoreVgpr(&code, 1, 3 + dst_u * 4 + dst_sel); } } AppendEnd(&code); TestCase test; test.name = "Vop1SdwaMovByteDestinations"; test.code = std::move(code); test.initial = {0xa1b2c3d4u, 0x12345680u}; test.expected = {0xa1b2c3d4u, 0x12345680u, 0xa100c3d4u, 0x00000080u, 0x00008000u, 0x00800000u, 0x80000000u, 0xffffff80u, 0xffff8000u, 0xff800000u, 0x80000000u, 0xa1b2c380u, 0xa1b280d4u, 0xa180c3d4u, 0x80b2c3d4u}; test.initial.resize(test.expected.size()); test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_MOV_B32 v1.sdwa(sel=2,sext=0), 0", 1}}; test.required_spirv = {"OpBitFieldInsert", "OpBitFieldSExtract", "OpBitwiseOr"}; return test; } TestCase Vop2SdwaSubNcExactByte2Destination() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 11, 2); code.push_back(0x4c1616f9u); code.push_back(0x0686128du); AppendStoreVgpr(&code, 11, 0); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaSubNcExactByte2Destination"; test.code = code; test.expected = {0x000b0002u}; test.opcodes = {O::V_MOV_B32, O::V_SUB_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase Vop2SdwaAddNcCapturedHighWordDestination() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 0, 30); code.push_back(EncodeVop1(0x02, 106, Vgpr(0))); AppendVMovLiteral(&code, 97, 0x00030004u); AppendVMovLiteral(&code, 4, 0xa1b2c3d4u); code.push_back(0x4a08c2f9u); code.push_back(0x0686156au); AppendStoreVgpr(&code, 4, 1); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaAddNcCapturedHighWordDestination"; test.code = std::move(code); test.initial = {0x00010002u, 0}; test.expected = {0x00010002u, 0x0006c3d4u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::V_MOV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpIAdd", "OpShiftLeftLogical", "OpBitwiseOr"}; return test; } TestCase Vop2SdwaAshrrevCapturedWord0SignExtends() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 1, 30); code.push_back(0x303202f9u); code.push_back(0x0c860686u); // v_ashrrev_i32 v25, 6, sign_extend(v1.word0) AppendStoreVgpr(&code, 25, 1); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaAshrrevCapturedWord0SignExtends"; test.code = std::move(code); test.initial = {0xabcd8000u, 0}; test.expected = {0xabcd8000u, 0xfffffe00u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_ASHRREV_I32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_ASHRREV_I32 v25, 6, v1.sdwa(sel=4,sext=1)", 1}}; test.ir_counts = {{" = BitFieldSExtract ", 1}, {" = ShiftRightArithmetic32 ", 1}}; test.required_spirv = {"OpBitFieldSExtract", "OpShiftRightArithmetic"}; return test; } TestCase Vop2SdwaLshrrevCapturedByte1Source() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 106, 4); AppendBufferLoadDword(&code, 8, 30); code.push_back(0x2c1210f9u); code.push_back(0x0186066au); // v_lshrrev_b32 v9, vcc_lo, v8.byte1 AppendStoreVgpr(&code, 9, 1); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaLshrrevCapturedByte1Source"; test.code = std::move(code); test.initial = {0x12348678u, 0}; test.expected = {0x12348678u, 8}; test.opcodes = {O::S_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_LSHRREV_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = { {"V_LSHRREV_B32 v9, vcc_lo, v8.sdwa(sel=1,sext=0)", 1}}; test.ir_counts = {{" = BitFieldUExtract ", 1}, {" = ShiftRightLogical32 ", 1}}; test.required_spirv = {"OpBitFieldUExtract", "OpShiftRightLogical"}; return test; } TestCase Vop2SdwaSubNcPreservesByteAndWordDestinations() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 3); for (u32 dst = 10; dst <= 15; dst++) { AppendVMovLiteral(&code, dst, 0xa1b2c3d4u); } for (u32 sel = 0; sel <= 5; sel++) { code.push_back(EncodeVop2(0x26, 10 + sel, 249, 0)); code.push_back( EncodeVop2Sdwa(InlineU32(20), sel, 2, 6, 6, 0, 0, 0, 0, 0, 0, 1)); AppendStoreVgpr(&code, 10 + sel, sel); } AppendEnd(&code); return {"Vop2SdwaSubNcPreservesByteAndWordDestinations", code, {}, {0xa1b2c311u, 0xa1b211d4u, 0xa111c3d4u, 0x11b2c3d4u, 0xa1b20011u, 0x0011c3d4u}, {O::V_MOV_B32, O::V_SUB_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop3CvtPkI16I32Captured() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 7, 30); AppendVMovU32(&code, 30, 4); AppendBufferLoadDword(&code, 16, 30); code.push_back(0xd76b0005u); code.push_back(0x00022107u); // v_cvt_pk_i16_i32 v5, v7, v16 AppendStoreVgpr(&code, 5, 2); AppendEnd(&code); TestCase test; test.name = "Vop3CvtPkI16I32Captured"; test.code = std::move(code); test.initial = {0x00018001u, 0xfffe7ffeu, 0}; test.expected = {0x00018001u, 0xfffe7ffeu, 0x7ffe8001u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::V_CVT_PK_I16_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"0x00000014: V_CVT_PK_I16_I32 v5, v7, v16\n", 1}}; test.ir_counts = {{" = BitwiseAnd32 ", 2}, {" = ShiftLeftLogical32 ", 1}, {" = BitwiseOr32 ", 1}}; test.required_spirv = {"OpBitwiseAnd", "OpShiftLeftLogical", "OpBitwiseOr"}; return test; } TestCase Vop3MulLoU16CapturedAndSelectors() { using O = ShaderOpcode; TestCase test; test.name = "Vop3MulLoU16CapturedAndSelectors"; // Distinct runtime halves expose source selection and prevent constant folding. test.initial = {0x00138001u, 0x0005ffffu, 0xffffffffu}; test.expected = {0x00138001u, 0x0005ffffu, 0xffffffffu, 0xa5a57fffu, 0xa5a5ffedu, 0xa5a58005u, 0xa5a5005fu, 0x7fff1234u, 0xffed1234u, 0x80051234u, 0x005f1234u, 0xffff0001u, 0x00231234u}; auto &code = test.code; AppendBufferLoadDword(&code, 12, 30); AppendVMovU32(&code, 30, 4); AppendBufferLoadDword(&code, 24, 30); u32 output = 3; for (u32 selectors : {0u, 1u, 2u, 3u, 8u, 9u, 10u, 11u}) { AppendVMovLiteral(&code, 2, 0xa5a51234u); if (selectors == 8u) { code.push_back(0xd7054002u); code.push_back(0x0002310cu); // Captured v_mul_lo_u16 v2.hi, v12.lo, v24.lo. } else { AppendVop3(&code, 0x305, 2, Vgpr(12), Vgpr(24), 0, 0, selectors); } AppendStoreVgpr(&code, 2, output++); } // 0xffff * 0xffff wraps to 1; aliasing preserves the original high half. AppendVMovU32(&code, 30, 8); AppendBufferLoadDword(&code, 12, 30); AppendVop3(&code, 0x305, 12, Vgpr(12), Vgpr(12), 0, 0, 1); AppendStoreVgpr(&code, 12, output++); AppendVMovLiteral(&code, 2, 0xa5a51234u); AppendVop3(&code, 0x305, 2, 255, Vgpr(24), 0, 0, 11); code.push_back(0x0007deadu); // Select literal high half: 7 * 5 = 35. AppendStoreVgpr(&code, 2, output); AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MUL_LO_U16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_MUL_LO_U16", 10}}; test.required_spirv = {"OpIMul"}; return test; } TestCase Vop3MadI16CapturedSelectorsAndSaturation() { using O = ShaderOpcode; struct MadCase { u32 lhs, rhs, addend, expected; u32 selectors = 0; bool clamp = false; u32 dst = 1; }; constexpr u32 a = 0x8000fffdu, b = 0xfffe0005u, c = 0xfff90007u; const std::array cases{{ {a, b, c, 0xa5a5fff8u}, // Captured: -3 * 5 + 7 = -8. {a, b, c, 0x8007beefu, 9}, // Source 0 high; destination high. {a, b, c, 0xa5a5000du, 2}, // Source 1 high: -3 * -2 + 7. {a, b, c, 0xa5a5ffeau, 4}, // Source 2 high: -3 * 5 - 7. {a, b, c, 0xfff9beefu, 15}, // All high: -32768 * -2 - 7 wraps. {a, b, c, 0xfff8fffdu, 8, false, 9}, // Destination aliases source 0. {0x7fffu, 1, 1, 0xa5a58000u}, {0x7fffu, 1, 1, 0x7fffbeefu, 8, true}, // Positive saturation. {0x8000u, 1, 0xffffu, 0xa5a57fffu}, {0x8000u, 1, 0xffffu, 0x8000beefu, 8, true}, // Negative saturation. {0x8000u, 0x8000u, 0xffffu, 0xa5a5ffffu}, {0x8000u, 0x8000u, 0xffffu, 0xa5a57fffu, 0, true}, {200, 200, 0xd8f0u, 0xa5a57530u, 0, true}, // 40000 - 10000 = 30000. {200, 0xff38u, 10000, 0xa5a58ad0u, 0, true}, // -40000 + 10000 = -30000. }}; TestCase test; test.name = "Vop3MadI16CapturedSelectorsAndSaturation"; for (const auto &entry : cases) { test.initial.insert(test.initial.end(), {entry.lhs, entry.rhs, entry.addend}); } test.expected = test.initial; auto &code = test.code; for (u32 i = 0; i < cases.size(); ++i) { const auto &entry = cases[i]; u32 offset = i * 12u; for (u32 reg : {9u, 6u, 15u}) { AppendVMovU32(&code, 30, offset); AppendBufferLoadDword(&code, reg, 30); // Runtime inputs prevent folding. offset += 4u; } AppendVMovLiteral(&code, 1, 0xa5a5beefu); if (i == 0u) { code.insert(code.end(), {0xd75e0001u, 0x043e0d09u}); } else { AppendVop3(&code, 0x35e, entry.dst, Vgpr(9), Vgpr(6), Vgpr(15), 0, entry.selectors, entry.clamp); } AppendStoreVgpr(&code, entry.dst, static_cast(test.initial.size()) + i); test.expected.push_back(entry.expected); } AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MAD_I16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_MAD_I16", cases.size()}}; test.required_spirv = {"OpIMul", "OpIAdd"}; return test; } TestCase Vop3Med3I16Captured() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 5, 0x8ad003e8u); // high=-30000, low=1000 AppendSMovLiteral(&code, 25, 0xb1e04e20u); // high=-20000, low=20000 AppendSMovLiteral(&code, 27, 0x7530fc18u); // high=30000, low=-1000 AppendVMovLiteral(&code, 3, 0x1234cafeu); code.push_back(0xd7584803u); code.push_back(0x006c3305u); // v_med3_i16 v3.hi, v5.hi, s25.lo, s27.lo AppendStoreVgpr(&code, 3, 0); AppendEnd(&code); TestCase test; test.name = "Vop3Med3I16Captured"; test.code = std::move(code); test.expected = {0xfc18cafeu}; test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::V_MED3_I16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"0x00000020: V_MED3_I16 v3.sdwa(sel=5,sext=0), " "v5.opsel(lo=1,hi=0,neghi=0), s25, s27\n", 1}}; test.required_spirv = {"OpSLessThan"}; return test; } TestCase Vop2SdwaMinU32PreservesWordDestination() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 10, 0xa1b2c3d4u); AppendVMovU32(&code, 12, 7); code.push_back(0x261418f9u); code.push_back(0x0686149fu); AppendStoreVgpr(&code, 10, 0); AppendVMovLiteral(&code, 10, 0xa1b2c3d4u); AppendVMovU32(&code, 12, 64); code.push_back(0x261418f9u); code.push_back(0x0686149fu); AppendStoreVgpr(&code, 10, 1); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaMinU32PreservesWordDestination"; test.code = code; test.expected = {0xa1b20007u, 0xa1b2001fu}; test.opcodes = {O::V_MOV_B32, O::V_MIN_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpULessThan", "OpSelect"}; return test; } TestCase VectorShiftCountsMaskLowBits() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 1); AppendVMovU32(&code, 1, 32); AppendVMovU32(&code, 2, 33); AppendVMovLiteral(&code, 3, 0x80000000u); code.push_back(EncodeVop2(0x19, 10, Vgpr(0), 1)); code.push_back(EncodeVop2(0x1a, 11, Vgpr(2), 0)); code.push_back(EncodeVop2(0x15, 12, Vgpr(3), 1)); code.push_back(EncodeVop2(0x16, 13, Vgpr(2), 3)); code.push_back(EncodeVop2(0x17, 14, Vgpr(3), 1)); code.push_back(EncodeVop2(0x18, 15, Vgpr(2), 3)); for (u32 i = 0; i < 6u; i++) { AppendStoreVgpr(&code, 10u + i, i); } AppendEnd(&code); return {"VectorShiftCountsMaskLowBits", code, {}, {1, 2, 0x80000000u, 0x40000000u, 0x80000000u, 0xc0000000u}, {O::V_MOV_B32, O::V_LSHL_B32, O::V_LSHLREV_B32, O::V_LSHR_B32, O::V_LSHRREV_B32, O::V_ASHR_I32, O::V_ASHRREV_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3LshrrevB64Captured() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 106, 0x89abcdefu); AppendSMovLiteral(&code, 107, 0x01234567u); AppendVMovU32(&code, 12, 68); code.push_back(0xd7000001u); code.push_back(0x0000d50cu); // v_lshrrev_b64 v[1:2], v12, vcc AppendStoreVgpr(&code, 1, 0); AppendStoreVgpr(&code, 2, 1); AppendEnd(&code); TestCase test; test.name = "VectorVop3LshrrevB64Captured"; test.code = std::move(code); test.expected = {0x789abcdeu, 0x00123456u}; test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::V_LSHRREV_B64, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpShiftRightLogical", "OpSelect"}; test.forbidden_spirv = {"OpTypeInt 64"}; return test; } TestCase VectorVop3LshlrevB64Captured() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 3, 30); code.push_back(0xd6ff0021u); code.push_back(0x00010303u); // v_lshlrev_b64 v[33:34], v3, 1 AppendStoreVgpr(&code, 33, 1); AppendStoreVgpr(&code, 34, 2); code.push_back(EncodeVop2(0x03, 3, InlineU32(31), 3)); code.push_back(0xd6ff0021u); code.push_back(0x00010303u); AppendStoreVgpr(&code, 33, 3); AppendStoreVgpr(&code, 34, 4); AppendEnd(&code); TestCase test; test.name = "VectorVop3LshlrevB64Captured"; test.code = std::move(code); test.initial = {33u, 0u, 0u, 0u, 0u}; test.expected = {33u, 0u, 2u, 1u, 0u}; test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_LSHLREV_B64, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpShiftLeftLogical", "OpSelect"}; test.forbidden_spirv = {"OpTypeInt 64"}; return test; } TestCase VectorDynamicU64ShiftEdges() { using O = ShaderOpcode; TestCase test; test.name = "VectorDynamicU64ShiftEdges"; test.initial = {0u, 1u, 31u, 32u, 33u, 63u, 64u, 65u}; test.expected = test.initial; auto& code = test.code; for (const uint64_t source : {uint64_t{0}, uint64_t{1}, uint64_t{0x8000000000000000ull}, uint64_t{0xffffffffffffffffull}, uint64_t{0x0123456789abcdefull}}) { AppendVMovLiteral(&code, 4, static_cast(source)); AppendVMovLiteral(&code, 5, static_cast(source >> 32u)); for (u32 index = 0; index < test.initial.size(); ++index) { // A buffer load prevents the constant-shift specialization. AppendVMovU32(&code, 30, index * sizeof(u32)); AppendBufferLoadDword(&code, 3, 30); for (const bool left : {true, false}) { AppendVop3(&code, left ? 0x2ff : 0x300, 33, Vgpr(3), Vgpr(4)); const auto amount = test.initial[index] & 63u; const auto expected = left ? source << amount : source >> amount; const auto output = static_cast(test.expected.size()); AppendStoreVgpr(&code, 33, output); AppendStoreVgpr(&code, 34, output + 1u); test.expected.push_back(static_cast(expected)); test.expected.push_back(static_cast(expected >> 32u)); } } } AppendEnd(&code); test.initial.resize(test.expected.size()); test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_LSHLREV_B64, O::V_LSHRREV_B64, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpShiftLeftLogical", "OpShiftRightLogical", "OpSelect"}; test.forbidden_spirv = {"OpTypeInt 64"}; return test; } TestCase VectorVop3IntegerOps() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 0, InlineU32(2))); code.push_back(EncodeVop1(0x01, 1, InlineU32(3))); code.push_back(EncodeVop1(0x01, 2, InlineU32(4))); AppendVMovLiteral(&code, 3, 0xfffffff8u); code.push_back(EncodeVop1(0x01, 4, InlineU32(5))); AppendVMovLiteral(&code, 5, 0x11223344u); AppendVMovLiteral(&code, 6, 0x55667788u); AppendVMovLiteral(&code, 7, 0x0f0f0f0fu); AppendVMovLiteral(&code, 8, 0x33333333u); AppendVMovLiteral(&code, 9, 0xaaaaaaaau); AppendVMovLiteral(&code, 14, 0x00f00000u); code.push_back(EncodeVop1(0x01, 15, InlineU32(8))); AppendVMovLiteral(&code, 30, 0x00010000u); AppendVMovLiteral(&code, 31, 0x00010000u); AppendVop3(&code, 0x36d, 10, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x152, 11, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x155, 12, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x158, 13, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x153, 16, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x156, 17, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x159, 18, Vgpr(3), Vgpr(4), Vgpr(0)); AppendVop3(&code, 0x15d, 19, Vgpr(0), Vgpr(4), Vgpr(2)); AppendVop3(&code, 0x346, 20, Vgpr(1), Vgpr(0), Vgpr(2)); AppendVop3(&code, 0x347, 21, Vgpr(0), Vgpr(1), Vgpr(0)); AppendVop3(&code, 0x345, 22, Vgpr(7), Vgpr(8), Vgpr(0)); AppendVop3(&code, 0x36f, 23, Vgpr(1), Vgpr(2), Vgpr(0)); AppendVop3(&code, 0x371, 24, Vgpr(7), Vgpr(8), Vgpr(2)); AppendVop3(&code, 0x372, 25, Vgpr(7), Vgpr(8), Vgpr(2)); AppendVop3(&code, 0x178, 26, Vgpr(7), Vgpr(8), Vgpr(2)); AppendVop3(&code, 0x148, 27, Vgpr(14), InlineU32(20), Vgpr(2)); AppendVop3(&code, 0x149, 28, Vgpr(3), Vgpr(2), Vgpr(2)); AppendVop3(&code, 0x14a, 29, Vgpr(8), Vgpr(7), Vgpr(9)); AppendVop3(&code, 0x14e, 32, Vgpr(5), Vgpr(6), Vgpr(15)); AppendVop3(&code, 0x363, 33, Vgpr(2), Vgpr(15)); AppendVop3(&code, 0x169, 34, Vgpr(30), Vgpr(31)); AppendVop3(&code, 0x16a, 35, Vgpr(30), Vgpr(31)); AppendVop3(&code, 0x16b, 36, Vgpr(30), Vgpr(31)); AppendVop3(&code, 0x16c, 44, Vgpr(3), Vgpr(30)); AppendVop3(&code, 0x142, 37, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x143, 38, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x30f, 39, Vgpr(0), Vgpr(1)); AppendVop3(&code, 0x310, 40, Vgpr(0), Vgpr(1)); AppendVop3(&code, 0x319, 41, Vgpr(0), Vgpr(1)); const u32 results[] = {10, 11, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 32, 33, 34, 35, 36, 44, 37, 38, 39, 40, 41}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"VectorVop3IntegerOps", code, {}, {9, 0xfffffff8u, 5, 2, 2, 0xfffffff8u, 5, 7, 16, 20, 0x3c3c3c3eu, 0x32, 0x03030307u, 0x3f3f3f3fu, 0x3c3c3c38u, 0x0fu, 0xffffffffu, 0x8b8b8b8bu, 0x44556677u, 0x00000f00u, 0, 1, 0, 0xffffffffu, 10, 10, 5, 0xffffffffu, 1}, {O::V_MOV_B32, O::V_ADD3_U32, O::V_MIN3_I32, O::V_MAX3_I32, O::V_MED3_I32, O::V_MIN3_U32, O::V_MAX3_U32, O::V_MED3_U32, O::V_SAD_U32, O::V_LSHL_ADD_U32, O::V_ADD_LSHL_U32, O::V_XAD_U32, O::V_LSHL_OR_B32, O::V_AND_OR_B32, O::V_OR3_B32, O::V_XOR3_B32, O::V_BFE_U32, O::V_BFE_I32, O::V_BFI_B32, O::V_ALIGNBIT_B32, O::V_BFM_B32, O::V_MUL_LO_U32, O::V_MUL_HI_U32, O::V_MUL_LO_I32, O::V_MUL_HI_I32, O::V_MAD_I32_I24, O::V_MAD_U32_U24, O::V_ADD_I32, O::V_SUB_I32, O::V_SUBREV_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorBfeI32SignExtendsField() { using O = ShaderOpcode; std::vector code; for (u32 vgpr = 0; vgpr < 6; vgpr++) { AppendVMovU32(&code, 30, vgpr * sizeof(u32)); AppendBufferLoadDword(&code, vgpr, 30); } AppendVop3(&code, 0x149, 10, Vgpr(0), Vgpr(2), Vgpr(3)); AppendVop3(&code, 0x149, 11, Vgpr(1), Vgpr(4), Vgpr(5)); AppendStoreVgpr(&code, 10, 0); AppendStoreVgpr(&code, 11, 1); AppendEnd(&code); TestCase test; test.name = "VectorBfeI32SignExtendsField"; test.code = std::move(code); test.initial = {0x80000000u, 0xfffffff8u, 31, 1, 3, 4}; test.expected = {0xffffffffu, 0xffffffffu}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_BFE_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpBitFieldSExtract"}; return test; } TestCase VectorAlignByteUsesFiveBitByteOffset() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x11223344u); AppendVMovLiteral(&code, 1, 0x55667788u); constexpr u32 offsets[] = {0, 1, 3, 4, 5, 7, 8, 31}; for (u32 i = 0; i < static_cast(std::size(offsets)); i++) { AppendVMovU32(&code, 2, offsets[i]); AppendVop3(&code, 0x14f, 10u + i, Vgpr(0), Vgpr(1), Vgpr(2)); AppendStoreVgpr(&code, 10u + i, i); } AppendEnd(&code); return { "VectorAlignByteUsesFiveBitByteOffset", code, {}, {0x55667788u, 0x44556677u, 0x22334455u, 0x11223344u, 0x00112233u, 0x00000011u, 0u, 0u}, {O::V_MOV_B32, O::V_ALIGNBYTE_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorCarryAndBitCountOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xffffffffu); code.push_back(EncodeVop1(0x01, 1, InlineU32(0))); AppendVMovLiteral(&code, 3, 0x0000f0f0u); code.push_back(EncodeVop1(0x01, 4, InlineU32(5))); code.push_back(EncodeVopc(0xc7, Vgpr(1), 1)); code.push_back(EncodeVop2(0x28, 2, Vgpr(0), 1)); code.push_back(EncodeVop2(0x22, 5, Vgpr(3), 4)); AppendVMovLiteral(&code, 6, 0xffffffffu); code.push_back(EncodeVop2(0x23, 7, Vgpr(6), 4)); code.push_back(EncodeVop2(0x24, 8, Vgpr(6), 4)); AppendStoreVgpr(&code, 2, 0); AppendStoreSgpr(&code, 106, 1); AppendStoreVgpr(&code, 5, 2); AppendStoreVgpr(&code, 7, 3); AppendStoreVgpr(&code, 8, 4); AppendEnd(&code); return {"VectorCarryAndBitCountOps", code, {}, {0, 1, 13, 5, 5}, {O::V_MOV_B32, O::V_CMP_T_U32, O::V_ADDC_U32, O::V_BCNT_U32_B32, O::V_MBCNT_LO_U32_B32, O::V_MBCNT_HI_U32_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMbcntUsesThreadMask() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVop2(0x1a, 3, InlineU32(2), 1), }; AppendVMovLiteral(&code, 2, 0xffffffffu); code.push_back(EncodeVop1(0x01, 4, InlineU32(0))); code.push_back(EncodeVop2(0x23, 5, Vgpr(2), 4)); code.push_back(EncodeVop2(0x24, 6, Vgpr(2), 5)); AppendBufferStoreDword(&code, 6, 3); AppendEnd(&code); TestCase test; test.name = "VectorMbcntUsesThreadMask"; test.code = code; test.expected = {0, 1, 2, 3, 0, 1, 2, 3}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_MBCNT_LO_U32_B32, O::V_MBCNT_HI_U32_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase VectorAddcUsesPerLaneCarryIn() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVopc(0xc7, Vgpr(0), 0), EncodeVop1(0x01, 2, InlineU32(0)), EncodeVop2(0x28, 3, Vgpr(2), 2), EncodeVop2(0x1a, 4, InlineU32(2), 1), }; AppendBufferStoreDword(&code, 3, 4); AppendEnd(&code); TestCase test; test.name = "VectorAddcUsesPerLaneCarryIn"; test.code = code; test.expected = std::vector(8, 1); test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_T_U32, O::V_ADDC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase VectorAddcWritesPerLaneCarryOut() { using O = ShaderOpcode; std::vector code = {EncodeVop1(0x01, 1, Vgpr(0)), EncodeVop2(0x1a, 4, InlineU32(2), 1), EncodeVopc(0xc7, Vgpr(0), 0)}; AppendBufferLoadDword(&code, 26, 4); code.push_back(0x500434f9u); code.push_back(0x0c860680u); code.push_back(EncodeVop1(0x01, 6, 106)); AppendBufferStoreDword(&code, 6, 4); AppendEnd(&code); TestCase test; test.name = "VectorAddcWritesPerLaneCarryOut"; test.code = code; test.initial = {0x0000ffffu, 0u, 0x0000ffffu, 0u}; test.expected = {5u, 5u, 5u, 5u}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_CMP_T_U32, O::BUFFER_LOAD_DWORD, O::V_ADDC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = { {"V_ADDC_U32 v2, vcc_lo, 0, v26.sdwa(sel=4,sext=1), vcc_lo", 1}}; test.ir_counts = {{" = BitFieldSExtract ", 1}, {" = IAddCarry32 ", 2}}; test.required_spirv = {"OpBitFieldSExtract"}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorSubCoCiU32CompactAndVop3() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 0)); AppendBufferLoadDword(&code, 1, 4); code.push_back(EncodeVop2(0x25, 5, InlineU32(16), 4)); AppendBufferLoadDword(&code, 2, 5); code.push_back(EncodeVop2(0x1b, 3, InlineU32(1), 0)); code.push_back(EncodeVopc(0xc5, InlineU32(0), 3)); code.push_back(EncodeSop1(0x04, 20, 106)); code.push_back(EncodeVop2(0x29, 10, Vgpr(1), 2)); AppendVMovU32(&code, 6, 1); code.push_back(EncodeVop2(0x01, 11, InlineU32(0), 6)); AppendStoreVgprAtLaneDwordOffset(&code, 10, 0, 0); AppendStoreVgprAtLaneDwordOffset(&code, 11, 0, 4); code.push_back(EncodeVopc(0xc2, InlineU32(0), 6)); AppendVop3B(&code, 0x129u, 12, 22, Vgpr(1), Vgpr(2), 20); AppendVop3(&code, 0x101u, 13, InlineU32(0), Vgpr(6), 22); code.push_back(EncodeVop2(0x01, 14, InlineU32(0), 6)); AppendStoreVgprAtLaneDwordOffset(&code, 12, 0, 8); AppendStoreVgprAtLaneDwordOffset(&code, 13, 0, 12); AppendStoreVgprAtLaneDwordOffset(&code, 14, 0, 16); AppendEnd(&code); TestCase test; test.name = "VectorSubCoCiU32CompactAndVop3"; test.code = std::move(code); test.initial = {5u, 5u, 3u, 3u, 3u, 3u, 5u, 3u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}; test.expected = {2u, 1u, 0xfffffffeu, 0xffffffffu, 0u, 0u, 1u, 1u, 2u, 1u, 0xfffffffeu, 0xffffffffu, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_AND_B32, O::V_CMP_NE_U32, O::V_CMP_EQ_U32, O::V_SUB_CO_CI_U32, O::V_CNDMASK_B32, O::S_MOV_B64, O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_SUB_CO_CI_U32", 2}}; test.required_spirv = {"OpISub", "OpUGreaterThan", "OpLogicalOr"}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = 32; test.has_compute_info = true; return test; } TestCase VectorSubrevCoCiU32ExactRawOnGpu() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 24, Vgpr(0))); code.push_back(EncodeSop1(0x04, 20, 126)); // s_mov_b64 s[20:21], exec code.push_back(EncodeVop1(0x01, 1, InlineU32(2))); code.push_back(EncodeVop1(0x01, 2, InlineU32(1))); code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 0)); code.push_back(EncodeVop2(0x25, 5, InlineU32(16), 4)); code.push_back(EncodeVopc(0xc1, Vgpr(0), 1)); code.push_back(EncodeSop1(0x04, 22, 106)); // s_mov_b64 s[22:23], vcc code.push_back(EncodeVopc(0xc5, InlineU32(0), 0)); AppendVMovLiteral(&code, 0, 0xdeadbeefu); code.push_back(EncodeSop1(0x04, 126, 106)); // s_mov_b64 exec, vcc code.push_back(EncodeSop1(0x04, 106, 22)); // s_mov_b64 vcc, s[22:23] code.push_back(0x54003080u); // v_subrev_co_ci_u32 v0, vcc, 0, v24, vcc code.push_back(EncodeSop1(0x04, 126, 20)); // s_mov_b64 exec, s[20:21] code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 2)); AppendBufferStoreDword(&code, 0, 4); AppendBufferStoreDword(&code, 3, 5); AppendEnd(&code); TestCase test; test.name = "VectorSubrevCoCiU32ExactRawOnGpu"; test.code = code; test.expected = {0xdeadbeefu, 0, 2, 3, 0, 0, 0, 0}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_LT_U32, O::S_MOV_B64, O::V_CMP_NE_U32, O::V_SUBREV_CO_CI_U32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpISub", "OpUGreaterThan"}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; return test; } TestCase VectorVop3BSubrevCoCiUsesEncodedMasks() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 24, Vgpr(0))); code.push_back(EncodeVop2(0x25, 24, InlineU32(3), 24)); code.push_back(EncodeVop1(0x01, 1, InlineU32(2))); code.push_back(EncodeVop1(0x01, 2, InlineU32(1))); code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 0)); code.push_back(EncodeVop2(0x25, 5, InlineU32(16), 4)); code.push_back(EncodeVopc(0xc2, InlineU32(1), 0)); code.push_back(EncodeSop1(0x04, 20, 106)); // s_mov_b64 s[20:21], vcc AppendSMovLiteral(&code, 106, 0); AppendSMovLiteral(&code, 107, 0); AppendVop3B(&code, 0x12au, 10, 22, InlineU32(5), Vgpr(24), 20); AppendVop3(&code, 0x101u, 3, InlineU32(0), Vgpr(2), 22); AppendBufferStoreDword(&code, 10, 4); AppendBufferStoreDword(&code, 3, 5); AppendEnd(&code); TestCase test; test.name = "VectorVop3BSubrevCoCiUsesEncodedMasks"; test.code = code; test.expected = {0xfffffffeu, 0xfffffffeu, 0, 1, 1, 1, 0, 0}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_MOV_B32, O::V_SUBREV_CO_CI_U32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; return test; } TestCase VectorVop3BCarryOutWritesSgprMask() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0xffffffffu); AppendVMovU32(&code, 2, 1); AppendVMovU32(&code, 3, 0); AppendVop3B(&code, 0x30fu, 10, 0, Vgpr(1), Vgpr(2)); AppendStoreSgprAtLaneDwordOffset(&code, 0, 0, 0); AppendVop3B(&code, 0x310u, 11, 0, Vgpr(3), Vgpr(2)); AppendStoreSgprAtLaneDwordOffset(&code, 0, 0, 4); AppendVop3B(&code, 0x319u, 12, 0, Vgpr(2), Vgpr(3)); AppendStoreSgprAtLaneDwordOffset(&code, 0, 0, 8); AppendEnd(&code); TestCase test; test.name = "VectorVop3BCarryOutWritesSgprMask"; test.code = code; test.expected = std::vector(12, 15u); test.opcodes = {O::V_MOV_B32, O::V_ADD_I32, O::V_SUB_I32, O::V_SUBREV_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; return test; } TestCase VectorVop3BCarryOutUsesEncodedSdst() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0xffffffffu); AppendVMovU32(&code, 2, 1); AppendVMovU32(&code, 3, 0); AppendVop3B(&code, 0x30fu, 10, 20, Vgpr(1), Vgpr(2)); AppendStoreSgprAtLaneDwordOffset(&code, 20, 0, 0); AppendVop3B(&code, 0x310u, 11, 22, Vgpr(3), Vgpr(2)); AppendStoreSgprAtLaneDwordOffset(&code, 22, 0, 4); AppendVop3B(&code, 0x319u, 12, 24, Vgpr(2), Vgpr(3)); AppendStoreSgprAtLaneDwordOffset(&code, 24, 0, 8); AppendEnd(&code); TestCase test; test.name = "VectorVop3BCarryOutUsesEncodedSdst"; test.code = code; test.expected = std::vector(12, 15u); test.opcodes = {O::V_MOV_B32, O::V_ADD_I32, O::V_SUB_I32, O::V_SUBREV_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; return test; } TestCase VectorVop3BSubCoU32UsesRdna2Opcode310() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovU32(&code, 2, 1); AppendVMovU32(&code, 3, 1); AppendVMovLiteral(&code, 4, 0xffffffffu); AppendVMovLiteral(&code, 5, 0x80000000u); code.push_back(EncodeVopc(0xc2, InlineU32(1), 0)); code.push_back(EncodeVop2(0x01, 1, Vgpr(1), Vgpr(3))); code.push_back(EncodeVopc(0xc2, InlineU32(2), 0)); code.push_back(EncodeVop2(0x01, 1, Vgpr(1), Vgpr(4))); code.push_back(EncodeVopc(0xc2, InlineU32(3), 0)); code.push_back(EncodeVop2(0x01, 1, Vgpr(1), Vgpr(5))); code.push_back(EncodeVop2(0x01, 2, Vgpr(2), Vgpr(4))); AppendVop3B(&code, 0x310u, 10, 20, Vgpr(1), Vgpr(2)); AppendStoreVgprAtLaneDwordOffset(&code, 10, 0, 0); AppendStoreSgprAtLaneDwordOffset(&code, 20, 0, 4); AppendEnd(&code); TestCase test; test.name = "VectorVop3BSubCoU32UsesRdna2Opcode310"; test.code = code; test.expected = {0xffffffffu, 0, 0xfffffffeu, 0x80000001u, 9, 9, 9, 9}; test.opcodes = {O::V_MOV_B32, O::V_CMP_EQ_U32, O::V_CNDMASK_B32, O::V_SUB_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; return test; } TestCase VectorMadU64U32UnsignedCarryOut() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0xffffffffu); AppendVMovU32(&code, 2, 2); AppendVMovU32(&code, 4, 2); AppendVMovLiteral(&code, 5, 0xffffffffu); AppendVop3B(&code, 0x176u, 10, 20, Vgpr(1), Vgpr(2), Vgpr(4)); AppendStoreVgpr(&code, 10, 0); AppendStoreVgpr(&code, 11, 1); AppendStoreSgprPair(&code, 20, 2); AppendEnd(&code); return {"VectorMadU64U32UnsignedCarryOut", code, {}, {0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u}, {O::V_MOV_B32, O::V_MAD_U64_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorLaneAndPackedOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeVop1(0x01, 1, InlineU32(0))); AppendVMovLiteral(&code, 4, 0x40003c00u); AppendVMovLiteral(&code, 5, 0x44004200u); AppendVMovLiteral(&code, 6, 0x3c003c00u); AppendVMovLiteral(&code, 7, 0x3f800000u); AppendVMovLiteral(&code, 8, 0x40000000u); AppendVMovLiteral(&code, 9, 0x12345678u); AppendVMovLiteral(&code, 10, 0xabcdef01u); code.push_back(EncodeVop1(0x02, 0, Vgpr(0))); AppendVop3(&code, 0x360, 1, Vgpr(0), InlineU32(0)); AppendVop3(&code, 0x361, 1, 0, InlineU32(0)); AppendVop3(&code, 0x377, 2, Vgpr(0), InlineU32(0), InlineU32(0)); code.push_back(EncodeVop2(0x2f, 3, Vgpr(7), 8)); AppendVop3p(&code, 0x0f, 11, Vgpr(4), Vgpr(5), 0, 0x3); AppendVop3p(&code, 0x10, 12, Vgpr(4), Vgpr(5), 0, 0x3); AppendVop3p(&code, 0x11, 13, Vgpr(4), Vgpr(5), 0, 0x3); AppendVop3p(&code, 0x12, 14, Vgpr(4), Vgpr(5), 0, 0x3); AppendVop3p(&code, 0x0e, 15, Vgpr(4), Vgpr(5), Vgpr(6), 0x7); AppendVop3(&code, 0x362, 16, Vgpr(7), InlineU32(1)); AppendVop3(&code, 0x36a, 17, Vgpr(9), Vgpr(10)); AppendStoreSgpr(&code, 0, 0); AppendStoreSgpr(&code, 1, 1); const u32 results[] = {1, 2, 3, 11, 12, 13, 14, 15, 16, 17}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i + 2u); } AppendEnd(&code); return {"VectorLaneAndPackedOps", code, {}, {0x12345678u, 0x12345678u, 0x12345678u, 0x12345678u, 0x40003c00u, 0x46004400u, 0x48004200u, 0x40003c00u, 0x44004200u, 0x48804400u, 0x40000000u, 0xef015678u}, {O::V_MOV_B32, O::V_READFIRSTLANE_B32, O::V_READLANE_B32, O::V_WRITELANE_B32, O::V_PERMLANE16_B32, O::V_CVT_PKRTZ_F16_F32, O::V_PK_ADD_F16, O::V_PK_MUL_F16, O::V_PK_MIN_F16, O::V_PK_MAX_F16, O::V_PK_FMA_F16, O::V_LDEXP_F32, O::V_CVT_PK_U16_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop2PkFmacF16AccumulatesPackedHalvesIndependently() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x50004c00u); // low=16.0h, high=32.0h AppendVMovLiteral(&code, 1, 0x42003c00u); // low=1.0h, high=3.0h AppendVMovLiteral(&code, 2, 0x48004400u); // low=4.0h, high=8.0h code.push_back(EncodeVop2(0x3c, 0, Vgpr(1), 2)); AppendStoreVgpr(&code, 0, 0); code.push_back(EncodeVop2(0x3c, 0, Vgpr(1), 2)); AppendStoreVgpr(&code, 0, 1); AppendVMovLiteral(&code, 3, 0x50004c00u); code.push_back(EncodeVop2(0x3c, 3, 242, 2)); // Inline 1.0h is {1.0h, 0.0h}. AppendStoreVgpr(&code, 3, 2); AppendEnd(&code); return {"Vop2PkFmacF16AccumulatesPackedHalvesIndependently", code, {}, {0x53004d00u, 0x55004e00u, 0x50004d00u}, {O::V_MOV_B32, O::V_PK_FMAC_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop2PkFmacF16DppNegatesBothPackedHalves() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x50004c00u); // low=16.0h, high=32.0h AppendVMovLiteral(&code, 1, 0x42003c00u); // low=1.0h, high=3.0h AppendVMovLiteral(&code, 2, 0x48004400u); // low=4.0h, high=8.0h code.push_back(EncodeVop2(0x3c, 0, 250, 2)); code.push_back(EncodeVop2Dpp(1, 0, 0xf, 0xf, 1)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"Vop2PkFmacF16DppNegatesBothPackedHalves", code, {}, {0x48004a00u}, {O::V_MOV_B32, O::V_PK_FMAC_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop3pOpselHiUsesArchitecturalSourceBits() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x42003c00u); // low=1.0h, high=3.0h AppendVMovLiteral(&code, 1, 0x48004400u); // low=4.0h, high=8.0h AppendVMovLiteral(&code, 2, 0x50004c00u); // low=16.0h, high=32.0h // Raw V_PK_FMA_F16 words keep this test independent of the encoders above. code.insert(code.end(), {0xcc0e000au, 0x0c0a0300u}); // src0 high: 3*4+16=28 code.insert(code.end(), {0xcc0e000bu, 0x140a0300u}); // src1 high: 1*8+16=24 code.insert(code.end(), {0xcc0e400cu, 0x040a0300u}); // src2 high: 1*4+32=36 for (u32 i = 0; i < 3; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"Vop3pOpselHiUsesArchitecturalSourceBits", code, {}, {0x4f004d00u, 0x4e004d00u, 0x50804d00u}, {O::V_MOV_B32, O::V_PK_FMA_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop3pIntegerNegationCapturedAndSelectedHalves() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 6, 0xfed4ff38u); // (-200, -300) AppendVMovLiteral(&code, 3, 0x012c00c8u); // (200, 300) // Captured temporal shader: NEG_LO/HI turns 0x7fff into 0xffff (-1). // Adding that clamped offset must select (199,299), not collapse to (0,0). code.insert(code.end(), {0xcc074107u, 0x30020cffu, 0x00007fffu}); code.insert(code.end(), {0xcc0a4010u, 0x18020f03u}); AppendStoreVgpr(&code, 7, 0); AppendStoreVgpr(&code, 16, 1); AppendVMovLiteral(&code, 0, 0x80011234u); AppendVMovLiteral(&code, 1, 0xbeef5678u); // Independently negate src0 low and src1 high, then reverse both the // modified sources and src1 half selection. NEG flips the selected sign bit. AppendVop3p(&code, 0x0a, 10, Vgpr(0), Vgpr(1), 0, 0x3, 0, 0x2, 0x1); AppendVop3p(&code, 0x0a, 11, Vgpr(0), Vgpr(1), 0, 0x1, 0x2, 0x1, 0x2); AppendStoreVgpr(&code, 10, 2); AppendStoreVgpr(&code, 11, 3); AppendEnd(&code); return {"Vop3pIntegerNegationCapturedAndSelectedHalves", code, {}, {0xffffffffu, 0x012b00c7u, 0xbef0e8acu, 0x56795123u}, {O::V_MOV_B32, O::V_PK_MAX_I16, O::V_PK_ADD_U16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase CvtPkU8F32PacksSelectedByte() { using O = ShaderOpcode; std::vector code; // RDNA2 V_CVT_PK_U8_F32: uint8(S0) replaces byte S1[1:0] in S2. AppendVMovLiteral(&code, 0, 0x414c0000u); // 12.75f truncates to 12. AppendVMovLiteral(&code, 1, 0x43960000u); // 300.0f saturates to 255. AppendVMovLiteral(&code, 2, 0xbf800000u); // Negative values saturate to zero. AppendVMovLiteral(&code, 3, 0x7fc00000u); // NaN saturates to zero. AppendVMovLiteral(&code, 4, 0x11223344u); AppendVop3(&code, 0x15eu, 10, Vgpr(0), InlineU32(0), Vgpr(4)); AppendVop3(&code, 0x15eu, 11, Vgpr(1), InlineU32(5), Vgpr(4)); AppendVop3(&code, 0x15eu, 12, Vgpr(2), InlineU32(2), Vgpr(4)); AppendVop3(&code, 0x15eu, 13, Vgpr(3), InlineU32(3), Vgpr(4)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return { "CvtPkU8F32PacksSelectedByte", code, {}, {0x1122330cu, 0x1122ff44u, 0x11003344u, 0x00223344u}, {O::V_MOV_B32, O::V_CVT_PK_U8_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase CvtPkrtzF16F32SubnormalRoundsTowardZero() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x33c00000u); AppendVMovLiteral(&code, 1, 0xb3c00000u); code.push_back(EncodeVop2(0x2f, 2, Vgpr(0), 1)); AppendStoreVgpr(&code, 2, 0); AppendEnd(&code); return {"CvtPkrtzF16F32SubnormalRoundsTowardZero", code, {}, {0x80010001u}, {O::V_MOV_B32, O::V_CVT_PKRTZ_F16_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase CvtPkrtzF16F32SdwaAndOutputModifiers() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f000000u); // 0.5f AppendVMovLiteral(&code, 1, 0x40000000u); // 2.0f AppendVMovLiteral(&code, 2, 0x3f802000u); // exactly 0x3c01h AppendVMovLiteral(&code, 3, 0x00000000u); AppendVMovLiteral(&code, 4, 0xbf000000u); // -0.5f for (u32 dst = 10; dst <= 18; dst++) { AppendVMovLiteral(&code, dst, 0xa1b2c3d4u); } code.push_back(EncodeVop2(0x2f, 10, 249, 1)); code.push_back(EncodeVop2Sdwa(0, 6, 2, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 1)); code.push_back(EncodeVop2(0x2f, 11, 249, 3)); code.push_back(EncodeVop2Sdwa(2, 0, 0)); code.push_back(EncodeVop2(0x2f, 12, 249, 1)); code.push_back(EncodeVop2Sdwa(0, 6, 0, 3, 5)); code.push_back(EncodeVop2(0x2f, 13, 249, 1)); code.push_back(EncodeVop2Sdwa(4, 6, 0, 6, 6, 0, 0, 0, 1, 1)); code.push_back(EncodeVop2(0x2f, 14, 250, 1)); code.push_back(EncodeVop2Dpp(0, 0, 0xf, 0xf, 1)); AppendVop3(&code, 0x12f, 15, Vgpr(0), Vgpr(1), 0, 0, 0, true); code.push_back(EncodeVop2(0x2f, 16, 249, 1)); code.push_back(EncodeVop2Sdwa(0, 4, 2)); code.push_back(EncodeVop2(0x2f, 17, 249, 1)); code.push_back(EncodeVop2Sdwa(0, 6, 2, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)); AppendVop3(&code, 0x12f, 18, Vgpr(0), Vgpr(1), 0, 0, 0, false, 1); for (u32 dst = 10; dst <= 18; dst++) { AppendStoreVgpr(&code, dst, dst - 10); } AppendEnd(&code); return {"CvtPkrtzF16F32SdwaAndOutputModifiers", code, {}, {0x3c003800u, 0x00000001u, 0x00000000u, 0xc0003800u, 0x4000b800u, 0x3c003800u, 0xa1b23800u, 0x44003c00u, 0x44003c00u}, {O::V_MOV_B32, O::V_CVT_PKRTZ_F16_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase PackedMinMaxF16NanAndSignedZeroEdges() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x80004000u); AppendVMovLiteral(&code, 1, 0x00007e00u); AppendVMovLiteral(&code, 2, 0x00004000u); AppendVMovLiteral(&code, 3, 0x80007e00u); AppendVMovLiteral(&code, 4, 0x40007d01u); AppendVMovLiteral(&code, 5, 0x7d014000u); AppendVop3p(&code, 0x11, 10, Vgpr(0), Vgpr(1), 0, 0x3); AppendVop3p(&code, 0x12, 11, Vgpr(2), Vgpr(3), 0, 0x3); AppendVop3p(&code, 0x11, 12, Vgpr(4), Vgpr(5), 0, 0x3); AppendVop3p(&code, 0x12, 13, Vgpr(4), Vgpr(5), 0, 0x3); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"PackedMinMaxF16NanAndSignedZeroEdges", code, {}, {0x80004000u, 0x00004000u, 0x7f017f01u, 0x7f017f01u}, {O::V_MOV_B32, O::V_PK_MIN_F16, O::V_PK_MAX_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMinMaxF16Ops() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xaaaa4000u); // low=2.0h AppendVMovLiteral(&code, 1, 0xbbbb3c00u); // low=1.0h AppendVMovLiteral(&code, 2, 0x12345678u); AppendVMovLiteral(&code, 3, 0x87654321u); AppendVMovLiteral(&code, 4, 0x5555aaaau); AppendVMovLiteral(&code, 5, 0x6666bbbbu); AppendVMovLiteral(&code, 6, 0x7777bbbbu); AppendVMovLiteral(&code, 7, 0x8888bbbbu); AppendVMovLiteral(&code, 8, 0x9999bbbbu); AppendVMovLiteral(&code, 9, 0x99990000u); code.push_back(EncodeVop2(0x32, 5, Vgpr(0), 1)); code.push_back(EncodeVop2(0x33, 6, Vgpr(0), 1)); code.push_back(EncodeVop2(0x34, 7, Vgpr(0), 1)); code.push_back(EncodeVop2(0x35, 4, Vgpr(0), 1)); code.push_back(EncodeVop2(0x39, 2, Vgpr(0), 1)); code.push_back(EncodeVop2(0x3a, 3, Vgpr(0), 1)); code.push_back(EncodeVop2(0x35, 9, 249, 1)); code.push_back(EncodeVop2Sdwa(0, 4, 2, 4, 4)); code.push_back(0x660000f9u); code.push_back(EncodeVop2Sdwa(0, 4, 2, 4, 4)); AppendStoreVgpr(&code, 5, 0); AppendStoreVgpr(&code, 6, 1); AppendStoreVgpr(&code, 7, 2); AppendStoreVgpr(&code, 0, 3); AppendStoreVgpr(&code, 4, 4); AppendStoreVgpr(&code, 2, 5); AppendStoreVgpr(&code, 3, 6); AppendStoreVgpr(&code, 9, 7); AppendEnd(&code); return {"VectorMinMaxF16Ops", code, {}, {0x66664200u, 0x77773c00u, 0x8888bc00u, 0xaaaa0000u, 0x55554000u, 0x12344000u, 0x87653c00u, 0x99994000u}, {O::V_MOV_B32, O::V_ADD_F16, O::V_SUB_F16, O::V_SUBREV_F16, O::V_MUL_F16, O::V_MAX_F16, O::V_MIN_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorCvtU16F16Sdwa() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x45004200u); // low=3.0h, high=5.0h AppendVMovLiteral(&code, 1, 0xbc007c00u); // low=+inf, high=-1.0h AppendVMovLiteral(&code, 2, 0x00007e00u); // low=qNaN AppendVMovLiteral(&code, 10, 0x12345678u); AppendVMovLiteral(&code, 11, 0x87654321u); AppendVMovLiteral(&code, 12, 0xabcd1111u); AppendVMovLiteral(&code, 13, 0x77772222u); AppendVMovLiteral(&code, 14, 0x5555aaaau); code.push_back(EncodeVop1(0x52, 10, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 4)); code.push_back(EncodeVop1(0x52, 11, 249)); code.push_back(EncodeVop1Sdwa(0, 5, 2, 5)); code.push_back(EncodeVop1(0x52, 12, 249)); code.push_back(EncodeVop1Sdwa(1, 4, 2, 4)); code.push_back(EncodeVop1(0x52, 13, 249)); code.push_back(EncodeVop1Sdwa(1, 4, 2, 5)); code.push_back(EncodeVop1(0x52, 14, 249)); code.push_back(EncodeVop1Sdwa(2, 4, 2, 4)); for (u32 i = 0; i < 5; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"VectorCvtU16F16Sdwa", code, {}, {0x12340003u, 0x00054321u, 0xabcdffffu, 0x77770000u, 0x55550000u}, {O::V_MOV_B32, O::V_CVT_U16_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase NativeAndSdwa16BitDestinationWrites() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3fc00000u); // 1.5f -> 0x3e00h AppendVMovLiteral(&code, 1, 0x00004200u); // low=3.0h AppendVMovLiteral(&code, 2, 0x0000c000u); // low=-2.0h AppendVMovLiteral(&code, 3, 0x00003c00u); // low=1.0h AppendVMovLiteral(&code, 4, 0x00004000u); // low=2.0h const u32 sentinels[] = { 0x3c001234u, 0xbbbb2345u, 0xcccc3456u, 0xdddd4567u, 0xeeee5678u, 0xffff6789u, 0xf00d789au, 0x11118888u, 0x22229999u, 0x3333aaaau, 0xaaaa0000u, 0xbbbb0000u, 0xcccc0000u, 0xabcd1234u, 0xbcde2345u, 0xcdef3456u, 0xdef01234u, 0xef012345u, 0xf0123456u, 0x01234567u, 0xaaaa1234u, 0xbbbb5678u, 0x24681234u, 0x13572468u, 0x89abcdefu, 0xfedcba98u, 0x0bad1234u, }; // AppendStoreVgpr uses v31 as its address scratch register. const auto output_vgpr = [](u32 index) { return 10u + index + (index >= 21u ? 1u : 0u); }; for (u32 i = 0; i < static_cast(std::size(sentinels)); i++) { AppendVMovLiteral(&code, output_vgpr(i), sentinels[i]); } code.push_back(EncodeVop1(0x0a, 10, Vgpr(0))); code.push_back(EncodeVop1(0x0a, 11, 250)); code.push_back(EncodeVop1Dpp(0)); AppendVop3(&code, 0x18a, 12, Vgpr(0), 0); code.push_back(EncodeVop1(0x52, 13, Vgpr(1))); code.push_back(EncodeVop1(0x53, 14, Vgpr(2))); code.push_back(EncodeVop1(0x0a, 15, 249)); code.push_back(EncodeVop1Sdwa(0)); code.push_back(EncodeVop1(0x54, 16, 249)); code.push_back(EncodeVop1Sdwa(10, 4, 2, 5)); code.push_back(EncodeVop1(0x52, 17, 249)); code.push_back(EncodeVop1Sdwa(1, 6, 0, 4)); code.push_back(EncodeVop1(0x52, 18, 249)); code.push_back(EncodeVop1Sdwa(1, 4, 0, 4)); code.push_back(EncodeVop1(0x53, 19, 249)); code.push_back(EncodeVop1Sdwa(2, 4, 1, 4)); code.push_back(EncodeVop2(0x32, 20, 250, 4)); code.push_back(EncodeVop2Dpp(3)); AppendVop3(&code, 0x132, 21, Vgpr(3), Vgpr(4)); code.push_back(EncodeVop2(0x32, 22, 249, 4)); code.push_back(EncodeVop2Sdwa(3, 4, 0, 4, 4)); code.push_back(EncodeVop1(0x0a, 23, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 3)); code.push_back(EncodeVop1(0x0a, 24, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 5)); code.push_back(EncodeVop1(0x0a, 25, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 6, 0, 1, 1)); code.push_back(EncodeVop1(0x0a, 26, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 6, 0, 0, 0, 0, 0, 1)); code.push_back(EncodeVop1(0x0a, 27, 249)); code.push_back(EncodeVop1Sdwa(0, 4, 2, 6, 0, 0, 0, 0, 1)); AppendVop3(&code, 0x18a, 28, Vgpr(0), 0, 0, 0, 0, false, 1); AppendVop3(&code, 0x18a, 29, Vgpr(0), 0, 0, 0, 0, true); AppendVop3(&code, 0x34b, 30, Vgpr(3), Vgpr(4), Vgpr(3)); AppendVop3(&code, 0x34b, 32, Vgpr(3), Vgpr(4), Vgpr(3), 0, 0x8); code.push_back(EncodeVop1(0x52, 33, 249)); code.push_back(EncodeVop1Sdwa(1, 4, 2, 4, 0, 1, 1, 0, 1)); AppendVop3(&code, 0x1d3, 34, Vgpr(1), 0, 0, 1, 0, true, 0, 1); code.push_back(EncodeVop1(0x07, 35, 249)); code.push_back(EncodeVop1Sdwa(0, 6, 0, 6, 0, 1, 1, 0, 1)); AppendVop3(&code, 0x188, 36, Vgpr(0), 0, 0, 1, 0, true, 0, 1); code.push_back(EncodeVop1(0x54, 37, 249)); code.push_back(EncodeVop1Sdwa(4, 4, 2, 4, 0, 1, 1)); for (u32 i = 0; i < static_cast(std::size(sentinels)); i++) { AppendStoreVgpr(&code, output_vgpr(i), i); } AppendEnd(&code); return {"NativeAndSdwa16BitDestinationWrites", code, {}, {0x3c003e00u, 0xbbbb3e00u, 0xcccc3e00u, 0xdddd0003u, 0xeeeefffeu, 0x00003e00u, 0xf00d3c00u, 0x00000003u, 0x00000003u, 0xfffffffeu, 0xaaaa4200u, 0xbbbb4200u, 0x00004200u, 0xabcd0000u, 0xbcde0000u, 0xcdefbe00u, 0xdef04200u, 0xef013c00u, 0xf0124200u, 0x01233c00u, 0xaaaa4200u, 0x42005678u, 0x24680000u, 0x1357fffdu, 0x00000000u, 0xffffffffu, 0x0badb800u}, {O::V_MOV_B32, O::V_CVT_F16_F32, O::V_CVT_U32_F32, O::V_CVT_I32_F32, O::V_CVT_U16_F16, O::V_CVT_I16_F16, O::V_RCP_F16, O::V_ADD_F16, O::V_FMA_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMinMaxMed3F16Ops() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x40003c00u); // low=1.0h, high=2.0h AppendVMovLiteral(&code, 1, 0x44004200u); // low=3.0h, high=4.0h AppendVMovLiteral(&code, 2, 0xc8004000u); // low=2.0h, high=-8.0h AppendVMovLiteral(&code, 3, 0x00007e00u); // low=qNaN AppendVMovLiteral(&code, 4, 0x00004000u); // low=2.0h AppendVMovLiteral(&code, 5, 0x00003c00u); // low=1.0h AppendVMovLiteral(&code, 10, 0xaaaa5555u); AppendVMovLiteral(&code, 11, 0x12345678u); AppendVMovLiteral(&code, 12, 0x77772222u); AppendVop3(&code, 0x351, 10, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x354, 11, Vgpr(0), Vgpr(1), Vgpr(2), 0, 0xf, false, 0, 0x4); AppendVop3(&code, 0x357, 12, Vgpr(3), Vgpr(4), Vgpr(5)); for (u32 i = 0; i < 3; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"VectorMinMaxMed3F16Ops", code, {}, {0xaaaa3c00u, 0x48005678u, 0x77773c00u}, {O::V_MOV_B32, O::V_MIN3_F16, O::V_MAX3_F16, O::V_MED3_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorSpecialF16Ops() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xc000fc00u); // low=-inf, high=-2.0h AppendVMovLiteral(&code, 1, 0x00004400u); // low=+4.0h AppendVMovLiteral(&code, 2, 0x00003c00u); // low=+1.0h AppendVMovLiteral(&code, 3, 0x0000bc00u); // low=-1.0h AppendVMovLiteral(&code, 4, 0x0000fc00u); // low=-inf AppendVMovLiteral(&code, 11, 0x12345678u); code.push_back(EncodeVop1(0x54, 10, Vgpr(0))); code.push_back(EncodeVop1(0x54, 11, 249)); code.push_back(EncodeVop1Sdwa(0, 5, 2, 5)); code.push_back(EncodeVop1(0x56, 12, Vgpr(1))); code.push_back(EncodeVop1(0x57, 13, Vgpr(2))); code.push_back(EncodeVop1(0x57, 14, Vgpr(3))); code.push_back(EncodeVop1(0x58, 15, Vgpr(4))); AppendVop3(&code, 0x1d7, 16, Vgpr(3), 0, 0, 0, 0, true); for (u32 i = 0; i < 7; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"VectorSpecialF16Ops", code, {}, {0x00008000u, 0xb8005678u, 0x00003800u, 0x00000000u, 0x0000fe00u, 0x00000000u, 0x00000000u}, {O::V_MOV_B32, O::V_RCP_F16, O::V_RSQ_F16, O::V_LOG_F16, O::V_EXP_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorFractF16CapturedAndEdges() { using O = ShaderOpcode; // RDNA2 section 12.8: x - floor(x), rounded to half precision. // Load at runtime to exercise the emitted operation, including half subnormals. const std::array inputs{ 0x3d00u, 0xbd00u, 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x7bffu, 0xfbffu, 0x3800u, 0xb800u, 0x8c00u, 0x8c01u, 0x7c00u, 0xfc00u, 0x7e55u, 0x7d01u}; const std::array fractions{ 0x3400u, 0x3a00u, 0x0000u, 0x0000u, 0x0001u, 0x3c00u, 0x03ffu, 0x3c00u, 0x0000u, 0x0000u, 0x3800u, 0x3800u, 0x3c00u, 0x3bffu, 0x7e00u, 0x7e00u, 0x7e00u, 0x7e00u}; TestCase test; test.name = "VectorFractF16CapturedAndEdges"; for (u32 bits : inputs) { test.initial.push_back(0x42000000u | bits); // Distinct high half must be ignored. } test.expected = test.initial; auto &code = test.code; for (u32 i = 0; i < inputs.size(); i++) { AppendVMovU32(&code, 30, i * 4u); AppendBufferLoadDword(&code, 7, 30); AppendVMovLiteral(&code, 9, 0xa5a51234u); code.push_back(0x7e12bf07u); // Captured v_fract_f16 v9, v7. if (i >= 14u) { // Check quiet NaN class without constraining its sign or payload. code.push_back(EncodeVop2(0x1b, 9, 255u, 9)); code.push_back(0xffff7e00u); } AppendStoreVgpr(&code, 9, static_cast(inputs.size()) + i); test.expected.push_back(0xa5a50000u | fractions[i]); } AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_FRACT_F16, O::V_AND_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_FRACT_F16 v9, v7", inputs.size()}}; test.ir_counts = {{" = FPFract32 ", inputs.size()}}; test.required_spirv = {" Fract ", " PackHalf2x16 ", " UnpackHalf2x16 "}; test.forbidden_spirv = {"OpCapability Float16"}; return test; } TestCase VectorFractF16Modifiers() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 7, 0xbd003d00u); // high=-1.25h, low=+1.25h for (u32 dst = 10; dst <= 19; dst++) { AppendVMovLiteral(&code, dst, 0xabcd1234u); } code.push_back(EncodeVop1(0x5f, 10, 249)); code.push_back(EncodeVop1Sdwa(7, 5, 2, 5)); // high -> high, preserve low code.push_back(EncodeVop1(0x5f, 11, 249)); code.push_back(EncodeVop1Sdwa(7, 4, 0, 5, 0, 1, 1)); // -abs(-1.25), pad high code.push_back(EncodeVop1(0x5f, 12, 249)); code.push_back(EncodeVop1Sdwa(7, 4, 2, 5, 0, 0, 1)); // abs(-1.25) code.push_back(EncodeVop1(0x5f, 13, 249)); code.push_back(EncodeVop1Sdwa(7, 4, 2, 5, 0, 0, 0, 0, 1, 1)); // *2, clamp code.push_back(EncodeVop1(0x5f, 14, 250)); code.push_back(EncodeVop1Dpp(7, 0x0e4) | (1u << 20u)); // DPP identity, negate AppendVop3(&code, 0x1df, 15, Vgpr(7), 0, 0, 1, 0, true, 1, 1); code.push_back(EncodeVop1(0x5f, 16, 255)); code.push_back(0x4200bd00u); // literal -1.25h code.push_back(EncodeVop1(0x5f, 17, 240)); // inline +0.5, interpreted as FP16 AppendSMovLiteral(&code, 20, 0x4200bd00u); code.push_back(EncodeVop1(0x5f, 18, 20)); // SGPR source AppendVop3(&code, 0x1df, 19, Vgpr(7), 0, 0, 0, 0, false, 3); // *0.5 for (u32 i = 0; i < 10; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); TestCase test{"VectorFractF16Modifiers", code, {}, {0x3a001234u, 0x00003a00u, 0xabcd3400u, 0xabcd3c00u, 0xabcd3a00u, 0xabcd3c00u, 0xabcd3a00u, 0xabcd3800u, 0xabcd3a00u, 0xabcd3000u}, {O::V_MOV_B32, O::S_MOV_B32, O::V_FRACT_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_FRACT_F16", 10}}; test.ir_counts = {{" = FPFract32 ", 10}}; test.required_spirv = {" Fract ", " PackHalf2x16 "}; return test; } TestCase VectorCosF16CapturedSdwaAndEdges() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel code.push_back(0x7e08c2f9u); code.push_back(0x00051504u); // captured v_cos_f16 v4.word1, v4.word1 AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h code.push_back(EncodeVop1(0x61, 6, Vgpr(5))); AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite code.push_back(EncodeVop1(0x61, 8, Vgpr(7))); AppendVMovLiteral(&code, 9, 0x00007c00u); // +inf code.push_back(EncodeVop1(0x61, 10, Vgpr(9))); AppendVMovLiteral(&code, 11, 0x00000001u); // minimum positive denormal code.push_back(EncodeVop1(0x61, 12, Vgpr(11))); AppendVMovLiteral(&code, 13, 0x00003800u); // +0.5h code.push_back(EncodeVop1(0x61, 14, Vgpr(13))); for (u32 i = 0; i < 6; i++) { AppendStoreVgpr(&code, 4 + i * 2, i); } AppendEnd(&code); TestCase test{"VectorCosF16CapturedSdwaAndEdges", code, {}, {0x00001234u, 0x00003c00u, 0x00003c00u, 0x0000fe00u, 0x00003c00u, 0x0000bc00u}, {O::V_MOV_B32, O::V_COS_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_COS_F16", 6}, {"V_COS_F16 v4.sdwa(sel=5,sext=0), " "v4.sdwa(sel=5,sext=0)", 1}}; test.ir_counts = {{" = FPCos ", 6}}; test.required_spirv = {" Fract ", " Cos ", " PackHalf2x16 "}; test.forbidden_spirv = {"OpCapability Float16"}; return test; } TestCase VectorSinF16SdwaAndEdges() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel code.push_back(0x7e08c0f9u); code.push_back(0x00051504u); // v_sin_f16 v4.word1, v4.word1 AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h code.push_back(EncodeVop1(0x60, 6, Vgpr(5))); AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite code.push_back(EncodeVop1(0x60, 8, Vgpr(7))); AppendVMovLiteral(&code, 9, 0x0000fbffu); // minimum finite code.push_back(EncodeVop1(0x60, 10, Vgpr(9))); AppendVMovLiteral(&code, 11, 0x00007c00u); // +inf code.push_back(EncodeVop1(0x60, 12, Vgpr(11))); AppendVMovLiteral(&code, 13, 0x0000fc00u); // -inf code.push_back(EncodeVop1(0x60, 14, Vgpr(13))); AppendVMovLiteral(&code, 15, 0x00003800u); // +0.5h code.push_back(EncodeVop1(0x60, 16, Vgpr(15))); AppendVMovLiteral(&code, 17, 0x00003a00u); // +0.75h code.push_back(EncodeVop1(0x60, 18, Vgpr(17))); AppendVMovLiteral(&code, 19, 0x00003400u); // +0.25h AppendVop3(&code, 0x1e0, 20, Vgpr(19), 0); for (u32 i = 0; i < 9; i++) { AppendStoreVgpr(&code, 4 + i * 2, i); } AppendEnd(&code); TestCase test{"VectorSinF16SdwaAndEdges", code, {}, {0x3c001234u, 0x00008000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x0000fe00u, 0x00000000u, 0x0000bc00u, 0x00003c00u}, {O::V_MOV_B32, O::V_SIN_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_SIN_F16", 9}, {"V_SIN_F16 v4.sdwa(sel=5,sext=0), " "v4.sdwa(sel=5,sext=0)", 1}}; test.ir_counts = {{" = FPSin ", 9}}; test.required_spirv = {" Fract ", " Sin ", " PackHalf2x16 "}; test.forbidden_spirv = {"OpCapability Float16"}; return test; } TestCase VectorWritelaneIgnoresExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 2, 0xaaaaaaaau); AppendSMovLiteral(&code, 4, 0x12345678u); code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeSMovB32(127, InlineU32(0))); AppendVop3(&code, 0x361, 2, 4, InlineU32(1)); code.push_back(EncodeSMovB32(126, InlineU32(0xf))); code.push_back(EncodeSMovB32(127, InlineU32(0))); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "VectorWritelaneIgnoresExecMask"; test.code = code; test.expected = {0xaaaaaaaau, 0x12345678u, 0xaaaaaaaau, 0xaaaaaaaau}; test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::V_WRITELANE_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorReadlaneFromInactiveWrittenLane() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 4, 0x12345678u); AppendVop3(&code, 0x361, 2, 4, InlineU32(4)); AppendSMovLiteral(&code, 4, 0xdeadbeefu); AppendVop3(&code, 0x360, 5, Vgpr(2), InlineU32(4)); AppendStoreSgpr(&code, 5, 0); AppendEnd(&code); TestCase test; test.name = "VectorReadlaneInactiveWrittenLane"; test.code = code; test.expected = {0x12345678u}; test.opcodes = {O::S_MOV_B32, O::V_WRITELANE_B32, O::V_READLANE_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.forbidden_spirv = {"OpGroupNonUniformShuffle"}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorLaneWave32RuntimeSelectorWraps() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSop2(0x00, 5, 4, InlineU32(33))); AppendSMovLiteral(&code, 6, 0x12345678u); AppendVMovU32(&code, 2, 0); AppendVop3(&code, 0x361, 2, 6, 5); AppendVop3(&code, 0x360, 7, Vgpr(2), InlineU32(1)); AppendStoreSgpr(&code, 7, 0); AppendEnd(&code); TestCase test; test.name = "VectorLaneWave32RuntimeSelectorWraps"; test.code = code; test.expected = {0x12345678u}; test.opcodes = {O::S_ADD_U32, O::S_MOV_B32, O::V_WRITELANE_B32, O::V_READLANE_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpGroupNonUniformShuffle"}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 4; test.has_compute_info = true; return test; } TestCase VectorPermlanex16() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 3, 0x76543210u); AppendVMovLiteral(&code, 4, 0xfedcba98u); code.push_back(EncodeVop2(0x1a, 2, InlineU32(2), 0)); AppendVMovLiteral(&code, 5, 0xfeedbabeu); AppendVop3(&code, 0x378, 1, Vgpr(5), Vgpr(3), Vgpr(4)); AppendBufferStoreDword(&code, 1, 2); AppendEnd(&code); TestCase test; test.name = "VectorPermlanex16"; test.code = code; test.expected = std::vector(32, 0xfeedbabeu); test.opcodes = {O::V_MOV_B32, O::V_PERMLANEX16_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 32; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorPermlane16FetchInactiveZero() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x25, 1, InlineU32(10), 1), EncodeVopc(0xc2, InlineU32(0), 0), EncodeSop1(0x04, 126, 106), EncodeSMovB32(0, InlineU32(1)), EncodeSMovB32(1, InlineU32(0)), }; AppendVop3(&code, 0x377, 2, Vgpr(1), 0, 1); AppendStoreVgpr(&code, 2, 0); AppendEnd(&code); TestCase test; test.name = "VectorPermlane16FetchInactiveZero"; test.code = code; test.expected = {0}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_MOV_B32, O::V_PERMLANE16_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorPermlane16FetchInactiveFi() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, Vgpr(0)), EncodeVop2(0x25, 1, InlineU32(10), 1), EncodeVopc(0xc2, InlineU32(0), 0), EncodeSop1(0x04, 126, 106), EncodeSMovB32(0, InlineU32(1)), EncodeSMovB32(1, InlineU32(0)), }; AppendVop3(&code, 0x377, 2, Vgpr(1), 0, 1, 0, 1); AppendStoreVgpr(&code, 2, 0); AppendEnd(&code); TestCase test; test.name = "VectorPermlane16FetchInactiveFi"; test.code = code; test.expected = {11}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::S_MOV_B32, O::V_PERMLANE16_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorDpp8Captured(bool masked_exec) { using O = ShaderOpcode; constexpr u32 sentinel = 0xaaaaaaaau; const u32 masks[] = {masked_exec ? 0x0f0f0f0fu : 0xffffffffu, masked_exec ? 0xf0f0f0f0u : 0xffffffffu}; std::vector code; AppendVMovU32(&code, 5, 100); code.push_back(EncodeVop2(0x25, 5, Vgpr(0), 5)); AppendVMovLiteral(&code, 2, sentinel); AppendVMovLiteral(&code, 3, sentinel); AppendSMovLiteral(&code, 126, masks[0]); AppendSMovLiteral(&code, 127, masks[1]); // Captured DPP8 selectors broadcast each group's upper/lower four lanes. code.push_back(EncodeVop1(0x01, 2, 233)); code.push_back(0xfacfac05u); code.push_back(EncodeVop1(0x01, 3, 233)); code.push_back(0x68868805u); AppendSMovLiteral(&code, 126, 0xffffffffu); AppendSMovLiteral(&code, 127, 0xffffffffu); code.push_back(EncodeVop2(0x1a, 6, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 6); AppendVMovU32(&code, 7, 64u * sizeof(u32)); code.push_back(EncodeVop2(0x25, 6, Vgpr(7), 6)); AppendBufferStoreDword(&code, 3, 6); AppendEnd(&code); TestCase test; test.name = masked_exec ? "VectorDpp8CapturedMaskedExec" : "VectorDpp8Captured"; test.code = std::move(code); test.expected.resize(128, sentinel); for (u32 lane = 0; lane < 64; ++lane) { const auto mask = masks[lane / 32u]; if ((mask & (1u << (lane % 32u))) == 0) { continue; } const u32 sources[] = {(lane & ~7u) | 4u | (lane & 3u), (lane & ~7u) | (lane & 3u)}; for (u32 permutation = 0; permutation < 2; ++permutation) { const auto source = sources[permutation]; test.expected[permutation * 64u + lane] = (mask & (1u << (source % 32u))) != 0 ? 100u + source : 0u; } } test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::S_MOV_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{".dpp8(", 2}}; test.compute_info.wave_size = 64; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorDppQuadPermuteReverse() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 100); code.push_back(EncodeVop2(0x25, 2, 250, 1)); code.push_back(EncodeVop2Dpp(0, 0x01b)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "VectorDppQuadPermuteReverse"; test.code = code; test.expected = {103, 102, 101, 100, 107, 106, 105, 104}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 8; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorDppRowXmask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 100); code.push_back(EncodeVop2(0x25, 2, 250, 1)); code.push_back(EncodeVop2Dpp(0, 0x164)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "VectorDppRowXmask"; test.code = code; test.expected = {104, 105, 106, 107, 100, 101, 102, 103}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 8; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorDppBankMaskPreservesDestination() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 2, 0xaaaaaaaau); AppendVMovU32(&code, 1, 100); code.push_back(EncodeVop2(0x25, 2, 250, 1)); code.push_back(EncodeVop2Dpp(0, 0x0e4, 0xf, 0xe)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "VectorDppBankMaskPreservesDestination"; test.code = code; test.expected = {0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 104, 105, 106, 107}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 8; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorDppBoundsControlZeroPreservesDestination() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 2, 0xaaaaaaaau); AppendVMovU32(&code, 1, 100); code.push_back(EncodeVop2(0x25, 2, 250, 1)); code.push_back(EncodeVop2Dpp(0, 0x111)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "VectorDppBoundsControlZeroPreservesDestination"; test.code = code; test.expected = {0xaaaaaaaau, 100, 101, 102, 103, 104, 105, 106}; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 8; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase Vop3FmacF32NegatedSourceAccumulates() { using O = ShaderOpcode; std::vector code; constexpr u32 registers[] = {13, 20, 29}; for (u32 sample = 0; sample < 2; sample++) { for (u32 source = 0; source < 3; source++) { AppendVMovU32(&code, 30, (sample * 3 + source) * sizeof(u32)); AppendBufferLoadDword(&code, registers[source], 30); } code.push_back(0xd52b001du); code.push_back(0x4002290du); // v_fmac_f32_e64 v29, v13, -v20 AppendStoreVgpr(&code, 29, sample); } AppendEnd(&code); TestCase test; test.name = "Vop3FmacF32NegatedSourceAccumulates"; test.code = std::move(code); test.initial = {0x40000000u, 0x40400000u, 0x41200000u, 0x3f800001u, 0x3f7ffffeu, 0x3f800000u}; // 10 - 2*3 = 4; 1 - (1+2^-23)*(1-2^-23) = 2^-46, not rounded zero. test.expected = {0x40800000u, 0x28800000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MAC_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpFNegate", "Fma"}; return test; } TestCase Vop3LdexpSourceModifier() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 9, 0xbfc00000u); code.push_back(0xd7620107u); code.push_back(0x00018509u); AppendStoreVgpr(&code, 7, 0); AppendEnd(&code); return {"Vop3LdexpSourceModifier", code, {}, {0x3ec00000u}, {O::V_MOV_B32, O::V_LDEXP_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop1MoveRelSource() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 12, 0x11111111u); AppendVMovLiteral(&code, 13, 0x22222222u); AppendVMovLiteral(&code, 14, 0x33333333u); code.push_back(EncodeSMovB32(124, InlineU32(2))); code.push_back(0x7e6e870cu); AppendStoreVgpr(&code, 55, 0); AppendEnd(&code); return {"Vop1MoveRelSource", code, {}, {0x33333333u}, {O::V_MOV_B32, O::S_MOV_B32, O::V_MOVRELS_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop1MoveRelDestination() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0x12345678u); AppendVMovLiteral(&code, 5, 0xaaaaaaaau); AppendVMovLiteral(&code, 6, 0xbbbbbbbbu); AppendVMovLiteral(&code, 7, 0xccccccccu); code.push_back(EncodeSMovB32(124, InlineU32(2))); code.push_back(EncodeSop1(0x04, 8, 126)); code.push_back(EncodeVopc(0xc2, InlineU32(0), 0)); code.push_back(EncodeSop1(0x04, 126, 106)); code.push_back(EncodeVop1(0x42, 5, Vgpr(1))); code.push_back(EncodeSop1(0x04, 126, 8)); code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 0)); AppendBufferStoreDword(&code, 7, 4); AppendEnd(&code); TestCase test; test.name = "Vop1MoveRelDestination"; test.code = code; test.expected = {0x12345678u, 0xccccccccu, 0xccccccccu, 0xccccccccu}; test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::V_MOVRELD_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorFloatSpecialOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x40000000u); AppendVMovLiteral(&code, 2, 0x40400000u); AppendVMovLiteral(&code, 3, 0x40800000u); AppendVMovLiteral(&code, 4, 0x40003c00u); AppendVMovLiteral(&code, 5, 0x44004200u); AppendVMovLiteral(&code, 6, 0x3f800000u); AppendVMovLiteral(&code, 7, 0x3f800000u); AppendVMovLiteral(&code, 8, 0xbf800000u); AppendVMovLiteral(&code, 10, 0x3f000000u); AppendVMovLiteral(&code, 12, 0xaaaabbbbu); AppendVMovLiteral(&code, 13, 0xaaaabbbbu); code.push_back(EncodeVop2(0x02, 6, Vgpr(4), 5)); AppendVop3(&code, 0x368, 9, Vgpr(7), Vgpr(8)); AppendVop3(&code, 0x369, 11, Vgpr(7), Vgpr(10)); AppendVop3p(&code, 0x21, 12, Vgpr(1), Vgpr(2), Vgpr(0)); AppendVop3p(&code, 0x22, 13, Vgpr(1), Vgpr(2), Vgpr(0)); AppendVop3(&code, 0x144, 14, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x145, 15, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x146, 16, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x147, 17, Vgpr(0), Vgpr(1), Vgpr(2)); const u32 results[] = {6, 9, 11, 12, 13, 14, 15, 16, 17}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"VectorFloatSpecialOps", code, {}, {0x41400000u, 0x80017fffu, 0x8000ffffu, 0xaaaa4700u, 0x4700bbbbu, 0x40800000u, 0x3f800000u, 0xc0000000u, 0x40c00000u}, {O::V_MOV_B32, O::V_DOT2C_F32_F16, O::V_CVT_PKNORM_I16_F32, O::V_CVT_PKNORM_U16_F32, O::V_MAD_MIXLO_F16, O::V_MAD_MIXHI_F16, O::V_CUBEID_F32, O::V_CUBESC_F32, O::V_CUBETC_F32, O::V_CUBEMA_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase MadMixF16LiteralHalfSourceUsesOpsel() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 12, 0xaaaabbbbu); AppendVMovLiteral(&code, 13, 0x11112222u); AppendVop3p(&code, 0x21, 12, 255u, Vgpr(0), Vgpr(0), 0x1, 0x1); code.push_back(0x40003c00u); AppendVop3p(&code, 0x22, 13, 255u, Vgpr(0), Vgpr(0), 0x1, 0x0); code.push_back(0x40003c00u); AppendStoreVgpr(&code, 12, 0); AppendStoreVgpr(&code, 13, 1); AppendEnd(&code); return {"MadMixF16LiteralHalfSourceUsesOpsel", code, {}, {0xaaaa4200u, 0x40002222u}, {O::V_MOV_B32, O::V_MAD_MIXLO_F16, O::V_MAD_MIXHI_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase MadMixF16NegHiIsAbsAndNegIsIndependent() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0xc0000000u); AppendVMovLiteral(&code, 2, 0x40000000u); AppendVMovLiteral(&code, 12, 0xaaaabbbbu); AppendVMovLiteral(&code, 13, 0x11112222u); AppendVop3p(&code, 0x21, 12, Vgpr(1), Vgpr(0), Vgpr(0), 0, 0, 0x1); AppendVop3p(&code, 0x22, 13, Vgpr(2), Vgpr(0), Vgpr(0), 0, 0, 0, 0x1); AppendStoreVgpr(&code, 12, 0); AppendStoreVgpr(&code, 13, 1); AppendEnd(&code); return {"MadMixF16NegHiIsAbsAndNegIsIndependent", code, {}, {0xaaaa4200u, 0xbc002222u}, {O::V_MOV_B32, O::V_MAD_MIXLO_F16, O::V_MAD_MIXHI_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3FmaF16UsesRdna2Opcode34b() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0x40003c00u); AppendVMovLiteral(&code, 2, 0x44004200u); AppendVMovLiteral(&code, 3, 0x3c003c00u); AppendVMovLiteral(&code, 10, 0xaaaa5555u); AppendVMovLiteral(&code, 11, 0xbbbb5555u); AppendVop3(&code, 0x34bu, 10, Vgpr(1), Vgpr(2), Vgpr(3)); AppendVop3(&code, 0x34bu, 11, Vgpr(1), Vgpr(2), Vgpr(3), 0, 0xfu); AppendStoreVgpr(&code, 10, 0); AppendStoreVgpr(&code, 11, 1); AppendEnd(&code); return {"VectorVop3FmaF16UsesRdna2Opcode34b", code, {}, {0xaaaa4400u, 0x48805555u}, {O::V_MOV_B32, O::V_FMA_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase FloatInlineConstantF16UsesNumericValue() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 1, 0x38003800u); // 0.5h, 0.5h AppendVMovLiteral(&code, 2, 0x3c003c00u); // 1.0h, 1.0h AppendVMovLiteral(&code, 10, 0xaaaa0000u); AppendVMovLiteral(&code, 12, 0xbbbb0000u); AppendVop3(&code, 0x34bu, 10, 246u, Vgpr(1), Vgpr(2)); AppendVop3p(&code, 0x0eu, 11, 246u, Vgpr(1), Vgpr(2), 0x1u); AppendVop3(&code, 0x34bu, 12, 248u, 242u, 128u); AppendStoreVgpr(&code, 10, 0); AppendStoreVgpr(&code, 11, 1); AppendStoreVgpr(&code, 12, 2); AppendEnd(&code); TestCase test; test.name = "FloatInlineConstantF16UsesNumericValue"; test.code = std::move(code); test.expected = {0xaaaa4200u, 0x3c004200u, 0xbbbb3118u}; test.opcodes = {O::V_MOV_B32, O::V_FMA_F16, O::V_PK_FMA_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"4.000000", 2}, {"0.159155", 1}}; return test; } TestCase VectorFloatControlContractPreservesInfNan() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSop2(0x24, 0, InlineU32(8), InlineU32(23))); code.push_back(EncodeVop1(0x01, 0, InlineU32(0))); AppendVMovU32(&code, 1, 1); AppendVMovU32(&code, 2, 3); AppendVop3(&code, 0x108, 10, 0, Vgpr(0)); code.push_back(EncodeVopc(0x88, Vgpr(10), 2)); code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, 0); AppendEnd(&code); TestCase test; test.name = "VectorFloatControlContractPreservesInfNan"; test.code = std::move(code); test.expected = {1u}; test.opcodes = {O::S_BFM_B32, O::V_MOV_B32, O::V_MUL_F32, O::V_CMP_CLASS_F32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"SPV_KHR_float_controls", "SignedZeroInfNanPreserve 32"}; return test; } TestCase VectorFloatArithmeticOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x40000000u); AppendVMovLiteral(&code, 2, 0x40800000u); AppendVMovLiteral(&code, 3, 0x40a00000u); AppendVMovLiteral(&code, 4, 0x40400000u); AppendVMovLiteral(&code, 11, 0x40800000u); AppendVMovLiteral(&code, 17, 0x40800000u); code.push_back(EncodeVop2(0x03, 5, Vgpr(0), 1)); code.push_back(EncodeVop2(0x04, 6, Vgpr(3), 1)); code.push_back(EncodeVop2(0x05, 7, Vgpr(1), 3)); code.push_back(EncodeVop2(0x08, 8, Vgpr(1), 2)); code.push_back(EncodeVop1(0x01, 21, Vgpr(8))); code.push_back(EncodeVop2(0x0f, 9, Vgpr(3), 1)); code.push_back(EncodeVop2(0x10, 10, Vgpr(3), 1)); code.push_back(EncodeVop2(0x1f, 11, Vgpr(1), 4)); AppendVop3(&code, 0x141, 12, Vgpr(1), Vgpr(4), Vgpr(2)); AppendVop3(&code, 0x14b, 13, Vgpr(1), Vgpr(4), Vgpr(2)); AppendVop3(&code, 0x151, 14, Vgpr(3), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x154, 15, Vgpr(3), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x157, 16, Vgpr(3), Vgpr(1), Vgpr(2)); code.push_back(EncodeVop2(0x20, 18, Vgpr(1), 17)); code.push_back(0x3f800000u); code.push_back(EncodeVop2(0x21, 19, Vgpr(1), 4)); code.push_back(0x40000000u); code.push_back(EncodeVop2(0x03, 20, 249, 1)); code.push_back(EncodeVop2Sdwa(0)); AppendSMovLiteral(&code, 70, 0x40000000u); code.push_back(0x06088cf9u); code.push_back(0x868606f2u); AppendSMovLiteral(&code, 26, 0x40000000u); code.push_back(0x081034f9u); code.push_back(0x868606f2u); code.push_back(0xd51f8011u); code.push_back(0x00020d0bu); AppendVMovLiteral(&code, 22, 0xbf800000u); code.push_back(EncodeVop2(0x05, 23, 249, 26)); code.push_back(EncodeVop2Sdwa(22, 6, 0, 6, 6, 0, 0, 0, 1, 0, 0, 0, 1)); AppendVMovLiteral(&code, 24, 0x3f800000u); AppendVMovLiteral(&code, 25, 0x3f800000u); code.push_back(EncodeVop2(0x03, 26, 249, 25)); code.push_back(EncodeVop2Sdwa(24, 6, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)); AppendVMovLiteral(&code, 27, 0xbf800000u); AppendVMovLiteral(&code, 28, 0x40000000u); AppendVop3(&code, 0x104, 29, Vgpr(27), Vgpr(28), 0, 1); AppendVop3(&code, 0x103, 30, Vgpr(0), Vgpr(1), 0, 0, 0, false, 1); AppendVop3(&code, 0x151, 32, Vgpr(0), Vgpr(1), Vgpr(2), 0, 0, false, 1); AppendVMovLiteral(&code, 33, 0xbf800000u); AppendVMovLiteral(&code, 34, 0x40000000u); code.push_back(EncodeVop2(0x03, 35, 250, 34)); code.push_back(EncodeVop2Dpp(33, 0, 0xf, 0xf, 0, 1)); const u32 results[] = {5, 6, 7, 21, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 4, 8, 17, 23, 26, 29, 30, 32, 35}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"VectorFloatArithmeticOps", code, {}, {0x40400000u, 0x40400000u, 0x40400000u, 0x41000000u, 0x40000000u, 0x40a00000u, 0x41200000u, 0x41200000u, 0x41200000u, 0x40000000u, 0x40a00000u, 0x40800000u, 0x40c00000u, 0x41000000u, 0x40400000u, 0x40400000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, 0x40800000u, 0xbf800000u, 0x40c00000u, 0x40000000u, 0x40400000u}, {O::S_MOV_B32, O::V_MOV_B32, O::V_ADD_F32, O::V_SUB_F32, O::V_SUBREV_F32, O::V_MUL_F32, O::V_MIN_F32, O::V_MAX_F32, O::V_MAC_F32, O::V_MAD_F32, O::V_FMA_F32, O::V_MIN3_F32, O::V_MAX3_F32, O::V_MED3_F32, O::V_MADMK_F32, O::V_MADAK_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMinMaxF32NanAndSignedZeroEdges() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x40000000u); AppendVMovLiteral(&code, 1, 0x7fc00000u); AppendVMovLiteral(&code, 2, 0x00000000u); AppendVMovLiteral(&code, 3, 0x80000000u); AppendVMovLiteral(&code, 4, 0x40400000u); AppendVMovLiteral(&code, 5, 0x3f800000u); AppendVMovLiteral(&code, 6, 0x7fa00001u); code.push_back(EncodeVop2(0x0f, 10, Vgpr(0), 1)); code.push_back(EncodeVop2(0x10, 11, Vgpr(0), 1)); code.push_back(EncodeVop2(0x0f, 12, Vgpr(3), 2)); code.push_back(EncodeVop2(0x10, 13, Vgpr(2), 3)); code.push_back(EncodeVop2(0x0f, 14, Vgpr(6), 0)); code.push_back(EncodeVop2(0x10, 15, Vgpr(0), 6)); AppendVop3(&code, 0x151, 16, Vgpr(0), Vgpr(1), Vgpr(4)); AppendVop3(&code, 0x154, 17, Vgpr(0), Vgpr(1), Vgpr(5)); for (u32 i = 0; i < 8; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"VectorMinMaxF32NanAndSignedZeroEdges", code, {}, {0x40000000u, 0x40000000u, 0x80000000u, 0x00000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u}, {O::V_MOV_B32, O::V_MIN_F32, O::V_MAX_F32, O::V_MIN3_F32, O::V_MAX3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorMed3F32NanUsesMin3Path() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x40000000u); AppendVMovLiteral(&code, 1, 0x40400000u); AppendVMovLiteral(&code, 2, 0x7fa00001u); AppendVMovLiteral(&code, 3, 0x7fc00000u); AppendVop3(&code, 0x157, 10, Vgpr(0), Vgpr(1), Vgpr(2)); AppendVop3(&code, 0x157, 11, Vgpr(0), Vgpr(1), Vgpr(3)); AppendVop3(&code, 0x157, 12, Vgpr(2), Vgpr(0), Vgpr(1)); AppendStoreVgpr(&code, 10, 0); AppendStoreVgpr(&code, 11, 1); AppendStoreVgpr(&code, 12, 2); AppendEnd(&code); return {"VectorMed3F32NanUsesMin3Path", code, {}, {0x40000000u, 0x40000000u, 0x40000000u}, {O::V_MOV_B32, O::V_MED3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorFloatConversionOps() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xfffffffdu); code.push_back(EncodeVop1(0x01, 1, InlineU32(7))); AppendVMovLiteral(&code, 2, 0x40e00000u); AppendVMovLiteral(&code, 3, 0xc0400000u); AppendVMovLiteral(&code, 4, 0x3f800000u); AppendVMovLiteral(&code, 5, 0x00003c00u); AppendVMovLiteral(&code, 6, 0x40700000u); AppendVMovLiteral(&code, 7, 0x44332211u); AppendVMovLiteral(&code, 8, 0x40000000u); AppendVMovLiteral(&code, 9, 0x40800000u); AppendVMovLiteral(&code, 10, 0x40100000u); AppendVMovLiteral(&code, 11, 0x40300000u); code.push_back(EncodeVop1(0x01, 12, InlineU32(0))); code.push_back(EncodeVop1(0x01, 37, InlineU32(15))); code.push_back(EncodeVop1(0x05, 13, Vgpr(0))); code.push_back(EncodeVop1(0x06, 14, Vgpr(1))); code.push_back(EncodeVop1(0x07, 15, Vgpr(2))); code.push_back(EncodeVop1(0x08, 16, Vgpr(3))); code.push_back(EncodeVop1(0x0a, 17, Vgpr(4))); code.push_back(EncodeVop1(0x0b, 18, Vgpr(5))); code.push_back(EncodeVop1(0x0c, 42, Vgpr(11))); code.push_back(EncodeVop1(0x0d, 19, Vgpr(6))); code.push_back(EncodeVop1(0x11, 20, Vgpr(7))); code.push_back(EncodeVop1(0x12, 21, Vgpr(7))); code.push_back(EncodeVop1(0x13, 22, Vgpr(7))); code.push_back(EncodeVop1(0x14, 23, Vgpr(7))); code.push_back(EncodeVop1(0x2a, 24, Vgpr(8))); code.push_back(EncodeVop1(0x20, 25, Vgpr(10))); code.push_back(EncodeVop1(0x21, 26, Vgpr(11))); code.push_back(EncodeVop1(0x22, 27, Vgpr(10))); code.push_back(EncodeVop1(0x23, 28, Vgpr(10))); code.push_back(EncodeVop1(0x24, 29, Vgpr(11))); code.push_back(EncodeVop1(0x25, 30, Vgpr(4))); code.push_back(EncodeVop1(0x27, 36, Vgpr(9))); code.push_back(EncodeVop1(0x2e, 32, Vgpr(9))); code.push_back(EncodeVop1(0x33, 33, Vgpr(9))); code.push_back(EncodeVop1(0x35, 34, Vgpr(12))); code.push_back(EncodeVop1(0x36, 35, Vgpr(12))); code.push_back(EncodeVop1(0x0e, 38, Vgpr(37))); code.push_back(EncodeVop1(0x2a, 39, 249)); code.push_back(EncodeVop1Sdwa(8, 6, 0, 6, 0, 0, 0, 0, 0, 1)); AppendVMovLiteral(&code, 40, 0xc0000000u); AppendVop3(&code, 0x1aa, 41, Vgpr(40), 0, 0, 1, 0, false, 1); AppendVMovLiteral(&code, 2, 0xc0003c00u); // low=1.0h, high=-2.0h code.push_back(0x7e1016f9u); code.push_back(0x00250602u); const u32 results[] = {13, 14, 15, 16, 17, 18, 42, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 36, 32, 33, 34, 35, 38, 39, 41, 8}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"VectorFloatConversionOps", code, {}, {0xc0400000u, 0x40e00000u, 7, 0xfffffffdu, 0x00003c00u, 0x3f800000u, 3, 3, 0x41880000u, 0x42080000u, 0x424c0000u, 0x42880000u, 0x3f000000u, 0x3e800000u, 0x40000000u, 0x40400000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x3f000000u, 0x40000000u, 0, 0x3f800000u, 0xbd800000u, 0x3f800000u, 0x3f800000u, 0x40000000u}, {O::V_MOV_B32, O::V_CVT_F32_I32, O::V_CVT_F32_U32, O::V_CVT_U32_F32, O::V_CVT_I32_F32, O::V_CVT_F16_F32, O::V_CVT_F32_F16, O::V_CVT_RPI_I32_F32, O::V_CVT_FLR_I32_F32, O::V_CVT_OFF_F32_I4, O::V_CVT_F32_UBYTE0, O::V_CVT_F32_UBYTE1, O::V_CVT_F32_UBYTE2, O::V_CVT_F32_UBYTE3, O::V_RCP_F32, O::V_FRACT_F32, O::V_TRUNC_F32, O::V_CEIL_F32, O::V_RNDNE_F32, O::V_FLOOR_F32, O::V_EXP_F32, O::V_LOG_F32, O::V_RSQ_F32, O::V_SQRT_F32, O::V_SIN_F32, O::V_COS_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorFrexpF32Edges() { using O = ShaderOpcode; const std::vector inputs = { 0x00000000u, 0x80000000u, 0x3f000000u, 0xbf800000u, 0x40800000u, 0xc1000000u, 0x00800000u, 0x007fffffu, 0x00000001u, 0x7f800000u, 0x7fc00000u, 0xff800000u, 0x7fa00001u, }; std::vector code; for (u32 i = 0; i < static_cast(inputs.size()); i++) { AppendVMovU32(&code, 31, i * sizeof(u32)); AppendBufferLoadDword(&code, i == 3u ? 23u : i, 31); } for (u32 i = 0; i < static_cast(inputs.size()); i++) { const auto exponent_dst = i == 3u ? 19u : 32u + i; const auto mantissa_dst = i == 3u ? 21u : 48u + i; if (i == 3u) { code.push_back(0x7e267ef9u); // captured v_frexp_exp_i32_f32 v19, abs(v23) code.push_back(0x00260617u); } else { code.push_back(EncodeVop1(0x3f, exponent_dst, Vgpr(i))); } AppendStoreVgpr(&code, exponent_dst, i); if (i == 3u) { code.push_back(0x7e2a80f9u); // captured v_frexp_mant_f32 v21, abs(v23) code.push_back(0x00260617u); } else { code.push_back(EncodeVop1(0x40, mantissa_dst, Vgpr(i))); } AppendStoreVgpr(&code, mantissa_dst, inputs.size() + i); } AppendEnd(&code); TestCase test; test.name = "VectorFrexpF32Edges"; test.code = std::move(code); test.initial = inputs; test.initial.resize(inputs.size() * 2u); test.expected = { 0u, 0u, 0u, 1u, 3u, 4u, 0xffffff83u, 0xffffff82u, 0xffffff6cu, 0u, 0u, 0u, 0u, 0x00000000u, 0x80000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0xbf000000u, 0x3f000000u, 0x3f7ffffeu, 0x3f000000u, 0x7f800000u, 0x7fc00000u, 0xff800000u, 0x7fa00001u, }; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_FREXP_EXP_I32_F32, O::V_FREXP_MANT_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_FREXP_EXP_I32_F32", inputs.size()}, {"V_FREXP_MANT_F32", inputs.size()}}; test.required_spirv = {"FindUMsb", "OpBitFieldUExtract", "OpShiftLeftLogical"}; return test; } TestCase CvtF32ToIntSaturatesNaNAndOutOfRange() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x7fc00000u); AppendVMovLiteral(&code, 1, 0xbf800000u); AppendVMovLiteral(&code, 2, 0x7f800000u); AppendVMovLiteral(&code, 3, 0x4f800000u); AppendVMovLiteral(&code, 4, 0xff800000u); AppendVMovLiteral(&code, 5, 0x4f000000u); code.push_back(EncodeVop1(0x07, 10, Vgpr(0))); code.push_back(EncodeVop1(0x07, 11, Vgpr(1))); code.push_back(EncodeVop1(0x07, 12, Vgpr(2))); code.push_back(EncodeVop1(0x07, 13, Vgpr(3))); code.push_back(EncodeVop1(0x08, 14, Vgpr(0))); code.push_back(EncodeVop1(0x08, 15, Vgpr(2))); code.push_back(EncodeVop1(0x08, 16, Vgpr(4))); code.push_back(EncodeVop1(0x08, 17, Vgpr(5))); for (u32 i = 0; i < 8; i++) { AppendStoreVgpr(&code, 10 + i, i); } AppendEnd(&code); return {"CvtF32ToIntSaturatesNaNAndOutOfRange", code, {}, {0, 0, 0xffffffffu, 0xffffffffu, 0, 0x7fffffffu, 0x80000000u, 0x7fffffffu}, {O::V_MOV_B32, O::V_CVT_U32_F32, O::V_CVT_I32_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorSpecialF32FlushesDenormalInputs() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x00000001u); code.push_back(EncodeVop1(0x27, 1, Vgpr(0))); code.push_back(EncodeVop1(0x2a, 2, Vgpr(0))); code.push_back(EncodeVop1(0x2e, 3, Vgpr(0))); code.push_back(EncodeVop1(0x33, 4, Vgpr(0))); AppendStoreVgpr(&code, 1, 0); AppendStoreVgpr(&code, 2, 1); AppendStoreVgpr(&code, 3, 2); AppendStoreVgpr(&code, 4, 3); AppendEnd(&code); return {"VectorSpecialF32FlushesDenormalInputs", code, {}, {0xff800000u, 0x7f800000u, 0x7f800000u, 0x00000000u}, {O::V_MOV_B32, O::V_LOG_F32, O::V_RCP_F32, O::V_RSQ_F32, O::V_SQRT_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorRcpIflagF32IntegerReciprocal() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 0); AppendVMovU32(&code, 1, 1); AppendVMovU32(&code, 2, 2); AppendVMovLiteral(&code, 3, 0xffffffffu); for (u32 i = 0; i < 4; i++) { code.push_back(EncodeVop1(0x06, 4 + i, Vgpr(i))); code.push_back(EncodeVop1(0x2b, 8 + i, Vgpr(4 + i))); } AppendVMovLiteral(&code, 12, 0xc0000000u); AppendVop3(&code, 0x1ab, 13, Vgpr(12), 0, 0, 1, 0, false, 1); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 8 + i, i); } AppendStoreVgpr(&code, 13, 4); AppendEnd(&code); TestCase test; test.name = "VectorRcpIflagF32IntegerReciprocal"; test.code = std::move(code); test.expected = {0x7f800000u, 0x3f800000u, 0x3f000000u, 0x2f800000u, 0x3f800000u}; test.opcodes = {O::V_MOV_B32, O::V_CVT_F32_U32, O::V_RCP_IFLAG_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_RCP_IFLAG_F32", 5}}; test.ir_counts = {{"FPRecipIFlag32", 5}}; test.required_spirv = {"OpConvertUToF", "OpFDiv"}; test.forbidden_spirv = {"8388608"}; return test; } TestCase VectorSinCosMaxFiniteSpecialCases() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xff7fffffu); code.push_back(EncodeVop1(0x35, 1, Vgpr(0))); code.push_back(EncodeVop1(0x36, 2, Vgpr(0))); AppendStoreVgpr(&code, 1, 0); AppendStoreVgpr(&code, 2, 1); AppendEnd(&code); return {"VectorSinCosMaxFiniteSpecialCases", code, {}, {0x00000000u, 0x3f800000u}, {O::V_MOV_B32, O::V_SIN_F32, O::V_COS_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorCompareOps() { using O = ShaderOpcode; std::vector code; std::vector results; std::vector expected; u32 dst = 10; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); AppendVMovLiteral(&code, 2, 0x40000000u); AppendVMovLiteral(&code, 3, 0x40400000u); code.push_back(EncodeVop1(0x01, 4, InlineU32(1))); AppendVMovLiteral(&code, 5, 0xffffffffu); auto append_compare = [&](u32 opcode, u32 src0, u32 src1, bool value) { if (dst == 31u) { dst++; } code.push_back(EncodeVopc(opcode, src0, src1)); code.push_back(EncodeVop2(0x01, dst, InlineU32(0), 1)); results.push_back(dst); expected.push_back(value ? 1u : 0u); dst++; }; append_compare(0x00, Vgpr(2), 3, false); append_compare(0x01, Vgpr(2), 3, true); append_compare(0x02, Vgpr(2), 3, false); append_compare(0x03, Vgpr(2), 3, true); append_compare(0x04, Vgpr(2), 3, false); append_compare(0x05, Vgpr(2), 3, true); append_compare(0x06, Vgpr(2), 3, false); append_compare(0x07, Vgpr(2), 3, true); append_compare(0x08, Vgpr(2), 3, false); append_compare(0x09, Vgpr(2), 3, true); append_compare(0x0a, Vgpr(2), 3, false); append_compare(0x0b, Vgpr(2), 3, true); append_compare(0x0c, Vgpr(2), 3, false); append_compare(0x0d, Vgpr(2), 3, true); append_compare(0x0e, Vgpr(2), 3, false); append_compare(0x0f, Vgpr(2), 3, true); append_compare(0x80, Vgpr(5), 4, false); append_compare(0x81, Vgpr(5), 4, true); append_compare(0x82, Vgpr(5), 4, false); append_compare(0x83, Vgpr(5), 4, true); append_compare(0x84, Vgpr(5), 4, false); append_compare(0x85, Vgpr(5), 4, true); append_compare(0x86, Vgpr(5), 4, false); append_compare(0x87, Vgpr(5), 4, true); append_compare(0xc0, Vgpr(4), 5, false); append_compare(0xc1, Vgpr(4), 5, true); append_compare(0xc2, Vgpr(4), 5, false); append_compare(0xc3, Vgpr(4), 5, true); append_compare(0xc4, Vgpr(4), 5, false); append_compare(0xc5, Vgpr(4), 5, true); append_compare(0xc6, Vgpr(4), 5, false); append_compare(0xc7, Vgpr(4), 5, true); code.push_back(0x7c1d00f9u); code.push_back(0x86069201u); code.push_back(EncodeVopc(0x84, 249, 4)); code.push_back(EncodeVopcSdwa(5, 19, 1)); code.push_back(EncodeVopc(0x01, 250, 3)); code.push_back(EncodeVop2Dpp(2)); AppendVMovLiteral(&code, 6, 0xc0000000u); AppendVMovLiteral(&code, 7, 0x3f800000u); AppendVop3(&code, 0x04, 20, Vgpr(6), Vgpr(7), 0, 1); for (u32 i = 0; i < static_cast(results.size()); i++) { AppendStoreVgpr(&code, results[i], i); } AppendStoreSgpr(&code, 18, static_cast(results.size())); expected.push_back(1u); AppendStoreSgpr(&code, 19, static_cast(results.size() + 1u)); expected.push_back(0u); AppendStoreSgpr(&code, 106, static_cast(results.size() + 2u)); expected.push_back(1u); AppendStoreSgpr(&code, 20, static_cast(results.size() + 3u)); expected.push_back(1u); AppendEnd(&code); return {"VectorCompareOps", code, {}, expected, {O::V_MOV_B32, O::V_CNDMASK_B32, O::V_CMP_F_F32, O::V_CMP_LT_F32, O::V_CMP_EQ_F32, O::V_CMP_LE_F32, O::V_CMP_GT_F32, O::V_CMP_LG_F32, O::V_CMP_GE_F32, O::V_CMP_O_F32, O::V_CMP_U_F32, O::V_CMP_NGE_F32, O::V_CMP_NLG_F32, O::V_CMP_NGT_F32, O::V_CMP_NLE_F32, O::V_CMP_NEQ_F32, O::V_CMP_NLT_F32, O::V_CMP_TRU_F32, O::V_CMP_F_I32, O::V_CMP_LT_I32, O::V_CMP_EQ_I32, O::V_CMP_LE_I32, O::V_CMP_GT_I32, O::V_CMP_NE_I32, O::V_CMP_GE_I32, O::V_CMP_T_I32, O::V_CMP_F_U32, O::V_CMP_LT_U32, O::V_CMP_EQ_U32, O::V_CMP_LE_U32, O::V_CMP_GT_U32, O::V_CMP_NE_U32, O::V_CMP_GE_U32, O::V_CMP_T_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3CompareEqU64OnGpu() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); code.push_back(EncodeSop1(0x04, 106, 126)); // s_mov_b64 vcc, exec code.push_back(0xd4e2006au); code.push_back(0x0000d47eu); // v_cmp_eq_u64 vcc, exec, vcc code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1)); AppendStoreVgpr(&code, 2, 0); AppendSMovLiteral(&code, 107, 1); // high-dword mismatch code.push_back(0xd4e2006au); code.push_back(0x0000d47eu); // v_cmp_eq_u64 vcc, exec, vcc code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, 1); AppendEnd(&code); TestCase test{"VectorVop3CompareEqU64OnGpu", code, {}, {1, 0}, {O::V_MOV_B32, O::S_MOV_B64, O::V_CMP_EQ_U64, O::V_CNDMASK_B32, O::S_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMP_EQ_U64 vcc_lo, exec_lo, vcc_lo", 2}}; return test; } TestCase VectorVop3CompareNeU64OnGpu() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); code.push_back(EncodeSop1(0x04, 106, 126)); // s_mov_b64 vcc, exec code.push_back(0xd4e5006au); code.push_back(0x0000d47eu); // v_cmp_ne_u64 vcc, exec, vcc code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1)); AppendStoreVgpr(&code, 2, 0); AppendSMovLiteral(&code, 106, 0); AppendSMovLiteral(&code, 107, 0); code.push_back(0xd4e5006au); code.push_back(0x0000d47eu); // v_cmp_ne_u64 vcc, exec, vcc code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, 1); AppendEnd(&code); return {"VectorVop3CompareNeU64OnGpu", code, {}, {0, 1}, {O::V_MOV_B32, O::S_MOV_B64, O::V_CMP_NE_U64, O::V_CNDMASK_B32, O::S_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVopcCmpxNeU64CapturedExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 7, 0); AppendVMovU32(&code, 8, 0); AppendVMovU32(&code, 31, 0); code.push_back(0x7dea0e80u); // v_cmpx_ne_u64 exec, 0, v[7:8] AppendBufferStoreDword(&code, 8, 31); code.push_back(EncodeSMovB32(126, InlineU32(1))); AppendVMovU32(&code, 8, 1); AppendVMovU32(&code, 31, 4); code.push_back(0x7dea0e80u); // v_cmpx_ne_u64 exec, 0, v[7:8] AppendBufferStoreDword(&code, 8, 31); AppendVMovU32(&code, 2, 0); AppendVMovU32(&code, 3, 0); AppendVMovU32(&code, 31, 8); code.push_back(0x7dea0480u); // v_cmpx_ne_u64 exec, 0, v[2:3] AppendBufferStoreDword(&code, 3, 31); code.push_back(EncodeSMovB32(126, InlineU32(1))); AppendVMovU32(&code, 3, 1); AppendVMovU32(&code, 31, 12); code.push_back(0x7dea0480u); // v_cmpx_ne_u64 exec, 0, v[2:3] AppendBufferStoreDword(&code, 3, 31); AppendEnd(&code); TestCase test{"VectorVopcCmpxNeU64CapturedExecMask", code, {0x11223344u, 0x55667788u, 0xaabbccddu, 0xeeff0011u}, {0x11223344u, 1u, 0xaabbccddu, 1u}, {O::V_MOV_B32, O::V_CMPX_NE_U64, O::BUFFER_STORE_DWORD, O::S_MOV_B32, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMPX_NE_U64 exec_lo, 0, v7", 2}, {"V_CMPX_NE_U64 exec_lo, 0, v2", 2}}; return test; } TestCase VectorVop3CmpxNeI64CapturedExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 7, 0); AppendVMovU32(&code, 8, 0); AppendVMovU32(&code, 31, 0); code.push_back(0xd4b5007eu); code.push_back(0x00020e80u); // v_cmpx_ne_i64 exec, 0, v[7:8] AppendBufferStoreDword(&code, 8, 31); code.push_back(EncodeSMovB32(126, InlineU32(1))); AppendVMovU32(&code, 8, 1); AppendVMovU32(&code, 31, 4); code.push_back(0xd4b5007eu); code.push_back(0x00020e80u); // v_cmpx_ne_i64 exec, 0, v[7:8] AppendBufferStoreDword(&code, 8, 31); AppendEnd(&code); TestCase test{"VectorVop3CmpxNeI64CapturedExecMask", code, {0x11223344u, 0x55667788u}, {0x11223344u, 1u}, {O::V_MOV_B32, O::V_CMPX_NE_I64, O::BUFFER_STORE_DWORD, O::S_MOV_B32, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMPX_NE_I64 exec_lo, 0, v7", 2}}; return test; } TestCase VectorVop3CompareGtU64OnGpu() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); AppendSMovLiteral(&code, 4, 0); AppendSMovLiteral(&code, 5, 0x80000000u); code.push_back(0xd4e4006au); code.push_back(0x00010004u); // v_cmp_gt_u64 vcc, s[4:5], 0 code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1)); AppendStoreVgpr(&code, 2, 0); code.push_back(0xd4e4006au); code.push_back(0x0001006au); // v_cmp_gt_u64 vcc, vcc, 0 code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, 1); AppendSMovLiteral(&code, 106, 0); AppendSMovLiteral(&code, 107, 0); code.push_back(0xd4e4006au); code.push_back(0x0001006au); // v_cmp_gt_u64 vcc, vcc, 0 code.push_back(EncodeVop2(0x01, 4, InlineU32(0), 1)); AppendStoreVgpr(&code, 4, 2); AppendEnd(&code); TestCase test{"VectorVop3CompareGtU64OnGpu", code, {}, {1, 1, 0}, {O::V_MOV_B32, O::S_MOV_B32, O::V_CMP_GT_U64, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase VectorVopcCompareLtU64OnGpu() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 5); AppendVMovU32(&code, 1, 0); AppendVMovU32(&code, 2, 1); AppendSMovLiteral(&code, 4, 4); AppendSMovLiteral(&code, 5, 0); code.push_back(EncodeVopc(0xe1, 4, 0)); code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 2)); AppendStoreVgpr(&code, 3, 0); AppendSMovLiteral(&code, 4, 6); code.push_back(EncodeVopc(0xe1, 4, 0)); code.push_back(EncodeVop2(0x01, 4, InlineU32(0), 2)); AppendStoreVgpr(&code, 4, 1); AppendEnd(&code); TestCase test{"VectorVopcCompareLtU64OnGpu", code, {}, {1, 0}, {O::V_MOV_B32, O::S_MOV_B32, O::V_CMP_LT_U64, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMP_LT_U64 vcc_lo, s4, v0", 2}}; return test; } TestCase VectorVop3CompareEqI64OnGpu() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); code.push_back(EncodeSop1(0x04, 4, 126)); // s_mov_b64 s[4:5], exec code.push_back(0xd4a2006au); code.push_back(0x0000087eu); // v_cmp_eq_i64 vcc, exec, s[4:5] code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1)); AppendStoreVgpr(&code, 2, 0); AppendSMovLiteral(&code, 4, 0); code.push_back(0xd4a2006au); code.push_back(0x0000087eu); // low dword mismatch code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, 1); AppendEnd(&code); return {"VectorVop3CompareEqI64OnGpu", code, {}, {1, 0}, {O::V_MOV_B32, O::S_MOV_B64, O::V_CMP_EQ_I64, O::V_CNDMASK_B32, O::S_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorCompareClassF32() { using O = ShaderOpcode; std::vector code; std::vector expected; AppendVMovU32(&code, 1, 1); u32 out = 0; auto append_class = [&](u32 bits, u32 class_mask, bool match) { AppendVMovLiteral(&code, 0, bits); AppendVMovU32(&code, 2, class_mask); code.push_back(EncodeVopc(0x88, Vgpr(0), 2)); code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1)); AppendStoreVgpr(&code, 3, out++); expected.push_back(match ? 1u : 0u); }; append_class(0x7fc00000u, 1u << 1u, true); append_class(0xff800000u, 1u << 2u, true); append_class(0xbf800000u, 1u << 3u, true); append_class(0x80000000u, 1u << 5u, true); append_class(0x00000000u, 1u << 6u, true); append_class(0x40000000u, 1u << 8u, true); append_class(0x7f800000u, 1u << 9u, true); append_class(0x40000000u, 1u << 3u, false); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovU32(&code, 2, 1u << 3u); AppendVop3(&code, 0x88, 20, Vgpr(0), Vgpr(2), 0, 0, 0, false, 0, 1); AppendStoreSgprPair(&code, 20, out); expected.push_back(1u); expected.push_back(0u); AppendEnd(&code); return {"VectorCompareClassF32", code, {}, expected, {O::V_MOV_B32, O::V_CMP_CLASS_F32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVopcSdwaCmpxClassF32CapturedExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 13, 0x7fc00000u); // Quiet NaN. AppendVMovU32(&code, 3, 7); AppendVMovU32(&code, 31, 0); code.push_back(EncodeSMovB32(106, InlineU32(2))); // qNaN class mask. code.insert(code.end(), {0x7d30d4f9u, 0x8606000du}); AppendBufferStoreDword(&code, 3, 31); code.push_back(EncodeSMovB32(126, InlineU32(1))); // Restore lane-zero EXEC. AppendVMovLiteral(&code, 13, 0x3f800000u); // Positive normal. AppendVMovU32(&code, 3, 9); AppendVMovU32(&code, 31, 4); code.push_back(EncodeSMovB32(106, InlineU32(2))); // qNaN class mask. code.insert(code.end(), {0x7d30d4f9u, 0x8606000du}); AppendBufferStoreDword(&code, 3, 31); AppendEnd(&code); TestCase test{ "VectorVopcSdwaCmpxClassF32CapturedExecMask", code, {0x11111111u, 0x22222222u}, {7u, 0x22222222u}, {O::S_MOV_B32, O::V_MOV_B32, O::V_CMPX_CLASS_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = { {"V_CMPX_CLASS_F32 exec_lo, v13, vcc_lo", 2}}; return test; } TestCase Wave32VccMasksPreserveOtherHalf() { using O = ShaderOpcode; TestCase test; test.name = "Wave32VccMasksPreserveOtherHalf"; auto &code = test.code; AppendSMovLiteral(&code, 106, 0x12345678u); AppendSMovLiteral(&code, 107, 0x2468ace0u); AppendVMovU32(&code, 1, 20); u32 offset = 0; const auto check_masks = [&](u32 low, u32 high) { AppendStoreSgprAtLaneDwordOffset(&code, 106, 0, offset); AppendStoreSgprAtLaneDwordOffset(&code, 107, 0, offset + 4); AppendVop3(&code, 0x101, 2, InlineU32(10), Vgpr(1), 107); code.push_back(EncodeVop2(0x01, 3, InlineU32(10), 1)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, offset + 8); AppendStoreVgprAtLaneDwordOffset(&code, 3, 0, offset + 12); test.expected.insert(test.expected.end(), 4, low); test.expected.insert(test.expected.end(), 4, high); for (u32 mask : {high, low}) { for (u32 lane = 0; lane < 4; ++lane) { test.expected.push_back((mask & (1u << lane)) != 0 ? 20u : 10u); } } offset += 16; }; // PPSA10737 keeps a table offset in VCC_LO while comparing into VCC_HI. AppendVop3(&code, 0xc2, 107, InlineU32(1), Vgpr(0)); check_masks(0x12345678u, 2u); AppendVop3(&code, 0xc2, 106, InlineU32(2), Vgpr(0)); check_masks(4u, 2u); code.push_back(EncodeSop2(0x10, 106, 106, 107)); // s_or_b32 check_masks(6u, 2u); code.push_back(EncodeSop1(0x04, 106, 193u)); // Explicit s_mov_b64 writes both. check_masks(0xffffffffu, 0xffffffffu); AppendEnd(&code); test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::S_OR_B32, O::V_MOV_B32, O::V_CMP_EQ_U32, O::V_CNDMASK_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = 32; test.has_compute_info = true; return test; } TestCase VectorCompareF16Ops() { using O = ShaderOpcode; std::vector code; std::vector expected; AppendVMovU32(&code, 1, 1); AppendVMovLiteral(&code, 2, 0x40003c00u); // low=1.0h, high=2.0h AppendVMovLiteral(&code, 3, 0x44004200u); // low=3.0h, high=4.0h AppendVMovLiteral(&code, 4, 0x00007e00u); // low=qNaN u32 dst = 10; u32 out = 0; auto append_compare = [&](u32 opcode, u32 src0, u32 src1, bool value) { code.push_back(EncodeVopc(opcode, src0, src1)); code.push_back(EncodeVop2(0x01, dst, InlineU32(0), 1)); AppendStoreVgpr(&code, dst, out++); expected.push_back(value ? 1u : 0u); dst++; }; append_compare(0xc9, Vgpr(2), 3, true); append_compare(0xca, Vgpr(2), 2, true); append_compare(0xcb, Vgpr(2), 3, true); append_compare(0xcc, Vgpr(3), 2, true); append_compare(0xcd, Vgpr(2), 3, true); append_compare(0xce, Vgpr(3), 2, true); append_compare(0xed, Vgpr(4), 2, true); auto append_cmpx = [&](u32 opcode, u32 src0, u32 src1) { code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeVopc(opcode, src0, src1)); AppendStoreVgpr(&code, 1, out++); expected.push_back(1u); }; append_cmpx(0xd9, Vgpr(2), 3); append_cmpx(0xda, Vgpr(2), 2); append_cmpx(0xdb, Vgpr(2), 3); append_cmpx(0xdc, Vgpr(3), 2); append_cmpx(0xde, Vgpr(3), 2); append_cmpx(0xfd, Vgpr(4), 2); append_cmpx(0xfe, Vgpr(2), 2); AppendEnd(&code); return {"VectorCompareF16Ops", code, {}, expected, {O::V_MOV_B32, O::S_MOV_B32, O::V_CMP_LT_F16, O::V_CMP_EQ_F16, O::V_CMP_LE_F16, O::V_CMP_GT_F16, O::V_CMP_LG_F16, O::V_CMP_GE_F16, O::V_CMP_NEQ_F16, O::V_CMPX_LT_F16, O::V_CMPX_EQ_F16, O::V_CMPX_LE_F16, O::V_CMPX_GT_F16, O::V_CMPX_GE_F16, O::V_CMPX_NEQ_F16, O::V_CMPX_NLT_F16, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop2SdwaCndmaskSourceModifier() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 6, 0x00000000u); AppendVMovLiteral(&code, 47, 0x12345678u); AppendVMovLiteral(&code, 53, 0x3f800000u); code.push_back(EncodeVopc(0xc7, Vgpr(6), 6)); // v_cmp_t_u32 code.push_back(0x025e6af9u); code.push_back(0x16060635u); AppendStoreVgpr(&code, 47, 0); AppendEnd(&code); return {"Vop2SdwaCndmaskSourceModifier", code, {}, {0xbf800000u}, {O::V_MOV_B32, O::V_CMP_T_U32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase Vop2SdwaCndmaskCapturedByte3Source() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x55667788u); AppendBufferLoadDword(&code, 3, 30); code.push_back(EncodeVopc(0xc0, Vgpr(0), 0)); // v_cmp_f_u32 code.push_back(0x023a00f9u); code.push_back(0x06030603u); AppendStoreVgpr(&code, 29, 1); code.push_back(EncodeVopc(0xc7, Vgpr(0), 0)); // v_cmp_t_u32 code.push_back(0x023a00f9u); code.push_back(0x06030603u); AppendStoreVgpr(&code, 29, 2); AppendEnd(&code); TestCase test; test.name = "Vop2SdwaCndmaskCapturedByte3Source"; test.code = code; test.initial = {0xabcd1234u, 0, 0}; test.expected = {0xabcd1234u, 0x000000abu, 0x55667788u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CMP_F_U32, O::V_CMP_T_U32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_CNDMASK_B32 v29, v3.sdwa(sel=3,sext=0), v0", 2}}; test.required_spirv = {"OpBitFieldUExtract", "OpSelect"}; return test; } TestCase Vop3CndmaskUsesSgprMaskLaneBits() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVopc(0xc2, InlineU32(0), 0), EncodeSop1(0x04, 4, 106), }; AppendVop3(&code, 0x101, 2, InlineU32(10), InlineU32(20), 4); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 1)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "Vop3CndmaskUsesSgprMaskLaneBits"; test.code = code; test.expected = {20, 10, 10, 10, 20, 10, 10, 10}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase Vop3CndmaskAllowsDataSourceModifier() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 7, 0x3f800000u); AppendSMovLiteral(&code, 34, 1); AppendSMovLiteral(&code, 35, 0); code.push_back(0xd5010004u); code.push_back(0x408a0f07u); AppendStoreVgpr(&code, 4, 0); AppendEnd(&code); return {"Vop3CndmaskAllowsDataSourceModifier", code, {}, {0xbf800000u}, {O::V_MOV_B32, O::S_MOV_B32, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorCompareExecOps() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, InlineU32(1))); AppendVMovLiteral(&code, 2, 0x40000000u); AppendVMovLiteral(&code, 3, 0x40400000u); code.push_back(EncodeVop1(0x01, 4, InlineU32(1))); AppendVMovLiteral(&code, 5, 0xffffffffu); u32 out = 0; auto append_cmpx = [&](u32 opcode, u32 src0, u32 src1) { code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeVopc(opcode, src0, src1)); AppendStoreVgpr(&code, 1, out++); }; append_cmpx(0x11, Vgpr(2), 3); append_cmpx(0x12, Vgpr(3), 3); append_cmpx(0x13, Vgpr(2), 3); append_cmpx(0x14, Vgpr(3), 2); append_cmpx(0x15, Vgpr(2), 3); append_cmpx(0x16, Vgpr(3), 3); append_cmpx(0x19, Vgpr(2), 3); append_cmpx(0x1a, Vgpr(3), 3); append_cmpx(0x1b, Vgpr(2), 3); append_cmpx(0x1c, Vgpr(3), 2); append_cmpx(0x1d, Vgpr(2), 3); append_cmpx(0x1e, Vgpr(3), 3); append_cmpx(0x91, Vgpr(5), 4); append_cmpx(0x92, Vgpr(4), 4); append_cmpx(0x93, Vgpr(5), 4); append_cmpx(0x94, Vgpr(4), 5); append_cmpx(0x95, Vgpr(5), 4); append_cmpx(0x96, Vgpr(4), 4); append_cmpx(0xd1, Vgpr(4), 5); append_cmpx(0xd2, Vgpr(4), 4); append_cmpx(0xd3, Vgpr(4), 5); append_cmpx(0xd4, Vgpr(5), 4); append_cmpx(0xd5, Vgpr(4), 5); append_cmpx(0xd6, Vgpr(4), 4); AppendEnd(&code); return {"VectorCompareExecOps", code, {}, std::vector(out, 1), {O::S_MOV_B32, O::V_MOV_B32, O::V_CMPX_LT_F32, O::V_CMPX_EQ_F32, O::V_CMPX_LE_F32, O::V_CMPX_GT_F32, O::V_CMPX_LG_F32, O::V_CMPX_GE_F32, O::V_CMPX_NGE_F32, O::V_CMPX_NLG_F32, O::V_CMPX_NGT_F32, O::V_CMPX_NLE_F32, O::V_CMPX_NEQ_F32, O::V_CMPX_NLT_F32, O::V_CMPX_LT_I32, O::V_CMPX_EQ_I32, O::V_CMPX_LE_I32, O::V_CMPX_GT_I32, O::V_CMPX_NE_I32, O::V_CMPX_GE_I32, O::V_CMPX_LT_U32, O::V_CMPX_EQ_U32, O::V_CMPX_LE_U32, O::V_CMPX_GT_U32, O::V_CMPX_NE_U32, O::V_CMPX_GE_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3FloatCompareNegSourceModifier() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovU32(&code, 1, 0); AppendVop3(&code, 0x01, 20, Vgpr(0), Vgpr(1), 0, 0, 0, false, 0, 0x1); // -1.0 < 0.0 AppendStoreSgprPair(&code, 20, 0); AppendEnd(&code); return {"VectorVop3FloatCompareNegSourceModifier", code, {}, {1, 0}, {O::V_MOV_B32, O::V_CMP_LT_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVop3CmpxWritesExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 2); AppendVMovU32(&code, 1, 1); AppendVMovU32(&code, 2, 0); AppendVMovU32(&code, 30, 0); AppendVop3(&code, 0xd1, 5, Vgpr(0), Vgpr(1)); // v_cmpx_lt_u32, false AppendVMovU32(&code, 2, 7); AppendBufferStoreDword(&code, 2, 30); AppendEnd(&code); return {"VectorVop3CmpxWritesExecMask", code, {0}, {0}, {O::V_MOV_B32, O::V_CMPX_LT_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVopcSdwaCmpxWritesExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 2); AppendVMovU32(&code, 1, 1); AppendVMovU32(&code, 2, 0); AppendVMovU32(&code, 30, 0); code.push_back(EncodeVopc(0xd1, 249, 1)); // v_cmpx_lt_u32.sdwa, false code.push_back(EncodeVopcSdwa(0)); AppendVMovU32(&code, 2, 7); AppendBufferStoreDword(&code, 2, 30); AppendEnd(&code); return {"VectorVopcSdwaCmpxWritesExecMask", code, {0}, {0}, {O::V_MOV_B32, O::V_CMPX_LT_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase VectorVopcCmpxGtU16CapturedSdwaExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 2, 0x00008000u); AppendVMovU32(&code, 3, 7); AppendVMovU32(&code, 31, 0); code.insert(code.end(), {0x7d7900f9u, 0x86060002u}); AppendBufferStoreDword(&code, 3, 31); AppendVMovU32(&code, 2, 0x00010000u); AppendVMovU32(&code, 3, 9); AppendVMovU32(&code, 31, 4); code.insert(code.end(), {0x7d7900f9u, 0x86060002u}); AppendBufferStoreDword(&code, 3, 31); AppendEnd(&code); TestCase test{"VectorVopcCmpxGtU16CapturedSdwaExecMask", code, {0x11111111u, 0x22222222u}, {7, 0x22222222u}, {O::V_MOV_B32, O::V_CMPX_GT_U16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMPX_GT_U16", 2}}; return test; } TestCase VectorVopcCmpxLtU16CapturedSdwaExecMask() { using O = ShaderOpcode; struct CompareCase { u32 lhs; u32 rhs; u32 incoming_exec; u32 expected_exec; u32 encoding = 0; // Captured SDWA, compact, VOP3, WORD_1 src0/src1. }; const std::array cases{{ {0xffff0000u, 0x12340001u, 1, 1}, // Ignore high halves. {0xabcd0001u, 0x12340001u, 1, 0}, // Strict comparison: equal is false. {0x0000ffffu, 0x12340001u, 1, 0}, // Unsigned, not signed -1. {0x00007fffu, 0x12348000u, 1, 1}, {0x00008000u, 0x12347fffu, 1, 0}, {0x12340000u, 0x56780000u, 1, 0}, {0x0000fffeu, 0x1234ffffu, 1, 1}, {0x00000000u, 0x12340001u, 0, 0}, // CMPX cannot reactivate an inactive lane. {0x00008000u, 0x0000ffffu, 1, 1, 1}, {0x0000ffffu, 0x00008000u, 1, 0, 2}, {0x0001ffffu, 0x12340002u, 1, 1, 3}, {0x00000001u, 0x00020000u, 1, 1, 4}, }}; constexpr u32 vcc_hi = 0x89abcdefu; TestCase test; test.name = "VectorVopcCmpxLtU16CapturedSdwaExecMask"; for (const auto &entry : cases) { test.initial.push_back(entry.lhs); } test.expected = test.initial; auto &code = test.code; for (u32 i = 0; i < cases.size(); ++i) { const auto &entry = cases[i]; AppendVMovU32(&code, 30, i * 4u); AppendBufferLoadDword(&code, 0, 30); // Runtime input prevents constant folding. AppendVMovU32(&code, 1, entry.rhs); AppendSMovLiteral(&code, 106, entry.rhs); AppendSMovLiteral(&code, 107, vcc_hi); code.push_back(EncodeSMovB32(126, InlineU32(entry.incoming_exec))); switch (entry.encoding) { case 1: code.push_back(0x7d720300u); break; // compact v0, v1 case 2: code.insert(code.end(), {0xd4b9007eu, 0x00020300u}); break; case 3: code.insert(code.end(), {0x7d72d4f9u, 0x86050000u}); break; case 4: code.insert(code.end(), {0x7d72d4f9u, 0x85060000u}); break; default: code.insert(code.end(), {0x7d72d4f9u, 0x86060000u}); break; } code.push_back(EncodeSMovB32(20, 126)); // Snapshot EXEC before restoring it. code.push_back(EncodeSMovB32(21, 127)); code.push_back(EncodeSMovB32(126, InlineU32(1))); const u32 out = static_cast(cases.size()) + i * 4u; AppendStoreSgprPair(&code, 20, out); AppendStoreSgprPair(&code, 106, out + 2u); // CMPX must not overwrite either VCC half. test.expected.insert(test.expected.end(), {entry.expected_exec, 0u, entry.rhs, vcc_hi}); } AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CMPX_LT_U16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_CMPX_LT_U16", cases.size()}}; test.required_spirv = {"OpULessThan"}; return test; } TestCase VectorVopcCmpNgtF16CapturedSdwaAndEdges() { using O = ShaderOpcode; enum Encoding { Captured, Compact, Vop3NegAbs, HighWord, ScalarDst, SdwaNegAbs }; struct CompareCase { u32 lhs; u32 rhs; u32 expected; Encoding encoding = Captured; u32 exec = 1; }; const std::array cases{{ {0x7e003c00u, 0, 0}, // +1; ignore NaN in the upper half. {0x3c00bc00u, 0, 1}, // -1 {0x3c000000u, 0, 1}, // +0 {0x3c008000u, 0, 1}, // -0 {0x3c007e00u, 0, 1}, // Quiet NaN: NGT differs from ordered LE. {0x3c007c01u, 0, 1}, // Signaling NaN {0x00007c00u, 0, 0}, // +infinity {0x0000fc00u, 0, 1}, // -infinity {0x0000bc00u, 0, 0, Captured, 0}, // Inactive lane contributes no VCC bit. {0x00003c00u, 0x00007e00u, 1, Compact}, // NaN in src1. {0x00004000u, 0x00003c00u, 0, Compact}, // +2 > +1 {0x0000bc00u, 0, 1, Vop3NegAbs}, // -abs(-1) {0x00003c00u, 0, 1, Vop3NegAbs}, // -abs(+1) {0xbc004000u, 0x3c00c000u, 1, HighWord}, // Select both upper halves. {0x00004000u, 0x00003c00u, 0, ScalarDst}, {0x00003c00u, 0x00004000u, 1, ScalarDst}, {0x0000bc00u, 0, 1, SdwaNegAbs}, }}; constexpr u32 vcc_sentinel = 0x89abcdefu; TestCase test; test.name = "VectorVopcCmpNgtF16CapturedSdwaAndEdges"; for (const auto &entry : cases) { test.initial.insert(test.initial.end(), {entry.lhs, entry.rhs}); } test.expected = test.initial; auto &code = test.code; for (u32 i = 0; i < cases.size(); ++i) { const auto &entry = cases[i]; AppendVMovU32(&code, 30, i * 8u); AppendBufferLoadDword(&code, 21, 30); // Runtime inputs prevent constant folding. AppendVMovU32(&code, 30, i * 8u + 4u); AppendBufferLoadDword(&code, 1, 30); AppendSMovLiteral(&code, 106, vcc_sentinel); AppendSMovLiteral(&code, 107, vcc_sentinel); code.push_back(EncodeSMovB32(126, InlineU32(entry.exec))); switch (entry.encoding) { case Captured: code.insert(code.end(), {0x7dd700f9u, 0x86060015u}); break; case Compact: code.push_back(EncodeVopc(0xeb, Vgpr(21), 1)); break; case Vop3NegAbs: AppendVop3(&code, 0xeb, 106, Vgpr(21), Vgpr(1), 0, 1, 0, false, 0, 1); break; case HighWord: code.push_back(EncodeVopc(0xeb, 249, 1)); code.push_back(EncodeVopcSdwa(21, 0, 0, 5, 5)); break; case ScalarDst: code.push_back(EncodeVopc(0xeb, 249, 1)); code.push_back(EncodeVopcSdwa(21, 22, 1)); break; case SdwaNegAbs: code.push_back(EncodeVopc(0xeb, 249, 1)); code.push_back(EncodeVopcSdwa(21, 0, 0, 6, 6, 0, 0, 1, 1)); break; } code.push_back(EncodeSMovB32(20, entry.encoding == ScalarDst ? 22 : 106)); code.push_back(EncodeSMovB32(21, 126)); // CMP must preserve EXEC even if false. code.push_back(EncodeSMovB32(126, InlineU32(1))); const u32 out = static_cast(test.initial.size()) + i * 4u; AppendStoreSgprPair(&code, 20, out); AppendStoreSgprPair(&code, 106, out + 2u); test.expected.insert(test.expected.end(), {entry.expected, entry.exec, entry.encoding == ScalarDst ? vcc_sentinel : entry.expected, vcc_sentinel}); } AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CMP_NGT_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_CMP_NGT_F16", cases.size()}}; test.required_spirv = {"OpFUnordLessThanEqual"}; test.compute_info.wave_size = 32; test.has_compute_info = true; return test; } TestCase VectorVopcCmpNltF16CapturedSdwaAndEdges() { using O = ShaderOpcode; enum Encoding { Captured, Compact, Vop3NegAbs, SdwaNegAbs }; struct CompareCase { u32 packed; // Captured instruction compares low half against high half. u32 expected; Encoding encoding = Captured; u32 exec = 1; }; const std::array cases{{ {0x40003c00u, 0}, // +1 < +2 {0x3c004000u, 1}, // +2 > +1 {0x3c003c00u, 1}, // Equal {0x80000000u, 1}, // +0 versus -0 {0x00008000u, 1}, // -0 versus +0 {0x3c007e00u, 1}, // Quiet NaN in src0: unordered GE, not ordered GE. {0x7e003c00u, 1}, // Quiet NaN in src1 {0x3c007c01u, 1}, // Signaling NaN in src0 {0x7c013c00u, 1}, // Signaling NaN in src1 {0xfc007c00u, 1}, // +infinity versus -infinity {0x7c00fc00u, 0}, // -infinity versus +infinity {0x00003c00u, 0, Captured, 0}, // Inactive lane contributes no mask bit. {0x40003c00u, 0, Compact}, {0x7e003c00u, 1, Compact}, {0x0000bc00u, 0, Vop3NegAbs}, // -abs(-1) < 0 {0x0000bc00u, 0, SdwaNegAbs}, }}; constexpr u32 sentinel = 0x89abcdefu; TestCase test; test.name = "VectorVopcCmpNltF16CapturedSdwaAndEdges"; for (const auto &entry : cases) { test.initial.push_back(entry.packed); } test.expected = test.initial; auto &code = test.code; code.push_back(EncodeSMovB32(22, 2)); // Preserve the input buffer descriptor. code.push_back(EncodeSMovB32(23, 3)); for (u32 i = 0; i < cases.size(); ++i) { const auto &entry = cases[i]; AppendVMovU32(&code, 30, i * 4u); AppendBufferLoadDword(&code, 3, 30); // Runtime input prevents constant folding. AppendVMovU32(&code, 1, entry.packed >> 16u); for (u32 reg : {3u, 106u, 107u}) { AppendSMovLiteral(&code, reg, sentinel); } code.push_back(EncodeSMovB32(126, InlineU32(entry.exec))); switch (entry.encoding) { case Captured: code.insert(code.end(), {0x7ddc06f9u, 0x05068203u}); break; case Compact: code.push_back(EncodeVopc(0xee, Vgpr(3), 1)); break; case Vop3NegAbs: AppendVop3(&code, 0xee, 2, Vgpr(3), Vgpr(1), 0, 1, 0, false, 0, 1); break; case SdwaNegAbs: code.insert(code.end(), {0x7ddc06f9u, 0x05368203u}); break; } code.push_back(EncodeSMovB32(20, entry.encoding == Compact ? 106 : 2)); code.push_back(EncodeSMovB32(21, 3)); // Wave32 must preserve adjacent SGPR. code.push_back(EncodeSMovB32(24, 126)); code.push_back(EncodeSMovB32(25, 127)); code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeSMovB32(2, 22)); code.push_back(EncodeSMovB32(3, 23)); const u32 out = static_cast(cases.size()) + i * 6u; AppendStoreSgprPair(&code, 20, out); AppendStoreSgprPair(&code, 24, out + 2u); AppendStoreSgprPair(&code, 106, out + 4u); test.expected.insert(test.expected.end(), {entry.expected, sentinel, entry.exec, 0u, entry.encoding == Compact ? entry.expected : sentinel, sentinel}); } AppendEnd(&code); test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CMP_NLT_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.decoded_counts = {{"V_CMP_NLT_F16", cases.size()}}; test.required_spirv = {"OpFUnordGreaterThanEqual"}; test.compute_info.wave_size = 32; test.has_compute_info = true; return test; } TestCase VectorVopcCmpxNgtF16CapturedSdwaExecMask() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 31, 0x7e003c00u); AppendVMovU32(&code, 3, 7); AppendVMovU32(&code, 30, 0); code.insert(code.end(), {0x7df700f9u, 0x8605001fu}); AppendBufferStoreDword(&code, 3, 30); AppendVMovLiteral(&code, 31, 0x3c007e00u); AppendVMovU32(&code, 3, 9); AppendVMovU32(&code, 30, 4); code.insert(code.end(), {0x7df700f9u, 0x8605001fu}); AppendBufferStoreDword(&code, 3, 30); AppendEnd(&code); TestCase test{"VectorVopcCmpxNgtF16CapturedSdwaExecMask", code, {0x11111111u, 0x22222222u}, {7, 0x22222222u}, {O::V_MOV_B32, O::V_CMPX_NGT_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"V_CMPX_NGT_F16", 2}}; return test; } TestCase VectorCompareInvertedMaskSelect() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeVopc(0xc7, Vgpr(0), 0)); // v_cmp_t_u32 v0, v0 code.push_back(EncodeSop1(0x08, 106, 106)); // s_not_b64 vcc, vcc code.push_back(EncodeVop2(0x01, 1, Vgpr(0), 128)); // v_cndmask_b32 v1, v0, 0 AppendBufferStoreDword(&code, 1, 30); AppendEnd(&code); return {"VectorCompareInvertedMaskSelect", code, {}, {0x12345678u}, {O::V_MOV_B32, O::V_CMP_T_U32, O::S_NOT_B64, O::V_CNDMASK_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BranchSelect() { using O = ShaderOpcode; std::vector code = { EncodeSMovB32(0, InlineU32(0)), EncodeSopc(0x06, 0, InlineU32(0)), EncodeSopp(0x05, 2), EncodeSMovB32(1, InlineU32(1)), EncodeSopp(0x02, 1), EncodeSMovB32(1, InlineU32(7)), EncodeSop2(0x0a, 2, 1, InlineU32(2)), EncodeVop1(0x01, 0, 2), }; AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"BranchSelect", code, {}, {7}, {O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_CBRANCH_SCC1, O::S_BRANCH, O::S_CSELECT_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase SimpleLoop() { using O = ShaderOpcode; std::vector code = { EncodeSMovB32(0, InlineU32(0)), EncodeSopc(0x0a, 0, InlineU32(4)), EncodeSopp(0x04, 2), EncodeSop2(0x00, 0, 0, InlineU32(1)), EncodeSopp(0x02, 0xfffcu), EncodeVop1(0x01, 0, 0), }; AppendBufferStoreDword(&code, 0, 30); AppendEnd(&code); return {"SimpleLoop", code, {}, {4}, {O::S_MOV_B32, O::S_CMP_LT_U32, O::S_CBRANCH_SCC0, O::S_ADD_U32, O::S_BRANCH, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase SharedReturnKeepsSelectedValues() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 5, 4), EncodeVop2(0x1a, 5, InlineU32(6), 5), EncodeVop2(0x25, 5, Vgpr(0), 5), EncodeSMovB32(8, InlineU32(10)), EncodeSopc(0x06, 4, InlineU32(0)), 0, EncodeSMovB32(8, InlineU32(20)), EncodeSopc(0x06, 4, InlineU32(1)), 0, EncodeSMovB32(8, InlineU32(30)), }; AppendStoreSgprAtLaneDwordOffset(&code, 8, 5, 0); AppendEnd(&code); const auto shared_return = static_cast(code.size()); AppendStoreSgprAtLaneDwordOffset(&code, 8, 5, 0); AppendEnd(&code); code[5] = EncodeSopp(0x05, shared_return - 6u); code[8] = EncodeSopp(0x05, shared_return - 9u); TestCase test; test.name = "SharedReturnKeepsSelectedValues"; test.code = code; for (u32 group = 0; group < 3; group++) { test.expected.insert(test.expected.end(), 64, (group + 1u) * 10u); } test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::S_MOV_B32, O::S_CMP_EQ_U32, O::S_CBRANCH_SCC1, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpSelectionMerge"}; test.forbidden_spirv = {"OpSwitch"}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 4; test.has_compute_info = true; test.dispatch_x = 3; return test; } TestCase BranchVccnzUsesWaveMask() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVop2(0x1a, 3, InlineU32(2), 1), EncodeVopc(0xc2, InlineU32(0), 0), EncodeSopp(0x07, 2), EncodeVop1(0x01, 2, InlineU32(11)), EncodeSopp(0x02, 1), }; AppendVMovU32(&code, 2, 42); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "BranchVccnzUsesWaveMask"; test.code = code; test.expected = std::vector(8, 42); test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::S_CBRANCH_VCCNZ, O::S_BRANCH, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase ScalarMemoryLoadVariants() { using O = ShaderOpcode; struct Load { u32 opcode; u32 dst; u32 byte_offset; u32 dwords; }; const Load s_loads[] = { {0x00, 20, 0, 1}, {0x01, 22, 4, 2}, {0x02, 24, 12, 4}, {0x03, 28, 28, 8}, {0x04, 36, 60, 16}, }; const Load s_buffer_loads[] = { {0x08, 52, 0, 1}, {0x09, 54, 4, 2}, {0x0a, 56, 12, 4}, {0x0b, 60, 28, 8}, {0x0c, 68, 60, 16}, }; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(0))); for (const auto &load : s_loads) { AppendSmemLoadOpcode(&code, load.opcode, load.dst, load.byte_offset); } for (const auto &load : s_buffer_loads) { AppendSmemLoadOpcode(&code, load.opcode, load.dst, load.byte_offset); } u32 out = 0; for (const auto &load : s_loads) { for (u32 i = 0; i < load.dwords; i++) { AppendStoreSgpr(&code, load.dst + i, out++); } } for (const auto &load : s_buffer_loads) { for (u32 i = 0; i < load.dwords; i++) { AppendStoreSgpr(&code, load.dst + i, out++); } } AppendEnd(&code); std::vector initial; for (u32 i = 0; i < 31u; i++) { initial.push_back(0x10000000u + i); } std::vector expected = initial; expected.insert(expected.end(), initial.begin(), initial.end()); return {"ScalarMemoryLoadVariants", code, initial, expected, {O::S_MOV_B32, O::S_LOAD_DWORD, O::S_LOAD_DWORDX2, O::S_LOAD_DWORDX4, O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX16, O::S_BUFFER_LOAD_DWORD, O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4, O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase ScalarLoadSignedImmediateOffsetAddsSoffset() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(8))); code.push_back(EncodeSMovB32(2, InlineU32(0))); code.push_back(EncodeSMovB32(3, InlineU32(0))); code.push_back(EncodeSmem0(0x00, 1, 1)); code.push_back(EncodeSmem1(0x1ffffcu, 0)); AppendStoreSgpr(&code, 1, 0); AppendEnd(&code); return {"ScalarLoadSignedImmediateOffsetAddsSoffset", code, {0x11111111u, 0x22222222u}, {0x22222222u, 0x22222222u}, {O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferLoadStore() { using O = ShaderOpcode; std::vector code; AppendBufferLoadDword(&code, 0, 30); code.push_back(EncodeVop1(0x01, 31, InlineU32(4))); AppendBufferStoreDword(&code, 0, 31); AppendEnd(&code); return { "BufferLoadStore", code, {0x11223344u, 0}, {0x11223344u, 0x11223344u}, {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferLoadDwordOffenIdxenUsesVaddrPlusOneOffset() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 8); code.push_back(EncodeMubuf0(0x0cu, 0, true, true)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return { "BufferLoadDwordOffenIdxenUsesVaddrPlusOneOffset", code, {0x11111111u, 0x22222222u, 0x33333333u}, {0x33333333u, 0x22222222u, 0x33333333u}, {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferStoreDwordOffenIdxenUsesVaddrPlusOneOffset() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 12); AppendVMovLiteral(&code, 0, 0xabcdef01u); code.push_back(EncodeMubuf0(0x1cu, 0, true, true)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); return {"BufferStoreDwordOffenIdxenUsesVaddrPlusOneOffset", code, {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}, {0x11111111u, 0x22222222u, 0x33333333u, 0xabcdef01u}, {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferLoadDwordNoAddressFlagsIgnoresVaddr() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 8); code.push_back(EncodeMubuf0(0x0cu, 0, false, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 3); AppendEnd(&code); return { "BufferLoadDwordNoAddressFlagsIgnoresVaddr", code, {0x11111111u, 0x22222222u, 0x33333333u, 0}, {0x11111111u, 0x22222222u, 0x33333333u, 0x11111111u}, {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferLoadDwordIdxenUsesDescriptorStride() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); code.push_back(EncodeMubuf0(0x0cu, 0, true, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "BufferLoadDwordIdxenUsesDescriptorStride"; test.code = code; test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0x99aabbccu}; test.expected = {0x99aabbccu, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0x99aabbccu}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(16, 3); test.has_user_data = true; return test; } TestCase BufferStoreDwordIdxenUsesDescriptorStride() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovLiteral(&code, 0, 0xabcdef01u); code.push_back(EncodeMubuf0(0x1cu, 0, true, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreDwordIdxenUsesDescriptorStride"; test.code = code; test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0x99aabbccu}; test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0xabcdef01u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(16, 3); test.has_user_data = true; return test; } TestCase BufferStoreDwordAppliesHostOffset() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xabcdef01u); code.push_back(EncodeMubuf0(0x1cu, 0, false, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreDwordAppliesHostOffset"; test.code = code; test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0}; test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0xabcdef01u}; test.storage_buffer_range_dwords = 1; test.storage_buffer_offsets = {12}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(4, 1); test.has_user_data = true; return test; } TestCase BufferOffsetsUsePackedLaneAndStorageFallback() { using O = ShaderOpcode; std::vector code; for (u32 base : {0u, 4u}) { AppendSMovLiteral(&code, base, 0x1000u + base * 0x1000u); AppendSMovLiteral(&code, base + 1u, 4u << 16u); AppendSMovLiteral(&code, base + 2u, 1u); AppendSMovLiteral(&code, base + 3u, 1u << 24u); } AppendVMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeMubuf0(0x1cu, 0, false, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendVMovLiteral(&code, 1, 0xabcdef01u); code.push_back(EncodeMubuf0(0x1cu, 0, false, false)); code.push_back(EncodeMubuf1(1, 1, 20)); AppendEnd(&code); TestCase test; test.name = "BufferOffsetsUsePackedLaneAndStorageFallback"; test.code = std::move(code); test.initial = {0, 0, 0, 0}; test.expected = {0x12345678u, 0, 0, 0xabcdef01u}; test.storage_buffer_range_dwords = 1; test.storage_buffer_offsets = {0, 12}; test.expand_shader_data_storage = true; test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BufferLoadVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x08, 0, 20); AppendVMovU32(&code, 20, 2); AppendBufferLoadOpcode(&code, 0x09, 1, 20); AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x0a, 2, 20); AppendVMovU32(&code, 20, 2); AppendBufferLoadOpcode(&code, 0x0b, 3, 20); AppendVMovU32(&code, 20, 8); AppendBufferLoadOpcode(&code, 0x0d, 4, 20); AppendBufferLoadOpcode(&code, 0x0f, 6, 20); AppendBufferLoadOpcode(&code, 0x0e, 9, 20); for (u32 i = 0; i < 13u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"BufferLoadVariants", code, {0x80ff7f01u, 0x00008001u, 0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}, {0x01u, 0xffffffffu, 0x7f01u, 0xffff80ffu, 0x11223344u, 0x55667788u, 0x11223344u, 0x55667788u, 0x99aabbccu, 0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}, {O::BUFFER_LOAD_UBYTE, O::BUFFER_LOAD_SBYTE, O::BUFFER_LOAD_USHORT, O::BUFFER_LOAD_SSHORT, O::BUFFER_LOAD_DWORDX2, O::BUFFER_LOAD_DWORDX3, O::BUFFER_LOAD_DWORDX4, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferLoadDwordx4SnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMubuf0(0x0eu, 0, true, true)); code.push_back(EncodeMubuf1(21, 0, 21)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 21 + i, 4 + i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadDwordx4SnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0, 0, 0, 0}; test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX4, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(16, 2); test.has_user_data = true; return test; } TestCase BufferLoadDwordx2SnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMubuf0(0x0du, 0, true, true)); code.push_back(EncodeMubuf1(21, 0, 21)); for (u32 i = 0; i < 2; i++) { AppendStoreVgpr(&code, 21 + i, 2 + i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadDwordx2SnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0, 0}; test.expected = {0x11111111u, 0x22222222u, 0x11111111u, 0x22222222u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX2, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(8, 2); test.has_user_data = true; return test; } TestCase BufferLoadDwordx3SnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMubuf0(0x0fu, 0, true, true)); code.push_back(EncodeMubuf1(21, 0, 21)); for (u32 i = 0; i < 3; i++) { AppendStoreVgpr(&code, 21 + i, 3 + i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadDwordx3SnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0, 0, 0}; test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0x11111111u, 0x22222222u, 0x33333333u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX3, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(12, 2); test.has_user_data = true; return test; } TestCase BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 8); AppendBufferLoadOpcode(&code, 0x0e, 0, 20); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x33333333u, 0x44444444u, 0u, 0u}; test.storage_buffer_range_dwords = 4; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX4, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BufferStoreDwordx4DropsOnlyOutOfBoundsTail() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xaaaaaaaau); AppendVMovLiteral(&code, 1, 0xbbbbbbbbu); AppendVMovLiteral(&code, 2, 0xccccccccu); AppendVMovLiteral(&code, 3, 0xddddddddu); AppendVMovU32(&code, 20, 8); code.push_back(EncodeMubuf0(0x1eu)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreDwordx4DropsOnlyOutOfBoundsTail"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x11111111u, 0x22222222u, 0xaaaaaaaau, 0xbbbbbbbbu}; test.storage_buffer_range_dwords = 4; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORDX4, O::S_ENDPGM}; return test; } TestCase BufferLoadFormatXyzwRejectsPartialRecord() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 8); AppendBufferLoadOpcode(&code, 0x03, 0, 20); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwRejectsPartialRecord"; test.code = std::move(code); test.initial = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.expected = {0u, 0u, 0u, 0u}; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BufferStoreFormatXyzwDropsPartialRecord() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xaaaaaaaau); AppendVMovLiteral(&code, 1, 0xbbbbbbbbu); AppendVMovLiteral(&code, 2, 0xccccccccu); AppendVMovLiteral(&code, 3, 0xddddddddu); AppendVMovU32(&code, 20, 8); code.push_back(EncodeMubuf0(0x07u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyzwDropsPartialRecord"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = test.initial; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}; return test; } TestCase BufferLoadFormatXChecksOnlyTransferredComponent() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 12); AppendBufferLoadOpcode(&code, 0x00, 0, 20); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXChecksOnlyTransferredComponent"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x44444444u, 0x22222222u, 0x33333333u, 0x44444444u}; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BufferStoreFormatXChecksOnlyTransferredComponent() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xaaaaaaaau); AppendVMovU32(&code, 20, 12); code.push_back(EncodeMubuf0(0x04u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXChecksOnlyTransferredComponent"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0xaaaaaaaau}; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; return test; } TestCase BufferLoadFormatXyChecksOnlyTransferredComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 8); AppendBufferLoadOpcode(&code, 0x01, 0, 20); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyChecksOnlyTransferredComponents"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x33333333u, 0x44444444u, 0x33333333u, 0x44444444u}; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BufferStoreFormatXyChecksOnlyTransferredComponents() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0xaaaaaaaau); AppendVMovLiteral(&code, 1, 0xbbbbbbbbu); AppendVMovU32(&code, 20, 8); code.push_back(EncodeMubuf0(0x05u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyChecksOnlyTransferredComponents"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0x11111111u, 0x22222222u, 0xaaaaaaaau, 0xbbbbbbbbu}; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XY, O::S_ENDPGM}; return test; } TestCase BufferStoreVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 0, 0xaa); AppendBufferStoreOpcode(&code, 0x18, 0, 20); AppendVMovU32(&code, 20, 2); AppendVMovLiteral(&code, 1, 0x0000bbccu); AppendBufferStoreOpcode(&code, 0x1a, 1, 20); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); AppendBufferStoreOpcode(&code, 0x1d, 2, 20); AppendVMovU32(&code, 20, 12); AppendVMovLiteral(&code, 4, 0x33333333u); AppendVMovLiteral(&code, 5, 0x44444444u); AppendVMovLiteral(&code, 6, 0x55555555u); AppendBufferStoreOpcode(&code, 0x1f, 4, 20); AppendVMovU32(&code, 20, 24); AppendVMovLiteral(&code, 7, 0x66666666u); AppendVMovLiteral(&code, 8, 0x77777777u); AppendVMovLiteral(&code, 9, 0x88888888u); AppendVMovLiteral(&code, 10, 0x99999999u); AppendBufferStoreOpcode(&code, 0x1e, 7, 20); AppendEnd(&code); return {"BufferStoreVariants", code, std::vector(10, 0), {0xbbccaa00u, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0x99999999u}, {O::V_MOV_B32, O::BUFFER_STORE_BYTE, O::BUFFER_STORE_SHORT, O::BUFFER_STORE_DWORDX2, O::BUFFER_STORE_DWORDX3, O::BUFFER_STORE_DWORDX4, O::S_ENDPGM}}; } TestCase BufferFormatVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x00, 0, 20); AppendBufferLoadOpcode(&code, 0x01, 1, 20); AppendBufferLoadOpcode(&code, 0x02, 3, 20); AppendBufferLoadOpcode(&code, 0x03, 6, 20); for (u32 i = 0; i < 10u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase load; load.name = "BufferFormatLoadVariants"; load.code = code; load.initial = {0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u}; load.expected = {0x01020304u, 0x01020304u, 0x11121314u, 0x01020304u, 0x11121314u, 0x21222324u, 0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u}; load.opcodes = {O::BUFFER_LOAD_FORMAT_X, O::BUFFER_LOAD_FORMAT_XY, O::BUFFER_LOAD_FORMAT_XYZ, O::BUFFER_LOAD_FORMAT_XYZW, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return load; } TestCase BufferLoadFormatXyzwSnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMubuf0(0x03u, 0, true, true)); code.push_back(EncodeMubuf1(21, 0, 21)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 21 + i, 4 + i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwSnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0, 0, 0, 0}; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData( 16, 2, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; return test; } TestCase BufferLoadFormatXyzwResource32_32FloatAppliesXy01() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x03, 0, 20); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwResource32_32FloatAppliesXy01"; test.code = std::move(code); test.initial = {0x3f800000u, 0x40000000u, 0, 0}; test.expected = {0x3f800000u, 0x40000000u, 0, 0x3f800000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData( 16, 1, false, BufferFormat(Prospero::BufferFormat::k32_32Float)); test.user_data[3] = (test.user_data[3] & ~0xfffu) | DstSel(4, 5, 0, 1); test.has_user_data = true; return test; } TestCase BufferLoadFormatXyzwResource323232FloatRepeatsSource() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x03, 0, 20); for (u32 i = 0; i < 4; ++i) AppendStoreVgpr(&code, i, i + 4); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwResource323232FloatRepeatsSource"; test.code = std::move(code); test.initial = {0x3e800000u, 0x3f000000u, 0x3f400000u, 0xdeadbeefu, 0, 0, 0, 0}; test.expected = {0x3e800000u, 0x3f000000u, 0x3f400000u, 0xdeadbeefu, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3e800000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData( 0, 32, false, BufferFormat(Prospero::BufferFormat::k32_32_32Float)); test.has_user_data = true; return test; } TestCase BufferLoadFormatXyzwResource8_8_8_8UintAppliesWzy1() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendBufferLoadOpcode(&code, 0x03, 0, 20); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwResource8_8_8_8UintAppliesWzy1"; test.code = std::move(code); test.initial = {0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}; test.expected = {0x11u, 0x22u, 0x33u, 1u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData( 16, 1, false, BufferFormat(Prospero::BufferFormat::k8_8_8_8UInt)); test.user_data[3] = (test.user_data[3] & ~0xfffu) | DstSel(7, 6, 5, 1); test.has_user_data = true; return test; } TestCase BufferLoadFormatXyzwInactiveExecPreservesOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 21, 0x11111111u); AppendVMovLiteral(&code, 22, 0x22222222u); AppendVMovLiteral(&code, 23, 0x33333333u); AppendVMovLiteral(&code, 24, 0x44444444u); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeMubuf0(0x03u, 0, true, true)); code.push_back(EncodeMubuf1(21, 0, 21)); code.push_back(EncodeSMovB32(126, InlineU32(1))); code.push_back(EncodeSMovB32(127, InlineU32(0))); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 21 + i, 4 + i); } AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyzwInactiveExecPreservesOverlappingAddress"; test.code = std::move(code); test.initial = {0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc, 0xdddddddd, 0, 0, 0, 0}; test.expected = {0xaaaaaaaau, 0xbbbbbbbbu, 0xccccccccu, 0xddddddddu, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.opcodes = { O::V_MOV_B32, O::S_MOV_B64, O::BUFFER_LOAD_FORMAT_XYZW, O::S_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData( 16, 2, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); test.has_user_data = true; return test; } TestCase BufferFormatStoreVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x01020304u); AppendBufferStoreOpcode(&code, 0x04, 0, 20); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 1, 0x11121314u); AppendVMovLiteral(&code, 2, 0x21222324u); AppendBufferStoreOpcode(&code, 0x05, 1, 20); AppendVMovU32(&code, 20, 12); AppendVMovLiteral(&code, 3, 0x31323334u); AppendVMovLiteral(&code, 4, 0x41424344u); AppendVMovLiteral(&code, 5, 0x51525354u); AppendBufferStoreOpcode(&code, 0x06, 3, 20); AppendVMovU32(&code, 20, 24); AppendVMovLiteral(&code, 6, 0x61626364u); AppendVMovLiteral(&code, 7, 0x71727374u); AppendVMovLiteral(&code, 8, 0x81828384u); AppendVMovLiteral(&code, 9, 0x91929394u); AppendBufferStoreOpcode(&code, 0x07, 6, 20); AppendEnd(&code); return {"BufferFormatStoreVariants", code, std::vector(10, 0), {0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u, 0x41424344u, 0x51525354u, 0x61626364u, 0x71727374u, 0x81828384u, 0x91929394u}, {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::BUFFER_STORE_FORMAT_XY, O::BUFFER_STORE_FORMAT_XYZ, O::BUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}}; } TestCase BufferLoadFormatXResource8UintZeroExtendsByte() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMubuf0(0x00u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 1); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXResource8UintZeroExtendsByte"; test.code = code; test.initial = {0x11223344u, 0}; test.expected = {0x11223344u, 0x00000044u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 5); test.has_user_data = true; return test; } TestCase BufferStoreFormatXResource16UintWritesHalfword() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x0000aaaau); code.push_back(EncodeMubuf0(0x04u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXResource16UintWritesHalfword"; test.code = code; test.initial = {0x11223344u}; test.expected = {0x1122aaaau}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 4, false, 11); test.has_user_data = true; return test; } TestCase BufferStoreFormatXResource16UintPreservesAdjacentLanes() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop2(0x25, 1, InlineU32(1), 0)); code.push_back(EncodeMubuf0(0x04u, 0, true, false)); code.push_back(EncodeMubuf1(1, 0, 0)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXResource16UintPreservesAdjacentLanes"; test.code = code; test.initial = std::vector(32, 0); for (u32 index = 0; index < 32; index++) { test.expected.push_back(((index * 2 + 2) << 16) | (index * 2 + 1)); } test.opcodes = {O::V_ADD_NC_U32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = 64; test.has_compute_info = true; test.user_data = MakeStructuredStorageBufferData(2, 64, false, 11); test.has_user_data = true; test.required_spirv = {"OpAtomicLoad", "OpAtomicCompareExchange"}; return test; } TestCase BufferStoreFormatXyzwSnorm16CapturedSkinningVectors() { using O = ShaderOpcode; // Normal and tangent from the character skinning dispatch (eadfd178c07feebd). constexpr std::array values = { 0x3d98f7c5u, 0xbedae712u, 0xbf66a19eu, 0x3f800000u, 0x3f7e6baeu, 0xbd339a7du, 0x3dd0a2e2u, 0xbf800000u}; std::vector code; for (u32 record = 0; record < 2; record++) { for (u32 component = 0; component < 4; component++) { AppendVMovLiteral(&code, component, values[record * 4 + component]); } AppendVMovU32(&code, 20, record); code.push_back(EncodeMubuf0(0x07u, 0, true, false)); code.push_back(EncodeMubuf1(0, 0, 20)); } AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyzwSnorm16CapturedSkinningVectors"; test.code = std::move(code); test.initial = std::vector(4, 0xdeadbeefu); test.expected = {0xc947098fu, 0x7fff8cb0u, 0xfa637f35u, 0x80010d0au}; test.user_data = MakeStructuredStorageBufferData( 8, 2, false, BufferFormat(Prospero::BufferFormat::k16_16_16_16SNorm)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}; return test; } TestCase BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords() { using O = ShaderOpcode; constexpr std::array values = { -2.0f, -1.0f, -0.75f, -0.25f, 0.0f, 0.25f, 0.75f, 1.0f, 2.0f}; std::vector code; for (u32 i = 0; i < values.size(); i++) { AppendVMovLiteral(&code, 0, std::bit_cast(values[i])); AppendVMovU32(&code, 20, i * 2 + 2); code.push_back(EncodeMubuf0(0x04u)); code.push_back(EncodeMubuf1(0, 0, 20)); } AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords"; test.code = std::move(code); test.initial = std::vector(6, 0xdeadbeefu); test.expected = {0x8001beefu, 0xa0018001u, 0x0000e000u, 0x5fff2000u, 0x7fff7fffu, 0xdeadbeefu}; test.user_data = MakeStructuredStorageBufferData( 0, 24, false, BufferFormat(Prospero::BufferFormat::k16SNorm)); test.has_user_data = true; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; return test; } TestCase BufferLoadFormatXyResource88UintExtractsBytes() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMubuf0(0x01u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 2); AppendStoreVgpr(&code, 1, 3); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyResource8_8UintExtractsBytes"; test.code = code; test.initial = {0x0000807fu, 0x55667788u, 0, 0}; test.expected = {0x0000807fu, 0x55667788u, 0x0000007fu, 0x00000080u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 18); test.has_user_data = true; return test; } TestCase BufferLoadFormatXyResource8888UnormConvertsFirstTwoComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMubuf0(0x01u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); TestCase test; test.name = "BufferLoadFormatXyResource8_8_8_8UnormConvertsFirstTwoComponents"; test.code = code; test.initial = {0x44332211u, 0xdeadbeefu}; test.expected = {0x3d888889u, 0x3e088889u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 56); test.has_user_data = true; return test; } TestCase BufferStoreFormatXyResource88UintWritesBytes() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x000000aau); AppendVMovLiteral(&code, 1, 0x000000bbu); code.push_back(EncodeMubuf0(0x05u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyResource8_8UintWritesBytes"; test.code = code; test.initial = {0x11223344u, 0x55667788u}; test.expected = {0x1122bbaau, 0x55667788u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XY, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 18); test.has_user_data = true; return test; } TestCase BufferStoreFormatXyResource32UintWritesOneDword() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0xabcdef01u); AppendVMovLiteral(&code, 1, 0x12345678u); code.push_back(EncodeMubuf0(0x05u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyResource32UintWritesOneDword"; test.code = code; test.initial = {0x11111111u, 0x22222222u}; test.expected = {0xabcdef01u, 0x22222222u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XY, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 20); test.has_user_data = true; return test; } TestCase BufferStoreFormatXyzResource3232UintWritesTwoDwords() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0xabcdef01u); AppendVMovLiteral(&code, 1, 0x12345678u); AppendVMovLiteral(&code, 2, 0x0badc0deu); code.push_back(EncodeMubuf0(0x06u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyzResource32_32UintWritesTwoDwords"; test.code = code; test.initial = {0x11111111u, 0x22222222u, 0x33333333u}; test.expected = {0xabcdef01u, 0x12345678u, 0x33333333u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZ, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 62); test.has_user_data = true; return test; } TestCase BufferStoreFormatXyzwResource323232UintWritesThreeDwords() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0xabcdef01u); AppendVMovLiteral(&code, 1, 0x12345678u); AppendVMovLiteral(&code, 2, 0x0badc0deu); AppendVMovLiteral(&code, 3, 0xfeedfaceu); code.push_back(EncodeMubuf0(0x07u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyzwResource32_32_32UintWritesThreeDwords"; test.code = code; test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; test.expected = {0xabcdef01u, 0x12345678u, 0x0badc0deu, 0x44444444u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 72); test.has_user_data = true; return test; } TestCase BufferStoreFormatXyzResource8UintWritesOneByte() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x000000aau); AppendVMovLiteral(&code, 1, 0x000000bbu); AppendVMovLiteral(&code, 2, 0x000000ccu); code.push_back(EncodeMubuf0(0x06u)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXyzResource8UintWritesOneByte"; test.code = code; test.initial = {0x11223344u, 0x55667788u, 0x99aabbccu}; test.expected = {0x112233aau, 0x55667788u, 0x99aabbccu}; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZ, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(0, 8, false, 5); test.has_user_data = true; return test; } TestCase BufferStoreFormatXAddTidUsesLaneIndex() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeMubuf0(0x04u, 0, false, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXAddTidUsesLaneIndex"; test.code = code; test.initial = std::vector(64, 0); test.expected = std::vector(64, 0); std::fill_n(test.expected.begin(), 32, 0x12345678u); test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 0; test.compute_info.wave_size = 32; test.has_compute_info = true; test.user_data = MakeStructuredStorageBufferData(4, 64, true); test.has_user_data = true; test.required_spirv = {"BuiltIn SubgroupLocalInvocationId"}; test.forbidden_spirv = {"BuiltIn LocalInvocationId", "OpGroupNonUniformBallot"}; return test; } TestCase BufferStoreFormatXDropsOutOfRangeRecord() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovLiteral(&code, 0, 0xabcdef01u); code.push_back(EncodeMubuf0(0x04u, 0, true, false)); code.push_back(EncodeMubuf1(0, 0, 20)); AppendEnd(&code); TestCase test; test.name = "BufferStoreFormatXDropsOutOfRangeRecord"; test.code = code; test.initial = {0x11111111u, 0x22222222u}; test.expected = {0x11111111u, 0x22222222u}; test.storage_buffer_range_dwords = 1; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(4, 1, false, 20); test.has_user_data = true; return test; } TestCase TBufferLoadVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadOpcode(&code, 0x00, 0, 20); AppendTBufferLoadOpcode(&code, 0x01, 1, 20); AppendTBufferLoadOpcode(&code, 0x02, 3, 20); AppendTBufferLoadOpcode(&code, 0x03, 6, 20); for (u32 i = 0; i < 10u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadVariants", code, {0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u}, {0x01020304u, 0x01020304u, 0x11121314u, 0x01020304u, 0x11121314u, 0x21222324u, 0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u}, {O::TBUFFER_LOAD_FORMAT_X, O::TBUFFER_LOAD_FORMAT_XY, O::TBUFFER_LOAD_FORMAT_XYZ, O::TBUFFER_LOAD_FORMAT_XYZW, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyzwSnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); constexpr auto format = BufferFormat(Prospero::BufferFormat::k32_32_32_32Float); code.push_back( EncodeMtbuf0(0x03u, format & 0xfu, (format >> 4u) & 0x7u, 0, true, true)); code.push_back(EncodeMtbuf1(0x03u, 21, 0, 21)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 21 + i, 4 + i); } AppendEnd(&code); TestCase test; test.name = "TBufferLoadFormatXyzwSnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0, 0, 0, 0}; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.opcodes = {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(16, 2); test.has_user_data = true; return test; } TestCase TBufferLoadFormatXyzwPackedSnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); constexpr auto format = BufferFormat(Prospero::BufferFormat::k8_8_8_8UInt); code.push_back( EncodeMtbuf0(0x03u, format & 0xfu, (format >> 4u) & 0x7u, 0, true, true)); code.push_back(EncodeMtbuf1(0x03u, 21, 0, 21)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 21 + i, 4 + i); } AppendEnd(&code); TestCase test; test.name = "TBufferLoadFormatXyzwPackedSnapshotsOverlappingAddress"; test.code = std::move(code); test.initial = {0x44332211u, 0, 0, 0, 0, 0, 0, 0}; test.expected = {0x44332211u, 0, 0, 0, 0x11u, 0x22u, 0x33u, 0x44u}; test.opcodes = {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(4, 8); test.has_user_data = true; return test; } TestCase TBufferStoreFormatX8UintWritesOneByte() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 0, 0xaa); code.push_back(EncodeMtbuf0(0x04, 5, 0)); code.push_back(EncodeMtbuf1(0x04, 0, 0, 20)); AppendEnd(&code); return {"TBufferStoreFormatX8UintWritesOneByte", code, {0x11223344u}, {0x112233aau}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_X, O::S_ENDPGM}}; } TestCase TBufferLoadFormatX8UintZeroExtendsByte() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x00, 5, 0)); code.push_back(EncodeMtbuf1(0x00, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"TBufferLoadFormatX8UintZeroExtendsByte", code, {0x11223344u, 0}, {0x00000044u, 0}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatX8888UintExtractsFirstByte() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x00, 0, 20, Prospero::BufferFormat::k8_8_8_8UInt); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"TBufferLoadFormatX8_8_8_8UintExtractsFirstByte", code, {0x44332211u, 0}, {0x00000011u, 0}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXIdxenUsesDescriptorStride() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); code.push_back(EncodeMtbuf0(0x00, 5, 0, 0, true, false)); code.push_back(EncodeMtbuf1(0x00, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "TBufferLoadFormatXIdxenUsesDescriptorStride"; test.code = code; test.initial = {0x01020304u, 0, 0, 0, 0, 0, 0, 0, 0x0000007eu}; test.expected = {0x0000007eu, 0, 0, 0, 0, 0, 0, 0, 0x0000007eu}; test.opcodes = {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStructuredStorageBufferData(16, 3); test.has_user_data = true; return test; } TestCase TBufferLoadFormatX16FloatConvertsToFloat() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x00, 0, 20, Prospero::BufferFormat::k16Float); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); return {"TBufferLoadFormatX16FloatConvertsToFloat", code, {0x00003c00u, 0}, {0x3f800000u, 0}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXSintSignExtendsSubDword() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x00, 6, 0)); code.push_back(EncodeMtbuf1(0x00, 0, 0, 20)); AppendVMovU32(&code, 20, 4); code.push_back(EncodeMtbuf0(0x00, 12, 0)); code.push_back(EncodeMtbuf1(0x00, 1, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); return {"TBufferLoadFormatXSintSignExtendsSubDword", code, {0x00000080u, 0x00008001u}, {0xffffff80u, 0xffff8001u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_X, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferStoreFormatXSintWritesSubDword() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0xffffff80u); code.push_back(EncodeMtbuf0(0x04, 6, 0)); code.push_back(EncodeMtbuf1(0x04, 0, 0, 20)); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 1, 0xffff8001u); code.push_back(EncodeMtbuf0(0x04, 12, 0)); code.push_back(EncodeMtbuf1(0x04, 1, 0, 20)); AppendEnd(&code); return {"TBufferStoreFormatXSintWritesSubDword", code, {0x11223344u, 0x55667788u}, {0x11223380u, 0x55668001u}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_X, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy88IntegerComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x01, 2, 1)); code.push_back(EncodeMtbuf1(0x01, 0, 0, 20)); AppendVMovU32(&code, 20, 4); code.push_back(EncodeMtbuf0(0x01, 3, 1)); code.push_back(EncodeMtbuf1(0x01, 2, 0, 20)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXy8_8IntegerComponents", code, {0x0000807fu, 0x00007f80u, 0, 0}, {0x0000007fu, 0x00000080u, 0xffffff80u, 0x0000007fu}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferStoreFormatXy88IntegerComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 0, 0xaa); AppendVMovU32(&code, 1, 0xbb); code.push_back(EncodeMtbuf0(0x05, 2, 1)); code.push_back(EncodeMtbuf1(0x05, 0, 0, 20)); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 2, 0xffffff80u); AppendVMovU32(&code, 3, 0x7f); code.push_back(EncodeMtbuf0(0x05, 3, 1)); code.push_back(EncodeMtbuf1(0x05, 2, 0, 20)); AppendEnd(&code); return {"TBufferStoreFormatXy8_8IntegerComponents", code, {0x11223344u, 0x55667788u}, {0x1122bbaau, 0x55667f80u}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_XY, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy1616IntegerComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x01, 11, 1)); code.push_back(EncodeMtbuf1(0x01, 0, 0, 20)); AppendVMovU32(&code, 20, 4); code.push_back(EncodeMtbuf0(0x01, 12, 1)); code.push_back(EncodeMtbuf1(0x01, 2, 0, 20)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXy16_16IntegerComponents", code, {0x80017fffu, 0x7fff8000u, 0, 0}, {0x00007fffu, 0x00008001u, 0xffff8000u, 0x00007fffu}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferStoreFormatXy1616IntegerComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 0, 0xaaaa); AppendVMovU32(&code, 1, 0xbbbb); code.push_back(EncodeMtbuf0(0x05, 11, 1)); code.push_back(EncodeMtbuf1(0x05, 0, 0, 20)); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 2, 0xffff8000u); AppendVMovU32(&code, 3, 0x7fff); code.push_back(EncodeMtbuf0(0x05, 12, 1)); code.push_back(EncodeMtbuf1(0x05, 2, 0, 20)); AppendEnd(&code); return {"TBufferStoreFormatXy16_16IntegerComponents", code, {0x11223344u, 0x55667788u}, {0xbbbbaaaau, 0x7fff8000u}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_XY, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyz16161616UintLoadsHalfwords() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x02, 0, 20, Prospero::BufferFormat::k16_16_16_16UInt); for (u32 i = 0; i < 3; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXyz16_16_16_16UintLoadsHalfwords", code, {0x22221111u, 0x44443333u, 0}, {0x00001111u, 0x00002222u, 0x00003333u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZ, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy88UnormConvertsToFloat() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x01, 14, 0)); code.push_back(EncodeMtbuf1(0x01, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); return {"TBufferLoadFormatXy8_8UnormConvertsToFloat", code, {0x0000ff80u, 0}, {0x3f008081u, 0x3f800000u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy88SnormConvertsToFloat() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x01, 0, 20, Prospero::BufferFormat::k8_8SNorm); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); return {"TBufferLoadFormatXy8_8SnormConvertsToFloat", code, {0x00007f80u, 0}, {0xbf800000u, 0x3f800000u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy1616UnormConvertsToFloat() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x01, 7, 1)); code.push_back(EncodeMtbuf1(0x01, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); return {"TBufferLoadFormatXy16_16UnormConvertsToFloat", code, {0xffff8000u, 0}, {0x3f000080u, 0x3f800000u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXy8888UnormConvertsFirstTwoComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x01, 8, 3)); code.push_back(EncodeMtbuf1(0x01, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); return {"TBufferLoadFormatXy8_8_8_8UnormConvertsFirstTwoComponents", code, {0x44332211u, 0xdeadbeefu}, {0x3d888889u, 0x3e088889u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XY, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyzw8888UintExtractsBytes() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x03, 0, 20, Prospero::BufferFormat::k8_8_8_8UInt); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXyzw8_8_8_8UintExtractsBytes", code, {0x44332211u, 0xaaaaaaaau, 0xbbbbbbbbu, 0xccccccccu}, {0x00000011u, 0x00000022u, 0x00000033u, 0x00000044u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyzw1010102SnormConvertsToFloat() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x03, 0, 20, Prospero::BufferFormat::k10_10_10_2SNorm); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXyzw10_10_10_2SnormConvertsToFloat", code, {0x800801ffu, 0, 0, 0}, {0x3f800000u, 0xbf800000u, 0, 0xbf800000u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyz111110FloatUnpacks() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendTBufferLoadFormatOpcode(&code, 0x02, 0, 20, Prospero::BufferFormat::k11_11_10Float); for (u32 i = 0; i < 3; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXyz11_11_10FloatUnpacks", code, {0x781e03c0u, 0, 0}, {0x3f800000u, 0x3f800000u, 0x3f800000u}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZ, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferLoadFormatXyzw3232FloatZerosMissingComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); code.push_back(EncodeMtbuf0(0x03, 0, 4)); code.push_back(EncodeMtbuf1(0x03, 0, 0, 20)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); return {"TBufferLoadFormatXyzw32_32FloatZerosMissingComponents", code, {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}, {0x11111111u, 0x22222222u, 0, 0}, {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase TBufferStoreFormatXyzw3232FloatWritesOnlyPresentComponents() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x40000000u); AppendVMovLiteral(&code, 2, 0x40400000u); AppendVMovLiteral(&code, 3, 0x40800000u); code.push_back(EncodeMtbuf0(0x07, 0, 4)); code.push_back(EncodeMtbuf1(0x07, 0, 0, 20)); AppendEnd(&code); return {"TBufferStoreFormatXyzw32_32FloatWritesOnlyPresentComponents", code, {0xaaaaaaaau, 0xbbbbbbbbu, 0xccccccccu, 0xddddddddu}, {0x3f800000u, 0x40000000u, 0xccccccccu, 0xddddddddu}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}}; } TestCase TBufferStoreVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 0, 0x01020304u); AppendTBufferStoreOpcode(&code, 0x04, 0, 20); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 1, 0x11121314u); AppendVMovLiteral(&code, 2, 0x21222324u); AppendTBufferStoreOpcode(&code, 0x05, 1, 20); AppendVMovU32(&code, 20, 12); AppendVMovLiteral(&code, 3, 0x31323334u); AppendVMovLiteral(&code, 4, 0x41424344u); AppendVMovLiteral(&code, 5, 0x51525354u); AppendTBufferStoreOpcode(&code, 0x06, 3, 20); AppendVMovU32(&code, 20, 24); AppendVMovLiteral(&code, 6, 0x61626364u); AppendVMovLiteral(&code, 7, 0x71727374u); AppendVMovLiteral(&code, 8, 0x81828384u); AppendVMovLiteral(&code, 9, 0x91929394u); AppendTBufferStoreOpcode(&code, 0x07, 6, 20); AppendEnd(&code); return {"TBufferStoreVariants", code, std::vector(10, 0), {0x01020304u, 0x11121314u, 0x21222324u, 0x31323334u, 0x41424344u, 0x51525354u, 0x61626364u, 0x71727374u, 0x81828384u, 0x91929394u}, {O::V_MOV_B32, O::TBUFFER_STORE_FORMAT_X, O::TBUFFER_STORE_FORMAT_XY, O::TBUFFER_STORE_FORMAT_XYZ, O::TBUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}}; } TestCase FlatLoadVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 0); code.push_back(EncodeFlat0(0x08, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x09, 0, 2)); code.push_back(EncodeFlat1(1, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0a, 0, 0)); code.push_back(EncodeFlat1(2, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0b, 0, 2)); code.push_back(EncodeFlat1(3, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0c, 0, 8)); code.push_back(EncodeFlat1(13, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0d, 0, 8)); code.push_back(EncodeFlat1(4, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0f, 0, 8)); code.push_back(EncodeFlat1(6, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0e, 0, 8)); code.push_back(EncodeFlat1(9, 0x7d, 0, 20)); for (u32 i = 0; i < 14u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); const std::vector guest_memory{0x80ff7f01u, 0x00008001u, 0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u}; constexpr size_t byte_offset = 8; std::vector host_memory( (byte_offset + guest_memory.size() * sizeof(u32) + sizeof(u32) - 1u) / sizeof(u32)); std::memcpy(reinterpret_cast(host_memory.data()) + byte_offset, guest_memory.data(), guest_memory.size() * sizeof(u32)); TestCase test{"FlatLoadVariants", code, std::move(host_memory), {0x01u, 0xffffffffu, 0x7f01u, 0xffff80ffu, 0x11223344u, 0x55667788u, 0x11223344u, 0x55667788u, 0x99aabbccu, 0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u, 0x11223344u}, {O::V_MOV_B32, O::FLAT_LOAD_UBYTE, O::FLAT_LOAD_SBYTE, O::FLAT_LOAD_USHORT, O::FLAT_LOAD_SSHORT, O::FLAT_LOAD_DWORD, O::FLAT_LOAD_DWORDX2, O::FLAT_LOAD_DWORDX3, O::FLAT_LOAD_DWORDX4, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.bda_mappings = {{0, static_cast(byte_offset)}}; return test; } TestCase FlatSubdwordLoadsApplyByteOffset() { using O = ShaderOpcode; constexpr size_t byte_offset = 3; std::vector code; AppendVMovU32(&code, 20, static_cast(byte_offset)); AppendVMovU32(&code, 21, 0); code.push_back(EncodeFlat0(0x08, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0a, 0, 1)); code.push_back(EncodeFlat1(1, 0x7d, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); constexpr u32 guest_word = 0x80ff7f01u; std::vector host_memory( (byte_offset + sizeof(guest_word) + sizeof(u32) - 1u) / sizeof(u32)); std::memcpy(reinterpret_cast(host_memory.data()) + byte_offset, &guest_word, sizeof(guest_word)); TestCase test; test.name = "FlatSubdwordLoadsApplyByteOffset"; test.code = std::move(code); test.initial = std::move(host_memory); test.expected = {0x01u, 0x0000ff7fu}; test.bda_mappings = {{0, 0}}; test.opcodes = {O::V_MOV_B32, O::FLAT_LOAD_UBYTE, O::FLAT_LOAD_USHORT, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase BranchVccnzUsesCarryProducedWaveMask() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVop2(0x1a, 3, InlineU32(2), 1), EncodeVopc(0xc0, Vgpr(0), 0), }; AppendVMovLiteral(&code, 2, 0xffffffffu); code.push_back(EncodeVop2(0x28, 5, Vgpr(2), 1)); code.push_back(EncodeSopp(0x07, 2)); code.push_back(EncodeVop1(0x01, 2, InlineU32(11))); code.push_back(EncodeSopp(0x02, 1)); AppendVMovU32(&code, 2, 42); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "BranchVccnzUsesCarryProducedWaveMask"; test.code = code; test.expected = std::vector(8, 42); test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_F_U32, O::V_ADDC_U32, O::S_CBRANCH_VCCNZ, O::S_BRANCH, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase ScalarLoadAlignsComponentsAndMasksAddress() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 0, 1u); AppendSMovLiteral(&code, 2, 3u); AppendSMovLiteral(&code, 3, 0xffff0000u); code.push_back(EncodeSmem0(0x00, 1, 1)); code.push_back(EncodeSmem1(3, 0)); AppendStoreSgpr(&code, 1, 0); AppendEnd(&code); return {"ScalarLoadAlignsComponentsAndMasksAddress", code, {0x11111111u, 0x22222222u}, {0x11111111u, 0x22222222u}, {O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase FlatVirtualAddressRebasesGuestAllocation() { using O = ShaderOpcode; constexpr uint64_t GuestBase = 0x0000000110000000ull; std::vector code; AppendVMovLiteral(&code, 20, static_cast(GuestBase + 4u)); AppendVMovLiteral(&code, 21, static_cast(GuestBase >> 32u)); code.push_back(EncodeFlat0(0x0c, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 0, 20)); code.push_back(EncodeFlat0(0x0c, 2, 0)); code.push_back(EncodeFlat1(1, 0x7d, 0, 20)); AppendVMovLiteral(&code, 21, static_cast((GuestBase >> 32u) + 1u)); code.push_back(EncodeFlat0(0x0c, 0, 0)); code.push_back(EncodeFlat1(2, 0x7d, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendStoreVgpr(&code, 2, 2); AppendEnd(&code); TestCase test; test.name = "FlatVirtualAddressRebasesGuestAllocation"; test.code = std::move(code); test.initial = {0xfeedfaceu, 0xcafebabeu, 0, 0x12345678u}; test.expected = {0x12345678u, 0x12345678u, 0}; test.bda_mappings = {{GuestBase, 8}}; test.opcodes = {O::V_MOV_B32, O::FLAT_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase GlobalSignedImmediateRebasesBeforeSaddr() { using O = ShaderOpcode; constexpr uint64_t GuestBase = 0x0000000110000000ull; std::vector code; AppendSMovLiteral(&code, 0, static_cast(GuestBase)); AppendSMovLiteral(&code, 1, static_cast(GuestBase >> 32u)); AppendVMovU32(&code, 20, 8); code.push_back(EncodeFlat0(0x0c, 2, 0xffcu)); code.push_back(EncodeFlat1(0, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "GlobalSignedImmediateRebasesBeforeSaddr"; test.code = std::move(code); test.initial = {0xfeedfaceu, 0xcafebabeu, 0x11111111u, 0x12345678u}; test.expected = {0x12345678u}; test.bda_mappings = {{GuestBase, 8}}; test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::FLAT_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; return test; } TestCase FlatSegmentIgnoresSaddrAndMasksOffsetMsb() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(4))); AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 0); code.push_back(EncodeFlat0(0x0c, 0, 4)); code.push_back(EncodeFlat1(0, 0, 0, 20)); AppendStoreVgpr(&code, 0, 0); AppendVMovU32(&code, 20, 0); code.push_back(EncodeFlat0(0x0c, 0, 0x804)); code.push_back(EncodeFlat1(1, 0x7d, 0, 20)); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); std::vector initial(514, 0); initial[1] = 0x11111111u; initial[2] = 0x22222222u; initial[513] = 0x33333333u; TestCase test; test.name = "FlatSegmentIgnoresSaddrAndMasksOffsetMsb"; test.code = code; test.initial = initial; test.expected = {0x11111111u, 0x11111111u}; test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::FLAT_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.bda_mappings = {{0, 0}}; return test; } TestCase ScratchIsPrivatePerInvocation() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(20, InlineU32(0))); AppendVMovU32(&code, 21, 0); code.push_back(EncodeFlat0(0x1c, 1, 4)); code.push_back(EncodeFlat1(0, 20, 0, 21)); code.push_back(EncodeFlat0(0x0c, 1, 4)); code.push_back(EncodeFlat1(5, 20, 0, 21)); AppendStoreVgprAtLaneDwordOffset(&code, 5, 0, 0); AppendEnd(&code); TestCase test; test.name = "ScratchIsPrivatePerInvocation"; test.code = std::move(code); for (u32 lane = 0; lane < 64; lane++) { test.expected.push_back(lane); } test.opcodes = {O::S_MOV_B32, O::V_MOV_B32, O::FLAT_STORE_DWORD, O::FLAT_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.compute_info.scratch_size_dwords = 2; test.has_compute_info = true; return test; } #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS TestCase FlatStoreVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 0, 0xaa); code.push_back(EncodeFlat0(0x18, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 0, 20)); AppendVMovU32(&code, 20, 2); AppendVMovLiteral(&code, 1, 0x0000bbccu); code.push_back(EncodeFlat0(0x1a, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 1, 20)); AppendVMovU32(&code, 20, 4); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); code.push_back(EncodeFlat0(0x1d, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 2, 20)); AppendVMovU32(&code, 20, 12); AppendVMovLiteral(&code, 4, 0x33333333u); AppendVMovLiteral(&code, 5, 0x44444444u); AppendVMovLiteral(&code, 6, 0x55555555u); code.push_back(EncodeFlat0(0x1f, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 4, 20)); AppendVMovU32(&code, 20, 24); AppendVMovLiteral(&code, 7, 0x66666666u); AppendVMovLiteral(&code, 8, 0x77777777u); AppendVMovLiteral(&code, 9, 0x88888888u); AppendVMovLiteral(&code, 10, 0x99999999u); code.push_back(EncodeFlat0(0x1e, 0, 0)); code.push_back(EncodeFlat1(0, 0x7d, 7, 20)); AppendVMovU32(&code, 20, 0); code.push_back(EncodeFlat0(0x1c, 0, 40)); code.push_back(EncodeFlat1(0, 0x7d, 2, 20)); AppendEnd(&code); TestCase test{"FlatStoreVariants", code, std::vector(12, 0), {0xbbccaa00u, 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x77777777u, 0x88888888u, 0x99999999u, 0x11111111u, 0}, {O::V_MOV_B32, O::FLAT_STORE_BYTE, O::FLAT_STORE_SHORT, O::FLAT_STORE_DWORD, O::FLAT_STORE_DWORDX2, O::FLAT_STORE_DWORDX3, O::FLAT_STORE_DWORDX4, O::S_ENDPGM}}; return test; } #endif TestCase DsReadWriteVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x11223344u); code.push_back(EncodeDs0(0x0d, 0)); code.push_back(EncodeDs1(0, 2, 1)); code.push_back(EncodeDs0(0x36, 0)); code.push_back(EncodeDs1(3, 0, 1)); AppendVMovU32(&code, 4, 0xaa); code.push_back(EncodeDs0(0x1e, 4)); code.push_back(EncodeDs1(0, 4, 1)); code.push_back(EncodeDs0(0x3a, 4)); code.push_back(EncodeDs1(5, 0, 1)); code.push_back(EncodeDs0(0x39, 4)); code.push_back(EncodeDs1(6, 0, 1)); AppendVMovLiteral(&code, 7, 0x000080ffu); code.push_back(EncodeDs0(0x1f, 8)); code.push_back(EncodeDs1(0, 7, 1)); code.push_back(EncodeDs0(0x3c, 8)); code.push_back(EncodeDs1(8, 0, 1)); code.push_back(EncodeDs0(0x3b, 8)); code.push_back(EncodeDs1(9, 0, 1)); AppendVMovLiteral(&code, 10, 0x10101010u); AppendVMovLiteral(&code, 11, 0x11111111u); code.push_back(EncodeDs0(0x4d, 12)); code.push_back(EncodeDs1(0, 10, 1)); code.push_back(EncodeDs0(0x76, 12)); code.push_back(EncodeDs1(14, 0, 1)); AppendVMovLiteral(&code, 16, 0x20202020u); AppendVMovLiteral(&code, 17, 0x21212121u); AppendVMovLiteral(&code, 18, 0x22222222u); code.push_back(EncodeDs0(0xde, 20)); code.push_back(EncodeDs1(0, 16, 1)); code.push_back(EncodeDs0(0xfe, 20)); code.push_back(EncodeDs1(19, 0, 1)); AppendVMovLiteral(&code, 22, 0x30303030u); AppendVMovLiteral(&code, 23, 0x31313131u); AppendVMovLiteral(&code, 24, 0x32323232u); AppendVMovLiteral(&code, 25, 0x33333333u); code.push_back(EncodeDs0(0xdf, 32)); code.push_back(EncodeDs1(0, 22, 1)); code.push_back(EncodeDs0(0xff, 32)); code.push_back(EncodeDs1(26, 0, 1)); const u32 results[] = {3, 5, 6, 8, 9, 14, 15, 19, 20, 21, 26, 27, 28, 29}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"DsReadWriteVariants", code, std::vector(14, 0), {0x11223344u, 0xaau, 0xffffffaau, 0x80ffu, 0xffff80ffu, 0x10101010u, 0x11111111u, 0x20202020u, 0x21212121u, 0x22222222u, 0x30303030u, 0x31313131u, 0x32323232u, 0x33333333u}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_READ_B32, O::DS_WRITE_B8, O::DS_READ_U8, O::DS_READ_I8, O::DS_WRITE_B16, O::DS_READ_U16, O::DS_READ_I16, O::DS_WRITE_B64, O::DS_READ_B64, O::DS_WRITE_B96, O::DS_READ_B96, O::DS_WRITE_B128, O::DS_READ_B128, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsReadU16D16CapturedPreservesHighHalf() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x89abcdefu); code.push_back(EncodeDs0(0x0d)); code.push_back(EncodeDs1(0, 2, 1)); // ds_write_b32 v2, v1 AppendVMovLiteral(&code, 3, 0x12345678u); code.push_back(EncodeDs0(0xa6)); code.push_back(EncodeDs1(3, 0, 1)); // ds_read_u16_d16 v3, v1 AppendVMovU32(&code, 7, 0); code.push_back(0xda980000u); code.push_back(0x07000007u); // captured ds_read_u16_d16 v7, v7 AppendStoreVgpr(&code, 3, 0); AppendStoreVgpr(&code, 7, 1); AppendEnd(&code); TestCase test{"DsReadU16D16CapturedPreservesHighHalf", code, std::vector(2, 0), {0x1234cdefu, 0x0000cdefu}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_READ_U16_D16, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"DS_READ_U16_D16", 2}}; return test; } TestCase DsWriteB16D16HiCapturedUsesHighHalf() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 3, 0xdeadbeefu); code.push_back(EncodeDs0(0x0d)); code.push_back(EncodeDs1(0, 3, 1)); // ds_write_b32 v1, v3 AppendVMovU32(&code, 20, 2); AppendVMovLiteral(&code, 2, 0x1234abcdu); AppendVMovLiteral(&code, 0, 0x87654321u); code.push_back(0xda840000u); code.push_back(0x00000214u); // captured ds_write_b16_d16_hi v20, v2 code.push_back(EncodeDs0(0x36)); code.push_back(EncodeDs1(4, 0, 1)); // ds_read_b32 v4, v1 AppendStoreVgpr(&code, 4, 0); AppendStoreVgpr(&code, 0, 1); AppendEnd(&code); TestCase test{"DsWriteB16D16HiCapturedUsesHighHalf", code, std::vector(2, 0), {0x1234beefu, 0x87654321u}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_WRITE_B16_D16_HI, O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"DS_WRITE_B16_D16_HI", 1}}; return test; } TestCase DsReadU16D16HiCapturedPreservesLowHalf() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x89abcdefu); code.push_back(EncodeDs0(0x0d)); code.push_back(EncodeDs1(0, 2, 1)); // ds_write_b32 v2, v1 AppendVMovLiteral(&code, 5, 0x12345678u); AppendVMovU32(&code, 6, 0); code.push_back(0xda9c0000u); code.push_back(0x05000006u); // captured ds_read_u16_d16_hi v5, v6 AppendStoreVgpr(&code, 5, 0); AppendEnd(&code); TestCase test{"DsReadU16D16HiCapturedPreservesLowHalf", code, std::vector(1, 0), {0xcdef5678u}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_READ_U16_D16_HI, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.decoded_counts = {{"DS_READ_U16_D16_HI", 1}}; return test; } TestCase DsReadWrite2Variants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); code.push_back(EncodeDs0(0x0e, (3u << 8u) | 1u)); code.push_back(EncodeDs1Ex(0, 3, 2, 1)); code.push_back(EncodeDs0(0x37, (3u << 8u) | 1u)); code.push_back(EncodeDs1Ex(4, 0, 0, 1)); AppendVMovLiteral(&code, 6, 0x33333333u); AppendVMovLiteral(&code, 7, 0x44444444u); AppendVMovLiteral(&code, 8, 0x55555555u); AppendVMovLiteral(&code, 9, 0x66666666u); code.push_back(EncodeDs0(0x4d, 32)); code.push_back(EncodeDs1(0, 6, 1)); code.push_back(EncodeDs0(0x4d, 48)); code.push_back(EncodeDs1(0, 8, 1)); code.push_back(EncodeDs0(0x77, (6u << 8u) | 4u)); code.push_back(EncodeDs1Ex(10, 0, 0, 1)); AppendVMovLiteral(&code, 14, 0x77777777u); AppendVMovLiteral(&code, 15, 0x88888888u); code.push_back(EncodeDs0(0x0f, (2u << 8u) | 1u)); code.push_back(EncodeDs1Ex(0, 15, 14, 1)); code.push_back(EncodeDs0(0x38, (2u << 8u) | 1u)); code.push_back(EncodeDs1Ex(16, 0, 0, 1)); AppendVMovLiteral(&code, 18, 0x99999999u); AppendVMovLiteral(&code, 19, 0xaaaaaaaau); AppendVMovLiteral(&code, 20, 0xbbbbbbbbu); AppendVMovLiteral(&code, 21, 0xccccccccu); code.push_back(EncodeDs0(0x4e, (10u << 8u) | 8u)); code.push_back(EncodeDs1Ex(0, 20, 18, 1)); code.push_back(EncodeDs0(0x77, (10u << 8u) | 8u)); code.push_back(EncodeDs1Ex(22, 0, 0, 1)); AppendVMovLiteral(&code, 26, 0xddddddddu); AppendVMovLiteral(&code, 27, 0xeeeeeeeeu); AppendVMovLiteral(&code, 28, 0xf0f0f0f0u); AppendVMovLiteral(&code, 29, 0x12345678u); code.push_back(EncodeDs0(0x4f, (2u << 8u) | 1u)); code.push_back(EncodeDs1Ex(0, 28, 26, 1)); code.push_back(EncodeDs0(0x78, (2u << 8u) | 1u)); code.push_back(EncodeDs1Ex(34, 0, 0, 1)); const u32 results[] = {4, 5, 10, 11, 12, 13, 22, 23, 24, 25, 16, 17, 34, 35, 36, 37}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); return {"DsReadWrite2Variants", code, std::vector(16, 0), {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u, 0x66666666u, 0x99999999u, 0xaaaaaaaau, 0xbbbbbbbbu, 0xccccccccu, 0x77777777u, 0x88888888u, 0xddddddddu, 0xeeeeeeeeu, 0xf0f0f0f0u, 0x12345678u}, {O::V_MOV_B32, O::DS_WRITE2_B32, O::DS_READ2_B32, O::DS_WRITE_B64, O::DS_READ2_B64, O::DS_WRITE2_B64, O::DS_WRITE2ST64_B32, O::DS_READ2ST64_B32, O::DS_WRITE2ST64_B64, O::DS_READ2ST64_B64, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsWideReadSnapshotsOverlappingAddress() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 6, 0x11111111u); AppendVMovLiteral(&code, 7, 0x22222222u); code.push_back(EncodeDs0(0x4d, 0)); code.push_back(EncodeDs1(0, 6, 1)); AppendVMovLiteral(&code, 8, 0x33333333u); AppendVMovLiteral(&code, 9, 0x44444444u); code.push_back(EncodeDs0(0x4d, 16)); code.push_back(EncodeDs1(0, 8, 1)); code.push_back(EncodeDs0(0x77, 2u << 8u)); code.push_back(EncodeDs1Ex(1, 0, 0, 1)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 1u + i, i); } AppendEnd(&code); return {"DsWideReadSnapshotsOverlappingAddress", code, std::vector(4, 0), {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}, {O::V_MOV_B32, O::DS_WRITE_B64, O::DS_READ2_B64, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsReadWrite2EqualOffsetsUseData0() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); code.push_back(EncodeDs0(0x0e, (1u << 8u) | 1u)); code.push_back(EncodeDs1Ex(0, 3, 2, 1)); AppendVMovLiteral(&code, 4, 0x33333333u); AppendVMovLiteral(&code, 5, 0x44444444u); AppendVMovLiteral(&code, 6, 0x55555555u); AppendVMovLiteral(&code, 7, 0x66666666u); code.push_back(EncodeDs0(0x4e, (2u << 8u) | 2u)); code.push_back(EncodeDs1Ex(0, 6, 4, 1)); code.push_back(EncodeDs0(0x37, (1u << 8u) | 1u)); code.push_back(EncodeDs1Ex(8, 0, 0, 1)); code.push_back(EncodeDs0(0x77, (2u << 8u) | 2u)); code.push_back(EncodeDs1Ex(10, 0, 0, 1)); for (u32 i = 0; i < 6; i++) { AppendStoreVgpr(&code, 8u + i, i); } AppendEnd(&code); return {"DsReadWrite2EqualOffsetsUseData0", code, std::vector(6, 0), {0x11111111u, 0x11111111u, 0x33333333u, 0x44444444u, 0x33333333u, 0x44444444u}, {O::V_MOV_B32, O::DS_WRITE2_B32, O::DS_WRITE2_B64, O::DS_READ2_B32, O::DS_READ2_B64, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsWideLdsPartialBounds() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); AppendVMovLiteral(&code, 4, 0x33333333u); AppendVMovLiteral(&code, 5, 0x44444444u); code.push_back(EncodeDs0(0xdf)); code.push_back(EncodeDs1(0, 2, 1)); code.push_back(EncodeDs0(0xff)); code.push_back(EncodeDs1(6, 0, 1)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 6u + i, i); } AppendEnd(&code); TestCase test{"DsWideLdsPartialBounds", code, std::vector(4, 0), {0x11111111u, 0x22222222u, 0x33333333u, 0}, {O::V_MOV_B32, O::DS_WRITE_B128, O::DS_READ_B128, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.compute_info.lds_size_dwords = 3; return test; } TestCase DsWideGdsPartialBounds() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x11111111u); AppendVMovLiteral(&code, 3, 0x22222222u); AppendVMovLiteral(&code, 4, 0x33333333u); AppendVMovLiteral(&code, 5, 0x44444444u); code.push_back(EncodeDs0(0xdf, 0, true)); code.push_back(EncodeDs1(0, 2, 1)); code.push_back(EncodeDs0(0xff, 0, true)); code.push_back(EncodeDs1(6, 0, 1)); for (u32 i = 0; i < 4; i++) { AppendStoreVgpr(&code, 6u + i, i); } AppendEnd(&code); TestCase test{"DsWideGdsPartialBounds", code, std::vector(4, 0), {0x11111111u, 0x22222222u, 0x33333333u, 0}, {O::V_MOV_B32, O::DS_WRITE_B128, O::DS_READ_B128, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.gds_initial = {0, 0, 0}; test.expected_gds = {0x11111111u, 0x22222222u, 0x33333333u}; return test; } TestCase DsAtomicNoReturnVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); const u32 initial[] = {10, 10, 0xfffffff0u, 0xfffffff0u, 10, 10, 0xf0f0u, 0xf000u, 0xf00fu}; const u32 values[] = {5, 3, 5, 5, 5, 20, 0x0ff0u, 0x0f00u, 0x00ffu}; const u32 ops[] = {0x00, 0x01, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovLiteral(&code, 2, initial[i]); code.push_back(EncodeDs0(0x0d, i * 4u)); code.push_back(EncodeDs1(0, 2, 1)); AppendVMovLiteral(&code, 3, values[i]); code.push_back(EncodeDs0(ops[i], i * 4u)); code.push_back(EncodeDs1(0, 3, 1)); code.push_back(EncodeDs0(0x36, i * 4u)); code.push_back(EncodeDs1(10u + i, 0, 1)); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, 10u + i, i); } AppendEnd(&code); return {"DsAtomicNoReturnVariants", code, std::vector(9, 0), {15, 7, 0xfffffff0u, 5, 5, 20, 0x00f0u, 0xff00u, 0xf0f0u}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_ADD_U32, O::DS_SUB_U32, O::DS_MIN_I32, O::DS_MAX_I32, O::DS_MIN_U32, O::DS_MAX_U32, O::DS_AND_B32, O::DS_OR_B32, O::DS_XOR_B32, O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsAtomicReturnVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); const u32 initial[] = {10, 10, 0xfffffff0u, 0xfffffff0u, 10, 10, 0xf0f0u, 0xf000u, 0xf00fu, 10}; const u32 values[] = {5, 3, 5, 5, 5, 20, 0x0ff0u, 0x0f00u, 0x00ffu, 99}; const u32 ops[] = {0x20, 0x21, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2d}; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovLiteral(&code, 2, initial[i]); code.push_back(EncodeDs0(0x0d, i * 4u)); code.push_back(EncodeDs1(0, 2, 1)); AppendVMovLiteral(&code, 3, values[i]); code.push_back(EncodeDs0(ops[i], i * 4u)); code.push_back(EncodeDs1(10u + i, 3, 1)); code.push_back(EncodeDs0(0x36, i * 4u)); code.push_back(EncodeDs1(20u + i, 0, 1)); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, 10u + i, i); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, 20u + i, i + 10u); } AppendEnd(&code); return { "DsAtomicReturnVariants", code, std::vector(20, 0), {10, 10, 0xfffffff0u, 0xfffffff0u, 10, 10, 0xf0f0u, 0xf000u, 0xf00fu, 10, 15, 7, 0xfffffff0u, 5, 5, 20, 0x00f0u, 0xff00u, 0xf0f0u, 99}, {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_ADD_RTN_U32, O::DS_SUB_RTN_U32, O::DS_MIN_RTN_I32, O::DS_MAX_RTN_I32, O::DS_MIN_RTN_U32, O::DS_MAX_RTN_U32, O::DS_AND_RTN_B32, O::DS_OR_RTN_B32, O::DS_XOR_RTN_B32, O::DS_WRXCHG_RTN_B32, O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsBoundedAtomicBoundaries(bool decrement, bool gds) { using O = ShaderOpcode; constexpr u32 guard = 0x13579bdfu; struct Boundary { u32 before, limit, incremented, decremented; }; constexpr Boundary boundaries[] = { {4, 5, 5, 3}, {5, 5, 0, 4}, {6, 5, 0, 5}, {0, 5, 1, 5}, {0, 0, 0, 0}, {1, 0, 0, 0}, {0xffffffffu, 0xffffffffu, 0, 0xfffffffeu}, {0xfffffffeu, 0xffffffffu, 0xffffffffu, 0xfffffffdu}, {0, 0xffffffffu, 1, 0xffffffffu}, {0x80000000u, 0x7fffffffu, 0, 0x7fffffffu}, {0x7fffffffu, 0x80000000u, 0x80000000u, 0x7ffffffeu}, {0xffffffffu, 0x80000000u, 0, 0x80000000u}}; constexpr u32 count = static_cast(std::size(boundaries)); const u32 opcode = decrement ? 0x24u : 0x23u; TestCase test; test.name = decrement ? (gds ? "DsDecReturnGdsBoundaries" : "DsDecReturnLdsBoundaries") : (gds ? "DsIncReturnGdsBoundaries" : "DsIncReturnLdsBoundaries"); auto &code = test.code; AppendVMovU32(&code, 2, 0); std::vector memory(68, guard); for (u32 i = 0; i < count; ++i) memory[i + 1] = boundaries[i].before; if (!gds) { for (u32 i = 0; i < memory.size(); ++i) { AppendVMovLiteral(&code, 3, memory[i]); code.push_back(EncodeDs0(0x0d, i * 4)); code.push_back(EncodeDs1(0, 3, 2)); } } auto StoreResult = [&](u32 vgpr, u32 expected) { AppendStoreVgpr(&code, vgpr, static_cast(test.expected.size())); test.expected.push_back(expected); }; for (u32 i = 0; i < count; ++i) { AppendVMovU32(&code, 2, i * 4); AppendVMovLiteral(&code, 3, boundaries[i].limit); // Preserve the captured DATA0/VDST alias and exact increment words in GDS. code.push_back(decrement ? EncodeDs0(opcode, 4, gds) : (gds ? 0xd88e0004u : 0xd88c0004u)); code.push_back(0x03000302u); StoreResult(3, boundaries[i].before); code.push_back(EncodeDs0(0x36, 4, gds)); code.push_back(EncodeDs1(4, 0, 2)); StoreResult(4, decrement ? boundaries[i].decremented : boundaries[i].incremented); } // EXEC=0 preserves memory and the aliased destination register. AppendVMovU32(&code, 2, 0); AppendVMovLiteral(&code, 3, 0x12345678u); code.push_back(EncodeSop1(0x04, 10, 126)); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeDs0(opcode, 4, gds)); code.push_back(0x03000302u); code.push_back(EncodeSop1(0x04, 126, 10)); StoreResult(3, 0x12345678u); // Use both offset bytes and alias VDST with ADDR instead of DATA0. AppendVMovU32(&code, 2, 4); AppendVMovLiteral(&code, 3, 0xffffffffu); code.push_back(EncodeDs0(opcode, 0x104, gds)); code.push_back(EncodeDs1(2, 3, 2)); StoreResult(2, guard); // Overwriting an unused return must not let DCE discard the atomic update. AppendVMovU32(&code, 2, 0); code.push_back(EncodeDs0(opcode, 0x108, gds)); code.push_back(EncodeDs1(4, 3, 2)); AppendVMovU32(&code, 4, 0); // An active access just outside the resource returns zero. AppendVMovU32(&code, 2, static_cast(memory.size() * 4 - 4)); code.push_back(EncodeDs0(opcode, 4, gds)); code.push_back(0x03000302u); StoreResult(3, 0); // Read back all words, including guards around both address ranges. AppendVMovU32(&code, 2, 0); for (u32 i = 0; i < memory.size(); ++i) { u32 expected = memory[i]; if (i >= 1 && i <= count) { expected = decrement ? boundaries[i - 1].decremented : boundaries[i - 1].incremented; } else if (i == 66) { expected = decrement ? guard - 2 : guard + 2; } code.push_back(EncodeDs0(0x36, i * 4, gds)); code.push_back(EncodeDs1(4, 0, 2)); StoreResult(4, expected); if (gds) test.expected_gds.push_back(expected); } AppendEnd(&code); test.initial.resize(test.expected.size()); test.compute_info.lds_size_dwords = static_cast(memory.size()); test.opcodes = { O::S_MOV_B64, O::V_MOV_B32, O::DS_READ_B32, decrement ? O::DS_DEC_RTN_U32 : O::DS_INC_RTN_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpAtomicLoad", "OpAtomicCompareExchange", "OpLoopMerge"}; if (gds) test.gds_initial = memory; else test.opcodes.push_back(O::DS_WRITE_B32); return test; } TestCase DsBoundedAtomicContention(bool decrement, bool gds, u32 wave_size) { using O = ShaderOpcode; constexpr u32 guard = 0x13579bdfu; constexpr u32 limit = 15; TestCase test; if (wave_size == 32) { test.name = decrement ? (gds ? "DsDecReturnGdsContentionWave32" : "DsDecReturnLdsContentionWave32") : (gds ? "DsIncReturnGdsContentionWave32" : "DsIncReturnLdsContentionWave32"); } else { test.name = decrement ? (gds ? "DsDecReturnGdsContentionWave64" : "DsDecReturnLdsContentionWave64") : (gds ? "DsIncReturnGdsContentionWave64" : "DsIncReturnLdsContentionWave64"); } auto &code = test.code; AppendVMovU32(&code, 2, 0); AppendVMovU32(&code, 3, limit); AppendVMovU32(&code, 5, 1); AppendVMovLiteral(&code, 6, guard); if (!gds) { // One lane initializes LDS before every wave in the workgroup uses it. code.push_back(EncodeSop1(0x04, 10, 126)); code.push_back(EncodeVopc(0xc2, InlineU32(0), 0)); code.push_back(EncodeSop1(0x04, 126, 106)); code.push_back(EncodeDs0(0x0d, 4)); code.push_back(EncodeDs1(0, 2, 2)); code.push_back(EncodeSop1(0x04, 126, 10)); code.push_back(EncodeSopp(0x0a, 0)); } // Distinct masks in the wave64 halves also check inactive return // preservation. code.push_back(EncodeSop1(0x04, 10, 126)); AppendSMovLiteral(&code, 126, 0x55555555u); AppendSMovLiteral(&code, 127, 0xaaaaaaaau); code.push_back(EncodeDs0(decrement ? 0x24 : 0x23, 4, gds)); code.push_back(EncodeDs1(6, 3, 2)); // Returned old values form a histogram; no lane ordering is assumed. code.push_back(EncodeVop2(0x1a, 7, InlineU32(2), 6)); AppendBufferStoreOpcode(&code, 0x32, 5, 7); code.push_back(EncodeSop1(0x04, 126, 10)); AppendVMovLiteral(&code, 7, guard); code.push_back(EncodeVopc(0xc2, Vgpr(7), 6)); code.push_back(EncodeVop2(0x01, 8, InlineU32(0), 5)); AppendStoreVgprAtLaneDwordOffset(&code, 8, 0, 16); code.push_back(EncodeSopp(0x0a, 0)); // S_BARRIER orders LDS; use atomic add-zero to read GDS consistently. code.push_back(EncodeDs0(gds ? 0x20 : 0x36, 4, gds)); code.push_back(EncodeDs1(9, 2, 2)); AppendStoreVgprAtLaneDwordOffset(&code, 9, 0, 144); AppendEnd(&code); test.expected.assign(16, 4); // 64 active lanes, four complete wraps. for (u32 lane = 0; lane < 128; ++lane) { const uint64_t mask = 0xaaaaaaaa55555555ull; test.expected.push_back( (mask & (uint64_t{1} << (lane % wave_size))) == 0 ? 1 : 0); } test.expected.resize(272, 0); test.initial.resize(test.expected.size()); test.compute_info.threads_num[0] = 128; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.wave_size = wave_size; test.compute_info.lds_size_dwords = 3; test.has_compute_info = true; test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::S_BARRIER, O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::V_CMP_EQ_U32, O::V_CNDMASK_B32, decrement ? O::DS_DEC_RTN_U32 : O::DS_INC_RTN_U32, gds ? O::DS_ADD_RTN_U32 : O::DS_READ_B32, O::BUFFER_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpAtomicLoad", "OpAtomicCompareExchange", "OpLoopMerge"}; if (gds) { test.gds_initial = {guard, 0, guard}; test.expected_gds = test.gds_initial; } else test.opcodes.push_back(O::DS_WRITE_B32); return test; } TestCase DsMiscVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 0, 0); AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x40800000u); code.push_back(EncodeDs0(0x0d, 0)); code.push_back(EncodeDs1(0, 2, 1)); AppendVMovLiteral(&code, 3, 0x3f800000u); code.push_back(EncodeDs0(0x0d, 4)); code.push_back(EncodeDs1(0, 3, 1)); AppendVMovLiteral(&code, 4, 0x40000000u); code.push_back(EncodeDs0(0x12, 0)); code.push_back(EncodeDs1(0, 4, 1)); AppendVMovLiteral(&code, 5, 0x40400000u); code.push_back(EncodeDs0(0x13, 4)); code.push_back(EncodeDs1(0, 5, 1)); code.push_back(EncodeDs0(0x36, 0)); code.push_back(EncodeDs1(6, 0, 1)); code.push_back(EncodeDs0(0x36, 4)); code.push_back(EncodeDs1(7, 0, 1)); AppendVMovLiteral(&code, 8, 0x12345678u); code.push_back(EncodeDs0(0x35, 0x001f)); code.push_back(EncodeDs1(9, 0, 8)); code.push_back(EncodeSMovB32(124, InlineU32(0))); AppendVMovLiteral(&code, 10, 0xabcdef01u); code.push_back(EncodeDs0(0xb0, 8)); code.push_back(EncodeDs1(0, 10, 0)); code.push_back(EncodeDs0(0xb1, 8)); code.push_back(EncodeDs1(11, 0, 0)); const u32 results[] = {6, 7, 9, 11}; for (u32 i = 0; i < static_cast(std::size(results)); i++) { AppendStoreVgpr(&code, results[i], i); } AppendEnd(&code); TestCase test; test.name = "DsMiscVariants"; test.code = code; test.initial = std::vector(4, 0); test.expected = {0x40000000u, 0x40400000u, 0x12345678u, 0xabcdef01u}; test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::DS_WRITE_B32, O::DS_MIN_F32, O::DS_MAX_F32, O::DS_READ_B32, O::DS_SWIZZLE_B32, O::DS_WRITE_ADDTID_B32, O::DS_READ_ADDTID_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 1; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.has_compute_info = true; return test; } TestCase DsFloatMinMaxUsesSeparateCompareOperand() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 0); AppendVMovLiteral(&code, 2, 0x40800000u); code.push_back(EncodeDs0(0x0d, 0)); code.push_back(EncodeDs1(0, 2, 1)); AppendVMovLiteral(&code, 3, 0x40800000u); code.push_back(EncodeDs0(0x0d, 4)); code.push_back(EncodeDs1(0, 3, 1)); AppendVMovLiteral(&code, 4, 0x41100000u); AppendVMovLiteral(&code, 5, 0x40000000u); code.push_back(EncodeDs0(0x12, 0)); code.push_back(EncodeDs1Ex(0, 5, 4, 1)); AppendVMovLiteral(&code, 6, 0x3f800000u); AppendVMovLiteral(&code, 7, 0x40400000u); code.push_back(EncodeDs0(0x13, 4)); code.push_back(EncodeDs1Ex(0, 7, 6, 1)); code.push_back(EncodeDs0(0x36, 0)); code.push_back(EncodeDs1(8, 0, 1)); code.push_back(EncodeDs0(0x36, 4)); code.push_back(EncodeDs1(9, 0, 1)); AppendStoreVgpr(&code, 8, 0); AppendStoreVgpr(&code, 9, 1); AppendEnd(&code); TestCase test; test.name = "DsFloatMinMaxUsesSeparateCompareOperand"; test.code = code; test.initial = std::vector(2, 0); test.expected = {0x41100000u, 0x3f800000u}; test.opcodes = {O::V_MOV_B32, O::DS_WRITE_B32, O::DS_MIN_F32, O::DS_MAX_F32, O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 1; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.has_compute_info = true; return test; } TestCase DsSwizzleInvalidSourceLaneZero() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 100); code.push_back(EncodeDs0(0x35, 0x00e0)); code.push_back(EncodeDs1(2, 0, 1)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 0)); AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "DsSwizzleInvalidSourceLaneZero"; test.code = code; test.expected = {0, 0, 0, 0}; test.opcodes = {O::V_MOV_B32, O::DS_SWIZZLE_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase DsBpermuteCapturedExecOffsetAndWrap() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0)); AppendBufferLoadDword(&code, 17, 30); AppendVMovLiteral(&code, 1, 0xdeadbeefu); AppendVMovU32(&code, 3, 100); code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3)); code.push_back(EncodeSop1(0x04, 4, 126)); code.push_back(EncodeVop2(0x1b, 18, InlineU32(1), 0)); code.push_back(EncodeVopc(0xc2, InlineU32(1), 18)); code.push_back(EncodeSop1(0x04, 126, 106)); code.push_back(0xdacc0000u); code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3 code.push_back(EncodeSop1(0x04, 126, 4)); AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 4); AppendVMovU32(&code, 17, 124); code.push_back(EncodeDs0(0xb3, 4)); code.push_back(EncodeDs1(2, 3, 17)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 8); AppendEnd(&code); TestCase test; test.name = "DsBpermuteCapturedExecOffsetAndWrap"; test.code = code; test.initial = {0, 0, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0}; test.expected = {0, 0, 12, 4, 0xdeadbeefu, 0, 0xdeadbeefu, 101, 100, 100, 100, 100}; test.opcodes = {O::V_LSHLREV_B32, O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::V_ADD_NC_U32, O::V_AND_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::DS_BPERMUTE_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpGroupNonUniformShuffle", "OpGroupNonUniformBallot"}; test.forbidden_spirv = {" Workgroup", "OpControlBarrier", "OpMemoryBarrier"}; test.decoded_counts = {{"DS_BPERMUTE_B32", 2}}; test.ir_counts = {{"BpermuteU32", 2}}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase DsBpermuteWave64UsesIndependentHalves() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 3, 100); code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3)); AppendVMovU32(&code, 17, 128); code.push_back(0xdacc0000u); code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3 AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0); AppendEnd(&code); TestCase test; test.name = "DsBpermuteWave64UsesIndependentHalves"; test.code = code; test.initial = std::vector(64, 0); test.expected = std::vector(32, 100); test.expected.insert(test.expected.end(), 32, 132); test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::DS_BPERMUTE_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpGroupNonUniformShuffle"}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.wave_size = 64; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase Wave64CrossHalfLaneAndLds() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 100); code.push_back(EncodeVop2(0x25, 1, Vgpr(0), 1)); AppendVop3(&code, 0x360, 8, Vgpr(1), InlineU32(47)); AppendStoreSgprAtLaneDwordOffset(&code, 8, 0, 0); code.push_back(EncodeSop1(0x04, 10, 126)); code.push_back(EncodeVop2(0x1b, 2, InlineU32(63), 0)); code.push_back(EncodeVopc(0xc2, InlineU32(40), 2)); code.push_back(EncodeSop1(0x04, 126, 106)); code.push_back(EncodeVop1(0x02, 9, Vgpr(1))); code.push_back(EncodeSop1(0x04, 126, 10)); AppendStoreSgprAtLaneDwordOffset(&code, 9, 0, 128); AppendStoreSgprAtLaneDwordOffset(&code, 106, 0, 256); AppendStoreSgprAtLaneDwordOffset(&code, 107, 0, 384); code.push_back(EncodeVop2(0x1a, 2, InlineU32(2), 0)); code.push_back(EncodeDs0(0x0d)); code.push_back(EncodeDs1(0, 1, 2)); code.push_back(EncodeSopp(0x0a, 0)); code.push_back(EncodeVop2(0x1d, 3, InlineU32(64), 0)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(2), 3)); code.push_back(EncodeDs0(0x36)); code.push_back(EncodeDs1(4, 0, 3)); AppendStoreVgprAtLaneDwordOffset(&code, 4, 0, 512); AppendEnd(&code); TestCase test; test.name = "Wave64CrossHalfLaneAndLds"; test.code = std::move(code); test.initial.resize(640); test.expected.insert(test.expected.end(), 64, 147); test.expected.insert(test.expected.end(), 64, 211); test.expected.insert(test.expected.end(), 64, 140); test.expected.insert(test.expected.end(), 64, 204); test.expected.insert(test.expected.end(), 128, 0); test.expected.insert(test.expected.end(), 128, 256); for (u32 lane = 0; lane < 128; lane++) { test.expected.push_back(100 + (lane ^ 64)); } test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_READLANE_B32, O::V_READFIRSTLANE_B32, O::V_CMP_EQ_U32, O::S_MOV_B64, O::V_LSHLREV_B32, O::V_AND_B32, O::V_XOR_B32, O::DS_WRITE_B32, O::S_BARRIER, O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpGroupNonUniformBallot", "OpGroupNonUniformShuffle", "OpControlBarrier"}; test.ir_counts = {{"Barrier", 1}}; test.compute_info.threads_num[0] = 128; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase Wave64RawMasksAndScalarBranch() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 1, 7); AppendSMovLiteral(&code, 8, 33u << 16u); code.push_back(EncodeSop2(0x29, 126, 193u, 8)); // captured ES BFE EXEC,-1,width AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0); code.push_back(EncodeSop2(0x1f, 126, InlineU32(1), InlineU32(32))); const auto exec_branch = code.size(); code.push_back(0); code.push_back(EncodeSopp(0x0a, 0)); code.push_back(EncodeSop1(0x24, 10, 193u)); // s_and_saveexec_b64 s[10:11],-1 const auto scc_branch = code.size(); code.push_back(0); AppendVMovU32(&code, 1, 9); AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 64); const auto restore = code.size(); code[exec_branch] = EncodeSopp(0x08, restore - exec_branch - 1); code[scc_branch] = EncodeSopp(0x04, restore - scc_branch - 1); code.push_back(EncodeSop1(0x04, 126, 193u)); code.push_back(EncodeVopc(0xc2, Vgpr(0), 0)); code.push_back(EncodeSop1(0x04, 12, 106)); code.push_back(EncodeSMovB32(13, InlineU32(1))); AppendVop3(&code, 0x101, 2, InlineU32(0), InlineU32(11), 12); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 128); code.push_back(EncodeSMovB32(127, InlineU32(1))); AppendVMovU32(&code, 3, 13); AppendStoreVgprAtLaneDwordOffset(&code, 3, 0, 192); code.push_back(EncodeSop1(0x04, 126, InlineU32(1))); AppendStoreVgprAtLaneDwordOffset(&code, 3, 0, 256); code.push_back(EncodeSMovB32(107, InlineU32(2))); code.push_back(EncodeSMovB32(127, 107)); AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 320); AppendEnd(&code); TestCase test; test.name = "Wave64RawMasksAndScalarBranch"; test.code = std::move(code); test.initial.assign(384, 0xdeadbeef); test.expected = test.initial; for (u32 lane = 0; lane < 64; lane++) { if (lane < 33) { test.expected[lane] = 7; test.expected[192 + lane] = 13; } test.expected[128 + lane] = lane < 33 ? 11 : 0; } test.expected[64 + 32] = 9; test.expected[256] = 13; test.expected[320] = 7; test.expected[320 + 33] = 7; test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::S_BFE_U64, O::S_LSHL_B64, O::S_CBRANCH_EXECZ, O::S_AND_SAVEEXEC_B64, O::S_CBRANCH_SCC0, O::S_BARRIER, O::S_MOV_B64, O::V_CMP_EQ_U32, O::V_CNDMASK_B32, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase Wave64PartialMultidimensionalWorkgroup() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop2(0x25, 3, Vgpr(2), 2)); code.push_back(EncodeVop2(0x25, 3, Vgpr(2), 3)); code.push_back(EncodeVop2(0x25, 3, Vgpr(1), 3)); code.push_back(EncodeVop2(0x1a, 3, InlineU32(3), 3)); code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3)); code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 3)); AppendVMovU32(&code, 5, 1); AppendBufferStoreOpcode(&code, 0x32, 5, 4, false); AppendEnd(&code); TestCase test; test.name = "Wave64PartialMultidimensionalWorkgroup"; test.code = std::move(code); test.initial.resize(128); test.expected.assign(96, 1); test.expected.resize(128); test.opcodes = {O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::V_MOV_B32, O::BUFFER_ATOMIC_ADD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 8; test.compute_info.threads_num[1] = 3; test.compute_info.threads_num[2] = 4; test.compute_info.thread_ids_num = 3; test.has_compute_info = true; return test; } TestCase Wave64AppendConsumeHighHalf() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 124, 4); code.push_back(EncodeSop2(0x1f, 126, 193u, InlineU32(32))); code.push_back(EncodeDs0(0x3e, 0, true)); code.push_back(EncodeDs1(1, 0, 0)); AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0); code.push_back(EncodeDs0(0x3d, 0, true)); code.push_back(EncodeDs1(2, 0, 0)); AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 64); AppendEnd(&code); TestCase test; test.name = "Wave64AppendConsumeHighHalf"; test.code = std::move(code); test.initial.assign(128, 0xdeadbeef); test.expected = test.initial; for (u32 lane = 32; lane < 64; lane++) { test.expected[lane] = 10; test.expected[64 + lane] = 42; } test.gds_initial = {10}; test.expected_gds = {10}; test.opcodes = {O::S_MOV_B32, O::S_LSHL_B64, O::DS_APPEND, O::DS_CONSUME, O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase BufferAtomicVariants() { using O = ShaderOpcode; std::vector code; const u32 values[] = {100, 5, 3, 5, 5, 5, 20, 0x0ff0u, 0x0f00u, 0x00ffu}; const u32 ops[] = {0x30, 0x32, 0x33, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b}; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovU32(&code, 20, i * 4u); AppendVMovU32(&code, i, values[i]); AppendBufferStoreOpcode(&code, ops[i], i, 20, true); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, i, i + 10u); } AppendEnd(&code); return { "BufferAtomicVariants", code, {10, 10, 10, 0xfffffff0u, 10, 0xfffffff0u, 10, 0xf0f0u, 0xf000u, 0xf00fu, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {100, 15, 7, 0xfffffff0u, 5, 5, 20, 0x00f0u, 0xff00u, 0xf0f0u, 10, 10, 10, 0xfffffff0u, 10, 0xfffffff0u, 10, 0xf0f0u, 0xf000u, 0xf00fu}, {O::V_MOV_B32, O::BUFFER_ATOMIC_SWAP, O::BUFFER_ATOMIC_ADD, O::BUFFER_ATOMIC_SUB, O::BUFFER_ATOMIC_SMIN, O::BUFFER_ATOMIC_UMIN, O::BUFFER_ATOMIC_SMAX, O::BUFFER_ATOMIC_UMAX, O::BUFFER_ATOMIC_AND, O::BUFFER_ATOMIC_OR, O::BUFFER_ATOMIC_XOR, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferAtomicCmpSwapExactRaw() { using O = ShaderOpcode; std::vector code; const auto Store = [&](u32 value, u32 index) { AppendVMovU32(&code, 31, index * sizeof(u32)); code.push_back(EncodeMubuf0(0x1cu)); code.push_back(EncodeMubuf1(value, 8, 31)); }; AppendVMovU32(&code, 4, 1); AppendVMovLiteral(&code, 1, 0x22222222u); AppendVMovLiteral(&code, 2, 0x11111111u); code.push_back(0xe0c46000u); code.push_back(0x80080104u); Store(1, 2); AppendVMovLiteral(&code, 1, 0x33333333u); AppendVMovLiteral(&code, 2, 0x44444444u); code.push_back(0xe0c46000u); code.push_back(0x80080104u); Store(1, 3); Store(2, 4); AppendVMovLiteral(&code, 1, 0x55555555u); AppendVMovLiteral(&code, 2, 0x22222222u); code.push_back(EncodeMubuf0(0x31u, 0, true, false)); code.push_back(0x80080104u); Store(1, 5); AppendEnd(&code); TestCase test; test.name = "BufferAtomicCmpSwapExactRaw"; test.code = std::move(code); test.initial = {0xaaaaaaaau, 0x11111111u, 0, 0, 0, 0}; test.expected = {0xaaaaaaaau, 0x55555555u, 0x11111111u, 0x22222222u, 0x44444444u, 0x55555555u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_ATOMIC_CMPSWAP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpAtomicCompareExchange"}; const auto descriptor = MakeStructuredStorageBufferData( sizeof(u32), static_cast(test.initial.size())); std::copy_n(descriptor.begin(), 4, test.user_data.begin() + 32); test.has_user_data = true; return test; } TestCase BufferAtomicGlc0DoesNotReturnOldValue() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 0, 5); AppendBufferStoreOpcode(&code, 0x32, 0, 20); AppendStoreVgpr(&code, 0, 1); AppendEnd(&code); return { "BufferAtomicGlc0DoesNotReturnOldValue", code, {10, 0}, {15, 5}, {O::V_MOV_B32, O::BUFFER_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferAtomicFMinExactRawGlcModes() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x40000000u); // 2.0 code.push_back(0xe0fc0000u); code.push_back(0x80010000u); // exact failing buffer_atomic_fmin, GLC=0 AppendStoreVgpr(&code, 0, 1); AppendVMovLiteral(&code, 0, 0x3f800000u); // 1.0 code.push_back(0xe0fc4000u); code.push_back(0x80010000u); // same instruction with GLC=1 AppendStoreVgpr(&code, 0, 2); AppendEnd(&code); TestCase test; test.name = "BufferAtomicFMinExactRawGlcModes"; test.code = code; test.initial = {0x40800000u, 0, 0}; // 4.0 test.expected = {0x3f800000u, 0x40000000u, 0x40000000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_ATOMIC_FMIN, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; const auto descriptor = MakeStructuredStorageBufferData( 0, static_cast(test.initial.size() * sizeof(u32))); std::copy_n(descriptor.begin(), 4, test.user_data.begin() + 4); test.user_data[50] = 1u << 20u; test.has_user_data = true; return test; } TestCase BufferAtomicFMinSpecialValues() { using O = ShaderOpcode; const u32 values[] = { 0x40000000u, // 2.0 0x7f800000u, // +infinity 0xff800000u, // -infinity 0x3f800000u, // 1.0 0x7fc00000u, // quiet NaN 0x00000000u, // +0.0 0x80000000u, // -0.0 0x00000000u, // +0.0 0x80000001u, // smallest negative denorm }; std::vector code; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovU32(&code, 20, i * 4u); AppendVMovLiteral(&code, i, values[i]); AppendBufferStoreOpcode(&code, 0x3f, i, 20, true); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, i, i + static_cast(std::size(values))); } AppendEnd(&code); return {"BufferAtomicFMinSpecialValues", code, {0x40800000u, 0xbf800000u, 0x7f800000u, 0x7fc00000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0x40000000u, 0xbf800000u, 0xff800000u, 0x7fc00000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000001u, 0x40800000u, 0xbf800000u, 0x7f800000u, 0x7fc00000u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x00000001u, 0x00000000u}, {O::V_MOV_B32, O::BUFFER_ATOMIC_FMIN, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferAtomicFMinContendedWorkgroup() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // v_cvt_f32_u32 v1, thread_id.x AppendVMovU32(&code, 20, 0); AppendBufferStoreOpcode(&code, 0x3f, 1, 20); AppendEnd(&code); TestCase test; test.name = "BufferAtomicFMinContendedWorkgroup"; test.code = code; test.initial = {0x42c80000u}; // 100.0 test.expected = {0x00000000u}; // min(100.0, 0.0 .. 63.0) test.opcodes = {O::V_CVT_F32_U32, O::V_MOV_B32, O::BUFFER_ATOMIC_FMIN, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } TestCase BufferAtomicFMaxExactRawGlcModes() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 3, 0x40000000u); // 2.0 code.push_back(0xe100000cu); code.push_back( 0x80010300u); // exact failing buffer_atomic_fmax v3, offset 12, GLC=0 AppendStoreVgpr(&code, 3, 4); AppendVMovLiteral(&code, 0, 0x40800000u); // 4.0 code.push_back(0xe100400cu); code.push_back(0x80010000u); // same address with GLC=1 AppendStoreVgpr(&code, 0, 5); AppendEnd(&code); TestCase test; test.name = "BufferAtomicFMaxExactRawGlcModes"; test.code = code; test.initial = {0, 0, 0, 0x3f800000u, 0, 0}; // memory[3] = 1.0 test.expected = {0, 0, 0, 0x40800000u, 0x40000000u, 0x40000000u}; test.opcodes = {O::V_MOV_B32, O::BUFFER_ATOMIC_FMAX, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; const auto descriptor = MakeStructuredStorageBufferData( 0, static_cast(test.initial.size() * sizeof(u32))); std::copy_n(descriptor.begin(), 4, test.user_data.begin() + 4); test.user_data[50] = 1u << 20u; test.has_user_data = true; return test; } TestCase BufferAtomicFMaxSpecialValues() { using O = ShaderOpcode; const u32 values[] = { 0x40000000u, // 2.0, finite update 0x40000000u, // 2.0, finite no-update 0x7f800000u, // incoming +infinity 0xff800000u, // incoming -infinity 0x3f800000u, // finite against old +infinity 0x3f800000u, // finite against old -infinity 0x3f800000u, // finite against old quiet NaN 0x7fcabcdeu, // incoming quiet NaN 0x3f800000u, // finite against old signaling NaN 0x7faabcdeu, // incoming signaling NaN 0x00000000u, // +0.0 against old -0.0 0x80000000u, // -0.0 against old +0.0 0x80000000u, // -0.0 against a negative denorm 0x80000001u, // negative denorm against -0.0 0x00000001u, // positive denorm against +0.0 0x00000000u, // +0.0 against a positive denorm }; std::vector code; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovU32(&code, 20, i * 4u); AppendVMovLiteral(&code, i, values[i]); AppendBufferStoreOpcode(&code, 0x40, i, 20, true); } for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendStoreVgpr(&code, i, i + static_cast(std::size(values))); } AppendEnd(&code); return {"BufferAtomicFMaxSpecialValues", code, {0x3f800000u, 0x40800000u, 0xbf800000u, 0x3f800000u, 0x7f800000u, 0xff800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x80000001u, 0x80000000u, 0x00000000u, 0x00000001u, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0x40000000u, 0x40800000u, 0x7f800000u, 0x3f800000u, 0x7f800000u, 0x3f800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000001u, 0x00000001u, 0x3f800000u, 0x40800000u, 0xbf800000u, 0x3f800000u, 0x7f800000u, 0xff800000u, 0x7fc12345u, 0x3f800000u, 0x7fa54321u, 0x3f800000u, 0x80000000u, 0x00000000u, 0x80000001u, 0x80000000u, 0x00000000u, 0x00000001u}, {O::V_MOV_B32, O::BUFFER_ATOMIC_FMAX, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase BufferAtomicFMaxContendedWorkgroup() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // v_cvt_f32_u32 v1, thread_id.x AppendVMovU32(&code, 20, 0); AppendBufferStoreOpcode(&code, 0x40, 1, 20); AppendEnd(&code); TestCase test; test.name = "BufferAtomicFMaxContendedWorkgroup"; test.code = code; test.initial = {0xc2c80000u}; // -100.0 test.expected = {0x427c0000u}; // max(-100.0, 0.0 .. 63.0) test.opcodes = {O::V_CVT_F32_U32, O::V_MOV_B32, O::BUFFER_ATOMIC_FMAX, O::S_ENDPGM}; test.compute_info.threads_num[0] = 64; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; return test; } std::vector MakeRgbaImage(u32 width, u32 height, u32 value = 0) { return std::vector(static_cast(width) * height * 4u, value); } void SetRgbaPixel(std::vector *image, u32 width, u32 x, u32 y, u32 r, u32 g, u32 b, u32 a) { const auto base = static_cast((y * width + x) * 4u); (*image)[base + 0] = r; (*image)[base + 1] = g; (*image)[base + 2] = b; (*image)[base + 3] = a; } TestCase ImageLoadVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 23, 0); code.push_back(EncodeMimg0(0x00, 0xf)); code.push_back(EncodeMimg1(0, 20)); code.push_back(EncodeMimg0(0x01, 0xf)); code.push_back(EncodeMimg1(4, 20)); AppendVMovU32(&code, 24, 0); code.push_back(EncodeMimg0(0x0e, 0x1)); code.push_back(EncodeMimg1(8, 24)); for (u32 i = 0; i < 9u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 4); SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); TestCase test; test.name = "ImageLoadVariants"; test.code = code; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 4}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::IMAGE_LOAD_MIP, O::IMAGE_GET_RESINFO, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; return test; } TestCase DsAppendConsumeUsesEncodedLdsSelector() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 124, 0x0000ffffu); AppendVMovU32(&code, 1, 0); AppendVMovU32(&code, 2, 10); code.push_back(EncodeDs0(0x0d, 0)); code.push_back(EncodeDs1(0, 2, 1)); code.push_back(EncodeDs0(0x3e, 0)); code.push_back(EncodeDs1(3, 0, 0)); code.push_back(EncodeDs0(0x3d, 0)); code.push_back(EncodeDs1(4, 0, 0)); AppendSMovLiteral(&code, 124, 0); code.push_back(EncodeDs0(0x3e, 0)); code.push_back(EncodeDs1(5, 0, 0)); code.push_back(EncodeDs0(0x36, 0)); code.push_back(EncodeDs1(6, 0, 1)); AppendStoreVgpr(&code, 3, 0); AppendStoreVgpr(&code, 4, 1); AppendStoreVgpr(&code, 5, 2); AppendStoreVgpr(&code, 6, 3); AppendEnd(&code); return {"DsAppendConsumeLdsSelector", code, {}, {10, 11, 0, 10}, {O::S_MOV_B32, O::V_MOV_B32, O::DS_WRITE_B32, O::DS_READ_B32, O::DS_APPEND, O::DS_CONSUME, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } TestCase DsAppendUsesEncodedGdsSelector() { using O = ShaderOpcode; std::vector code; AppendSMovLiteral(&code, 124, 0x00000008u); code.push_back(EncodeDs0(0x3e, 0, true)); code.push_back(EncodeDs1(0, 0, 0)); code.push_back(EncodeDs0(0x3d, 0, true)); code.push_back(EncodeDs1(1, 0, 0)); code.push_back(EncodeDs0(0x3e, 4, true)); code.push_back(EncodeDs1(2, 0, 0)); code.push_back(EncodeDs0(0x3d, 4, true)); code.push_back(EncodeDs1(3, 0, 0)); AppendSMovLiteral(&code, 124, 0x00080008u); code.push_back(EncodeDs0(0x3e, 4, true)); code.push_back(EncodeDs1(4, 0, 0)); code.push_back(EncodeDs0(0x3d, 4, true)); code.push_back(EncodeDs1(5, 0, 0)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendStoreVgpr(&code, 2, 2); AppendStoreVgpr(&code, 3, 3); AppendStoreVgpr(&code, 4, 4); AppendStoreVgpr(&code, 5, 5); AppendEnd(&code); TestCase test{"DsAppendGdsSelector", code, {}, {10, 11, 20, 21, 40, 41}, {O::S_MOV_B32, O::DS_APPEND, O::DS_CONSUME, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; test.gds_initial = {10, 20, 30, 40}; test.expected_gds = {10, 20, 30, 40}; return test; } TestCase DsGdsSubdwordAndAtomicWrites() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVop1(0x01, 1, Vgpr(0))); code.push_back(EncodeVop2(0x25, 2, InlineU32(1), 0)); code.push_back(EncodeDs0(0x1e, 0, true)); code.push_back(EncodeDs1(0, 2, 1)); code.push_back(EncodeVop2(0x1a, 8, InlineU32(1), 0)); code.push_back(EncodeVop2(0x25, 8, InlineU32(12), 8)); code.push_back(EncodeDs0(0x1f, 0, true)); code.push_back(EncodeDs1(0, 2, 8)); AppendVMovU32(&code, 3, 4); AppendVMovU32(&code, 4, 1); code.push_back(EncodeDs0(0x00, 0, true)); code.push_back(EncodeDs1(0, 4, 3)); AppendVMovU32(&code, 5, 8); AppendVMovLiteral(&code, 6, 0x40a00000u); AppendVMovLiteral(&code, 7, 0x40a00000u); code.push_back(EncodeDs0(0x12, 0, true)); code.push_back(EncodeDs1Ex(0, 7, 6, 5)); AppendEnd(&code); TestCase test; test.name = "DsGdsSubdwordAndAtomics"; test.code = code; test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32, O::DS_WRITE_B8, O::DS_WRITE_B16, O::DS_ADD_U32, O::DS_MIN_F32, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; test.gds_initial = {0, 0, 0x42c80000u, 0, 0}; test.expected_gds = {0x04030201u, 4, 0x40a00000u, 0x00020001u, 0x00040003u}; return test; } TestCase ImageLoadR32UintUsesIntegerSampledImage() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); code.push_back(EncodeMimg0(0x00, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoadR32UintUsesIntegerSampledImage"; test.code = code; test.expected = {0xdeadbeefu}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba.resize(16); test.sampled_image_rgba[6] = 0xdeadbeefu; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32UInt); test.has_user_data = true; test.required_spirv = {"image_10", "OpTypeImage %uint", "OpImageFetch %v4uint"}; return test; } TestCase ImageLoadFmaskUsesNativeSampleMapping() { auto test = ImageLoadR32UintUsesIntegerSampledImage(); test.name = "ImageLoadFmaskUsesNativeSampleMapping"; test.expected = {0x76543210u}; test.user_data = {0x303ac300u, 0xca100000u, 0x021bc3bfu, 0x91800004u, 0u, 0x00700000u, 0u, 0u}; test.image_descriptor_swizzle = DstSel(4, 0, 0, 0); test.sampled_image_rgba.clear(); test.required_spirv = {"OpConstant %uint 1985229328"}; test.forbidden_spirv = {"OpTypeImage", "OpImageFetch"}; return test; } TestCase ImageLoadR32SintUsesSignedSampledImage() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); code.push_back(EncodeMimg0(0x00, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoadR32SintUsesSignedSampledImage"; test.code = code; test.expected = {0xffffff80u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba.resize(16); test.sampled_image_rgba[6] = 0xffffff80u; test.sampled_image_format = vk::Format::eR32Sint; test.sampled_image_dwords_per_pixel = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32SInt); test.has_user_data = true; test.required_spirv = {"image_17", "OpTypeImage %int", "OpImageFetch %v4int", "OpBitcast %uint"}; return test; } TestCase ImageLoadPackedUintUnpacksAndSwizzles() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); code.push_back(EncodeMimg0(0x00, 0xf)); code.push_back(EncodeMimg1(0, 20)); for (u32 component = 0; component < 4; component++) { AppendStoreVgpr(&code, component, component); } AppendEnd(&code); TestCase test; test.name = "ImageLoadPackedUintUnpacksAndSwizzles"; test.code = std::move(code); test.expected = {0x2abu, 0x456u, 0x321u, 0x321u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba.resize(16); test.sampled_image_rgba[6] = 0xaae2b321u; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k11_11_10UInt); test.has_user_data = true; test.image_descriptor_swizzle = DstSel(6, 5, 4, 7); test.required_spirv = {"OpImageFetch %v4uint", "OpBitFieldUExtract"}; return test; } TestCase ImageSamplePackedUintConvertsSampleAndGather() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x3f200000u); AppendVMovLiteral(&code, 21, 0x3ec00000u); code.push_back(EncodeMimg0(0x20, 0xf)); code.push_back(EncodeMimg1(0, 20)); code.push_back(EncodeMimg0(0x47, 0x2)); code.push_back(EncodeMimg1(4, 20)); for (u32 component = 0; component < 8; component++) { AppendStoreVgpr(&code, component, component); } AppendEnd(&code); TestCase test; test.name = "ImageSamplePackedUintConvertsSampleAndGather"; test.code = std::move(code); test.expected = {0x2abu, 0x456u, 0x321u, 0x321u, 0x456u, 0x456u, 0x456u, 0x456u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::IMAGE_GATHER4_LZ, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = std::vector(16, 0xaae2b321u); test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k11_11_10UInt); test.has_user_data = true; test.image_descriptor_swizzle = DstSel(6, 5, 4, 7); test.expected_force_point_sampler = true; test.required_spirv = {"OpImageSampleExplicitLod", "OpImageGather", "OpBitFieldUExtract"}; return test; } TestCase ImageLoadR128IgnoresAdjacentMaskSgprs() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); code.push_back(EncodeSop1(0x04, 4, 126)); // s_mov_b64 s[4:5], exec code.push_back(EncodeMimg0(0x00, 0x1, 0, false, 1, true)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoadR128IgnoresAdjacentMaskSgprs"; test.code = code; test.expected = {0xdeadbeefu}; test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba.resize(16); test.sampled_image_rgba[6] = 0xdeadbeefu; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32UInt); test.has_user_data = true; test.decoded_counts = {{"r128=1", 1}}; return test; } TestCase ImageLoad1DUsesScalarCoordinate() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); code.push_back(EncodeMimg0(0x00, 0x1, 0, false, 0)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoad1DUsesScalarCoordinate"; test.code = code; test.expected = {0xdeadbeefu}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.image_width = 4; test.image_height = 1; test.sampled_image_rgba = {0, 0, 0xdeadbeefu, 0}; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.sampled_image_type = vk::ImageType::e1D; test.sampled_image_view_type = vk::ImageViewType::e1D; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32UInt); test.user_data[3] = static_cast(Prospero::ImageType::kColor1D) << 28u; test.has_user_data = true; test.required_spirv = {"image_8"}; return test; } TestCase ImageGather2DInstructionWith1DDescriptor() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x3f000000u); AppendVMovLiteral(&code, 21, 0x3f000000u); code.push_back(EncodeMimg0(0x47, 0x1)); code.push_back(EncodeMimg1(0, 20)); for (u32 i = 0; i < 4u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 1); SetRgbaPixel(&image, 4, 1, 0, 0x40000000u, 0, 0, 0); SetRgbaPixel(&image, 4, 2, 0, 0x40400000u, 0, 0, 0); TestCase test; test.name = "ImageGather2DInstructionWith1DDescriptor"; test.code = code; test.expected = {0x40000000u, 0x40400000u, 0x40400000u, 0x40000000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_GATHER4_LZ, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.image_width = 4; test.image_height = 1; test.sampled_image_rgba = image; test.sampled_image_type = vk::ImageType::e1D; test.sampled_image_view_type = vk::ImageViewType::e1D; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32Float); test.user_data[3] = static_cast(Prospero::ImageType::kColor1D) << 28u; test.has_user_data = true; test.required_spirv = {"image_1", "OpImageQuerySizeLod", "OpImageSampleExplicitLod", "Floor"}; test.forbidden_spirv = {"OpImageGather"}; return test; } TestCase ImageLoad1DArrayUsesLayerCoordinate() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); code.push_back(EncodeMimg0(0x00, 0x1, 0, false, 4)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoad1DArrayUsesLayerCoordinate"; test.code = code; test.expected = {0xcafebabeu}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.image_width = 4; test.image_height = 1; test.sampled_image_rgba = {0, 0, 0, 0, 0, 0, 0xcafebabeu, 0}; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.sampled_image_type = vk::ImageType::e1D; test.sampled_image_view_type = vk::ImageViewType::e1DArray; test.sampled_image_layers = 2; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32UInt); test.user_data[3] = static_cast(Prospero::ImageType::kColor1DArray) << 28u; test.has_user_data = true; test.required_spirv = {"image_9"}; return test; } TestCase ImageLoad1DArrayDescriptorUsesSelectedLayer() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); code.push_back(EncodeMimg0(0x00, 0x1, 0, false, 0)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageLoad1DArrayDescriptorUsesSelectedLayer"; test.code = code; test.expected = {0xcafebabeu}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.image_width = 4; test.image_height = 1; test.sampled_image_rgba = { 0, 0, 0xdeadbeefu, 0, 0, 0, 0xcafebabeu, 0, }; test.sampled_image_format = vk::Format::eR32Uint; test.sampled_image_dwords_per_pixel = 1; test.sampled_image_type = vk::ImageType::e1D; test.sampled_image_view_type = vk::ImageViewType::e1D; test.sampled_image_layers = 2; test.sampled_image_view_base_layer = 1; test.sampled_image_view_layers = 1; test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32UInt); test.user_data[3] = static_cast(Prospero::ImageType::kColor1DArray) << 28u; test.user_data[4] = 1u | (1u << 16u); test.has_user_data = true; test.required_spirv = {"image_8"}; return test; } TestCase ImageLoadMipUsesVaddr2Lod2D() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 1); AppendVMovU32(&code, 23, 0); code.push_back(EncodeMimg0(0x01, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); auto base = MakeRgbaImage(4, 4); SetRgbaPixel(&base, 4, 1, 1, 0x3f800000u, 0, 0, 0); auto mip1 = MakeRgbaImage(2, 2); SetRgbaPixel(&mip1, 2, 1, 1, 0x40000000u, 0, 0, 0); TestCase test; test.name = "ImageLoadMipUsesVaddr2Lod2D"; test.code = code; test.expected = {0x40000000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD_MIP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba_mips = {base, mip1}; return test; } TestCase ImageLoadMipNsaUsesSelectedAddressVgprs() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 30, 1); AppendVMovU32(&code, 31, 1); code.push_back(EncodeMimg0(0x01, 0x1, 1)); code.push_back(EncodeMimg1(0, 20)); code.push_back((30u << 0u) | (31u << 8u)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); auto base = MakeRgbaImage(4, 4); SetRgbaPixel(&base, 4, 1, 0, 0x3f800000u, 0, 0, 0); auto mip1 = MakeRgbaImage(2, 2); SetRgbaPixel(&mip1, 2, 1, 1, 0x40a00000u, 0, 0, 0); TestCase test; test.name = "ImageLoadMipNsaUsesSelectedAddressVgprs"; test.code = code; test.expected = {0x40a00000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD_MIP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba_mips = {base, mip1}; return test; } TestCase ImageLoadA16UintCoordsOnGpu() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x00010002u); // x=2, y=1 packed as u16. AppendVMovU32(&code, 21, 0); code.push_back(EncodeMimg0(0x00, 0xf)); code.push_back(EncodeMimg1(0, 20, 0, 0, true)); for (u32 i = 0; i < 4u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 4); SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); TestCase test; test.name = "ImageLoadA16UintCoordsOnGpu"; test.code = code; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; test.required_spirv = {"OpShiftRightLogical", "OpBitwiseAnd"}; test.forbidden_spirv = {"UnpackHalf2x16"}; return test; } TestCase ImageGetResinfoDmaskWidthHeight() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 24, 0); code.push_back(EncodeMimg0(0x0e, 0x3)); code.push_back(EncodeMimg1(0, 24)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); TestCase test; test.name = "ImageGetResinfoDmaskWidthHeight"; test.code = code; test.expected = {4, 2}; test.opcodes = {O::V_MOV_B32, O::IMAGE_GET_RESINFO, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.image_width = 4; test.image_height = 2; test.sampled_image_rgba = MakeRgbaImage(test.image_width, test.image_height); return test; } TestCase ImageGetResinfoDmaskMipLevels() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 24, 0); code.push_back(EncodeMimg0(0x0e, 0x8)); code.push_back(EncodeMimg1(0, 24)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageGetResinfoDmaskMipLevels"; test.code = code; test.expected = {2}; test.opcodes = {O::V_MOV_B32, O::IMAGE_GET_RESINFO, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; auto base = MakeRgbaImage(4, 4); auto mip1 = MakeRgbaImage(2, 2); test.sampled_image_rgba_mips = {base, mip1}; return test; } TestCase ImageSampleAndGather() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x3f200000u); AppendVMovLiteral(&code, 21, 0x3ec00000u); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMimg0(0x20, 0xf)); code.push_back(EncodeMimg1(0, 20)); code.push_back(EncodeMimg0(0x47, 0x1)); code.push_back(EncodeMimg1(4, 20)); AppendVMovU32(&code, 24, 0); AppendVMovLiteral(&code, 25, 0x3f200000u); AppendVMovLiteral(&code, 26, 0x3ec00000u); AppendVMovU32(&code, 27, 0); code.push_back(EncodeMimg0(0x57, 0x1)); code.push_back(EncodeMimg1(8, 24)); code.push_back(EncodeMimg0(0x60, 0x1)); code.push_back(EncodeMimg1(12, 20)); for (u32 i = 0; i < 13u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 4); for (u32 y = 0; y < 4u; y++) { for (u32 x = 0; x < 4u; x++) { SetRgbaPixel(&image, 4, x, y, 0x3f800000u, 0, 0, 0); } } SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); TestCase test; test.name = "ImageSampleAndGather"; test.code = code; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0}; test.opcodes = { O::V_MOV_B32, O::IMAGE_SAMPLE, O::IMAGE_GET_LOD, O::IMAGE_GATHER4_LZ, O::IMAGE_GATHER4_LZ_O, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; return test; } TestCase ImageGatherLodApproximatesLevelZero() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x3f000000u); AppendVMovLiteral(&code, 21, 0x3f000000u); AppendVMovLiteral(&code, 22, 0x3f800000u); code.push_back(EncodeMimg0(0x44, 0x1)); code.push_back(EncodeMimg1(0, 20)); code.push_back(EncodeMimg0(0x24, 0x1)); code.push_back(EncodeMimg1(4, 20)); for (u32 i = 0; i < 5u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); TestCase test; test.name = "ImageGatherLodApproximatesLevelZero"; test.code = std::move(code); test.opcodes = {O::V_MOV_B32, O::IMAGE_GATHER4_L, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; // Explicit sampling proves mip 1 is accessible; the gather must still read mip 0. test.expected = {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x41000000u}; test.sampled_image_rgba_mips = {std::vector(4 * 4 * 4, 0x3f800000u), std::vector(2 * 2 * 4, 0x41000000u)}; test.required_spirv = {"OpImageGather", "OpImageSampleExplicitLod"}; return test; } TestCase ImageD16GatherPacksHalfPairs() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x3f000000u); AppendVMovLiteral(&code, 21, 0x3f000000u); AppendVMovU32(&code, 22, 0); code.push_back(EncodeMimg0(0x47, 0x1)); code.push_back(EncodeMimg1(0, 20, 0, 0, false, true)); AppendStoreVgpr(&code, 0, 0); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); auto image = MakeRgbaImage(4, 4); for (u32 y = 0; y < 4u; y++) { for (u32 x = 0; x < 4u; x++) { SetRgbaPixel(&image, 4, x, y, 0x3f000000u, 0, 0, 0); } } TestCase test; test.name = "ImageD16GatherPacksHalfPairs"; test.code = code; test.expected = {0x38003800u, 0x38003800u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_GATHER4_LZ, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; test.decoded_counts = {{"d16=1", 1}}; test.required_spirv = {"OpImageGather", "PackHalf2x16"}; return test; } TestCase ImageSampleA16SamplerCoordsOnGpu() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x36003900u); // x=0.625, y=0.375 packed as f16. AppendVMovU32(&code, 21, 0); code.push_back(EncodeMimg0(0x20, 0xf)); code.push_back(EncodeMimg1(0, 20, 0, 0, true)); for (u32 i = 0; i < 4u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 4); SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); TestCase test; test.name = "ImageSampleA16SamplerCoordsOnGpu"; test.code = code; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; test.required_spirv = {"UnpackHalf2x16"}; return test; } TestCase ImageSampleOpcodeAliasUsesNormalCoords() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 2, 0x3f000000u); AppendVMovLiteral(&code, 3, 0x3f000000u); code.push_back(0xf0800109u); // observed image_sample_a v6, v2, s0, s24 code.push_back(0x00c00602u); AppendStoreVgpr(&code, 6, 0); AppendEnd(&code); TestCase test; test.name = "ImageSampleOpcodeAliasUsesNormalCoords"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpImageSampleExplicitLod"}; test.forbidden_spirv = {"UnpackHalf2x16"}; test.compile_only = true; return test; } TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); // Non-constant +1 X offset is not a SPIR-V ConstOffset. AppendVMovLiteral(&code, 21, 0x36003900u); // x=0.625, y=0.375 packed as f16. AppendVMovU32(&code, 22, 0); code.push_back(EncodeMimg0(0x30, 0xf)); code.push_back(EncodeMimg1(0, 20, 0, 0, true)); for (u32 i = 0; i < 4u; i++) { AppendStoreVgpr(&code, i, i); } AppendEnd(&code); auto image = MakeRgbaImage(4, 4); SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); TestCase test; test.name = "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu"; test.code = code; test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.sampled_image_rgba = image; test.required_spirv = {"UnpackHalf2x16"}; test.forbidden_spirv = {"OpBitFieldSExtract"}; return test; } TestCase ImageSampleA16CompareBiasRdna2AddressOrder() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 20, 0x00003800u); // bias=0.5 as low f16. AppendVMovLiteral(&code, 21, 0x3f000000u); // PCF reference stays 32-bit. AppendVMovLiteral(&code, 22, 0x36003900u); // x=0.625, y=0.375 packed as f16. code.push_back(EncodeMimg0(0x2d, 0x1)); code.push_back(EncodeMimg1(0, 20, 0, 0, true)); AppendStoreVgpr(&code, 0, 0); // Ordinary and comparison reads of one sharp require distinct SPIR-V image types. code.push_back(EncodeMimg0(0x27, 0x1)); code.push_back(EncodeMimg1(1, 22, 0, 0, true)); AppendStoreVgpr(&code, 1, 1); AppendEnd(&code); TestCase test; test.name = "ImageSampleA16CompareBiasRdna2AddressOrder"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpTypeImage %float 2D 0", "OpTypeImage %float 2D 1", "OpImageSampleDrefExplicitLod", "OpImageSampleExplicitLod", "UnpackHalf2x16"}; test.compile_only = true; return test; } TestCase ImageGatherCompareOpcodes() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovLiteral(&code, 21, 0x3f000000u); AppendVMovLiteral(&code, 22, 0x3f200000u); AppendVMovLiteral(&code, 23, 0x3ec00000u); AppendVMovLiteral(&code, 24, 0x3f000000u); AppendVMovLiteral(&code, 25, 0x3f200000u); AppendVMovLiteral(&code, 26, 0x3ec00000u); AppendVMovLiteral(&code, 28, 0x3f000000u); AppendVMovLiteral(&code, 29, 0x3f200000u); AppendVMovLiteral(&code, 30, 0x3ec00000u); AppendVMovU32(&code, 32, 0); AppendVMovLiteral(&code, 33, 0x3f000000u); AppendVMovLiteral(&code, 34, 0x3f200000u); AppendVMovLiteral(&code, 35, 0x3ec00000u); code.push_back(EncodeMimg0(0x48, 0x1)); code.push_back(EncodeMimg1(4, 24)); code.push_back(EncodeMimg0(0x4f, 0x1)); code.push_back(EncodeMimg1(8, 28)); code.push_back(EncodeMimg0(0x58, 0x1)); code.push_back(EncodeMimg1(0, 20)); code.push_back(EncodeMimg0(0x5f, 0x1)); code.push_back(EncodeMimg1(12, 32)); AppendStoreVgpr(&code, 4, 0); AppendStoreVgpr(&code, 8, 1); AppendStoreVgpr(&code, 0, 2); AppendStoreVgpr(&code, 12, 3); AppendEnd(&code); TestCase test; test.name = "ImageGatherCompareOpcodes"; test.code = code; test.opcodes = { O::V_MOV_B32, O::IMAGE_GATHER4_C, O::IMAGE_GATHER4_C_LZ, O::IMAGE_GATHER4_C_O, O::IMAGE_GATHER4_C_LZ_O, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpImageDrefGather", "OpBitFieldSExtract"}; test.compile_only = true; return test; } TestCase ImageStoreVariants() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 2); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x40000000u); AppendVMovLiteral(&code, 2, 0x40400000u); AppendVMovLiteral(&code, 3, 0x40800000u); code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendVMovU32(&code, 24, 3); AppendVMovU32(&code, 25, 2); AppendVMovU32(&code, 26, 0); AppendVMovU32(&code, 27, 0); AppendVMovLiteral(&code, 4, 0x40a00000u); AppendVMovLiteral(&code, 5, 0x40c00000u); AppendVMovLiteral(&code, 6, 0x40e00000u); AppendVMovLiteral(&code, 7, 0x41000000u); code.push_back(EncodeMimg0(0x09, 0xf)); code.push_back(EncodeMimg1(4, 24)); AppendVMovLiteral(&code, 8, 0x12345678u); AppendStoreVgpr(&code, 8, 0); AppendEnd(&code); auto expected_image = MakeRgbaImage(4, 4); SetRgbaPixel(&expected_image, 4, 1, 2, 0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u); SetRgbaPixel(&expected_image, 4, 3, 2, 0x40a00000u, 0x40c00000u, 0x40e00000u, 0x41000000u); TestCase test; test.name = "ImageStoreVariants"; test.code = code; test.expected = {0x12345678u}; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::IMAGE_STORE_MIP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.storage_image_rgba = MakeRgbaImage(4, 4); test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_rgba = expected_image; return test; } TestCase ImageD16StoreUnpacksHalfPairs() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 2); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x38003400u); AppendVMovLiteral(&code, 1, 0x3c003a00u); code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20, 0, 0, false, true)); AppendEnd(&code); auto expected_image = MakeRgbaImage(4, 4); SetRgbaPixel(&expected_image, 4, 1, 2, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u); TestCase test; test.name = "ImageD16StoreUnpacksHalfPairs"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.storage_image_rgba = MakeRgbaImage(4, 4); test.expected_storage_image_rgba = expected_image; test.decoded_counts = {{"d16=1", 1}}; test.required_spirv = {"UnpackHalf2x16", "OpImageWrite"}; return test; } void CheckIndirectImageKeySwitch() { constexpr const char *name = "IndirectImageKeySwitch"; constexpr uint32_t mapping_capacity = 1793u; using namespace ShaderRecompiler::IR; Program program{}; program.stage = ShaderType::Compute; program.wave_size = 32; program.srt_plan_complete = true; program.resource_tracking_complete = true; program.shader_info_complete = true; program.block_storage.push_back(std::make_unique()); auto *block = program.block_storage.back().get(); program.blocks.push_back(block); program.block_info.push_back({.id = 0}); auto &key = block->AppendNewInst(ValueOpcode::LaneId); auto &image = block->AppendNewInst(ValueOpcode::GetImageResource, {Value(&key), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); image.SetFlags(0u); auto &sampler = block->AppendNewInst(ValueOpcode::GetSamplerResource, {Value(0u), Value(0u), Value(0u), Value(0u)}); sampler.SetFlags(0u); auto &address = block->AppendNewInst( ValueOpcode::MakeImageAddress, {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); MemoryInfo memory{}; memory.kind = ResourceKind::Image; memory.resource = 0; memory.sampler = 0; memory.dmask = 0xf; memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLod; memory.image_dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; memory.image_address_components = 3; program.memory_info.push_back(memory); const MemoryFlags memory_flags{0u, 0x10f0u}; uint64_t memory_flag_bits = 0; std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags)); auto &sample = block->AppendNewInst( ValueOpcode::ImageSampleRaw, {Value(&image), Value(&sampler), Value(&address)}, memory_flag_bits); auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4, {Value(&sample), Value(0u)}); block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)}); program.descriptor_sources.resize(2); program.descriptor_sources[0].dword_count = 8; program.descriptor_sources[0].indirect_image = DescriptorSource::IndirectImage{0u, 0u, 224u, 12u, 0u}; program.descriptor_sources[1].dword_count = 4; ImageResource root{}; root.source = 0; root.first_use_pc = 0x10f0u; root.resource_class = ImageResourceClass::Sampled; root.numeric_class = Prospero::TextureNumericClass::Float; root.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; root.read = true; root.indirect_root = 0; root.indirect_mapping_offset = 0; root.indirect_search_iterations = std::bit_width(mapping_capacity); root.indirect_resources = {0u, 1u}; auto candidate = root; candidate.indirect_search_iterations = 0; candidate.indirect_resources.clear(); program.info.images = {root, candidate}; program.info.samplers.push_back({1u, 0x10f0u}); program.info.sampled_pairs.push_back({0u, 0u, 0x10f0u}); AllocateBindings(program); ShaderComputeInputInfo compute{}; auto spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute}); ValidateSpirv(name, spirv); spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string text; Require(name, "SPIR-V disassembly", tools.Disassemble(spirv, &text), "failed to disassemble indirect image shader"); Require(name, "key switch", text.find("OpSwitch") != std::string::npos && text.find("OpPhi") != std::string::npos && CountText(text, "OpImageSampleExplicitLod") == 2 && CountText(text, "OpIEqual") == 11, "dynamic image key did not use a compact two-sample switch"); } TestCase ImageStoreMipSelectsPpsa01340Descriptor() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 2); AppendVMovU32(&code, 23, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x40000000u); AppendVMovLiteral(&code, 2, 0x40400000u); AppendVMovLiteral(&code, 3, 0x40800000u); code.push_back(EncodeMimg0(0x09, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); TestCase test; test.name = "ImageStoreMipSelectsPpsa01340Descriptor"; test.code = std::move(code); test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE_MIP, O::S_ENDPGM}; const std::array descriptor{0x0294dc00u, 0xc4700000u, 0x00b3c13fu, 0x91b31facu, 0x00000000u, 0x00700030u, 0xb07b0000u, 0x0002ac3cu}; std::copy(descriptor.begin(), descriptor.end(), test.user_data.begin()); test.has_user_data = true; test.compile_only = true; test.expected_storage_mip_descriptors = 3; test.required_spirv = {"OpSwitch", "OpImageWrite"}; return test; } TestCase ImageStoreRgbOneUsesInverseSwizzle() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 2); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x3f008081u); AppendVMovLiteral(&code, 2, 0x3e808081u); AppendVMovLiteral(&code, 3, 0x3f40c0c1u); code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); TestCase test; test.name = "ImageStoreRgbOneUsesInverseSwizzle"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.has_user_data = true; test.user_data[0] = 0x1000u; test.user_data[1] = static_cast(Prospero::BufferFormat::k8_8_8_8UNorm) << 20u; test.image_descriptor_swizzle = DstSel(4, 5, 6, 1); test.storage_image_format = vk::Format::eR8G8B8A8Unorm; test.storage_image_dwords_per_pixel = 1; test.storage_image_rgba = std::vector(16, 0); test.expected_storage_image_rgba = std::vector(16, 0); test.expected_storage_image_rgba[2 * 4 + 1] = 0x004080ffu; return test; } TestCase ImageStoreDuplicateSelectorUsesInverseSwizzle() { auto test = ImageStoreRgbOneUsesInverseSwizzle(); test.name = "ImageStoreDuplicateSelectorUsesInverseSwizzle"; test.image_descriptor_swizzle = DstSel(4, 4, 6, 7); test.expected_storage_image_rgba[2 * 4 + 1] = 0xc04000ffu; return test; } TestCase ImageStoreBgraUsesInverseSwizzle() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 2); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x3f008081u); // 128/255 AppendVMovLiteral(&code, 2, 0x3e808081u); // 64/255 AppendVMovLiteral(&code, 3, 0x3f3fbfc0u); // 191/255 code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); TestCase test; test.name = "ImageStoreBgraUsesInverseSwizzle"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.has_user_data = true; test.user_data[0] = 0x1000u; test.user_data[1] = static_cast(Prospero::BufferFormat::k8_8_8_8UNorm) << 20u; test.image_descriptor_swizzle = DstSel(6, 5, 4, 7); test.storage_image_format = vk::Format::eR8G8B8A8Unorm; test.storage_image_dwords_per_pixel = 1; test.storage_image_rgba = std::vector(16, 0); test.expected_storage_image_rgba = std::vector(16, 0); test.expected_storage_image_rgba[2 * 4 + 1] = 0xbfff8040u; return test; } TestCase ImageStoreYzwxUsesInverseSwizzle() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 1); AppendVMovU32(&code, 21, 2); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); AppendVMovLiteral(&code, 1, 0x3f000000u); AppendVMovLiteral(&code, 2, 0x3e800000u); AppendVMovLiteral(&code, 3, 0x3f400000u); code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); TestCase test; test.name = "ImageStoreYzwxUsesInverseSwizzle"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32_32_32_32Float); test.has_user_data = true; test.image_descriptor_swizzle = DstSel(5, 6, 7, 4); test.storage_image_rgba = MakeRgbaImage(4, 4); test.expected_storage_image_rgba = MakeRgbaImage(4, 4); SetRgbaPixel(&test.expected_storage_image_rgba, 4, 1, 2, 0x3f400000u, 0x3f800000u, 0x3f000000u, 0x3e800000u); return test; } TestCase ImageStoreR32FloatUsesFormatlessStorageImage() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x3f800000u); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 0x3f800000u; TestCase test; test.name = "ImageStoreR32FloatUsesFormatlessStorageImage"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.storage_image_format = vk::Format::eR32Sfloat; test.storage_image_dwords_per_pixel = 1; test.storage_image_rgba = std::vector(16, 0); test.expected_storage_image_rgba = expected_image; test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat"}; test.forbidden_spirv = {"OpCapability StorageImageReadWithoutFormat", "Rgba32f", "OpTypeSampler", "OpTypeSampledImage"}; return test; } TestCase ImageStoreR32SintUsesRawUintView() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x80000001u); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 0x80000001u; TestCase test; test.name = "ImageStoreR32SintUsesRawUintView"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32SInt); test.has_user_data = true; test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = expected_image; test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false)}; test.forbidden_spirv = {"R32ui"}; return test; } TestCase ImageStoreR32UintUsesFormatlessStorageImage() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, 0x12345678u); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 0x12345678u; TestCase test; test.name = "ImageStoreR32UintUsesFormatlessStorageImage"; test.code = code; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); test.has_user_data = true; test.storage_image_rgba = MakeRgbaImage(4, 4); test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = expected_image; test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false)}; test.forbidden_spirv = {"R32ui"}; return test; } TestCase ImageStorePackedUintSaturatesChannels() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 0x800u); AppendVMovLiteral(&code, 1, UINT32_MAX); AppendVMovU32(&code, 2, 0x400u); AppendVMovU32(&code, 3, 0x55u); code.push_back(EncodeMimg0(0x08, 0xf)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = UINT32_MAX; TestCase test; test.name = "ImageStorePackedUintSaturatesChannels"; test.code = std::move(code); test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k11_11_10UInt); test.has_user_data = true; test.image_descriptor_swizzle = DstSel(4, 5, 6, 0); test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false), "OpULessThan", "OpSelect"}; test.forbidden_spirv = {"R32ui"}; return test; } TestCase ImageStorePackedUintHonorsSparseDmask() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 0x321u); AppendVMovU32(&code, 1, 0x2abu); code.push_back(EncodeMimg0(0x08, 0x5)); code.push_back(EncodeMimg1(0, 20)); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 0xaac00321u; TestCase test; test.name = "ImageStorePackedUintHonorsSparseDmask"; test.code = std::move(code); test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k11_11_10UInt); test.has_user_data = true; test.image_descriptor_swizzle = DstSel(4, 5, 6, 0); test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false)}; test.forbidden_spirv = {"R32ui"}; return test; } TestCase ComputeTgSizeSgprUsesWaveMetadata() { using O = ShaderOpcode; std::vector code; AppendStoreSgpr(&code, 2, 0); AppendEnd(&code); TestCase test; test.name = "ComputeTgSizeSgprUsesWaveMetadata"; test.code = code; test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 4; test.compute_info.threads_num[2] = 16; test.compute_info.group_id[0] = true; test.compute_info.group_id[1] = true; test.compute_info.wave_size = 32; test.compute_info.thread_ids_num = 3; test.compute_info.workgroup_register = 0; test.compute_info.tg_size_en = true; test.has_compute_info = true; test.compile_only = true; test.required_spirv = {"OpUDiv", "OpShiftLeftLogical", "2147483648"}; return test; } TestCase ImageStoreAndAtomicShareTypedBinding() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 5); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendVMovU32(&code, 0, 3); code.push_back(EncodeMimg0(0x11, 0x1, 0, true)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 8; TestCase test; test.name = "ImageStoreAndAtomicShareTypedBinding"; test.code = std::move(code); test.expected = {5}; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::IMAGE_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); test.has_user_data = true; test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpImageWrite", "OpImageTexelPointer", "R32ui", StorageUint2DImageBindingName(true)}; test.forbidden_spirv = {"StorageImageReadWithoutFormat", "StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false)}; return test; } TestCase ImageAtomicSwapReturnsPreviousTexel() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 5); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendVMovU32(&code, 0, 3); code.push_back(EncodeMimg0(0x0f, 0x1, 0, true)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 3; TestCase test; test.name = "ImageAtomicSwapReturnsPreviousTexel"; test.code = std::move(code); test.expected = {5}; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::IMAGE_ATOMIC_SWAP, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); test.has_user_data = true; test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpAtomicExchange", "OpImageTexelPointer"}; return test; } TestCase ImageStoreAndAtomicUseSeparateBindings() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 2); AppendVMovU32(&code, 21, 1); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 5); code.push_back(EncodeMimg0(0x08, 0x1)); code.push_back(EncodeMimg1(0, 20, 0)); AppendVMovU32(&code, 0, 3); code.push_back(EncodeMimg0(0x11, 0x1, 0, true)); code.push_back(EncodeMimg1(0, 20, 2)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); auto user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt); std::copy_n(user_data.begin(), 8, user_data.begin() + 8); std::vector expected_image(16, 0); expected_image[1 * 4 + 2] = 8; TestCase test; test.name = "ImageStoreAndAtomicUseSeparateBindings"; test.code = std::move(code); test.expected = {5}; test.opcodes = {O::V_MOV_B32, O::IMAGE_STORE, O::IMAGE_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.user_data = user_data; test.has_user_data = true; test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", "OpImageWrite", "OpImageTexelPointer", "R32ui", StorageUint2DImageBindingName(false), StorageUint2DImageBindingName(true)}; return test; } TestCase ImageAtomicVariants() { using O = ShaderOpcode; const u32 initial[] = {10, 10, 0xf0f0u, 0xf000u, 0xf00fu}; const u32 values[] = {5, 5, 0x0ff0u, 0x0f00u, 0x00ffu}; const u32 ops[] = {0x11, 0x15, 0x18, 0x19, 0x1a}; std::vector code; for (u32 i = 0; i < static_cast(std::size(values)); i++) { AppendVMovU32(&code, 20, i & 3u); AppendVMovU32(&code, 21, i >> 2u); AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 0, values[i]); code.push_back(EncodeMimg0(ops[i], 0x1, 0, true)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, i); } AppendEnd(&code); TestCase test; test.name = "ImageAtomicVariants"; test.code = code; test.expected = {10, 10, 0xf0f0u, 0xf000u, 0xf00fu}; test.opcodes = {O::V_MOV_B32, O::IMAGE_ATOMIC_ADD, O::IMAGE_ATOMIC_UMIN, O::IMAGE_ATOMIC_AND, O::IMAGE_ATOMIC_OR, O::IMAGE_ATOMIC_XOR, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.required_spirv = {"OpImageTexelPointer", "R32ui", StorageUint2DImageBindingName(true)}; test.forbidden_spirv = {"OpTypeSampler", "OpTypeSampledImage", "StorageImageReadWithoutFormat", "StorageImageWriteWithoutFormat", StorageUint2DImageBindingName(false)}; test.storage_image_rgba = MakeRgbaImage(4, 4); test.storage_image_r32ui = std::vector(16, 0); for (u32 i = 0; i < static_cast(std::size(initial)); i++) { test.storage_image_r32ui[i] = initial[i]; } test.expected_storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui[0] = 15; test.expected_storage_image_r32ui[1] = 5; test.expected_storage_image_r32ui[2] = 0x00f0u; test.expected_storage_image_r32ui[3] = 0xff00u; test.expected_storage_image_r32ui[4] = 0xf0f0u; return test; } TestCase ImageAtomicGlc0DoesNotReturnOldValue() { using O = ShaderOpcode; std::vector code; AppendVMovU32(&code, 20, 0); AppendVMovU32(&code, 21, 0); AppendVMovU32(&code, 22, 0); AppendVMovU32(&code, 0, 5); code.push_back(EncodeMimg0(0x11, 0x1)); code.push_back(EncodeMimg1(0, 20)); AppendStoreVgpr(&code, 0, 0); AppendEnd(&code); TestCase test; test.name = "ImageAtomicGlc0DoesNotReturnOldValue"; test.code = code; test.expected = {5}; test.opcodes = {O::V_MOV_B32, O::IMAGE_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.storage_image_rgba = MakeRgbaImage(4, 4); test.storage_image_r32ui = std::vector(16, 0); test.storage_image_r32ui[0] = 10; test.expected_storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui[0] = 15; return test; } GraphicsCase GraphicsInterpolationExport() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVintrp(0x00, 0, 0, 0, 0)); code.push_back(EncodeVintrp(0x01, 0, 0, 0, 0)); code.push_back(EncodeVintrp(0x02, 1, 0, 1, 2)); AppendVMovLiteral(&code, 2, 0x3f400000u); AppendVMovLiteral(&code, 3, 0x3f800000u); code.push_back(EncodeExp0(0x00, 0xf)); code.push_back(EncodeExp1(0, 1, 2, 3)); AppendEnd(&code); return {"GraphicsInterpolationExport", code, {0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u}, {O::V_INTERP_P1_F32, O::V_INTERP_P2_F32, O::V_INTERP_MOV_F32, O::V_MOV_B32, O::EXP, O::S_ENDPGM}}; } GraphicsCase GraphicsPositionWExport() { GraphicsCase test; test.name = "GraphicsPositionWExport"; test.pixel_position_w = true; test.vertex_clip_w = 4.0f; // POS_W occupies v2; the rasterizer supplies FragCoord.w=1/4 for this triangle. test.fragment_code = {EncodeExp0(0x00, 0xf), EncodeExp1(2, 2, 2, 2)}; AppendEnd(&test.fragment_code); test.expected_pixel = {0x40800000u, 0x40800000u, 0x40800000u, 0x40800000u}; test.opcodes = {ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM}; return test; } GraphicsCase GraphicsPackedHalfCentroid() { GraphicsCase test; test.name = "GraphicsPackedHalfCentroid"; test.pixel_perspective_centroid_vgpr = 0; test.pixel_custom_interpolation_mask = 1; test.pixel_interpolator_settings = {0x420u}; // Captured ab810715011baef3 interpolation, with attr3.x remapped to attr0.x. test.fragment_code = { 0xc8120002u, 0xc80e0000u, // raw vertex0/vertex1 packed halves 0xcc204007u, 0x9c1206f2u, // low(vertex1) - low(vertex0) 0xcc207005u, 0x9c1206f2u, // high(vertex1) - high(vertex0) 0xc80a0001u, // raw vertex2 0xcc204006u, 0x9c1204f2u, 0xcc207003u, 0x9c1204f2u, 0xcc20400eu, 0x04120f00u, // low = I * delta10 + vertex0 0xcc20600fu, 0x04120b00u, // high = I * delta10 + vertex0 0x3e1c0306u, 0x3e1e0303u, // += J * delta20 EncodeExp0(0x00, 0xf), EncodeExp1(14, 15, 14, 15)}; AppendEnd(&test.fragment_code); // At the pixel center the vertex weights are (5/8, 1/8, 1/4). // Packed values are (0,1), (1,2), (3,3): expected result (0.875, 1.625). test.vertices = { 0xbf800000u, 0xbf800000u, 0x3c000000u, 0, 0, 0, 0x40e00000u, 0xbf800000u, 0x40003c00u, 0, 0, 0, 0xbf800000u, 0x40400000u, 0x42004200u, 0, 0, 0}; test.expected_pixel = {0x3f600000u, 0x3fd00000u, 0x3f600000u, 0x3fd00000u}; test.opcodes = {ShaderOpcode::V_INTERP_MOV_F32, ShaderOpcode::V_FMA_F32, ShaderOpcode::V_MAC_F32, ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM}; return test; } GraphicsCase GraphicsPackedHalfInputAlias(bool second_weights) { auto test = GraphicsPackedHalfCentroid(); test.name = second_weights ? "GraphicsPackedHalfAliasSecondWeights" : "GraphicsPackedHalfInputAlias"; test.pixel_custom_interpolation_mask = 2u; test.pixel_interpolator_settings = {0u, 0x420u}; // The head shader reads one exported vec4 through two logical inputs: // ordinary interpolation and raw per-vertex packed-half interpolation. // Move the captured raw attr0.x loads to logical attr1.x, still mapped to 0. for (const auto word : {0u, 1u, 6u}) { test.fragment_code[word] |= 1u << 10u; } test.fragment_code.resize(test.fragment_code.size() - 3u); test.fragment_code.push_back(EncodeVintrp(0x00, 16, 0, 1, 0)); test.fragment_code.push_back(EncodeVintrp(0x01, 16, 0, 1, 1)); AppendVMovLiteral(&test.fragment_code, 17, 0x3f800000u); test.fragment_code.push_back(EncodeExp0(0x00, 0xf)); test.fragment_code.push_back(EncodeExp1(14, 15, 16, 17)); AppendEnd(&test.fragment_code); // The smooth component (2,4,8) must vary independently of packed UVs. test.vertices[3] = 0x40000000u; test.vertices[9] = 0x40800000u; test.vertices[15] = 0x41000000u; if (second_weights) { // Change the right vertex from x=7 to x=3: weights become (1/2,1/4,1/4). test.vertices[6] = 0x40400000u; test.expected_pixel = {0x3f800000u, 0x3fe00000u, 0x40800000u, 0x3f800000u}; } else { test.expected_pixel = {0x3f600000u, 0x3fd00000u, 0x40700000u, 0x3f800000u}; } test.opcodes.insert(test.opcodes.end(), {ShaderOpcode::V_INTERP_P1_F32, ShaderOpcode::V_INTERP_P2_F32, ShaderOpcode::V_MOV_B32}); return test; } GraphicsCase GraphicsSmoothRawInputAlias() { auto test = GraphicsPackedHalfInputAlias(false); test.name = "GraphicsSmoothRawInputAlias"; test.pixel_perspective_centroid_vgpr = UINT32_MAX; // No explicit barycentric builtin: alias promotion must discover that the // ordinary input needs weights while the raw alias returns vertex zero. test.fragment_code = {EncodeVintrp(0x00, 0, 0, 1, 0), EncodeVintrp(0x01, 0, 0, 1, 1), EncodeVintrp(0x02, 1, 1, 1, 2), EncodeExp0(0x00, 0xf), EncodeExp1(0, 1, 0, 1)}; AppendEnd(&test.fragment_code); test.expected_pixel = {0x40700000u, 0x40000000u, 0x40700000u, 0x40000000u}; test.opcodes = {ShaderOpcode::V_INTERP_P1_F32, ShaderOpcode::V_INTERP_P2_F32, ShaderOpcode::V_INTERP_MOV_F32, ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM}; return test; } GraphicsCase GraphicsAncillaryLayer(bool front_face) { GraphicsCase test; test.name = front_face ? "GraphicsAncillaryAfterFrontFace" : "GraphicsAncillaryLayer"; test.pixel_ancillary = true; test.pixel_front_face = front_face; test.layers = 8; const auto ancillary = Vgpr(front_face ? 3 : 2); test.fragment_code.push_back(EncodeSop1(0x04, 8, 126)); test.fragment_code.push_back(EncodeSop1(0x0a, 126, 126)); AppendVop3(&test.fragment_code, 0x148, 4, ancillary, InlineU32(16), InlineU32(11)); AppendVop3(&test.fragment_code, 0x149, 5, ancillary, InlineU32(16), InlineU32(3)); test.fragment_code.push_back(EncodeSop1(0x04, 126, 8)); AppendVMovU32(&test.fragment_code, 6, test.layers); test.fragment_code.push_back(EncodeVopc(0xc1, Vgpr(4), 6)); // Dynamic layer < count. test.fragment_code.push_back(EncodeSop1(0x04, 126, 106)); test.fragment_code.push_back(EncodeVop1(0x06, 0, Vgpr(4))); // float(unsigned layer) test.fragment_code.push_back(EncodeVop1(0x05, 1, Vgpr(5))); // float(signed low 3 bits) AppendVMovLiteral(&test.fragment_code, 2, 0); AppendVMovLiteral(&test.fragment_code, 3, 0x3f800000u); test.fragment_code.push_back(EncodeExp0(0x00, 0xf)); test.fragment_code.push_back(EncodeExp1(0, 1, 2, 3)); AppendEnd(&test.fragment_code); test.opcodes = {ShaderOpcode::V_BFE_U32, ShaderOpcode::V_BFE_I32, ShaderOpcode::V_CVT_F32_U32, ShaderOpcode::V_CVT_F32_I32, ShaderOpcode::V_MOV_B32, ShaderOpcode::S_MOV_B64, ShaderOpcode::S_WQM_B64, ShaderOpcode::V_CMP_LT_U32, ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM}; for (u32 layer = 0; layer < test.layers; ++layer) { const auto signed_bits = layer < 4 ? static_cast(layer) : static_cast(layer) - 8; test.expected_pixel.insert(test.expected_pixel.end(), {std::bit_cast(static_cast(layer)), std::bit_cast(static_cast(signed_bits)), 0, 0x3f800000u}); } return test; } GraphicsCase GraphicsAncillarySampleId() { GraphicsCase test; test.name = "GraphicsAncillarySampleId"; test.pixel_ancillary = true; test.samples = vk::SampleCountFlagBits::e4; AppendVop3(&test.fragment_code, 0x148, 4, Vgpr(2), InlineU32(8), InlineU32(4)); AppendVop3(&test.fragment_code, 0x149, 5, Vgpr(2), InlineU32(8), InlineU32(2)); AppendVop3(&test.fragment_code, 0x148, 6, Vgpr(2), InlineU32(9), InlineU32(1)); test.fragment_code.push_back(EncodeVop1(0x06, 0, Vgpr(4))); test.fragment_code.push_back(EncodeVop1(0x05, 1, Vgpr(5))); test.fragment_code.push_back(EncodeVop1(0x06, 2, Vgpr(6))); AppendVMovLiteral(&test.fragment_code, 3, 0x3f800000u); test.fragment_code.push_back(EncodeExp0(0x00, 0xf)); test.fragment_code.push_back(EncodeExp1(0, 1, 2, 3)); AppendEnd(&test.fragment_code); test.opcodes = {ShaderOpcode::V_BFE_U32, ShaderOpcode::V_BFE_I32, ShaderOpcode::V_CVT_F32_U32, ShaderOpcode::V_CVT_F32_I32, ShaderOpcode::V_MOV_B32, ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM}; // Average samples 0..3, their signed low two bits, and their second bit. test.expected_pixel = {0x3fc00000u, 0xbf000000u, 0x3f000000u, 0x3f800000u}; return test; } GraphicsCase GraphicsFlatInterpolatorExport() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeVintrp(0x02, 0, 0, 0, 2)); AppendVMovLiteral(&code, 1, 0x00000000u); AppendVMovLiteral(&code, 2, 0x00000000u); AppendVMovLiteral(&code, 3, 0x3f800000u); code.push_back(EncodeExp0(0x00, 0xf)); code.push_back(EncodeExp1(0, 1, 2, 3)); AppendEnd(&code); GraphicsCase test; test.name = "GraphicsFlatInterpolatorExport"; test.fragment_code = code; test.expected_pixel = {0x3e800000u, 0x00000000u, 0x00000000u, 0x3f800000u}; test.opcodes = {O::V_INTERP_MOV_F32, O::V_MOV_B32, O::EXP, O::S_ENDPGM}; test.pixel_interpolator_settings = {0x00000400u}; test.vertices = { 0xbf800000u, 0xbf800000u, 0x3e800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40400000u, 0xbf800000u, 0x3f400000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0xbf800000u, 0x40400000u, 0x3e800000u, 0x00000000u, 0x00000000u, 0x3f800000u, }; return test; } GraphicsCase GraphicsDsAddtidScratchExport() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSMovB32(124, InlineU32(0))); AppendVMovLiteral(&code, 0, 0x3f000000u); code.push_back(EncodeDs0(0xb0, 0)); code.push_back(EncodeDs1(0, 0, 0)); code.push_back(EncodeDs0(0xb1, 0)); code.push_back(EncodeDs1(4, 0, 0)); code.push_back(EncodeExp0(0x00, 0xf)); code.push_back(EncodeExp1(4, 4, 4, 4)); AppendEnd(&code); return {"GraphicsDsAddtidScratchExport", code, {0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u}, {O::S_MOV_B32, O::V_MOV_B32, O::DS_WRITE_ADDTID_B32, O::DS_READ_ADDTID_B32, O::EXP, O::S_ENDPGM}}; } GraphicsCase GraphicsDirectSgprPushConstantExport() { using O = ShaderOpcode; const std::vector values = {0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u}; std::vector code; code.push_back(EncodeVop1(0x01, 0, 0)); code.push_back(EncodeVop1(0x01, 1, 1)); code.push_back(EncodeVop1(0x01, 2, 2)); code.push_back(EncodeVop1(0x01, 3, 3)); code.push_back(EncodeExp0(0x00, 0xf)); code.push_back(EncodeExp1(0, 1, 2, 3)); AppendEnd(&code); GraphicsCase test; test.name = "GraphicsDirectSgprPushConstantExport"; test.fragment_code = code; test.expected_pixel = values; test.opcodes = {O::V_MOV_B32, O::EXP, O::S_ENDPGM}; for (size_t i = 0; i < values.size(); i++) { test.user_data[i] = values[i]; } test.has_user_data = true; test.push_constants = values; return test; } GraphicsCase GraphicsInlineSrtScalarPromotionExport() { using O = ShaderOpcode; const std::vector values = {0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u}; std::vector code; AppendVop3(&code, 0x12fu, 0, 0, 1); AppendVop3(&code, 0x12fu, 1, 2, 3); code.push_back(EncodeExp0(0x00, 0xf, true, true, true)); code.push_back(EncodeExp1(0, 1, 0, 0)); AppendEnd(&code); GraphicsCase test; test.name = "GraphicsInlineSrtScalarPromotionExport"; test.fragment_code = code; test.expected_pixel = values; test.opcodes = {O::V_CVT_PKRTZ_F16_F32, O::EXP, O::S_ENDPGM}; for (size_t i = 0; i < values.size(); i++) { test.user_data[i] = values[i]; } test.has_user_data = true; test.push_constants = values; return test; } GraphicsCase GraphicsNullVmExportDiscardsInactiveExec() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f000000u); AppendVMovLiteral(&code, 1, 0x3f400000u); AppendVMovLiteral(&code, 2, 0x3f800000u); AppendVMovLiteral(&code, 3, 0x40000000u); code.push_back(EncodeExp0(0x00, 0xf, false)); code.push_back(EncodeExp1(0, 1, 2, 3)); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeExp0(0x09, 0x0, true, false, true)); code.push_back(EncodeExp1(0, 0, 0, 0)); AppendEnd(&code); return {"GraphicsNullVmExportDiscardsInactiveExec", code, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {O::V_MOV_B32, O::EXP, O::S_MOV_B64, O::S_ENDPGM}}; } GraphicsCase GraphicsMrt0OffVmExportDiscardsInactiveExec() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f000000u); AppendVMovLiteral(&code, 1, 0x3f400000u); AppendVMovLiteral(&code, 2, 0x3f800000u); AppendVMovLiteral(&code, 3, 0x40000000u); code.push_back(EncodeExp0(0x00, 0xf, false)); code.push_back(EncodeExp1(0, 1, 2, 3)); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeExp0(0x00, 0x0, true, true, true)); code.push_back(EncodeExp1(0, 0, 0, 0)); AppendEnd(&code); return {"GraphicsMrt0OffVmExportDiscardsInactiveExec", code, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {O::V_MOV_B32, O::EXP, O::S_MOV_B64, O::S_ENDPGM}}; } GraphicsCase GraphicsFinalVmExportSupersedesEarlierVmMask() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f000000u); AppendVMovLiteral(&code, 1, 0x3f400000u); AppendVMovLiteral(&code, 2, 0x3f800000u); AppendVMovLiteral(&code, 3, 0x40000000u); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeExp0(0x00, 0xf, false, false, true)); code.push_back(EncodeExp1(0, 1, 2, 3)); code.push_back(EncodeSMovB32(126, 193u)); code.push_back(EncodeSMovB32(127, 193u)); AppendVMovLiteral(&code, 4, 0x40400000u); AppendVMovLiteral(&code, 5, 0x40800000u); AppendVMovLiteral(&code, 6, 0x40a00000u); AppendVMovLiteral(&code, 7, 0x40c00000u); code.push_back(EncodeExp0(0x00, 0xf, true, false, true)); code.push_back(EncodeExp1(4, 5, 6, 7)); AppendEnd(&code); return {"GraphicsFinalVmExportSupersedesEarlierVmMask", code, {0x40400000u, 0x40800000u, 0x40a00000u, 0x40c00000u}, {O::V_MOV_B32, O::S_MOV_B64, O::EXP, O::S_MOV_B32, O::S_ENDPGM}}; } GraphicsCase GraphicsBranchPathFinalVmExportDiscardsInactiveExec() { using O = ShaderOpcode; std::vector code; AppendVMovLiteral(&code, 0, 0x3f000000u); AppendVMovLiteral(&code, 1, 0x3f400000u); AppendVMovLiteral(&code, 2, 0x3f800000u); AppendVMovLiteral(&code, 3, 0x40000000u); code.push_back(EncodeExp0(0x00, 0xf, false)); code.push_back(EncodeExp1(0, 1, 2, 3)); code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); code.push_back(EncodeExp0(0x09, 0x0, true, false, true)); code.push_back(EncodeExp1(0, 0, 0, 0)); code.push_back(EncodeSopc(0x06, InlineU32(0), InlineU32(1))); const auto branch_index = code.size(); code.push_back(0); AppendEnd(&code); const auto later_export_index = code.size(); code[branch_index] = EncodeSopp( 0x05, static_cast(later_export_index - branch_index - 1u)); code.push_back(EncodeSMovB32(126, 193u)); code.push_back(EncodeSMovB32(127, 193u)); AppendVMovLiteral(&code, 4, 0x40400000u); AppendVMovLiteral(&code, 5, 0x40800000u); AppendVMovLiteral(&code, 6, 0x40a00000u); AppendVMovLiteral(&code, 7, 0x40c00000u); code.push_back(EncodeExp0(0x00, 0xf, true, false, true)); code.push_back(EncodeExp1(4, 5, 6, 7)); AppendEnd(&code); return {"GraphicsBranchPathFinalVmExportDiscardsInactiveExec", code, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {O::V_MOV_B32, O::EXP, O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_CBRANCH_SCC1, O::S_MOV_B32, O::S_ENDPGM}}; } TestCase MultipleWorkitemsGlobalId() { using O = ShaderOpcode; std::vector code = { EncodeVop1(0x01, 1, 0), EncodeVop2(0x1a, 1, InlineU32(2), 1), EncodeVop2(0x25, 1, Vgpr(0), 1), EncodeVop2(0x25, 2, InlineU32(64), 1), EncodeVop2(0x1a, 3, InlineU32(2), 1), }; AppendBufferStoreDword(&code, 2, 3); AppendEnd(&code); TestCase test; test.name = "MultipleWorkitemsGlobalId"; test.code = code; test.expected = {64, 65, 66, 67, 68, 69, 70, 71}; test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; test.compute_info.threads_num[0] = 4; test.compute_info.threads_num[1] = 1; test.compute_info.threads_num[2] = 1; test.compute_info.group_id[0] = true; test.compute_info.thread_ids_num = 1; test.compute_info.workgroup_register = 0; test.has_compute_info = true; test.dispatch_x = 2; return test; } TestCase DispatcherIrreducibleControlFlow() { using O = ShaderOpcode; std::vector code; code.push_back(EncodeSopp(0x05, 5)); // entry -> B, fallthrough A AppendVMovU32(&code, 0, 7); AppendStoreVgpr(&code, 0, 0); code.push_back(EncodeSopp(0x04, 1)); // A exits while SCC is initially zero code.push_back(EncodeSopp(0x02, 0xfffau)); // B -> A AppendEnd(&code); TestCase test; test.name = "DispatcherIrreducibleControlFlow"; test.code = code; test.expected = {7}; test.opcodes = {O::S_CBRANCH_SCC1, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_CBRANCH_SCC0, O::S_BRANCH, O::S_ENDPGM}; return test; } std::vector MakeCases() { std::vector cases; cases.reserve(128); auto AddCase = [&cases](TestCase (*factory)()) { cases.push_back(factory()); }; AddCase(IntegerAddSubMul); AddCase(BitwiseOps); AddCase(Shifts); AddCase(ExactPushConstantExtent); AddCase(ScalarShiftCountsMaskLowBits); AddCase(Rdna2ScalarOpcodes); AddCase(ScalarTrapDisabledFallsThroughExactRaw); AddCase(ScalarWaitcntDepctrCapturedVmVsrc); AddCase(ScalarExtendedArithmetic); AddCase(ScalarArithmeticSccCarryBorrowOverflow); AddCase(ScalarMinMaxSccComparisonEdges); AddCase(ScalarAbsI32UpdatesScc); AddCase(ScalarAbsdiffBitset64AndSetprio); AddCase(ScalarShiftLeftAddSccCarryEdges); AddCase(ScalarCompareOps); AddCase(ScalarShiftAddAndMaskOps); AddCase(ScalarNotB64UpdatesScc); AddCase(ScalarQuadmaskB64); AddCase(ScalarFlbitI32B64Gpu); AddCase(ScalarFlbitI32B32Gpu); AddCase(ScalarSaveExecOps); AddCase(ScalarOrn2SaveexecUsesSourceOrNotExec); cases.push_back(ScalarOrn2SaveexecB32(32, 32)); cases.push_back(ScalarOrn2SaveexecB32(64, 64)); cases.push_back(ScalarOrn2SaveexecB32(32, 4)); cases.push_back(ScalarSubvectorLoops(32)); cases.push_back(ScalarSubvectorLoops(64)); AddCase(ScalarGetpcWritesNextInstructionPc); AddCase(ScalarBitfieldPack); AddCase(ScalarBrevB32PreservesScc); AddCase(ScalarBfeI32CapturedRawSignExtends); AddCase(BitfieldExtractWidthPastEndEdges); AddCase(Scalar64BitOps); cases.push_back(ScalarAshrI64Edges(false)); cases.push_back(ScalarAshrI64Edges(true)); AddCase(ScalarAshrI64OperandsAndAliases); cases.push_back(ScalarAshrI64Masks(32)); cases.push_back(ScalarAshrI64Masks(64)); AddCase(VectorDynamicU64ShiftEdges); AddCase(ScalarConditionalMoveB64); AddCase(ScalarConditionalMoveB64PreservesMasks); AddCase(ScalarAndn2B64SccUsesMaskShadow); AddCase(ScalarMaskHighWriteInvalidatesProvenance); AddCase(ScalarSelectB64PreservesMaskProvenance); AddCase(ScalarWqmB64SelectsSccDomain); AddCase(ScalarWqmB64PreservesPartialMasks); cases.push_back(ScalarWqmB32Masks(32)); cases.push_back(ScalarWqmB32Masks(64)); AddCase(ScalarMaskProvenanceOverlapAndMixedBinary); AddCase(ScalarLiteral); AddCase(VectorMoves); AddCase(VectorVop3MoveAppliesFloatSourceModifiers); AddCase(VectorIntegerOps); AddCase(VectorFfbhI32NativeAndVop3OnGpu); AddCase(Vop1SdwaFfblCapturedHighWordSource); AddCase(Vop1SdwaNotCapturedByte0Source); AddCase(Vop1SdwaMovByteDestinations); AddCase(Vop2SdwaSubNcExactByte2Destination); AddCase(Vop2SdwaAddNcCapturedHighWordDestination); AddCase(Vop2SdwaAshrrevCapturedWord0SignExtends); AddCase(Vop2SdwaLshrrevCapturedByte1Source); AddCase(Vop2SdwaSubNcPreservesByteAndWordDestinations); AddCase(Vop3CvtPkI16I32Captured); AddCase(Vop3MulLoU16CapturedAndSelectors); AddCase(Vop3MadI16CapturedSelectorsAndSaturation); AddCase(Vop3Med3I16Captured); AddCase(Vop2SdwaMinU32PreservesWordDestination); AddCase(VectorShiftCountsMaskLowBits); AddCase(VectorVop3LshrrevB64Captured); AddCase(VectorVop3LshlrevB64Captured); AddCase(VectorVop3IntegerOps); AddCase(VectorBfeI32SignExtendsField); AddCase(VectorAlignByteUsesFiveBitByteOffset); AddCase(VectorCarryAndBitCountOps); AddCase(VectorMbcntUsesThreadMask); AddCase(VectorAddcWritesPerLaneCarryOut); AddCase(VectorAddcUsesPerLaneCarryIn); AddCase(VectorSubCoCiU32CompactAndVop3); AddCase(VectorSubrevCoCiU32ExactRawOnGpu); AddCase(VectorVop3BSubrevCoCiUsesEncodedMasks); AddCase(VectorVop3BCarryOutWritesSgprMask); AddCase(VectorVop3BCarryOutUsesEncodedSdst); AddCase(VectorVop3BSubCoU32UsesRdna2Opcode310); AddCase(VectorMadU64U32UnsignedCarryOut); AddCase(VectorLaneAndPackedOps); AddCase(Vop2PkFmacF16AccumulatesPackedHalvesIndependently); AddCase(Vop2PkFmacF16DppNegatesBothPackedHalves); AddCase(Vop3pOpselHiUsesArchitecturalSourceBits); AddCase(Vop3pIntegerNegationCapturedAndSelectedHalves); AddCase(CvtPkU8F32PacksSelectedByte); AddCase(CvtPkrtzF16F32SubnormalRoundsTowardZero); AddCase(CvtPkrtzF16F32SdwaAndOutputModifiers); AddCase(PackedMinMaxF16NanAndSignedZeroEdges); AddCase(VectorMinMaxF16Ops); AddCase(VectorCvtU16F16Sdwa); AddCase(NativeAndSdwa16BitDestinationWrites); AddCase(VectorMinMaxMed3F16Ops); AddCase(VectorSpecialF16Ops); AddCase(VectorFractF16CapturedAndEdges); AddCase(VectorFractF16Modifiers); AddCase(VectorCosF16CapturedSdwaAndEdges); AddCase(VectorSinF16SdwaAndEdges); AddCase(VectorWritelaneIgnoresExecMask); AddCase(VectorReadlaneFromInactiveWrittenLane); AddCase(VectorLaneWave32RuntimeSelectorWraps); AddCase(VectorPermlanex16); AddCase(VectorPermlane16FetchInactiveZero); AddCase(VectorPermlane16FetchInactiveFi); cases.push_back(VectorDpp8Captured(false)); cases.push_back(VectorDpp8Captured(true)); AddCase(VectorDppQuadPermuteReverse); AddCase(VectorDppRowXmask); AddCase(VectorDppBankMaskPreservesDestination); AddCase(VectorDppBoundsControlZeroPreservesDestination); AddCase(Vop3FmacF32NegatedSourceAccumulates); AddCase(Vop3LdexpSourceModifier); AddCase(Vop1MoveRelSource); AddCase(Vop1MoveRelDestination); AddCase(VectorFloatSpecialOps); AddCase(MadMixF16LiteralHalfSourceUsesOpsel); AddCase(MadMixF16NegHiIsAbsAndNegIsIndependent); AddCase(VectorVop3FmaF16UsesRdna2Opcode34b); AddCase(FloatInlineConstantF16UsesNumericValue); AddCase(VectorFloatControlContractPreservesInfNan); AddCase(VectorFloatArithmeticOps); AddCase(VectorMinMaxF32NanAndSignedZeroEdges); AddCase(VectorMed3F32NanUsesMin3Path); AddCase(VectorFloatConversionOps); AddCase(VectorFrexpF32Edges); AddCase(CvtF32ToIntSaturatesNaNAndOutOfRange); AddCase(VectorSpecialF32FlushesDenormalInputs); AddCase(VectorRcpIflagF32IntegerReciprocal); AddCase(VectorSinCosMaxFiniteSpecialCases); AddCase(VectorCompareOps); AddCase(VectorVop3CompareEqI64OnGpu); AddCase(VectorVop3CompareEqU64OnGpu); AddCase(VectorVop3CompareGtU64OnGpu); AddCase(VectorVopcCompareLtU64OnGpu); AddCase(VectorVop3CompareNeU64OnGpu); AddCase(VectorVopcCmpxNeU64CapturedExecMask); AddCase(VectorVop3CmpxNeI64CapturedExecMask); AddCase(VectorCompareClassF32); AddCase(VectorVopcSdwaCmpxClassF32CapturedExecMask); AddCase(VectorCompareF16Ops); AddCase(Wave32VccMasksPreserveOtherHalf); AddCase(Vop2SdwaCndmaskSourceModifier); AddCase(Vop2SdwaCndmaskCapturedByte3Source); AddCase(Vop3CndmaskUsesSgprMaskLaneBits); AddCase(Vop3CndmaskAllowsDataSourceModifier); AddCase(VectorCompareExecOps); AddCase(VectorVop3FloatCompareNegSourceModifier); AddCase(VectorVop3CmpxWritesExecMask); AddCase(VectorVopcSdwaCmpxWritesExecMask); AddCase(VectorVopcCmpxGtU16CapturedSdwaExecMask); AddCase(VectorVopcCmpxLtU16CapturedSdwaExecMask); AddCase(VectorVopcCmpNgtF16CapturedSdwaAndEdges); AddCase(VectorVopcCmpNltF16CapturedSdwaAndEdges); AddCase(VectorVopcCmpxNgtF16CapturedSdwaExecMask); AddCase(VectorCompareInvertedMaskSelect); AddCase(BranchSelect); AddCase(SimpleLoop); AddCase(SharedReturnKeepsSelectedValues); AddCase(BranchVccnzUsesWaveMask); AddCase(BranchVccnzUsesCarryProducedWaveMask); AddCase(ScalarMemoryLoadVariants); AddCase(ScalarLoadSignedImmediateOffsetAddsSoffset); AddCase(ScalarLoadAlignsComponentsAndMasksAddress); AddCase(BufferLoadStore); AddCase(BufferLoadDwordOffenIdxenUsesVaddrPlusOneOffset); AddCase(BufferStoreDwordOffenIdxenUsesVaddrPlusOneOffset); AddCase(BufferLoadDwordNoAddressFlagsIgnoresVaddr); AddCase(BufferLoadDwordIdxenUsesDescriptorStride); AddCase(BufferStoreDwordIdxenUsesDescriptorStride); AddCase(BufferStoreDwordAppliesHostOffset); AddCase(BufferOffsetsUsePackedLaneAndStorageFallback); AddCase(BufferLoadVariants); AddCase(BufferLoadDwordx2SnapshotsOverlappingAddress); AddCase(BufferLoadDwordx3SnapshotsOverlappingAddress); AddCase(BufferLoadDwordx4SnapshotsOverlappingAddress); AddCase(BufferLoadDwordx4ZeroesOnlyOutOfBoundsTail); AddCase(BufferStoreDwordx4DropsOnlyOutOfBoundsTail); AddCase(BufferLoadFormatXyzwRejectsPartialRecord); AddCase(BufferStoreFormatXyzwDropsPartialRecord); AddCase(BufferLoadFormatXChecksOnlyTransferredComponent); AddCase(BufferStoreFormatXChecksOnlyTransferredComponent); AddCase(BufferLoadFormatXyChecksOnlyTransferredComponents); AddCase(BufferStoreFormatXyChecksOnlyTransferredComponents); AddCase(BufferStoreVariants); AddCase(BufferFormatVariants); AddCase(BufferLoadFormatXyzwSnapshotsOverlappingAddress); AddCase(BufferLoadFormatXyzwResource32_32FloatAppliesXy01); AddCase(BufferLoadFormatXyzwResource323232FloatRepeatsSource); AddCase(BufferLoadFormatXyzwResource8_8_8_8UintAppliesWzy1); AddCase(BufferLoadFormatXyzwInactiveExecPreservesOverlappingAddress); AddCase(BufferFormatStoreVariants); AddCase(BufferStoreFormatXResource16UintWritesHalfword); AddCase(BufferStoreFormatXResource16UintPreservesAdjacentLanes); AddCase(BufferStoreFormatXyzwSnorm16CapturedSkinningVectors); AddCase(BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords); AddCase(BufferLoadFormatXResource8UintZeroExtendsByte); AddCase(BufferLoadFormatXyResource88UintExtractsBytes); AddCase(BufferLoadFormatXyResource8888UnormConvertsFirstTwoComponents); AddCase(BufferStoreFormatXyResource88UintWritesBytes); AddCase(BufferStoreFormatXyzResource3232UintWritesTwoDwords); AddCase(BufferStoreFormatXyzwResource323232UintWritesThreeDwords); AddCase(BufferStoreFormatXyResource32UintWritesOneDword); AddCase(BufferStoreFormatXyzResource8UintWritesOneByte); AddCase(BufferStoreFormatXAddTidUsesLaneIndex); AddCase(BufferStoreFormatXDropsOutOfRangeRecord); AddCase(TBufferLoadVariants); AddCase(TBufferLoadFormatXyzwSnapshotsOverlappingAddress); AddCase(TBufferLoadFormatXyzwPackedSnapshotsOverlappingAddress); AddCase(TBufferLoadFormatX8UintZeroExtendsByte); AddCase(TBufferLoadFormatX8888UintExtractsFirstByte); AddCase(TBufferLoadFormatXIdxenUsesDescriptorStride); AddCase(TBufferLoadFormatX16FloatConvertsToFloat); AddCase(TBufferStoreFormatX8UintWritesOneByte); AddCase(TBufferStoreFormatXSintWritesSubDword); AddCase(TBufferLoadFormatXSintSignExtendsSubDword); AddCase(TBufferLoadFormatXy1616IntegerComponents); AddCase(TBufferStoreFormatXy1616IntegerComponents); AddCase(TBufferLoadFormatXyz16161616UintLoadsHalfwords); AddCase(TBufferLoadFormatXy1616UnormConvertsToFloat); AddCase(TBufferLoadFormatXy88UnormConvertsToFloat); AddCase(TBufferLoadFormatXy88SnormConvertsToFloat); AddCase(TBufferLoadFormatXy8888UnormConvertsFirstTwoComponents); AddCase(TBufferLoadFormatXyzw8888UintExtractsBytes); AddCase(TBufferLoadFormatXyzw1010102SnormConvertsToFloat); AddCase(TBufferLoadFormatXyz111110FloatUnpacks); AddCase(TBufferLoadFormatXyzw3232FloatZerosMissingComponents); AddCase(TBufferStoreFormatXyzw3232FloatWritesOnlyPresentComponents); AddCase(TBufferStoreFormatXy88IntegerComponents); AddCase(TBufferLoadFormatXy88IntegerComponents); AddCase(TBufferStoreVariants); AddCase(FlatLoadVariants); AddCase(FlatSubdwordLoadsApplyByteOffset); AddCase(FlatVirtualAddressRebasesGuestAllocation); AddCase(GlobalSignedImmediateRebasesBeforeSaddr); AddCase(FlatSegmentIgnoresSaddrAndMasksOffsetMsb); AddCase(ScratchIsPrivatePerInvocation); AddCase(DsReadWriteVariants); AddCase(DsWriteB16D16HiCapturedUsesHighHalf); AddCase(DsReadU16D16CapturedPreservesHighHalf); AddCase(DsReadU16D16HiCapturedPreservesLowHalf); AddCase(DsAppendConsumeUsesEncodedLdsSelector); AddCase(DsAppendUsesEncodedGdsSelector); AddCase(DsGdsSubdwordAndAtomicWrites); AddCase(DsReadWrite2Variants); AddCase(DsWideReadSnapshotsOverlappingAddress); AddCase(DsReadWrite2EqualOffsetsUseData0); AddCase(DsWideLdsPartialBounds); AddCase(DsWideGdsPartialBounds); AddCase(DsAtomicNoReturnVariants); AddCase(DsAtomicReturnVariants); for (bool decrement : {false, true}) { for (bool gds : {false, true}) { cases.push_back(DsBoundedAtomicBoundaries(decrement, gds)); for (u32 wave_size : {32u, 64u}) { cases.push_back(DsBoundedAtomicContention(decrement, gds, wave_size)); } } } AddCase(DsMiscVariants); AddCase(DsFloatMinMaxUsesSeparateCompareOperand); AddCase(DsSwizzleInvalidSourceLaneZero); AddCase(DsBpermuteCapturedExecOffsetAndWrap); AddCase(DsBpermuteWave64UsesIndependentHalves); AddCase(Wave64CrossHalfLaneAndLds); AddCase(Wave64RawMasksAndScalarBranch); AddCase(Wave64PartialMultidimensionalWorkgroup); AddCase(Wave64AppendConsumeHighHalf); AddCase(BufferAtomicVariants); AddCase(BufferAtomicCmpSwapExactRaw); AddCase(BufferAtomicGlc0DoesNotReturnOldValue); AddCase(BufferAtomicFMinExactRawGlcModes); AddCase(BufferAtomicFMinSpecialValues); AddCase(BufferAtomicFMinContendedWorkgroup); AddCase(BufferAtomicFMaxExactRawGlcModes); AddCase(BufferAtomicFMaxSpecialValues); AddCase(BufferAtomicFMaxContendedWorkgroup); AddCase(ImageLoadVariants); AddCase(ImageLoadR32UintUsesIntegerSampledImage); AddCase(ImageLoadFmaskUsesNativeSampleMapping); AddCase(ImageLoadR32SintUsesSignedSampledImage); AddCase(ImageLoadPackedUintUnpacksAndSwizzles); AddCase(ImageSamplePackedUintConvertsSampleAndGather); AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs); AddCase(ImageLoad1DUsesScalarCoordinate); AddCase(ImageGather2DInstructionWith1DDescriptor); AddCase(ImageLoad1DArrayUsesLayerCoordinate); AddCase(ImageLoad1DArrayDescriptorUsesSelectedLayer); AddCase(ImageLoadMipUsesVaddr2Lod2D); AddCase(ImageLoadMipNsaUsesSelectedAddressVgprs); AddCase(ImageLoadA16UintCoordsOnGpu); AddCase(ImageGetResinfoDmaskWidthHeight); AddCase(ImageGetResinfoDmaskMipLevels); AddCase(ImageSampleAndGather); AddCase(ImageGatherLodApproximatesLevelZero); AddCase(ImageD16GatherPacksHalfPairs); AddCase(ImageSampleA16SamplerCoordsOnGpu); AddCase(ImageSampleOpcodeAliasUsesNormalCoords); AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu); AddCase(ImageSampleA16CompareBiasRdna2AddressOrder); AddCase(ImageGatherCompareOpcodes); AddCase(ImageStoreVariants); AddCase(ImageD16StoreUnpacksHalfPairs); AddCase(ImageStoreMipSelectsPpsa01340Descriptor); AddCase(ImageStoreRgbOneUsesInverseSwizzle); AddCase(ImageStoreDuplicateSelectorUsesInverseSwizzle); AddCase(ImageStoreBgraUsesInverseSwizzle); AddCase(ImageStoreYzwxUsesInverseSwizzle); AddCase(ImageStoreR32FloatUsesFormatlessStorageImage); AddCase(ImageStoreR32SintUsesRawUintView); AddCase(ImageStoreR32UintUsesFormatlessStorageImage); AddCase(ImageStorePackedUintSaturatesChannels); AddCase(ImageStorePackedUintHonorsSparseDmask); AddCase(ComputeTgSizeSgprUsesWaveMetadata); AddCase(ImageStoreAndAtomicShareTypedBinding); AddCase(ImageAtomicSwapReturnsPreviousTexel); AddCase(ImageStoreAndAtomicUseSeparateBindings); AddCase(ImageAtomicVariants); AddCase(ImageAtomicGlc0DoesNotReturnOldValue); AddCase(MultipleWorkitemsGlobalId); AddCase(DispatcherIrreducibleControlFlow); return cases; } std::vector MakeGraphicsCases() { return { GraphicsInterpolationExport(), GraphicsPositionWExport(), GraphicsPackedHalfCentroid(), GraphicsPackedHalfInputAlias(false), GraphicsPackedHalfInputAlias(true), GraphicsSmoothRawInputAlias(), GraphicsAncillaryLayer(false), GraphicsAncillaryLayer(true), GraphicsAncillarySampleId(), GraphicsFlatInterpolatorExport(), GraphicsDsAddtidScratchExport(), GraphicsDirectSgprPushConstantExport(), GraphicsInlineSrtScalarPromotionExport(), GraphicsNullVmExportDiscardsInactiveExec(), GraphicsMrt0OffVmExportDiscardsInactiveExec(), GraphicsFinalVmExportSupersedesEarlierVmMask(), GraphicsBranchPathFinalVmExportDiscardsInactiveExec(), }; } void CheckPs5GameExampleImageClearRuntimeShape() { const auto MakeCode = [] { std::vector code; AppendVop3(&code, 0x346u, 4, 8, InlineU32(6), Vgpr(0)); for (u32 i = 0; i < 4; i++) { code.push_back(EncodeVop1(0x01u, i, i + 4u)); } code.push_back(EncodeMubuf0(0x07u, 0, true, false)); code.push_back(EncodeMubuf1(0, 0, 4)); AppendEnd(&code); return code; }; std::array user_data{}; user_data[0] = 0x00010000u; user_data[1] = 16u << 16u; user_data[2] = 64; user_data[3] = (static_cast(Prospero::BufferFormat::k32_32_32_32UInt) << 12u) | 4u | (5u << 3u) | (6u << 6u) | (7u << 9u); std::fill(user_data.begin() + 4, user_data.end(), 0xff000000u); ShaderComputeInputInfo compute{}; compute.threads_num[0] = 64; compute.threads_num[1] = 1; compute.threads_num[2] = 1; compute.dispatch_threads_num[0] = 64; compute.dispatch_threads_num[1] = 1; compute.dispatch_threads_num[2] = 1; compute.group_id[0] = true; compute.dispatch_thread_dimensions = true; compute.wave_size = 64; compute.thread_ids_num = 1; compute.workgroup_register = 8; u32 scalar_clear = 0x40404040u; bool clean_scalar = false; const auto Compile = [&](const std::vector &code) { ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Compute; options.wave_size = 64; options.user_data_base = 0; options.user_data = user_data; options.input_info.compute = &compute; options.dump_ir = false; auto translated = ShaderRecompiler::TranslateProgram(code, options); auto resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program); ShaderRecompiler::IR::ResourceSnapshot resources; ShaderRecompiler::IR::ResourceSpecialization specialization; const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(code.data()), .userdata = &scalar_clear, .read_specialization_memory = clean_scalar ? +[](void *data, uint64_t address, uint32_t *word) { if (address != reinterpret_cast(data)) return false; *word = *static_cast(data); return true; } : nullptr, }; Require("Ps5GameExampleImageClear", "resource materialization", ShaderRecompiler::IR::MaterializeResources( resource_plan, runtime, resources, specialization), "runtime clear resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( std::move(translated), options, specialization); ValidateSpirv("Ps5GameExampleImageClear", result.spirv); return resources; }; const auto code = MakeCode(); auto positive = Compile(code); compute.stage.resources = positive; ShaderBufferResource descriptor{}; u32 packed_clear = 0; uint64_t size = 0; Require("Ps5GameExampleImageClear", "runtime shape", ResolveComputeBufferFill(compute, 64, 1, 1, 0x61u, descriptor, packed_clear, size) && descriptor.Base48() == 0x10000u && size == 64u * 16u && packed_clear == 0xff000000u, "exact Prospero runtime binding did not resolve to a complete clear"); auto wrong_index = code; wrong_index[0] = (wrong_index[0] & ~(0x3ffu << 16u)) | (0x347u << 16u); compute.stage.resources = Compile(wrong_index); Require("Ps5GameExampleImageClear", "add-before-shift address", !ResolveComputeBufferFill(compute, 64, 1, 1, 0x61u, descriptor, packed_clear, size), "V_ADD_LSHL was mistaken for V_LSHL_ADD contiguous coverage"); user_data[7] ^= 1u; compute.stage.resources = Compile(code); Require("Ps5GameExampleImageClear", "non-repeated clear", !ResolveComputeBufferFill(compute, 64, 1, 1, 0x61u, descriptor, packed_clear, size), "non-uniform uint4 data was replaced with a color clear"); compute.stage.resources = positive; compute.dispatch_threads_num[0] = 32; Require("Ps5GameExampleImageClear", "partial dispatch", !ResolveComputeBufferFill(compute, 32, 1, 1, 0x61u, descriptor, packed_clear, size), "partial buffer coverage was classified as a complete clear"); // Same descriptor/dispatch as GTA3, with the stored value encoded in the // shader. user_data = {0x00200000u, 4u << 16u, 64u, 0x14204u}; compute.workgroup_register = 4; compute.dispatch_thread_dimensions = false; std::vector scalar_code; AppendVop3(&scalar_code, 0x346u, 0, 4, InlineU32(6), Vgpr(0)); scalar_code.push_back(EncodeVop1(0x01u, 1, InlineU32(0))); scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false)); scalar_code.push_back(EncodeMubuf1(1, 0, 0)); AppendEnd(&scalar_code); auto scalar = Compile(scalar_code); compute.stage.resources = scalar; Require("Ps5GameExampleImageClear", "inline scalar fill", ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size) && descriptor.Base48() == 0x200000u && size == 256u && packed_clear == 0u, "GTA3's actual scalar store was not recognized from IR"); Require("Ps5GameExampleImageClear", "excess dispatch", !ResolveComputeBufferFill(compute, 2, 1, 1, 0x41u, descriptor, packed_clear, size), "excess invocation coverage was accepted as a fill"); auto alias = scalar.buffers.front(); compute.stage.resources.buffers.push_back(alias); Require("Ps5GameExampleImageClear", "aliased scalar input", !ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size), "a scalar read aliasing the destination was accepted as uniform"); compute.stage.resources = scalar; compute.stage.resources.buffers[0].dwords[3] = (static_cast(Prospero::BufferFormat::k32Float) << 12u) | 0x204u; Require("Ps5GameExampleImageClear", "wrong store format", !ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size), "a format that changes the stored value was accepted as a uint fill"); // A body change preserving every descriptor and dispatch field must change // the result. scalar_code[2] = EncodeVop1(0x01u, 1, InlineU32(7)); compute.stage.resources = Compile(scalar_code); Require("Ps5GameExampleImageClear", "changed store value", ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size) && packed_clear == 7u, "clear recognition ignored the shader's actual stored value"); scalar_code[2] = EncodeVop1(0x01u, 1, Vgpr(0)); compute.stage.resources = Compile(scalar_code); Require("Ps5GameExampleImageClear", "varying store value", !ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size), "a varying store with identical resources was treated as a fill"); const auto scalar_address = reinterpret_cast(&scalar_clear); user_data[4] = static_cast(scalar_address); user_data[5] = static_cast(scalar_address >> 32u) | (16u << 16u); user_data[6] = 1; user_data[7] = 0x4dfacu; compute.workgroup_register = 8; scalar_code.clear(); AppendVop3(&scalar_code, 0x346u, 0, 8, InlineU32(6), Vgpr(0)); scalar_code.push_back(EncodeSmem0(0x08u, 106, 2)); scalar_code.push_back(EncodeSmem1(0, 125)); scalar_code.push_back(EncodeVop1(0x01u, 1, 106)); scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false)); scalar_code.push_back(EncodeMubuf1(1, 0, 0)); AppendEnd(&scalar_code); clean_scalar = true; auto loaded = Compile(scalar_code); compute.stage.resources = loaded; Require("Ps5GameExampleImageClear", "scalar-loaded fill", ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size) && packed_clear == scalar_clear && descriptor.Base48() == 0x200000u, "GTA3's scalar input was confused with its destination"); clean_scalar = false; compute.stage.resources = Compile(scalar_code); Require("Ps5GameExampleImageClear", "unavailable clean scalar", !ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor, packed_clear, size), "a scalar without a clean reader was replaced by a fill"); std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear"); } void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) { using namespace ShaderRecompiler; const bool multiplied_stride = hs_stride == 112; const u32 local_address = multiplied_stride ? 18 : 21; const u32 control_point = multiplied_stride ? 30 : 34; const u32 relative_patch = multiplied_stride ? 28 : 32; const u32 output_address = multiplied_stride ? 31 : 35; const u32 input_address = multiplied_stride ? 25 : 29; std::array, 3> code; auto &local = code[0]; // The fused LS addresses one control point using its v3 ordinal. local.push_back(EncodeVop2(0x0b, local_address, 255u, 3)); local.push_back(ls_stride); local.push_back(EncodeDs0(0x0e, (3u << 8u) | 1u)); local.push_back(EncodeDs1Ex(0, 5, 2, local_address)); local.push_back(EncodeSop1(0x20, 0, 6)); local.push_back(0xffffffffu); // Shader footer is not executable after LS handoff. auto &control = code[1]; AppendVop3(&control, 0x365, 0, 193u, InlineU32(0)); AppendVop3(&control, 0x366, 0, 193u, Vgpr(0)); control.push_back(EncodeSopp(0x0a)); control.push_back(EncodeVopc(0xd1, 249u, 3)); control.push_back(EncodeVopcSdwa(0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 1)); AppendVop3(&control, 0x148, control_point, Vgpr(1), InlineU32(8), InlineU32(5)); AppendVop3(&control, 0x148, relative_patch, Vgpr(1), InlineU32(0), InlineU32(8)); if (multiplied_stride) { // The later patch writes 112-byte outputs but reads 108-byte LS records. // The second multiply overwrites packed v1; only memory consumers identify // which of the two independently computed addresses supplies each stride. control.push_back(EncodeVop2(0x0b, 0, 255u, control_point)); control.push_back(hs_stride); control.push_back(EncodeVop2(0x0b, 1, 255u, control_point)); control.push_back(ls_stride); AppendVop3(&control, 0x143, output_address, 255u, Vgpr(relative_patch), Vgpr(0)); control.push_back(3 * hs_stride); AppendVop3(&control, 0x143, input_address, 255u, Vgpr(relative_patch), Vgpr(1)); control.push_back(3 * ls_stride); } else { control.push_back(EncodeSopk(0x00, 106, 3 * hs_stride)); control.push_back(EncodeVop2(0x0b, 2, 249u, 1)); control.push_back(EncodeVop2Sdwa(106, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 1)); AppendVop3(&control, 0x346, output_address, Vgpr(control_point), InlineU32(7), Vgpr(2)); control.push_back(EncodeVop2(0x0b, input_address, 255u, control_point)); control.push_back(ls_stride); } control.push_back(EncodeDs0(0x37, (3u << 8u) | 1u)); control.push_back(EncodeDs1Ex(10, 0, 0, input_address)); control.push_back(EncodeMubuf0(0x1d, 28)); control.push_back(EncodeMubuf1(10, 2, output_address, 2)); // Keep the control-point-dependent value and the last written dword live. control.push_back(EncodeMubuf0(0x1c, hs_stride - 8)); control.push_back(EncodeMubuf1(0, 2, output_address, 2)); // The game reuses previously written data VGPRs for CP0-only ring addresses. AppendVMovLiteral(&control, 4, 0x42280000u); AppendVMovLiteral(&control, 5, 0x422c0000u); control.push_back(EncodeVopc(0xd4, InlineU32(1), control_point)); control.push_back(EncodeVop2(0x1a, 4, InlineU32(4), relative_patch)); control.push_back(EncodeVop2(0x1a, 6, InlineU32(6), relative_patch)); control.push_back(EncodeVop2(0x26, 5, 255u, 6)); control.push_back(0x7fc0); for (u32 i = 0; i < 4; i++) { AppendVMovLiteral(&control, i, std::bit_cast(static_cast(i + 1))); } control.push_back(EncodeMubuf0(0x1e)); control.push_back(EncodeMubuf1(0, 2, 4, 4)); for (u32 i = 0; i < 4; i++) { control.push_back(EncodeMubuf0(0x1c, i * 16)); control.push_back(EncodeMubuf1(i, 2, 5, 2)); } AppendEnd(&control); auto &evaluation = code[2]; AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7)); evaluation.push_back(3 * hs_stride); evaluation.push_back(EncodeMubuf0(0x0d, 28)); evaluation.push_back(EncodeMubuf1(10, 2, 24, 4)); evaluation.push_back(EncodeMubuf0(0x0d, 2 * hs_stride + 28)); evaluation.push_back(EncodeMubuf1(12, 2, 24, 4)); evaluation.push_back(EncodeExp0(0x0c, 0xf)); evaluation.push_back(EncodeExp1(5, 6, 10, 12)); AppendEnd(&evaluation); ShaderTessellationInputInfo tess{.input_control_points = 3, .output_control_points = 3, .domain = 1, .partitioning = 2, .output_topology = 2}; AnalyzeTessellationPrograms(local, control, tess); Require(name, "decoded interface", tess.ls_stride == ls_stride && tess.hs_stride == hs_stride, "captured LS and HS address arithmetic must produce distinct strides"); constexpr std::array stages{ShaderType::Local, ShaderType::TessellationControl, ShaderType::TessellationEvaluation}; for (u32 stage = 0; stage < stages.size(); stage++) { ShaderVertexInputInfo vertex; vertex.logical_stage = stages[stage]; vertex.tess = tess; CompileOptions options; options.stage = stages[stage]; options.input_info.vertex = &vertex; auto translated = TranslateProgram(code[stage], options); std::set local_offsets, evaluation_offsets, factor_offsets, patch_offsets; for (const auto *block : translated.program.blocks) { for (const auto &inst : *block) { if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute && inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute) { continue; } if (!inst.Arg(1).IsImmediate()) { continue; } switch (static_cast(inst.Arg(0).U32())) { case IR::TessellationAttribute::LocalOutput: local_offsets.insert(inst.Arg(1).U32()); break; case IR::TessellationAttribute::EvaluationInput: evaluation_offsets.insert(inst.Arg(1).U32()); break; case IR::TessellationAttribute::Factor: factor_offsets.insert(inst.Arg(1).U32()); break; case IR::TessellationAttribute::PatchOutput: patch_offsets.insert(inst.Arg(1).U32()); break; default: break; } } } Require(name, "separate DS offsets", stage != 0 || local_offsets == std::set{4, 12}, "DS_WRITE2 must preserve both independently encoded offsets"); Require(name, "offchip component addressing", stage != 2 || evaluation_offsets == std::set{28, 2 * hs_stride + 28}, "TES control point and component offsets must survive lowering"); Require(name, "triangle factor layout", stage != 1 || factor_offsets == std::set{0, 4, 8, 12}, "the four packed guest factors must retain their order"); Require(name, "predicated patch addresses", stage != 1 || patch_offsets == std::set{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0}, "CP0 patch stores must use the address assigned on their active predicate"); auto result = CompileProgram(std::move(translated), options, {}); ValidateSpirv(name, result.spirv); spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string assembly; Require(name, "disassembly", tools.Disassemble(result.spirv, &assembly), "failed to disassemble tessellation shader"); Require(name, "native local inputs", stage != 0 || (assembly.find("VertexIndex") != std::string::npos && assembly.find("InstanceIndex") != std::string::npos), "LS v2/v5 must read native vertex/instance indices"); Require(name, "control point lane", stage != 1 || (assembly.find("InvocationId") != std::string::npos && assembly.find("SubgroupLocalInvocationId") == std::string::npos), "HS lane masks must use control point invocation IDs"); Require(name, "control point barrier", stage != 1 || (assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos && assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos), "HS barriers must match GLSL450 control point synchronization semantics"); Require(name, "factor builtins", stage != 1 || (assembly.find("TessLevelOuter") != std::string::npos && assembly.find("TessLevelInner") != std::string::npos), "triangle factors require outer[4] and inner[2] Vulkan builtins"); Require(name, "native domain and winding", stage != 2 || (assembly.find("TessCoord") != std::string::npos && assembly.find("Triangles") != std::string::npos && assembly.find("SpacingFractionalOdd") != std::string::npos && assembly.find("VertexOrderCw") != std::string::npos), "TES must preserve guest triangle domain, partitioning, and winding"); } std::printf("[host] %-32s ok\n", name); } void CheckTessellationPrograms() { CheckTessellationProgram("TessellationShiftedStride", 124, 128); CheckTessellationProgram("TessellationMultipliedStride", 108, 112); } void CheckEmbeddedFetchVertexOffset() { const auto MakeFetch = [](std::initializer_list> adds, std::optional> late_add = {}, u32 accumulator_vgpr = 0, bool ngg_sad = false, bool local = false) { std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(0))); code.push_back(EncodeSmem0(0x02u, 20, 4)); code.push_back(EncodeSmem1(0)); const auto AppendOffsets = [&]() { for (const auto [sgpr, index_vgpr] : adds) { if (ngg_sad) { AppendVop3(&code, 0x15du, accumulator_vgpr, sgpr, InlineU32(0), Vgpr(index_vgpr)); } else { AppendVop3B(&code, 0x30fu, accumulator_vgpr, 0, sgpr, Vgpr(index_vgpr)); } } }; if (ngg_sad) { AppendOffsets(); } code.push_back(EncodeVop2(0x01u, 0, Vgpr(local ? 5 : 8), local ? 2 : 5)); if (!ngg_sad) { AppendOffsets(); } code.push_back(EncodeMubuf0(0x03u, 0, true)); code.push_back(EncodeMubuf1(9, 5, 0)); if (late_add.has_value()) { AppendVop3B(&code, 0x30fu, 0, 0, late_add->first, Vgpr(late_add->second)); } if (local) { AppendVMovU32(&code, 20, 0); code.push_back(EncodeDs0(0x0e, 1u << 8u)); code.push_back(EncodeDs1Ex(0, 10, 9, 20)); code.push_back(EncodeDs0(0x0e, (3u << 8u) | 2u)); code.push_back(EncodeDs1Ex(0, 12, 11, 20)); code.push_back(EncodeSop1(0x20, 0, 6)); code.push_back(0xffffffffu); } else { AppendEnd(&code); } return code; }; const auto Compile = [&](const char *name, const std::vector &code, u32 slot10, ShaderType stage = ShaderType::Vertex, Prospero::BufferFormat format = Prospero::BufferFormat::k32_32_32_32Float) { std::array user_data{}; user_data[10] = slot10; ShaderVertexInputInfo vertex; vertex.logical_stage = stage; vertex.tess = {.input_control_points = 3, .output_control_points = 3, .ls_stride = 124, .hs_stride = 128, .domain = 1, .partitioning = 2, .output_topology = 2}; vertex.fetch_embedded = true; vertex.fetch_buffer_reg = 0; vertex.fetch_attrib_reg = 2; vertex.resources_num = 1; vertex.resources[0].fields[3] = BufferFormat(format) << 12u | DstSel(4, 5, 6, 7); vertex.resources_dst[0].attr_id = 0; vertex.resources_dst[0].registers_num = 4; ShaderRecompiler::CompileOptions options; options.stage = stage; options.user_data_base = 8; options.user_data = user_data; options.input_info.vertex = &vertex; auto translated = ShaderRecompiler::TranslateProgram(code, options); auto resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program); ShaderRecompiler::IR::ResourceSnapshot resources; ShaderRecompiler::IR::ResourceSpecialization specialization; const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(code.data()), }; Require(name, "resource materialization", ShaderRecompiler::IR::MaterializeResources( resource_plan, runtime, resources, specialization), "embedded fetch resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( std::move(translated), options, specialization); Require(name, "fetch rewrite", result.program.info.vertex_fetch_components[0] == 4, "encoded fetch sequence was not recognized and rewritten"); return CompiledShader{std::move(result.spirv), std::move(result.program), std::move(resources), {}}; }; const auto Resolve = [](const CompiledShader &result, u32 index_offset) { ShaderVertexInputInfo vertex; vertex.fetch_embedded = true; ShaderRecompiler::IR::CompiledShaderInfo program{}; program.user_data_base = result.program.user_data_base; program.info.vertex_offset_sgpr = result.program.info.vertex_offset_sgpr; vertex.stage.program = &program; vertex.stage.resources = result.resources; return ResolveDrawOffsets(index_offset, vertex).first; }; const auto ResolveInstance = [](const CompiledShader &result) { ShaderVertexInputInfo vertex; vertex.fetch_embedded = true; ShaderRecompiler::IR::CompiledShaderInfo program{}; program.user_data_base = result.program.user_data_base; program.info.instance_offset_sgpr = result.program.info.instance_offset_sgpr; vertex.stage.program = &program; vertex.stage.resources = result.resources; return ResolveDrawOffsets(0, vertex).second; }; for (const auto format : {Prospero::BufferFormat::k32_32_32_32Float, Prospero::BufferFormat::k32_32_32_32UInt}) { const auto local = Compile("LocalEmbeddedFetch", MakeFetch({}, {}, 0, false, true), 0, ShaderType::Local, format); ValidateSpirv("LocalEmbeddedFetch", local.spirv); Require("LocalEmbeddedFetch", "live vertex attributes", std::ranges::any_of(local.program.info.inputs, [](const auto &input) { return input.kind == ShaderRecompiler::IR::StageInputKind::Parameter && input.location == 0; }), "LS indexed fetch must consume native vertex attributes"); if (format == Prospero::BufferFormat::k32_32_32_32UInt) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string assembly; Require("LocalEmbeddedFetch", "disassembly", tools.Disassemble(local.spirv, &assembly), "failed to disassemble unsigned LS fetch"); Require("LocalEmbeddedFetch", "unsigned input type", assembly.find("OpTypePointer Input %v4uint") != std::string::npos && assembly.find("OpConvertUToF") == std::string::npos, "unsigned LS attributes must preserve their native integer bits"); } } const auto valid = Compile("EmbeddedFetchVertexOffset", MakeFetch({{18, 0}}), 7); Require( "EmbeddedFetchVertexOffset", "parse", valid.program.info.vertex_offset_sgpr == 18 && Resolve(valid, 0) == 7 && Resolve(valid, 5) == 5, "canonical fetch offset or register index-offset precedence is wrong"); const auto ngg_code = MakeFetch({{18, 5}}, {}, 5, true); Require("EmbeddedFetchNggVertexOffset", "encoding", ngg_code[3] == 0xd55d0005u && ngg_code[4] == 0x04150012u, "test does not encode the PS5 V_SAD_U32 vertex-offset prolog"); const auto ngg = Compile("EmbeddedFetchNggVertexOffset", ngg_code, 8); Require("EmbeddedFetchNggVertexOffset", "parse", ngg.program.info.vertex_offset_sgpr == 18 && Resolve(ngg, 0) == 8 && Resolve(ngg, 5) == 5, "PS5 NGG vertex-index offset or register index-offset precedence is " "wrong"); const auto ngg_instance = Compile("EmbeddedFetchNggInstanceOffset", MakeFetch({{18, 8}}, {}, 8, true), 81); Require("EmbeddedFetchNggInstanceOffset", "parse", ngg_instance.program.info.vertex_offset_sgpr == -1 && ngg_instance.program.info.instance_offset_sgpr == 18 && ResolveInstance(ngg_instance) == 81, "PS5 NGG instance-index offset was not detected or resolved"); const auto instance_conflict = Compile( "EmbeddedFetchConflictingInstanceOffset", MakeFetch({{17, 8}, {18, 8}}, {}, 8, true), 81); const auto instance_malformed = Compile("EmbeddedFetchMalformedInstanceOffset", MakeFetch({{18, 7}}, {}, 7, true), 81); const auto instance_outside = Compile("EmbeddedFetchOutsideInstanceOffset", MakeFetch({{19, 8}}, {}, 8, true), 81); for (const auto *result : {&instance_conflict, &instance_malformed, &instance_outside}) { Require("EmbeddedFetchNggInstanceOffset", "fail closed", result->program.info.instance_offset_sgpr == -1 && ResolveInstance(*result) == 0, "conflicting, malformed, or out-of-window instance add was " "classified"); } const auto pointer = 0x5b7c5100u; const auto late = Compile("EmbeddedFetchLateOffset", MakeFetch({}, std::pair{18, 0}), pointer); const auto conflict = Compile("EmbeddedFetchConflictingOffset", MakeFetch({{17, 0}, {18, 0}}), pointer); const auto malformed = Compile("EmbeddedFetchMalformedOffset", MakeFetch({{18, 1}}), pointer); const auto outside = Compile("EmbeddedFetchOutsideOffset", MakeFetch({{19, 0}}), pointer); for (const auto *result : {&late, &conflict, &malformed, &outside}) { Require("EmbeddedFetchVertexOffset", "fail closed", result->program.info.vertex_offset_sgpr == -1 && Resolve(*result, 0) == 0, "non-prolog, conflicting, malformed, or out-of-window add was " "classified"); } std::printf("[host] %-32s ok\n", "EmbeddedFetchVertexOffset"); } [[noreturn]] void RunReverseRenderTargetDeathCase() { (void)TextureGetRenderTargetFormat(Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kSrgb, Prospero::ChannelOrder::kStandard); std::_Exit(0x7f); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS void CheckRenderTargetFormatContract() { const auto r8_uint = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k8, Prospero::ChannelType::kUInt, Prospero::ChannelOrder::kStandard); Require("RenderTargetFormat", "R8 uint", r8_uint.format == vk::Format::eR8Uint && r8_uint.bytes_per_element == 1u && r8_uint.export_mapping.IsIdentity(), "R8 uint render-target tuple was rejected"); const auto rgb565 = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k5_6_5, Prospero::ChannelType::kUNorm, Prospero::ChannelOrder::kStandard); Require("RenderTargetFormat", "RGB565 UNorm", rgb565.format == vk::Format::eB5G6R5UnormPack16 && rgb565.bytes_per_element == 2u && rgb565.export_mapping.IsIdentity(), "RGB565 UNorm render-target tuple was rejected"); const auto uint_format = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kUInt, Prospero::ChannelOrder::kStandard); Require("RenderTargetFormat", "RGBA16 uint", uint_format.format == vk::Format::eR16G16B16A16Uint && uint_format.bytes_per_element == 8u && uint_format.export_mapping.IsIdentity(), "RGBA16 uint render-target tuple was rejected"); const auto format = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kFloat, Prospero::ChannelOrder::kReversed); Require("ReverseRenderTarget", "exact format", format.format == vk::Format::eR16G16B16A16Sfloat && format.bytes_per_element == 8u && format.export_mapping == Prospero::ColorMappingAbgr, "exact reverse RGBA16F render-target tuple was rejected"); Require("ReverseRenderTarget", "write masks", format.export_mapping.ApplyMask(0x1u) == 0x8u && format.export_mapping.ApplyMask(0x2u) == 0x4u && format.export_mapping.ApplyMask(0x4u) == 0x2u && format.export_mapping.ApplyMask(0x8u) == 0x1u && format.export_mapping.ApplyMask(0xfu) == 0xfu, "reverse RGBA16F component mask was not mapped exactly once"); const auto rg32 = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k32_32, Prospero::ChannelType::kFloat, Prospero::ChannelOrder::kReversed); Require( "ReverseRenderTarget", "GR32 float", rg32.format == vk::Format::eR32G32Sfloat && rg32.bytes_per_element == 8u && rg32.export_mapping == Prospero::ColorMappingGr && rg32.export_mapping.ApplyMask(0x1u) == 0x2u && rg32.export_mapping.ApplyMask(0x2u) == 0x1u && rg32.export_mapping.ApplyMask(0x3u) == 0x3u && rg32.export_mapping.ApplyMask(0xfu) == 0xfu, "reverse GR32F render-target export or write-mask mapping is incorrect"); const auto argb = TextureGetRenderTargetFormat( Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kFloat, Prospero::ChannelOrder::kAltReversed); Require( "ReverseRenderTarget", "ARGB float", argb.format == vk::Format::eR16G16B16A16Sfloat && argb.export_mapping == Prospero::ColorMappingArgb && argb.export_mapping.ApplyMask(0x1u) == 0x2u && argb.export_mapping.ApplyMask(0x2u) == 0x4u && argb.export_mapping.ApplyMask(0x4u) == 0x8u && argb.export_mapping.ApplyMask(0x8u) == 0x1u, "alternate-reversed render-target mapping did not invert its ARGB cycle"); char path[MAX_PATH]{}; Require("ReverseRenderTarget", "host", GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed"); std::string command = std::string("\"") + path + "\" --reverse-rt-death"; std::vector mutable_command(command.begin(), command.end()); mutable_command.push_back('\0'); STARTUPINFOA startup{sizeof(startup)}; PROCESS_INFORMATION process{}; Require("ReverseRenderTarget", "host", CreateProcessA(nullptr, mutable_command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != 0, "CreateProcess failed"); Require("ReverseRenderTarget", "host", WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0, "unsupported adjacent render-target tuple timed out"); DWORD exit_code = 0; const bool exited = GetExitCodeProcess(process.hProcess, &exit_code) != 0; CloseHandle(process.hThread); CloseHandle(process.hProcess); Require("ReverseRenderTarget", "hard failure", exited && exit_code == 321, "invalid render-target tuple did not retain the fatal guard"); std::printf("[host] %-32s ok\n", "RenderTargetFormat"); } #endif [[noreturn]] void RunImageViewDeathCase(const char *kind) { if (std::strcmp(kind, "sampled-invalid-selector") == 0) { (void)SelectSampledColorView(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, DstSel(4, 5, 6, 2)); } else if (std::strcmp(kind, "sampled-incompatible-format") == 0) { (void)SelectSampledColorView(vk::Format::eR8Unorm, vk::Format::eR16Unorm, DstSel(4, 0, 0, 1)); } else if (std::strcmp(kind, "sampled-invalid-high") == 0) { (void)SelectSampledColorView(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, DstSel(7, 6, 5, 3)); } else if (std::strcmp(kind, "storage-incompatible-format") == 0) { ValidateStorageColorView(vk::Format::eR8G8B8A8Srgb, vk::Format::eR16G16B16A16Unorm, DstSel(4, 5, 6, 7)); } else if (std::strcmp(kind, "volume-mip-count") == 0 || std::strcmp(kind, "volume-slice-range") == 0) { VulkanHarness vulkan; auto &graphics = vulkan.RuntimeContext(); CommandScheduler scheduler(vulkan.RuntimeRenderer(), graphics); ImageInfo volume_info{}; volume_info.pixel_format = vk::Format::eR8G8B8A8Unorm; volume_info.guest_format = Prospero::BufferFormat::k8_8_8_8UNorm; volume_info.type = Prospero::ImageType::kColor3D; volume_info.extent = {8, 8, 4}; volume_info.resources = {2, 1}; volume_info.pitch = 8; volume_info.bytes_per_block = 4; volume_info.samples = 1; volume_info.tile_mode = Prospero::TileMode::kLinear; Libs::Graphics::Image volume(graphics, scheduler, volume_info); ImageViewInfo view{}; view.format = volume_info.pixel_format; view.type = vk::ImageViewType::e2DArray; view.aspect = vk::ImageAspectFlagBits::eColor; view.base_level = 1; view.level_count = 1; view.layer_count = 1; view.usage = vk::ImageUsageFlagBits::eSampled; if (std::strcmp(kind, "volume-mip-count") == 0) { view.base_level = 0; view.level_count = 2; } else { view.base_layer = 1; view.layer_count = 2; } (void)volume.FindView(view); } else { ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; resource.numeric_class = Prospero::TextureNumericClass::Float; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.written = true; if (std::strcmp(kind, "storage-kind") == 0) { resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; } else if (std::strcmp(kind, "storage-no-write") == 0) { resource.written = false; } else if (std::strcmp(kind, "storage-nonuint-atomic") == 0) { resource.atomic = true; } else if (std::strcmp(kind, "storage-compare") == 0) { resource.depth_compare = true; } else if (std::strcmp(kind, "storage-dimension") == 0) { resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Unknown; } else { std::_Exit(0x7e); } ValidateStorageImageResource(resource); } std::_Exit(0x7f); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS void CheckSampledColorViews() { Require("SampledColorViews", "identity", SelectSampledColorView(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, DstSel(4, 5, 6, 7)) == DstSel(4, 5, 6, 7), "RGBA did not select the identity view"); uint32_t valid_swizzles = 0; for (uint32_t swizzle = 0; swizzle <= 0xfffu; swizzle++) { bool expected = true; for (uint32_t channel = 0; channel < 4; channel++) { const auto selector = GetDstSel(swizzle, channel); if (selector == 2 || selector == 3) { expected = false; } } const bool valid = IsValidImageSwizzle(swizzle); const bool supported = IsSupportedSampledColorView( vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, swizzle); Require("SampledColorViews", "exhaustive read swizzle domain", valid == expected && supported == expected, "sampled swizzle validator disagreed with the PS5 selector domain"); valid_swizzles += valid; } Require("SampledColorViews", "all PS5 read swizzles", valid_swizzles == 1296 && !IsValidImageSwizzle(DstSel(4, 5, 6, 2)) && !IsValidImageSwizzle(DstSel(4, 5, 6, 3)) && !IsValidImageSwizzle(0x1000), "valid PS5 sampled mappings were rejected or reserved selectors were " "admitted"); const auto arbitrary = DstSel(5, 1, 7, 0); const auto components = TextureGetComponentMapping(arbitrary, {}); Require("SampledColorViews", "generic Vulkan component mapping", SelectSampledColorView(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, arbitrary) == arbitrary && components.r == vk::ComponentSwizzle::eG && components.g == vk::ComponentSwizzle::eOne && components.b == vk::ComponentSwizzle::eA && components.a == vk::ComponentSwizzle::eZero, "arbitrary valid sampled mapping did not use the generic view path"); Require("SampledColorViews", "R8 R001", SelectSampledColorView(vk::Format::eR8Unorm, vk::Format::eR8Unorm, DstSel(4, 0, 0, 1)) == DstSel(4, 0, 0, 1), "R8 did not select its R001 view"); Require("SampledColorViews", "R8 000R", SelectSampledColorView(vk::Format::eR8Unorm, vk::Format::eR8Unorm, DstSel(0, 0, 0, 4)) == DstSel(0, 0, 0, 4), "R8 did not select its 000R component-mapped view"); Require("SampledColorViews", "mutable R8 uint/unorm 000R", SelectSampledColorView(vk::Format::eR8Uint, vk::Format::eR8Unorm, DstSel(0, 0, 0, 4)) == DstSel(0, 0, 0, 4), "compatible R8 integer target sampled view was rejected"); Require("SampledColorViews", "R16G16 RG01", SelectSampledColorView(vk::Format::eR16G16Sfloat, vk::Format::eR16G16Sfloat, DstSel(4, 5, 0, 1)) == DstSel(4, 5, 0, 1), "R16G16 did not select its RG01 view"); Require("SampledColorViews", "alpha one", SelectSampledColorView(vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Unorm, DstSel(4, 5, 6, 1)) == DstSel(4, 5, 6, 1), "RGB1 did not select the alpha-one view"); Require("SampledColorViews", "mutable BGRA target", SelectSampledColorView(vk::Format::eB8G8R8A8Unorm, vk::Format::eR8G8B8A8Unorm, DstSel(6, 5, 4, 7)) == DstSel(6, 5, 4, 7), "BGRA target did not select the exact RGBA/BGRA mutable view"); Require( "SampledColorViews", "mutable sRGB view of UNORM BGRA target", SelectSampledColorView(vk::Format::eB8G8R8A8Unorm, vk::Format::eR8G8B8A8Srgb, DstSel(6, 5, 4, 7)) == DstSel(6, 5, 4, 7), "UNORM BGRA target did not select its compatible sRGB RGBA sampled view"); Require("SampledColorViews", "mutable sRGB BGRA target", SelectSampledColorView(vk::Format::eB8G8R8A8Srgb, vk::Format::eR8G8B8A8Srgb, DstSel(6, 5, 4, 7)) == DstSel(6, 5, 4, 7), "sRGB BGRA target did not select its matching mutable RGBA view"); constexpr auto colorspace_swizzle = DstSel(5, 1, 7, 0); Require("SampledColorViews", "mutable sRGB/UNORM views", SelectSampledColorView(vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm, DstSel(4, 5, 6, 7)) == DstSel(4, 5, 6, 7) && SelectSampledColorView(vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Srgb, colorspace_swizzle) == colorspace_swizzle, "same-order mutable sRGB/UNORM sampled views were rejected"); Require("SampledColorViews", "mutable packed RGB10 view", SelectSampledColorView(vk::Format::eA2R10G10B10UnormPack32, vk::Format::eA2B10G10R10UnormPack32, DstSel(6, 5, 4, 7)) == DstSel(6, 5, 4, 7), "packed RGB10 target did not select its matching mutable " "channel-order view"); Require( "SampledColorViews", "reverse RGBA16F sampled view", SelectSampledColorView(vk::Format::eR16G16B16A16Sfloat, vk::Format::eR16G16B16A16Sfloat, DstSel(7, 6, 5, 4)) == DstSel(7, 6, 5, 4), "reverse RGBA16F target did not select its reciprocal ABGR sampled view"); Require("SampledColorViews", "mutable integer-class views", SelectSampledColorView(vk::Format::eR16G16B16A16Sfloat, vk::Format::eR16G16B16A16Uint, DstSel(4, 5, 6, 7)) == DstSel(4, 5, 6, 7) && SelectSampledColorView( vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Uint, DstSel(4, 5, 6, 7)) == DstSel(4, 5, 6, 7) && SelectSampledColorView(vk::Format::eR8G8B8A8Uint, vk::Format::eR8G8B8A8Unorm, DstSel(4, 5, 6, 7)) == DstSel(4, 5, 6, 7), "compatible integer render-target sampled view was rejected"); Require("SampledColorViews", "invalid depth format", !IsSupportedSampledDepthView(vk::Format::eD24UnormS8Uint, vk::Format::eR8Unorm, DstSel(4, 4, 4, 4)), "incompatible sampled depth format was accepted"); Require("SampledColorViews", "invalid depth swizzle", !IsSupportedSampledDepthView(vk::Format::eD32SfloatS8Uint, vk::Format::eR32Sfloat, DstSel(4, 5, 6, 7)), "incompatible sampled depth swizzle was accepted"); Require("SampledColorViews", "D32 depth target", IsSupportedSampledDepthView(vk::Format::eD32SfloatS8Uint, vk::Format::eR32Sfloat, DstSel(4, 4, 4, 4)), "D32 depth target did not select its depth-aspect view"); Require("SampledColorViews", "D16 R000 depth target", IsSupportedSampledDepthView( vk::Format::eD16Unorm, vk::Format::eR16Unorm, DstSel(4, 0, 0, 0)), "D16 depth target did not select its R000 depth-aspect view"); Require("SampledColorViews", "D16S8 R001 depth target", IsSupportedSampledDepthView(vk::Format::eD16UnormS8Uint, vk::Format::eR16Unorm, DstSel(4, 0, 0, 1)), "D16S8 depth target did not select its R001 depth-aspect view"); Require("SampledColorViews", "promoted D24S8 R001 depth target", IsSupportedSampledDepthView(vk::Format::eD24UnormS8Uint, vk::Format::eR16Unorm, DstSel(4, 0, 0, 1)), "D24S8 host fallback did not preserve the guest R16 depth view"); Require("SampledColorViews", "promoted D32S8 R001 depth target", IsSupportedSampledDepthView(vk::Format::eD32SfloatS8Uint, vk::Format::eR16Unorm, DstSel(4, 0, 0, 1)), "D32S8 host fallback did not preserve the guest R16 depth view"); Require("SampledColorViews", "D32S8 R001 depth target", IsSupportedSampledDepthView(vk::Format::eD32SfloatS8Uint, vk::Format::eR32Sfloat, DstSel(4, 0, 0, 1)), "D32S8 depth target did not select its R001 depth-aspect view"); ShaderRecompiler::IR::ImageResource storage_resource{}; storage_resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; storage_resource.numeric_class = Prospero::TextureNumericClass::Float; storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; storage_resource.written = true; Require("SampledColorViews", "storage resource", IsSupportedStorageImageResource(storage_resource), "exact storage resource contract was rejected"); storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim3D; storage_resource.read = true; Require("SampledColorViews", "read-write 3D storage resource", IsSupportedStorageImageResource(storage_resource), "basic read-write 3D storage resource was rejected"); storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; storage_resource.read = false; Require("SampledColorViews", "write-only 2D-array storage resource", IsSupportedStorageImageResource(storage_resource), "basic write-only 2D-array storage resource was rejected"); storage_resource.numeric_class = Prospero::TextureNumericClass::Uint; Require("SampledColorViews", "write-only uint 2D-array storage resource", IsSupportedStorageImageResource(storage_resource), "basic write-only uint 2D-array storage resource was rejected"); storage_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; storage_resource.read = true; storage_resource.atomic = true; Require("SampledColorViews", "atomic uint 2D storage resource", IsSupportedStorageImageResource(storage_resource), "atomic uint storage resource was rejected"); storage_resource.atomic = false; storage_resource.mip_mode = ShaderRecompiler::IR::ImageMipMode::DynamicStorage; storage_resource.mip_count = 3; Require("SampledColorViews", "dynamic storage mip resource", IsSupportedStorageImageResource(storage_resource), "dynamic storage mip resource was rejected"); char path[MAX_PATH]{}; Require("SampledColorViews", "host", GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed"); for (const char *kind : {"sampled-invalid-selector", "sampled-incompatible-format", "sampled-invalid-high", "storage-incompatible-format", "storage-kind", "storage-no-write", "storage-nonuint-atomic", "storage-compare", "storage-dimension", "volume-mip-count", "volume-slice-range"}) { std::string command = std::string("\"") + path + "\" --image-view-death " + kind; std::vector mutable_command(command.begin(), command.end()); mutable_command.push_back('\0'); STARTUPINFOA startup{sizeof(startup)}; PROCESS_INFORMATION process{}; Require("SampledColorViews", "host", CreateProcessA(nullptr, mutable_command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != 0, "CreateProcess failed"); Require("SampledColorViews", "host", WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0, "unsupported view death case timed out"); DWORD exit_code = 0; const bool exited = GetExitCodeProcess(process.hProcess, &exit_code) != 0; CloseHandle(process.hThread); CloseHandle(process.hProcess); Require("SampledColorViews", "host", exited && exit_code == 321, std::string(kind) + " component mapping did not report a fatal error"); } std::printf("[host] %-32s ok\n", "SampledColorRenderTargetViews"); } #endif void CheckSampledDepthResource() { ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; resource.numeric_class = Prospero::TextureNumericClass::Float; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = true; resource.depth_compare = true; Require("SampledDepthResource", "comparison read", IsSupportedSampledDepthResource(resource), "basic comparison-sampled depth resource was rejected"); resource.depth_compare = false; Require("SampledDepthResource", "ordinary read", IsSupportedSampledDepthResource(resource), "basic non-comparison depth read was rejected"); const auto basic = resource; resource.read = false; Require("SampledDepthResource", "read required", !IsSupportedSampledDepthResource(resource), "non-reading depth resource was accepted"); resource = basic; resource.written = true; Require("SampledDepthResource", "write rejected", !IsSupportedSampledDepthResource(resource), "writable depth resource was accepted"); resource = basic; resource.atomic = true; Require("SampledDepthResource", "atomic rejected", !IsSupportedSampledDepthResource(resource), "atomic depth resource was accepted"); resource = basic; resource.mip_mode = ShaderRecompiler::IR::ImageMipMode::DynamicStorage; Require("SampledDepthResource", "dynamic mip rejected", !IsSupportedSampledDepthResource(resource), "dynamic-storage mip depth resource was accepted"); resource = basic; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; Require("SampledDepthResource", "singleton array accepted", IsSupportedSampledDepthResource(resource), "array depth resource was rejected"); resource = basic; resource.numeric_class = Prospero::TextureNumericClass::Uint; Require("SampledDepthResource", "integer read", IsSupportedSampledDepthResource(resource), "read-only uint depth resource was rejected"); resource.depth_compare = true; Require("SampledDepthResource", "integer comparison rejected", !IsSupportedSampledDepthResource(resource), "uint depth resource accepted comparison sampling"); Require("SampledDepthResource", "R32 uint depth view", IsSupportedSampledDepthFormat(vk::Format::eD32Sfloat, vk::Format::eR32Uint) && !IsSupportedSampledDepthFormat(vk::Format::eR32Uint, vk::Format::eR32Uint), "R32 uint depth-view compatibility diverged from its backing role"); std::printf("[host] %-32s ok\n", "SampledDepthResource"); } void CheckImageTransitionState(RenderContext &renderer) { constexpr const char *name = "ImageTransitionState"; auto &context = renderer.GetGraphics(); CommandScheduler scheduler(renderer, context); const auto MakeInfo = [](vk::Format format, uint32_t levels, uint32_t layers) { ImageInfo info{}; info.pixel_format = format; info.guest_format = Prospero::BufferFormat::k8UNorm; info.type = Prospero::ImageType::kColor2D; info.extent = {4, 4, 1}; info.resources = {levels, layers}; info.pitch = 4; info.bytes_per_block = 1; info.samples = 1; info.tile_mode = Prospero::TileMode::kLinear; return info; }; const auto graphics_stage = vk::PipelineStageFlagBits2::eAllGraphics | vk::PipelineStageFlagBits2::eComputeShader; constexpr uint64_t copy_capacity = 128ull << 20; Require( name, "buffered-copy capacity bands", ImageTestAccess::CopyRows(32ull << 10, 4096, copy_capacity) == 4096 && ImageTestAccess::CopyRows(32ull << 10, 4097, copy_capacity) == 4096 && ImageTestAccess::CopyRows(copy_capacity + 4, 1, copy_capacity) == 0, "buffered image copy does not split at the fixed scratch capacity"); Image image(context, scheduler, MakeInfo(vk::Format::eR8Unorm, 2, 3)); auto barriers = image.GetBarriers(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, graphics_stage, {}); Require( name, "initial full state", barriers.size() == 1 && barriers[0].srcStageMask == vk::PipelineStageFlagBits2::eAllCommands && barriers[0].srcAccessMask == vk::AccessFlagBits2::eNone && barriers[0].dstStageMask == graphics_stage && barriers[0].dstAccessMask == vk::AccessFlagBits2::eShaderRead && barriers[0].oldLayout == vk::ImageLayout::eUndefined && barriers[0].newLayout == vk::ImageLayout::eShaderReadOnlyOptimal && barriers[0].subresourceRange.aspectMask == vk::ImageAspectFlagBits::eColor && barriers[0].subresourceRange.levelCount == VK_REMAINING_MIP_LEVELS && barriers[0].subresourceRange.layerCount == VK_REMAINING_ARRAY_LAYERS && image.backing.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal && image.backing.state.access_mask == vk::AccessFlagBits2::eShaderRead && image.backing.state.pl_stage == graphics_stage, "initial transition did not record or retain pinned state"); Require(name, "repeat read elision", image .GetBarriers(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, graphics_stage, {}) .empty(), "identical read-only transition emitted a barrier"); barriers = image.GetBarriers(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, graphics_stage, {}); Require(name, "access-only transition", barriers.size() == 1 && barriers[0].oldLayout == barriers[0].newLayout && barriers[0].srcAccessMask == vk::AccessFlagBits2::eShaderRead, "same-layout access change was lost"); const auto CheckRepeatedWrite = [&](vk::AccessFlagBits2 access, const char *label) { image.backing.state = {}; image.backing.subresource_states.clear(); (void)image.GetBarriers(vk::ImageLayout::eGeneral, access, graphics_stage, {}); Require( name, label, image.GetBarriers(vk::ImageLayout::eGeneral, access, graphics_stage, {}) .size() == 1, "pinned repeated-write dependency was elided"); }; CheckRepeatedWrite(vk::AccessFlagBits2::eTransferWrite, "repeat transfer write"); CheckRepeatedWrite(vk::AccessFlagBits2::eShaderWrite, "repeat shader write"); CheckRepeatedWrite(vk::AccessFlagBits2::eMemoryWrite, "repeat memory write"); image.backing.state = {}; image.backing.subresource_states.clear(); const auto attachment_access = vk::AccessFlagBits2::eColorAttachmentRead | vk::AccessFlagBits2::eColorAttachmentWrite; (void)image.GetBarriers(vk::ImageLayout::eColorAttachmentOptimal, attachment_access, graphics_stage, {}); Require(name, "pinned attachment-write quirk", image .GetBarriers(vk::ImageLayout::eColorAttachmentOptimal, attachment_access, graphics_stage, {}) .empty(), "attachment writes diverged from the pinned repeated-write set"); image.backing.state = {}; image.backing.subresource_states.clear(); (void)image.GetBarriers(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, graphics_stage, {}); constexpr ImageSubresourceRange mip1_layer0{1, 1, 0, 1}; barriers = image.GetBarriers(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderWrite, graphics_stage, mip1_layer0); Require(name, "first partial", barriers.size() == 1 && barriers[0].subresourceRange.baseMipLevel == 1 && barriers[0].subresourceRange.baseArrayLayer == 0 && image.backing.subresource_states.size() == 6 && image.backing.subresource_states[3].layout == vk::ImageLayout::eGeneral && image.backing.subresource_states[3].access_mask == vk::AccessFlagBits2::eShaderWrite && image.backing.subresource_states[1].layout == vk::ImageLayout::eShaderReadOnlyOptimal, "partial state did not use mip-major indexing"); constexpr ImageSubresourceRange mip0_layer1{0, 1, 1, 1}; barriers = image.GetBarriers( vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, vk::PipelineStageFlagBits2::eTransfer, mip0_layer1); Require(name, "second partial", barriers.size() == 1 && barriers[0].subresourceRange.baseMipLevel == 0 && barriers[0].subresourceRange.baseArrayLayer == 1 && image.backing.subresource_states[1].layout == vk::ImageLayout::eTransferDstOptimal && image.backing.subresource_states[1].access_mask == vk::AccessFlagBits2::eTransferWrite && image.backing.subresource_states[3].layout == vk::ImageLayout::eGeneral, "second partial transition changed the wrong cell"); const ImageSubresourceRange explicit_full{0, 2, 0, 3}; barriers = image.GetBarriers(vk::ImageLayout::eShaderReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, graphics_stage, explicit_full); bool saw_mip0_layer1 = false; bool saw_mip1_layer0 = false; for (const auto &barrier : barriers) { saw_mip0_layer1 |= barrier.subresourceRange.baseMipLevel == 0 && barrier.subresourceRange.baseArrayLayer == 1 && barrier.oldLayout == vk::ImageLayout::eTransferDstOptimal && barrier.srcStageMask == vk::PipelineStageFlagBits2::eTransfer; saw_mip1_layer0 |= barrier.subresourceRange.baseMipLevel == 1 && barrier.subresourceRange.baseArrayLayer == 0 && barrier.oldLayout == vk::ImageLayout::eGeneral && barrier.srcAccessMask == vk::AccessFlagBits2::eShaderWrite; } Require(name, "partial normalization", barriers.size() == 2 && saw_mip0_layer1 && saw_mip1_layer0 && image.backing.subresource_states.empty() && image.backing.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal && image.backing.state.access_mask == vk::AccessFlagBits2::eShaderRead && image.backing.state.pl_stage == graphics_stage, "full transition did not reconcile and collapse split state"); barriers = image.GetBarriers(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, vk::PipelineStageFlagBits2::eTransfer, {}); Require(name, "normalized full transition", barriers.size() == 1 && barriers[0].subresourceRange.levelCount == VK_REMAINING_MIP_LEVELS && barriers[0].subresourceRange.layerCount == VK_REMAINING_ARRAY_LAYERS && barriers[0].srcStageMask == graphics_stage && barriers[0].dstStageMask == vk::PipelineStageFlagBits2::eTransfer, "normalized state did not return to one full barrier"); auto depth_info = MakeInfo(vk::Format::eD32SfloatS8Uint, 1, 1); depth_info.guest_format = Prospero::BufferFormat::k32Float; depth_info.bytes_per_block = 4; depth_info.tile_mode = Prospero::TileMode::kDepth; Image depth(context, scheduler, depth_info); barriers = depth.GetBarriers(vk::ImageLayout::eDepthStencilReadOnlyOptimal, vk::AccessFlagBits2::eShaderRead, graphics_stage, {}); Require(name, "depth stencil aspects", barriers.size() == 1 && barriers[0].subresourceRange.aspectMask == (vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil), "depth/stencil transition did not use the full image aspect"); HW::Context registers{}; HW::UserConfig user_config{}; HW::Shader shaders{}; scheduler.Begin(registers, user_config, shaders); image.backing.state = {}; image.backing.subresource_states.clear(); image.Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}, scheduler.Current().Handle()); Require(name, "mixed access stages", image.backing.state.pl_stage == (graphics_stage | vk::PipelineStageFlagBits2::eTransfer), "mixed shader/transfer access omitted a required pipeline stage"); auto source_info = depth_info; source_info.extent = {2, 2, 1}; source_info.pitch = 2; source_info.stencil = {0x1000, 4}; auto destination_info = depth_info; destination_info.resources.levels = 2; destination_info.stencil = {0x2000, 16}; Image source(context, scheduler, source_info); Image destination(context, scheduler, destination_info); StreamBuffer upload(context, scheduler, MemoryUsage::Upload, 4096); StreamBuffer download(context, scheduler, MemoryUsage::Download, 4096); const auto [upload_data, upload_offset] = upload.Map(128, 4); Require(name, "depth/stencil upload map", upload_data != nullptr, "combined depth/stencil upload allocation failed"); constexpr std::array expected_depth{0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u}; constexpr std::array expected_stencil{0x12, 0x34, 0x56, 0x78}; std::memcpy(upload_data, expected_depth.data(), sizeof(expected_depth)); std::memcpy(upload_data + 64, expected_stencil.data(), sizeof(expected_stencil)); upload.Commit(); std::array source_copies{}; source_copies[0].bufferOffset = upload_offset; source_copies[0].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, 0, 1}; source_copies[0].imageExtent = {2, 2, 1}; source_copies[1].bufferOffset = upload_offset + 64; source_copies[1].imageSubresource = {vk::ImageAspectFlagBits::eStencil, 0, 0, 1}; source_copies[1].imageExtent = {2, 2, 1}; source.Upload(source_copies, upload.Handle(), upload_offset, 128); destination.CopyMip(source, 1, 0); const auto [download_data, download_offset] = download.Map(128, 4); Require(name, "depth/stencil download map", download_data != nullptr, "combined depth/stencil download allocation failed"); download.Commit(); auto destination_copies = source_copies; destination_copies[0].bufferOffset = download_offset; destination_copies[0].imageSubresource.mipLevel = 1; destination_copies[1].bufferOffset = download_offset + 64; destination_copies[1].imageSubresource.mipLevel = 1; destination.Download(destination_copies, download.Handle(), download_offset, 128); auto buffered_info = MakeInfo(vk::Format::eR8Unorm, 1, 1); buffered_info.extent = {8, 4, 1}; buffered_info.pitch = 8; buffered_info.mip_layout[0] = {0, 32, 8, 4}; Image buffered_source(context, scheduler, buffered_info); Image buffered_destination(context, scheduler, buffered_info); constexpr std::array buffered_expected{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87, 0x98, 0xa9, 0xba, 0xcb, 0xdc, 0xed, 0xfe, 0x0f}; const auto [buffered_upload_data, buffered_upload_offset] = upload.Map(buffered_expected.size(), 4); Require(name, "buffered-copy upload map", buffered_upload_data != nullptr, "buffered-copy source allocation failed"); std::memcpy(buffered_upload_data, buffered_expected.data(), buffered_expected.size()); upload.Commit(); vk::BufferImageCopy buffered_region{}; buffered_region.bufferOffset = buffered_upload_offset; buffered_region.bufferRowLength = buffered_info.pitch; buffered_region.bufferImageHeight = buffered_info.extent.height; buffered_region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}; buffered_region.imageExtent = buffered_info.extent; buffered_source.Upload(std::span{&buffered_region, 1}, upload.Handle(), buffered_upload_offset, buffered_expected.size()); Buffer copy_scratch(context, scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16); buffered_destination.CopyImageWithBuffer(buffered_source, copy_scratch); const auto [buffered_download_data, buffered_download_offset] = download.Map(buffered_expected.size(), 4); Require(name, "buffered-copy download map", buffered_download_data != nullptr, "buffered-copy destination allocation failed"); download.Commit(); buffered_region.bufferOffset = buffered_download_offset; buffered_destination.Download(std::span{&buffered_region, 1}, download.Handle(), buffered_download_offset, buffered_expected.size()); scheduler.Finish(); download.Invalidate(download_offset, 128); download.Invalidate(buffered_download_offset, buffered_expected.size()); Require(name, "depth/stencil mip contents", std::memcmp(download_data, expected_depth.data(), sizeof(expected_depth)) == 0 && std::memcmp(download_data + 64, expected_stencil.data(), sizeof(expected_stencil)) == 0, "combined depth/stencil mip copy lost an aspect"); Require(name, "buffered-copy chunk contents", std::memcmp(buffered_download_data, buffered_expected.data(), buffered_expected.size()) == 0, "real buffered image copy lost a row across its scratch boundary"); std::printf("[host] %-32s ok\n", name); } void CheckDepthTextureEncoding() { const ShaderTextureResource disabled_sampler_tweaks{{ 0x05135600u, 0xc1600000u, 0x010dc1dfu, 0x91800924u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, }}; Require("DepthTextureEncoding", "disabled sampler tweaks", disabled_sampler_tweaks.PerfMod5() == 0 && IsSupportedDepthTextureEncoding(disabled_sampler_tweaks), "valid sampler modulation factor zero was rejected"); const ShaderTextureResource uncompressed_msaa{{ 0x00705d00u, 0xc1600000u, 0x010dc1dfu, 0xe1810924u, 0x00000000u, 0x00700010u, 0x00000000u, 0x00000000u, }}; Require("DepthTextureEncoding", "uncompressed 2x MSAA depth", IsSupportedDepthTextureEncoding(uncompressed_msaa), "valid uncompressed MSAA depth descriptor required an HTILE " "compatibility flag"); auto r128_msaa = uncompressed_msaa; std::fill(r128_msaa.fields + 4, r128_msaa.fields + 8, 0u); Require("DepthTextureEncoding", "R128 uncompressed 2x MSAA depth", IsSupportedDepthTextureEncoding(r128_msaa, true), "valid R128 MSAA depth descriptor required omitted dwords"); const ShaderTextureResource compressed_descriptor{{ 0x0104d500u, 0xc1600000u, 0x021bc3bfu, 0x91800924u, 0x00000000u, 0x00700000u, 0x80280000u, 0x000106d4u, }}; Require("DepthTextureEncoding", "compressed HTILE descriptor", IsSupportedDepthTextureEncoding(compressed_descriptor), "valid compressed depth descriptor required a prior depth target"); const ShaderTextureResource first_use_depth{{ 0x0225fc00u, 0x01600000u, 0x00000000u, 0x91800924u, 0x00000000u, 0x00700000u, 0x80280000u, 0x000225fdu, }}; Require("DepthTextureEncoding", "PPSA30490 first-use 1x1 depth", IsSupportedDepthTextureEncoding(first_use_depth), "captured comparison texture required tracked HTILE metadata"); auto missing_metadata = compressed_descriptor; missing_metadata.fields[6] &= 0x00ffffffu; missing_metadata.fields[7] = 0; auto out_of_range_metadata = compressed_descriptor; out_of_range_metadata.fields[7] = TRACKER_ADDRESS_SIZE >> 16u; auto unaligned_metadata = compressed_descriptor; unaligned_metadata.fields[6] ^= 1u << 24u; auto dcc_only_control = compressed_descriptor; dcc_only_control.fields[6] |= 1u << 22u; auto non_msaa_iterate_256 = compressed_descriptor; non_msaa_iterate_256.fields[6] |= 1u << 10u; Require( "DepthTextureEncoding", "invalid HTILE encoding", !IsSupportedDepthTextureEncoding(missing_metadata) && !IsSupportedDepthTextureEncoding(out_of_range_metadata) && !IsSupportedDepthTextureEncoding(unaligned_metadata) && !IsSupportedDepthTextureEncoding(dcc_only_control) && !IsSupportedDepthTextureEncoding(non_msaa_iterate_256), "compressed sampled depth accepted an unsupported address or control"); std::printf("[host] %-32s ok\n", "DepthTextureEncoding"); } ShaderRecompiler::IR::ImageResource BasicStorageTextureResource() { ShaderRecompiler::IR::ImageResource resource{}; resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; resource.numeric_class = Prospero::TextureNumericClass::Float; resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim3D; resource.read = true; resource.written = true; return resource; } ShaderTextureResource BasicStorageTextureDescriptor() { return {{0x00785d00u, 0x04700000u, 0x00080008u, 0xa1b00facu, 0x00000020u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource BasicLinearStorageTextureResource() { auto resource = BasicStorageTextureResource(); resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = false; return resource; } ShaderTextureResource BasicLinearStorageTextureDescriptor() { return {{0x04bcc401u, 0xc3800000u, 0x021bc3bfu, 0x900003acu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource BasicBgraStorageTextureResource() { auto resource = BasicStorageTextureResource(); resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; resource.read = false; return resource; } ShaderTextureResource BasicBgraStorageTextureDescriptor() { return {{0x007c6500u, 0xc3800000u, 0x010dc1dfu, 0x91b00f2eu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa06228R11G11B10StorageTextureDescriptor() { return {{0x10c6b500u, 0xc2400000u, 0x010dc1dfu, 0x91b003acu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa01530MaxMipStorageTextureDescriptor() { return {{0x04a42900u, 0xc3e00000u, 0x000fc00fu, 0x91b0022cu, 0x00000000u, 0x00700050u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa01340MipRangeStorageTextureDescriptor() { return {{0x0294dc00u, 0xc4700000u, 0x00b3c13fu, 0x91b31facu, 0x00000000u, 0x00700030u, 0xb07b0000u, 0x0002ac3cu}}; } ShaderTextureResource Ppsa01340OverwideMipStorageTextureDescriptor() { return {{0x0346c000u, 0xc4b00000u, 0x007fc07fu, 0x90990facu, 0x00000000u, 0x00700080u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource Ppsa01530MaxMipStorageTextureResource() { auto resource = BasicBgraStorageTextureResource(); resource.numeric_class = Prospero::TextureNumericClass::Uint; return resource; } ShaderTextureResource Ppsa02527R16FloatStorageTextureDescriptor() { return {{0x00ce3500u, 0xc0d00000u, 0x010dc1dfu, 0x91b00204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa02527R32FloatStorageTextureDescriptor() { return {{0x00cea900u, 0xc1600000u, 0x0086c0efu, 0x91b00204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa02527R8UnormStorageTextureDescriptor() { return {{0x00c7d500u, 0xc0100000u, 0x0086c0efu, 0x91b00204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource BasicYzwxStorageTextureDescriptor() { return {{0x00627801u, 0xc4d00000u, 0x0001c001u, 0x900009f5u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource BasicArrayStorageTextureResource() { auto resource = BasicLinearStorageTextureResource(); resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2DArray; return resource; } ShaderTextureResource BasicArrayStorageTextureDescriptor() { return {{0x20179000u, 0x03800000u, 0x00000000u, 0xd1b00f2eu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource BasicUintArrayStorageTextureResource() { auto resource = BasicArrayStorageTextureResource(); resource.numeric_class = Prospero::TextureNumericClass::Uint; return resource; } ShaderTextureResource BasicUintArrayStorageTextureDescriptor() { return {{0x20179200u, 0x01400000u, 0x00000000u, 0xd1b00204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Standard4KBUintArrayStorageTextureDescriptor() { return {{0x006c6600u, 0x01400000u, 0x00000000u, 0xd0500204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Standard64KBStorageTextureDescriptor() { return {{0x011fab00u, 0xc3800000u, 0x0003c003u, 0xd0900facu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource Ppsa14053DepthTileStorageTextureResource() { return BasicUintArrayStorageTextureResource(); } ShaderTextureResource Ppsa14053DepthTileStorageTextureDescriptor() { return {{0x20144c00u, 0x00500000u, 0x00000000u, 0xd1800204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderTextureResource Ppsa10112D16StorageTextureDescriptor() { return {{0x205b9000u, 0xc0700000u, 0x0021803bu, 0xd1800204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource BasicUintVolumeStorageTextureResource() { auto resource = BasicStorageTextureResource(); resource.numeric_class = Prospero::TextureNumericClass::Uint; resource.read = false; return resource; } ShaderTextureResource BasicUintVolumeStorageTextureDescriptor() { return {{0x20180600u, 0xc0b00000u, 0x0003c003u, 0xa0000004u, 0x0000000fu, 0x00700000u, 0x00000000u, 0x00000000u}}; } ShaderRecompiler::IR::ImageResource AtomicStorageTextureResource() { auto resource = BasicLinearStorageTextureResource(); resource.numeric_class = Prospero::TextureNumericClass::Uint; resource.read = true; resource.atomic = true; return resource; } ShaderTextureResource AtomicStorageTextureDescriptor() { return {{0x304bb700u, 0xc1400000u, 0x0000001fu, 0x91b00204u, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u}}; } [[noreturn]] void RunStorageTextureDescriptorDeathCase(const char *kind) { auto resource = BasicStorageTextureResource(); auto descriptor = BasicStorageTextureDescriptor(); if (std::strcmp(kind, "linear-rgb1-read") == 0) { resource = BasicLinearStorageTextureResource(); descriptor = BasicLinearStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "bgra-read") == 0) { resource = BasicBgraStorageTextureResource(); descriptor = BasicBgraStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "r16-float-read") == 0) { resource = BasicBgraStorageTextureResource(); descriptor = Ppsa02527R16FloatStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "r8-unorm-read") == 0) { resource = BasicBgraStorageTextureResource(); descriptor = Ppsa02527R8UnormStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "yzwx-read") == 0) { resource = BasicLinearStorageTextureResource(); descriptor = BasicYzwxStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "reserved-swizzle") == 0) { resource = BasicLinearStorageTextureResource(); descriptor = BasicLinearStorageTextureDescriptor(); descriptor.fields[3] = (descriptor.fields[3] & ~0xfffu) | DstSel(4, 5, 6, 2); } else if (std::strcmp(kind, "resource") == 0) { resource.written = false; } else if (std::strcmp(kind, "type") == 0) { descriptor.fields[3] = (descriptor.fields[3] & 0x0fffffffu) | (static_cast(Prospero::ImageType::kColor2D) << 28u); } else if (std::strcmp(kind, "standard256b-volume") == 0) { descriptor.fields[3] = (descriptor.fields[3] & ~(0x1fu << 20u)) | (static_cast(Prospero::TileMode::kStandard256B) << 20u); } else if (std::strcmp(kind, "inverted-mip-range") == 0) { descriptor.fields[3] |= 1u << 12u; descriptor.fields[5] |= 1u << 4u; } else if (std::strcmp(kind, "swizzle") == 0) { descriptor.fields[3] = (descriptor.fields[3] & ~0xfffu) | DstSel(4, 5, 6, 1); } else if (std::strcmp(kind, "array-base-out-of-range") == 0) { resource = BasicArrayStorageTextureResource(); descriptor = BasicArrayStorageTextureDescriptor(); descriptor.fields[4] |= 1u << 16u; } else if (std::strcmp(kind, "reserved") == 0) { descriptor.fields[1] |= 1u << 29u; } else if (std::strcmp(kind, "uint-format") == 0) { descriptor.fields[1] = (descriptor.fields[1] & ~0x1ff00000u) | (static_cast(Prospero::BufferFormat::k8_8_8_8UInt) << 20u); } else if (std::strcmp(kind, "uint-resource-float-format") == 0) { resource = BasicUintArrayStorageTextureResource(); descriptor = BasicArrayStorageTextureDescriptor(); } else if (std::strcmp(kind, "atomic-format") == 0) { resource = AtomicStorageTextureResource(); descriptor = AtomicStorageTextureDescriptor(); descriptor.fields[1] = (descriptor.fields[1] & ~0x1ff00000u) | (static_cast(Prospero::BufferFormat::k8UInt) << 20u); } else if (std::strcmp(kind, "depth-tile-read") == 0) { resource = Ppsa14053DepthTileStorageTextureResource(); descriptor = Ppsa14053DepthTileStorageTextureDescriptor(); resource.read = true; } else if (std::strcmp(kind, "depth-tile-extent") == 0) { resource = Ppsa14053DepthTileStorageTextureResource(); descriptor = Ppsa14053DepthTileStorageTextureDescriptor(); resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; descriptor.fields[3] = (descriptor.fields[3] & 0x0fffffffu) | (static_cast(Prospero::ImageType::kColor2D) << 28u); descriptor.fields[4] |= 1u; } else if (std::strcmp(kind, "depth-tile-fmask") == 0) { resource = BasicArrayStorageTextureResource(); descriptor = Ppsa10112D16StorageTextureDescriptor(); descriptor.fields[1] = (descriptor.fields[1] & ~(0x1ffu << 20u)) | (static_cast(Prospero::BufferFormat::kFmask8_S4_F4) << 20u); } else { std::_Exit(0x7e); } ValidateStorageTexture(resource, descriptor, 0x10000); std::_Exit(0x7f); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS void CheckBasicStorageTextureDescriptor() { const auto descriptor = BasicStorageTextureDescriptor(); Require("BasicStorageTexture", "descriptor", descriptor.Base40() == 0x785d0000ull && descriptor.Width5() + 1u == 33 && descriptor.Height5() + 1u == 33 && descriptor.Depth() + 1u == 33, "basic 3D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicStorageTextureResource(), descriptor, 0x10000); const ShaderTextureResource extended{{0x204aca00u, 0xc4700000u, 0x000fc00fu, 0xa1b00facu, 0x0000003fu, 0x00700000u, 0x006b0000u, 0x00204b0au}}; auto extended_resource = BasicStorageTextureResource(); extended_resource.read = false; Require("BasicStorageTexture", "extended descriptor", extended.fields[6] != 0 && extended.fields[7] != 0, "extended 3D storage descriptor fixture is malformed"); ValidateStorageTexture(extended_resource, extended, 0x400000); const auto linear = BasicLinearStorageTextureDescriptor(); Require("BasicStorageTexture", "linear descriptor", linear.Base40() == 0x4bcc40100ull && linear.Width5() + 1u == 3840 && linear.Height5() + 1u == 2160 && linear.Depth() + 1u == 1 && linear.Format() == Prospero::BufferFormat::k8_8_8_8UNorm && linear.TileMode() == Prospero::TileMode::kLinear, "PPSA07429 linear 2D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicLinearStorageTextureResource(), linear, 0x1fa4000); const auto bgra = BasicBgraStorageTextureDescriptor(); Require("BasicStorageTexture", "BGRA descriptor", bgra.Base40() == 0x7c650000ull && bgra.Width5() + 1u == 1920 && bgra.Height5() + 1u == 1080 && bgra.Depth() + 1u == 1 && bgra.Format() == Prospero::BufferFormat::k8_8_8_8UNorm && bgra.TileMode() == Prospero::TileMode::kRenderTarget && bgra.DstSelXYZW() == DstSel(6, 5, 4, 7), "PPSA02604 BGRA 2D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), bgra, 0x870000); const ShaderTextureResource r128{ {0x0202e500u, 0xc8200000u, 0x010dc1dfu, 0x91b00facu, 0, 0, 0, 0}}; auto r128_resource = BasicBgraStorageTextureResource(); r128_resource.r128 = true; Require("BasicStorageTexture", "PPSA01736 R128 descriptor", r128.Base40() == 0x202e50000ull && r128.Width5() + 1u == 1920 && r128.Height5() + 1u == 1080 && r128.Depth() + 1u == 1 && r128.Format() == Prospero::BufferFormat::k8_8_8_8Srgb && r128.TileMode() == Prospero::TileMode::kRenderTarget && r128.DstSelXYZW() == DstSel(4, 5, 6, 7), "PPSA01736 R128 storage descriptor fixture is malformed"); ValidateStorageTexture(r128_resource, r128, 0x870000); const auto r11g11b10 = Ppsa06228R11G11B10StorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA06228 R11G11B10 descriptor", r11g11b10.Base40() == 0x10c6b50000ull && r11g11b10.Width5() + 1u == 1920 && r11g11b10.Height5() + 1u == 1080 && r11g11b10.Depth() + 1u == 1 && r11g11b10.Format() == Prospero::BufferFormat::k11_11_10Float && r11g11b10.TileMode() == Prospero::TileMode::kRenderTarget && r11g11b10.DstSelXYZW() == DstSel(4, 5, 6, 1), "PPSA06228 R11G11B10 storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), r11g11b10, 0x870000); ValidateStorageColorView(vk::Format::eB8G8R8A8Unorm, vk::Format::eB10G11R11UfloatPack32, r11g11b10.DstSelXYZW()); const auto max_mip = Ppsa01530MaxMipStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA01530 max-mip descriptor", max_mip.Base40() == 0x4a4290000ull && max_mip.Width5() + 1u == 64 && max_mip.Height5() + 1u == 64 && max_mip.Depth() + 1u == 1 && max_mip.BaseLevel() == 0 && max_mip.LastLevel() == 0 && max_mip.MaxMip() == 5 && max_mip.Format() == Prospero::BufferFormat::k32_32UInt && max_mip.TileMode() == Prospero::TileMode::kRenderTarget && max_mip.DstSelXYZW() == DstSel(4, 5, 0, 1), "PPSA01530 max-mip storage descriptor fixture is malformed"); ValidateStorageTexture(Ppsa01530MaxMipStorageTextureResource(), max_mip, 0x20000); auto mip_one = max_mip; mip_one.fields[3] |= (1u << 12u) | (1u << 16u); Require("BasicStorageTexture", "PPSA01530 mip-one descriptor", mip_one.BaseLevel() == 1 && mip_one.LastLevel() == 1 && mip_one.MaxMip() == 5, "PPSA01530 mip-one storage descriptor fixture is malformed"); ValidateStorageTexture(Ppsa01530MaxMipStorageTextureResource(), mip_one, 0x20000); const auto mip_range = Ppsa01340MipRangeStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA01340 mip-range descriptor", mip_range.Base40() == 0x294dc0000ull && mip_range.Width5() + 1u == 1280 && mip_range.Height5() + 1u == 720 && mip_range.Depth() + 1u == 1 && mip_range.BaseLevel() == 1 && mip_range.LastLevel() == 3 && mip_range.MaxMip() == 3 && mip_range.Format() == Prospero::BufferFormat::k16_16_16_16Float && mip_range.TileMode() == Prospero::TileMode::kRenderTarget && mip_range.DstSelXYZW() == DstSel(4, 5, 6, 7), "PPSA01340 mip-range storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), mip_range, 0xa30000); const auto overwide_mip = Ppsa01340OverwideMipStorageTextureDescriptor(); Require( "BasicStorageTexture", "PPSA01340 over-wide mip view", overwide_mip.Base40() == 0x346c00000ull && overwide_mip.Width5() + 1u == 512 && overwide_mip.Height5() + 1u == 512 && overwide_mip.BaseLevel() == 0 && overwide_mip.LastLevel() == 9 && overwide_mip.MaxMip() == 8 && overwide_mip.Format() == Prospero::BufferFormat::k32_32_32_32UInt && overwide_mip.TileMode() == Prospero::TileMode::kStandard64KB, "captured over-wide storage mip view is malformed"); TileSizeAlign overwide_size{}; TileGetTextureTotalSize(overwide_mip.Format(), overwide_mip.Width5() + 1u, overwide_mip.Height5() + 1u, 1, overwide_mip.MaxMip() + 1u, overwide_mip.TileMode(), false, overwide_size); Require("BasicStorageTexture", "PPSA01340 over-wide mip footprint", overwide_size.size == 0x560000 && overwide_size.align == 0x10000, "captured nine-level Standard64KB footprint changed"); ValidateStorageTexture(Ppsa01530MaxMipStorageTextureResource(), overwide_mip, overwide_size.size); const auto r16_float = Ppsa02527R16FloatStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA02527 R16F descriptor", r16_float.Base40() == 0xce350000ull && r16_float.Width5() + 1u == 1920 && r16_float.Height5() + 1u == 1080 && r16_float.Depth() + 1u == 1 && r16_float.Format() == Prospero::BufferFormat::k16Float && r16_float.TileMode() == Prospero::TileMode::kRenderTarget && r16_float.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA02527 R16F 2D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), r16_float, 0x480000); const auto r32_float = Ppsa02527R32FloatStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA02527 R32F descriptor", r32_float.Base40() == 0xcea90000ull && r32_float.Width5() + 1u == 960 && r32_float.Height5() + 1u == 540 && r32_float.Depth() + 1u == 1 && r32_float.Format() == Prospero::BufferFormat::k32Float && r32_float.TileMode() == Prospero::TileMode::kRenderTarget && r32_float.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA02527 R32F 2D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), r32_float, 0x280000); const auto r8_unorm = Ppsa02527R8UnormStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA02527 R8 UNORM descriptor", r8_unorm.Base40() == 0xc7d50000ull && r8_unorm.Width5() + 1u == 960 && r8_unorm.Height5() + 1u == 540 && r8_unorm.Depth() + 1u == 1 && r8_unorm.Format() == Prospero::BufferFormat::k8UNorm && r8_unorm.TileMode() == Prospero::TileMode::kRenderTarget && r8_unorm.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA02527 R8 UNORM 2D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), r8_unorm, 0xc0000); const auto yzwx = BasicYzwxStorageTextureDescriptor(); Require("BasicStorageTexture", "YZWX descriptor", yzwx.Base40() == 0x62780100ull && yzwx.Width5() + 1u == 8 && yzwx.Height5() + 1u == 8 && yzwx.Depth() + 1u == 1 && yzwx.Format() == Prospero::BufferFormat::k32_32_32_32Float && yzwx.TileMode() == Prospero::TileMode::kLinear && yzwx.DstSelXYZW() == DstSel(5, 6, 7, 4), "PPSA04181 linear YZWX storage descriptor fixture is malformed"); ValidateStorageTexture(BasicLinearStorageTextureResource(), yzwx, 0x800); auto all_swizzles = yzwx; all_swizzles.fields[1] = (all_swizzles.fields[1] & ~0x1ff00000u) | (static_cast(Prospero::BufferFormat::k16_16_16_16Float) << 20u); uint32_t valid_storage_swizzles = 0; for (uint32_t swizzle = 0; swizzle <= 0xfffu; swizzle++) { if (!IsValidImageSwizzle(swizzle)) { continue; } all_swizzles.fields[3] = (all_swizzles.fields[3] & ~0xfffu) | swizzle; ValidateStorageTexture(BasicLinearStorageTextureResource(), all_swizzles, 0x800); ValidateStorageColorView(vk::Format::eR16G16B16A16Sfloat, vk::Format::eR16G16B16A16Sfloat, swizzle); ValidateStorageColorView(vk::Format::eR8G8B8A8Srgb, vk::Format::eR8G8B8A8Unorm, swizzle); ValidateStorageColorView(vk::Format::eB8G8R8A8Srgb, vk::Format::eR8G8B8A8Unorm, swizzle); valid_storage_swizzles++; } Require("BasicStorageTexture", "all write swizzles", valid_storage_swizzles == 1296, "valid write-only storage image mappings were rejected"); const ShaderTextureResource cube{{0x025bca00u, 0xc4700000u, 0x003fc03fu, 0xb1b00facu, 0x00000005u, 0x00700080u, 0x00000000u, 0x00000000u}}; auto cube_resource = BasicArrayStorageTextureResource(); cube_resource.cube = true; Require("BasicStorageTexture", "PPSA07429 cube descriptor", cube.Base40() == 0x25bca0000ull && cube.Width5() + 1u == 256 && cube.Height5() + 1u == 256 && cube.Depth() + 1u == 6 && cube.BaseArray5() == 0 && cube.MaxMip() == 8 && cube.Type() == Prospero::ImageType::kCube && cube.Format() == Prospero::BufferFormat::k16_16_16_16Float && cube.TileMode() == Prospero::TileMode::kRenderTarget && cube.DstSelXYZW() == DstSel(4, 5, 6, 7), "PPSA07429 cube storage descriptor fixture is malformed"); ValidateStorageTexture(cube_resource, cube, 0x420000); const auto array = BasicArrayStorageTextureDescriptor(); Require("BasicStorageTexture", "2D-array descriptor", array.Base40() == 0x2017900000ull && array.Width5() + 1u == 1 && array.Height5() + 1u == 1 && array.Depth() + 1u == 1 && array.BaseArray5() == 0 && array.Type() == Prospero::ImageType::kColor2DArray && array.Format() == Prospero::BufferFormat::k8_8_8_8UNorm && array.TileMode() == Prospero::TileMode::kRenderTarget && array.DstSelXYZW() == DstSel(6, 5, 4, 7), "PPSA21268 2D-array storage descriptor fixture is malformed"); ValidateStorageTexture(BasicArrayStorageTextureResource(), array, 0x10000); const ShaderTextureResource mip_array{{0x20268d00u, 0xc4700000u, 0x001fc01fu, 0xd1b11facu, 0x00000000u, 0x00700070u, 0x00000000u, 0x00000000u}}; Require("BasicStorageTexture", "PPSA14457 mip-one 2D-array descriptor", mip_array.BaseLevel() == 1 && mip_array.LastLevel() == 1 && mip_array.MaxMip() == 7 && mip_array.Type() == Prospero::ImageType::kColor2DArray && mip_array.Depth() == 0 && mip_array.BaseArray5() == 0, "PPSA14457 mip-one 2D-array storage descriptor fixture is malformed"); ValidateStorageTexture(BasicArrayStorageTextureResource(), mip_array, 0x30000); const auto uint_array = BasicUintArrayStorageTextureDescriptor(); Require("BasicStorageTexture", "uint 2D-array descriptor", uint_array.Base40() == 0x2017920000ull && uint_array.Width5() + 1u == 1 && uint_array.Height5() + 1u == 1 && uint_array.Depth() + 1u == 1 && uint_array.BaseArray5() == 0 && uint_array.Type() == Prospero::ImageType::kColor2DArray && uint_array.Format() == Prospero::BufferFormat::k32UInt && uint_array.TileMode() == Prospero::TileMode::kRenderTarget && uint_array.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA21268 uint 2D-array storage descriptor fixture is malformed"); ValidateStorageTexture(BasicUintArrayStorageTextureResource(), uint_array, 0x10000); const ShaderTextureResource standard256b{{ 0x204e4900u, 0x43c00000u, 0x00004000u, 0x90100facu, 0x00000000u, 0x00700000u, 0x00000000u, 0x00000000u, }}; auto standard256b_resource = BasicBgraStorageTextureResource(); standard256b_resource.numeric_class = Prospero::TextureNumericClass::Uint; TileSizeAlign standard256b_size{}; TileGetTextureTotalSize(standard256b.Format(), standard256b.Width5() + 1u, standard256b.Height5() + 1u, standard256b.Depth() + 1u, 1, standard256b.TileMode(), false, standard256b_size); Require("BasicStorageTexture", "Standard256B uint 2D descriptor", standard256b.Width5() + 1u == 2 && standard256b.Height5() + 1u == 2 && standard256b.Depth() + 1u == 1 && standard256b.Type() == Prospero::ImageType::kColor2D && standard256b.Format() == Prospero::BufferFormat::k8_8_8_8UInt && standard256b.TileMode() == Prospero::TileMode::kStandard256B && standard256b.DstSelXYZW() == DstSel(4, 5, 6, 7) && standard256b_size.size == 0x100 && standard256b_size.align == 0x100, "captured PPSA08511 Standard256B storage descriptor is malformed"); ValidateStorageTexture(standard256b_resource, standard256b, standard256b_size.size); const auto standard4kb_array = Standard4KBUintArrayStorageTextureDescriptor(); TileSizeAlign standard4kb_size{}; TileGetTextureTotalSize(standard4kb_array.Format(), 1, 1, 1, 1, standard4kb_array.TileMode(), false, standard4kb_size); Require( "BasicStorageTexture", "Standard4KB uint 2D-array descriptor", standard4kb_array.Type() == Prospero::ImageType::kColor2DArray && standard4kb_array.Format() == Prospero::BufferFormat::k32UInt && standard4kb_array.TileMode() == Prospero::TileMode::kStandard4KB && standard4kb_array.DstSelXYZW() == DstSel(4, 0, 0, 1) && standard4kb_size.size == 0x1000 && standard4kb_size.align == 0x1000, "captured Standard4KB uint 2D-array descriptor is malformed"); ValidateStorageTexture(BasicUintArrayStorageTextureResource(), standard4kb_array, standard4kb_size.size); auto based_standard4kb_array = standard4kb_array; based_standard4kb_array.fields[0] = 0x006c6800u; based_standard4kb_array.fields[4] = 0x00010001u; TileSizeAlign based_standard4kb_size{}; TileGetTextureTotalSize(based_standard4kb_array.Format(), 1, 1, based_standard4kb_array.Depth() + 1u, 1, based_standard4kb_array.TileMode(), false, based_standard4kb_size); Require("BasicStorageTexture", "based Standard4KB array view", based_standard4kb_array.Base40() == 0x6c680000ull && based_standard4kb_array.BaseArray5() == 1 && based_standard4kb_array.Depth() == 1 && based_standard4kb_size.size == 0x2000 && based_standard4kb_size.align == 0x1000, "captured based Standard4KB array view is malformed"); ValidateStorageTexture(BasicUintArrayStorageTextureResource(), based_standard4kb_array, based_standard4kb_size.size); const auto standard64kb = Standard64KBStorageTextureDescriptor(); TileSizeAlign standard64kb_size{}; TileGetTextureTotalSize(standard64kb.Format(), standard64kb.Width5() + 1u, standard64kb.Height5() + 1u, standard64kb.Depth() + 1u, 1, standard64kb.TileMode(), false, standard64kb_size); Require("BasicStorageTexture", "Standard64KB 2D descriptor", standard64kb.Type() == Prospero::ImageType::kColor2DArray && standard64kb.Format() == Prospero::BufferFormat::k8_8_8_8UNorm && standard64kb.TileMode() == Prospero::TileMode::kStandard64KB && standard64kb.DstSelXYZW() == DstSel(4, 5, 6, 7) && standard64kb_size.size == 0x10000 && standard64kb_size.align == 0x10000, "captured Standard64KB storage descriptor is malformed"); ValidateStorageTexture(BasicBgraStorageTextureResource(), standard64kb, standard64kb_size.size); const auto uint_volume = BasicUintVolumeStorageTextureDescriptor(); Require("BasicStorageTexture", "uint 3D descriptor", uint_volume.Base40() == 0x2018060000ull && uint_volume.Width5() + 1u == 16 && uint_volume.Height5() + 1u == 16 && uint_volume.Depth() + 1u == 16 && uint_volume.BaseArray5() == 0 && uint_volume.Type() == Prospero::ImageType::kColor3D && uint_volume.Format() == Prospero::BufferFormat::k16UInt && uint_volume.TileMode() == Prospero::TileMode::kLinear && uint_volume.DstSelXYZW() == DstSel(4, 0, 0, 0), "PPSA21268 uint 3D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicUintVolumeStorageTextureResource(), uint_volume, 0x10000); const ShaderTextureResource tiled_uint_volume{ {0x1ac0e530u, 0xc0b00000u, 0x0003c003u, 0xa0500004u, 0x0000000fu, 0x00700000u, 0x00000000u, 0x00000000u}}; Require("BasicStorageTexture", "tiled uint 3D descriptor", tiled_uint_volume.Width5() + 1u == 16 && tiled_uint_volume.Height5() + 1u == 16 && tiled_uint_volume.Depth() + 1u == 16 && tiled_uint_volume.Type() == Prospero::ImageType::kColor3D && tiled_uint_volume.TileMode() == Prospero::TileMode::kStandard4KB, "captured tiled uint 3D storage descriptor fixture is malformed"); ValidateStorageTexture(BasicUintVolumeStorageTextureResource(), tiled_uint_volume, 0x2000); const auto depth_tile = Ppsa14053DepthTileStorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA14053 depth-tile descriptor", depth_tile.Base40() == 0x20144c0000ull && depth_tile.Width5() + 1u == 1 && depth_tile.Height5() + 1u == 1 && depth_tile.Depth() + 1u == 1 && depth_tile.BaseArray5() == 0 && depth_tile.Type() == Prospero::ImageType::kColor2DArray && depth_tile.Format() == Prospero::BufferFormat::k8UInt && depth_tile.TileMode() == Prospero::TileMode::kDepth && depth_tile.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA14053 write-only depth-tile storage descriptor fixture is " "malformed"); ValidateStorageTexture(Ppsa14053DepthTileStorageTextureResource(), depth_tile, 0x10000); const auto d16_depth_tile = Ppsa10112D16StorageTextureDescriptor(); Require("BasicStorageTexture", "PPSA10112 D16 depth-tile descriptor", d16_depth_tile.Base40() == 0x205b900000ull && d16_depth_tile.Width5() + 1u == 240 && d16_depth_tile.Height5() + 1u == 135 && d16_depth_tile.Depth() + 1u == 1 && d16_depth_tile.Type() == Prospero::ImageType::kColor2DArray && d16_depth_tile.Format() == Prospero::BufferFormat::k16UNorm && d16_depth_tile.TileMode() == Prospero::TileMode::kDepth && d16_depth_tile.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA10112 writable D16 depth-plane descriptor fixture is malformed"); const auto d16_pitch = TileGetTexturePitch(d16_depth_tile.Format(), d16_depth_tile.Width5() + 1u, d16_depth_tile.TileMode()); TileSizeAlign d16_size{}; TileGetTextureTotalSize(d16_depth_tile.Format(), d16_depth_tile.Width5() + 1u, d16_depth_tile.Height5() + 1u, d16_depth_tile.Depth() + 1u, 1, d16_depth_tile.TileMode(), false, d16_size); Require("BasicStorageTexture", "PPSA10112 D16 depth-tile footprint", d16_pitch == 256 && d16_size.size == 0x20000 && d16_size.align == 0x10000, "PPSA10112 writable D16 depth-plane footprint is incorrect"); ValidateStorageTexture(BasicArrayStorageTextureResource(), d16_depth_tile, d16_size.size); auto depth_tile_r32 = depth_tile; depth_tile_r32.fields[1] = (depth_tile_r32.fields[1] & ~(0x1ffu << 20u)) | (static_cast(Prospero::BufferFormat::k32UInt) << 20u); depth_tile_r32.fields[3] = (depth_tile_r32.fields[3] & ~(0xfu << 28u)) | (static_cast(Prospero::ImageType::kColor2D) << 28u); auto depth_tile_r32_resource = Ppsa14053DepthTileStorageTextureResource(); depth_tile_r32_resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; ValidateStorageTexture(depth_tile_r32_resource, depth_tile_r32, 0x10000); Require("BasicStorageTexture", "R32_UINT replicated write mapping", IsValidImageSwizzle(DstSel(4, 4, 4, 4)), "single-channel replicated destination selection was rejected"); const auto atomic = AtomicStorageTextureDescriptor(); Require("BasicStorageTexture", "atomic R32_UINT descriptor", atomic.Width5() + 1u == 128 && atomic.Height5() + 1u == 1 && atomic.Depth() + 1u == 1 && atomic.Type() == Prospero::ImageType::kColor2D && atomic.Format() == Prospero::BufferFormat::k32UInt && atomic.DstSelXYZW() == DstSel(4, 0, 0, 1), "PPSA22102 image-atomic descriptor fixture is malformed"); ValidateStorageTexture(AtomicStorageTextureResource(), atomic, 0x10000); char path[MAX_PATH]{}; Require("BasicStorageTexture", "host", GetModuleFileNameA(nullptr, path, MAX_PATH) != 0, "GetModuleFileName failed"); for (const char *kind : {"resource", "type", "standard256b-volume", "inverted-mip-range", "swizzle", "linear-rgb1-read", "bgra-read", "r16-float-read", "r8-unorm-read", "yzwx-read", "reserved-swizzle", "array-base-out-of-range", "reserved", "uint-format", "uint-resource-float-format", "atomic-format", "depth-tile-read", "depth-tile-extent", "depth-tile-fmask"}) { std::string command = std::string("\"") + path + "\" --storage-texture-descriptor-death " + kind; std::vector mutable_command(command.begin(), command.end()); mutable_command.push_back('\0'); STARTUPINFOA startup{sizeof(startup)}; PROCESS_INFORMATION process{}; Require("BasicStorageTexture", "host", CreateProcessA(nullptr, mutable_command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != 0, "CreateProcess failed"); Require("BasicStorageTexture", "host", WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0, "descriptor death case timed out"); DWORD exit_code = 0; const bool exited = GetExitCodeProcess(process.hProcess, &exit_code) != 0; CloseHandle(process.hThread); CloseHandle(process.hProcess); Require("BasicStorageTexture", "host", exited && exit_code == 321, std::string(kind) + " storage descriptor did not report a fatal error"); } std::printf("[host] %-32s ok\n", "BasicStorageTextureDescriptor"); } #endif void CheckStorageTextureLinearUploadLayout() { constexpr auto format = Prospero::BufferFormat::k8_8_8_8UNorm; constexpr uint32_t width = 3840; constexpr uint32_t height = 2160; constexpr uint32_t depth = 1; constexpr auto tile = Prospero::TileMode::kLinear; const auto pitch = TileGetTexturePitch(format, width, tile); TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, depth, 1, tile, false, total); const auto layout = TextureCalcUploadLayout(format, width, height, 1, depth, tile, total.size, true, false, "StorageTextureLinearTest"); const auto regions = TextureBuildImageCopies(layout); Require("StorageTextureLinearUpload", "layout", pitch == width && total.size == 0x1fa4000 && total.align == 256 && layout.surface.description.tile_mode == tile && layout.pitch == width && layout.slice_stride == total.size && regions.size() == 1 && regions[0].bufferOffset == 0 && regions[0].imageExtent.width == width && regions[0].imageExtent.height == height && regions[0].bufferRowLength == width, "linear RGBA8 storage upload lost Prospero pitch or allocation size"); std::printf("[host] %-32s ok\n", "StorageTextureLinearUpload"); } void CheckStorageTextureDepthTileUploadLayout() { constexpr auto format = Prospero::BufferFormat::k8UInt; constexpr uint32_t width = 1; constexpr uint32_t height = 1; constexpr uint32_t depth = 1; constexpr auto tile = Prospero::TileMode::kDepth; const auto pitch = TileGetTexturePitch(format, width, tile); TileSizeAlign slice{}; TileSizeAlign total{}; TileSizeOffset level{}; TilePaddedSize padded{}; TileGetTextureSize(format, width, height, 1, tile, &slice, &level, &padded); TileGetTextureTotalSize(format, width, height, depth, 1, tile, false, total); const auto layout = TextureCalcUploadLayout(format, width, height, 1, depth, tile, total.size, true, false, "StorageTextureDepthTileTest"); const auto regions = TextureBuildImageCopies(layout); Require("StorageTextureDepthTileUpload", "PPSA14053 layout", pitch == 256 && padded.width == 256 && padded.height == 256 && slice.size == 0x10000 && slice.align == 0x10000 && level.size == slice.size && level.offset == 0 && total.size == slice.size && total.align == slice.align && layout.surface.description.tile_mode == tile && layout.surface.texture.block.family == TileBlockFamily::Depth64KB && layout.pitch == pitch && layout.slice_stride == pitch && layout.source_slice_stride == total.size && layout.mips[0].size == pitch && layout.surface.mips[0].size == total.size && regions.size() == 1 && regions[0].bufferOffset == 0 && regions[0].imageExtent.width == width && regions[0].imageExtent.height == height && regions[0].bufferRowLength == pitch, "1x1 R8_UINT depth tile lost its 64 KiB source footprint"); std::printf("[host] %-32s ok\n", "StorageTextureDepthTileUpload"); } void CheckImageSamplerSpecialization() { ShaderSamplerResource filtered_sampler{}; filtered_sampler.fields[2] = (1u << 20u) | (3u << 22u) | (2u << 24u) | (2u << 26u); filtered_sampler.SetPointFiltering(); Require("ImageSamplerSpecialization", "forced point sampler", filtered_sampler.XyMagFilter() == 0u && filtered_sampler.XyMinFilter() == 0u && filtered_sampler.ZFilter() == 1u && filtered_sampler.MipFilter() == 1u, "point-only image format retained a filtered sampler mode"); ShaderSamplerResource base_mip_sampler{}; base_mip_sampler.fields[2] = (3u << 20u) | (1u << 22u) | (2u << 24u); base_mip_sampler.SetPointFiltering(); Require("ImageSamplerSpecialization", "base-mip point sampler", base_mip_sampler.XyMagFilter() == 0u && base_mip_sampler.XyMinFilter() == 0u && base_mip_sampler.ZFilter() == 1u && base_mip_sampler.MipFilter() == 0u, "forcing point filtering enabled mipmapping on a base-mip sampler"); ShaderRecompiler::IR::Program mixed_sampler_program; mixed_sampler_program.resource_tracking_complete = true; ShaderRecompiler::IR::ImageResource sampled_image; sampled_image.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled; sampled_image.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; sampled_image.read = true; mixed_sampler_program.info.images = {sampled_image, sampled_image, sampled_image}; mixed_sampler_program.info.samplers.push_back({0u, 4u}); mixed_sampler_program.info.sampled_pairs = { {0u, 0u, 8u}, {1u, 0u, 12u}, {2u, 0u, 16u}}; ShaderRecompiler::IR::MemoryInfo signed_memory; signed_memory.kind = ShaderRecompiler::IR::ResourceKind::Image; signed_memory.resource = 2u; signed_memory.sampler = 0u; mixed_sampler_program.memory_info.push_back(signed_memory); mixed_sampler_program.block_storage.push_back( std::make_unique()); auto *mixed_sampler_block = mixed_sampler_program.block_storage.back().get(); mixed_sampler_program.blocks.push_back(mixed_sampler_block); using ShaderRecompiler::IR::Value; using ShaderRecompiler::IR::ValueOpcode; auto &image_handle = mixed_sampler_block->AppendNewInst( ValueOpcode::GetImageResource, {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); auto &sampler_handle = mixed_sampler_block->AppendNewInst( ValueOpcode::GetSamplerResource, {Value(0u), Value(0u), Value(0u), Value(0u)}); auto &image_address = mixed_sampler_block->AppendNewInst( ValueOpcode::MakeImageAddress, {Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u), Value(0u)}); const ShaderRecompiler::IR::MemoryFlags signed_memory_flags{0u, 16u}; uint64_t signed_memory_flag_bits = 0; std::memcpy(&signed_memory_flag_bits, &signed_memory_flags, sizeof(signed_memory_flags)); mixed_sampler_block->AppendNewInst( ValueOpcode::ImageSampleRaw, {Value(&image_handle), Value(&sampler_handle), Value(&image_address)}, signed_memory_flag_bits); ShaderRecompiler::IR::DescriptorValue native_image_descriptor{}; native_image_descriptor.dword_count = 8; native_image_descriptor.dwords[0] = 0x1000u; native_image_descriptor.dwords[1] = static_cast(Prospero::BufferFormat::k32Float) << 20u; native_image_descriptor.dwords[2] = 3u | (3u << 14u); native_image_descriptor.dwords[3] = DstSel(4, 5, 6, 7) | (static_cast(Prospero::ImageType::kColor2D) << 28u); auto packed_image_descriptor = native_image_descriptor; packed_image_descriptor.dwords[0] = 0x2000u; packed_image_descriptor.dwords[1] = static_cast(Prospero::BufferFormat::k11_11_10UInt) << 20u; auto signed_image_descriptor = native_image_descriptor; signed_image_descriptor.dwords[0] = 0x3000u; signed_image_descriptor.dwords[1] = static_cast(Prospero::BufferFormat::k32SInt) << 20u; ShaderRecompiler::IR::DescriptorValue sampler_descriptor{}; sampler_descriptor.dword_count = 4; const std::array descriptors{native_image_descriptor, packed_image_descriptor, signed_image_descriptor, sampler_descriptor}; mixed_sampler_program.descriptor_sources.resize(descriptors.size()); for (u32 source = 0; source < descriptors.size(); source++) { auto &destination = mixed_sampler_program.descriptor_sources[source]; destination.dword_count = descriptors[source].dword_count; for (u32 dword = 0; dword < destination.dword_count; dword++) { destination.dwords[dword] = Value(descriptors[source].dwords[dword]); } } for (u32 image = 0; image < mixed_sampler_program.info.images.size(); image++) { mixed_sampler_program.info.images[image].source = image; } mixed_sampler_program.info.samplers[0].source = 3; mixed_sampler_program.srt_plan_complete = true; auto mixed_sampler_plan = ShaderRecompiler::IR::ExtractResourcePlan(mixed_sampler_program); ShaderRecompiler::IR::ResourceSnapshot mixed_sampler_snapshot; ShaderRecompiler::IR::ResourceSpecialization mixed_sampler_specialization; Require("ImageSamplerSpecialization", "resource materialization", ShaderRecompiler::IR::MaterializeResources( mixed_sampler_plan, {}, mixed_sampler_snapshot, mixed_sampler_specialization), "mixed sampler descriptors could not be materialized"); ShaderRecompiler::IR::ApplyResourceSpecialization( mixed_sampler_program, mixed_sampler_specialization); Require("ImageSamplerSpecialization", "mixed sampler variant", mixed_sampler_program.info.samplers.size() == 2u && !mixed_sampler_program.info.samplers[0].force_point_filtering && mixed_sampler_program.info.samplers[1].force_point_filtering && mixed_sampler_program.info.sampled_pairs[0].sampler == 0u && mixed_sampler_program.info.sampled_pairs[1].sampler == 1u && mixed_sampler_program.info.sampled_pairs[2].sampler == 1u && mixed_sampler_program.memory_info[0].sampler == 1u && mixed_sampler_snapshot.samplers.size() == 2u, "a shared float/integer sampler was not split into point and native " "variants or the signed instruction retained the native sampler"); std::printf("[host] %-32s ok\n", "ImageSpecializationPipelineId"); } void CheckNativeImageDescriptorTypes() { using ShaderRecompiler::IR::DescriptorBindingKind; using ShaderRecompiler::IR::FirstImageBinding; using ShaderRecompiler::IR::FirstStorageImageBinding; for (uint32_t value = FirstImageBinding; value < static_cast(DescriptorBindingKind::Samplers); value++) { const auto expected = value < FirstStorageImageBinding ? vk::DescriptorType::eSampledImage : vk::DescriptorType::eStorageImage; Require("NativeImageDescriptorTypes", "image binding", NativeDescriptorType(static_cast(value)) == expected, "generated image binding has the wrong Vulkan descriptor type"); } Require("NativeImageDescriptorTypes", "non-image bindings", NativeDescriptorType(DescriptorBindingKind::Samplers) == vk::DescriptorType::eSampler && NativeDescriptorType(DescriptorBindingKind::Buffers) == vk::DescriptorType::eStorageBuffer && NativeDescriptorType(DescriptorBindingKind::Gds) == vk::DescriptorType::eStorageBuffer && NativeDescriptorType(DescriptorBindingKind::BdaPagetable) == vk::DescriptorType::eStorageBuffer && NativeDescriptorType(DescriptorBindingKind::FaultBuffer) == vk::DescriptorType::eStorageBuffer && NativeDescriptorType(DescriptorBindingKind::FlattenedSrt) == vk::DescriptorType::eStorageBuffer && NativeDescriptorType(DescriptorBindingKind::ShaderData) == vk::DescriptorType::eStorageBuffer, "non-image binding has the wrong Vulkan descriptor type"); std::printf("[host] %-32s ok\n", "NativeImageDescriptorTypes"); } #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS void CheckShaderRecompilerFatalContracts() { ExpectFatal("InvalidDescriptorBindingRejection", [] { (void)NativeDescriptorType( ShaderRecompiler::IR::DescriptorBindingKind::Count); }); ExpectFatal("WritableFlatStoreRejection", [] { const auto test = FlatStoreVariants(); (void)CompileCase(test); }); { ShaderRecompiler::IR::Program program; program.resource_tracking_complete = true; ShaderRecompiler::IR::ImageResource image; image.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage; image.numeric_class = Prospero::TextureNumericClass::Uint; image.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D; image.read = true; image.written = true; image.atomic = true; program.info.images.push_back(image); ShaderRecompiler::IR::DescriptorValue descriptor{}; descriptor.dword_count = 8; descriptor.dwords[0] = 0x2000u; descriptor.dwords[1] = static_cast(Prospero::BufferFormat::k11_11_10UInt) << 20u; descriptor.dwords[2] = 3u | (3u << 14u); descriptor.dwords[3] = DstSel(4, 5, 6, 7) | (static_cast(Prospero::ImageType::kColor2D) << 28u); image.source = 0; program.info.images[0].source = 0; program.descriptor_sources.resize(1); program.descriptor_sources[0].dword_count = descriptor.dword_count; for (u32 dword = 0; dword < descriptor.dword_count; dword++) { program.descriptor_sources[0].dwords[dword] = ShaderRecompiler::IR::Value(descriptor.dwords[dword]); } program.srt_plan_complete = true; auto plan = ShaderRecompiler::IR::ExtractResourcePlan(program); ShaderRecompiler::IR::ResourceSnapshot snapshot; ShaderRecompiler::IR::ResourceSpecialization specialization; Require("PackedAtomicImageRejection", "resource materialization", !ShaderRecompiler::IR::MaterializeResources( plan, {}, snapshot, specialization), "packed atomic image descriptor was accepted"); } } #endif void CheckStorageTextureVolumeUploadLayout() { constexpr auto format = Prospero::BufferFormat::k16_16_16_16Float; constexpr uint32_t width = 33; constexpr uint32_t height = 33; constexpr uint32_t depth = 33; const auto pitch = TileGetTexturePitch(format, width, Prospero::TileMode::kRenderTarget); TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, depth, 1, Prospero::TileMode::kRenderTarget, true, total); const auto layout = TextureCalcUploadLayout( format, width, height, 1, depth, Prospero::TileMode::kRenderTarget, total.size, true, true, "StorageTextureVolumeTest"); const auto regions = TextureBuildImageCopies(layout); Require( "StorageTextureVolumeUpload", "layout", pitch == 128 && total.size == 0x210000 && layout.slice_stride == 0x2208 && layout.source_slice_stride == 0 && layout.mips[0].size == 0x2208 && layout.surface.mips[0].size == 0x10000 && regions.size() == depth, "3D render-target upload did not preserve its compact linear layout"); std::vector infos; Require("StorageTextureVolumeUpload", "GPU records", TextureBuildGpuTileInfos(total.size, regions, layout, 1, infos) && infos.size() == depth, "3D render-target GPU records were not built"); for (const uint32_t z : {0u, 1u, depth - 1u}) { Require("StorageTextureVolumeUpload", "slice offsets", infos[z].linear_offset == static_cast(z) * 0x2208 && infos[z].tiled_offset == static_cast(z) * 0x10000 && infos[z].surface_z == z && infos[z].pitch == width, "volume slice lost its linear stride, block slice, or Z swizzle"); } std::printf("[host] %-32s ok\n", "StorageTextureVolumeUpload"); } void CheckStorageTextureVolumeMipRegions() { constexpr auto format = Prospero::BufferFormat::k8_8_8_8UNorm; constexpr uint32_t width = 8; constexpr uint32_t height = 4; constexpr uint32_t depth = 5; constexpr uint32_t levels = 3; constexpr auto tile = Prospero::TileMode::kLinear; TileSizeAlign total{}; TileGetTextureTotalSize(format, width, height, depth, levels, tile, true, total); const auto layout = TextureCalcUploadLayout( format, width, height, levels, depth, tile, total.size, true, true, "StorageTextureVolumeMipTest"); const auto copies = TextureBuildImageCopies(layout); bool valid = copies.size() == 8; size_t index = 0; for (uint32_t level = 0; level < levels && valid; level++) { const uint32_t mip_depth = std::max(depth >> level, 1u); const uint32_t mip_width = std::max(width >> level, 1u); const uint32_t mip_height = std::max(height >> level, 1u); for (uint32_t z = 0; z < mip_depth; z++, index++) { const auto © = copies[index]; valid &= copy.imageSubresource.mipLevel == level && copy.imageOffset.z == static_cast(z) && copy.imageExtent.width == mip_width && copy.imageExtent.height == mip_height && copy.bufferOffset == layout.mips[level].offset + z * layout.slice_stride && copy.bufferRowLength == layout.mips[level].row_length && copy.bufferImageHeight == layout.mips[level].image_height; } } valid &= index == copies.size(); Require("StorageTextureVolumeMipRegions", "per-mip depth", valid, "direction-neutral image copies did not shrink depth or preserve " "Vulkan Z coordinates"); std::printf("[host] %-32s ok\n", "StorageTextureVolumeMipRegions"); } void CheckStandard64RenderTargetTileRoundTrip() { constexpr auto format = Prospero::BufferFormat::k32Float; constexpr auto tile = Prospero::TileMode::kStandard64KB; constexpr uint32_t observed_width = 3840; constexpr uint32_t observed_height = 2160; const auto observed_pitch = TileGetTexturePitch(format, observed_width, tile); TileSizeAlign observed{}; TileGetTextureSize(format, observed_width, observed_height, 1, tile, &observed, nullptr, nullptr); Require("Standard64RenderTarget", "observed layout", observed_pitch == 3840 && observed.size == 0x1fe0000 && observed.align == 0x10000, "PPSA02721 Standard64KB render-target footprint changed"); constexpr uint32_t width = 257; constexpr uint32_t height = 131; const auto pitch = TileGetTexturePitch(format, width, tile); TileSizeAlign storage{}; TileGetTextureSize(format, width, height, 1, tile, &storage, nullptr, nullptr); Require("Standard64RenderTarget", "partial layout", pitch == 384 && storage.size == 0x60000 && storage.align == 0x10000, "partial Standard64KB footprint was not padded in 128x128 blocks"); std::printf("[host] %-32s ok\n", "Standard64RenderTarget"); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS void CheckStorageTextureGpuOwnedRebindState() { constexpr uintptr_t base = 0x0000000200200000ull; constexpr uint64_t size = 0x10000; const auto guest_memory = Libs::LibKernel::Memory::AllocateRuntimeMemory( base, size, Common::VirtualMemory::Mode::ReadWrite, "storage_texture_gpu_owned_rebind", true); auto *memory = reinterpret_cast(guest_memory); Require("StorageTextureGpuOwnedRebind", "allocation", guest_memory == base, "fixed guest-owner allocation failed"); PageManager page_manager; MemoryTracker tracker(page_manager); tracker.ForEachUploadRange( base, size, true, [](uint64_t, uint64_t) noexcept {}, []() noexcept {}); uint64_t readable = 0; uint64_t mapped = 0; MEMORY_BASIC_INFORMATION protection{}; Require( "StorageTextureGpuOwnedRebind", "owned", tracker.IsRegionGpuModified(base, size) && (!HostMemoryQueryReadable(base, size, readable) || readable < size) && HostMemoryQueryRange(base, size, HostMemoryAccess::Mapped, mapped) && mapped == size && VirtualQuery(memory, &protection, sizeof(protection)) != 0 && protection.Protect == PAGE_NOACCESS, "GPU-owned storage pages remained host-readable or lost tracker " "identity"); tracker.UnmarkRegionAsGpuModified(base, size); readable = 0; Require("StorageTextureGpuOwnedRebind", "clean readback", !tracker.IsRegionGpuModified(base, size) && !tracker.IsRegionCpuModified(base, size) && HostMemoryQueryReadable(base, size, readable) && readable == size, "clean storage readback did not publish readable coherent backing"); tracker.MarkRegionAsGpuModified(base, size); readable = 0; Require( "StorageTextureGpuOwnedRebind", "clean reclaim", tracker.IsRegionGpuModified(base, size) && !tracker.IsRegionCpuModified(base, size) && (!HostMemoryQueryReadable(base, size, readable) || readable < size), "clean storage rebind did not reclaim GPU ownership without an upload"); tracker.UnmarkRegionAsGpuModified(base, size); tracker.MarkRegionAsCpuModified(base, size); uint32_t dirty_ranges = 0; bool upload_called = false; tracker.ForEachUploadRange( base, size, true, [&](uint64_t, uint64_t) noexcept { dirty_ranges++; }, [&]() noexcept { upload_called = true; }); readable = 0; Require( "StorageTextureGpuOwnedRebind", "dirty refresh", dirty_ranges == 1 && upload_called && tracker.IsRegionGpuModified(base, size) && !tracker.IsRegionCpuModified(base, size) && (!HostMemoryQueryReadable(base, size, readable) || readable < size), "CPU-dirty storage rebind did not refresh once and reclaim GPU " "ownership"); tracker.UnmarkRegionAsGpuModified(base, size); tracker.UntrackMemory(base, size); Require("StorageTextureGpuOwnedRebind", "free", Libs::LibKernel::Memory::FreeGuestMemory(base, size), "guest-owner free failed"); std::printf("[host] %-32s ok\n", "StorageTextureGpuOwnedRebind"); } #endif void CheckNativeMsaaState() { Require("NativeMsaaState", "sample encoding", render_sample_count(0) == 1 && render_sample_count(1) == 2 && render_sample_count(2) == 4 && render_sample_count(3) == 8 && render_sample_count(4) == 0, "PS5 sample encodings were not mapped exactly"); Require("NativeMsaaState", "Vulkan sample mapping", vulkan_sample_count(1) == vk::SampleCountFlagBits::e1 && vulkan_sample_count(2) == vk::SampleCountFlagBits::e2 && vulkan_sample_count(4) == vk::SampleCountFlagBits::e4 && vulkan_sample_count(8) == vk::SampleCountFlagBits::e8 && vulkan_sample_count(3) == vk::SampleCountFlagBits{}, "native sample counts were not mapped exactly to Vulkan"); TileSizeAlign color{}; const auto color_pitch = TileGetRenderTargetPitch(1920, 8, 3); Require("NativeMsaaState", "8x color footprint", color_pitch == 1920 && TileGetRenderTargetSize(1920, 1080, color_pitch, 8, color, 3) && color.align == 0x10000 && color.size == 0x07f80000, "8x R16G16B16A16 color footprint was not preserved"); TileSizeAlign depth{}; TileSizeAlign stencil{}; TileSizeAlign htile{}; Require("NativeMsaaState", "8x depth/stencil footprint", TileGetDepthPitch(1920, 4, 3) == 1920 && TileGetDepthSize(1920, 1080, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth, 3) && depth.align == 0x10000 && depth.size == 0x03fc0000 && stencil.align == 0x10000 && stencil.size == 0x010e0000 && htile.align == 0x8000 && htile.size == 0x00030000, "8x depth/stencil or fragment-independent HTile footprint regressed"); std::printf("[host] %-32s ok\n", "NativeMsaaState"); } void CheckDepthHtileStencilCompatibility() { Require("DepthHtileStencilCompatibility", "inactive stencil plane", depth_htile_stencil_acceleration_compatible(false, true, false), "inactive Hi-Stencil state was treated as an attachment"); Require("DepthHtileStencilCompatibility", "disabled acceleration", depth_htile_stencil_acceleration_compatible(true, false, true), "disabled Hi-Stencil state was rejected"); Require("DepthHtileStencilCompatibility", "PS5 stencil plus HTile", depth_htile_stencil_acceleration_compatible(true, true, false), "valid PS5 Hi-Stencil attachment was rejected"); Require("DepthHtileStencilCompatibility", "missing HTile metadata", !depth_htile_stencil_acceleration_compatible(true, false, false), "active Hi-Stencil without HTile metadata was admitted"); std::printf("[host] %-32s ok\n", "DepthHtileStencilCompatibility"); } void CheckPs5DepthRegisterDecoding() { constexpr uint32_t captured_z = 0x22900803u; constexpr uint32_t captured_stencil = 0x00100801u; const auto z = HW::DepthZInfo::Decode(captured_z); const auto stencil = HW::DepthStencilInfo::Decode(captured_stencil); Require( "Ps5DepthRegisterDecoding", "captured texture-compatible state", z.format == Prospero::DepthFormat::kZ32F && z.num_samples == 0 && z.htile_acceleration && !z.expclear_enabled && !z.partially_resident && z.max_mip_level == 0 && z.z_compare_base == Prospero::ZCompareBase::kZMin && z.texture_compatibility == Prospero::TextureCompatiblePlaneCompression::kEnable && z.HasValidTextureCompatibility() && stencil.format == Prospero::StencilFormat::k8UInt && !stencil.htile_stencil_disabled && !stencil.expclear_enabled && !stencil.partially_resident && stencil.texture_compatibility == Prospero::TextureCompatibleStencil::kEnable && stencil.HasValidTextureCompatibility(), "valid PS5 depth/stencil aggregate encodings were not decoded exactly"); const auto malformed_z = HW::DepthZInfo::Decode(0x00000803u); const auto malformed_stencil = HW::DepthStencilInfo::Decode(0x00100001u); Require("Ps5DepthRegisterDecoding", "malformed aggregate state", !malformed_z.HasValidTextureCompatibility() && !malformed_stencil.HasValidTextureCompatibility(), "partial texture-compatible aggregate encodings were accepted"); std::printf("[host] %-32s ok\n", "Ps5DepthRegisterDecoding"); } void CheckDepthAttachmentWrites() { RenderDepthInfo target{}; target.desc.view_info.format = vk::Format::eD32SfloatS8Uint; Require("DepthAttachmentWrites", "disabled depth test", !target.AttachmentWriteAspects(), "disabled depth testing claimed a depth write"); target.depth_test_enable = true; target.depth_write_enable = true; target.depth_compare_op = vk::CompareOp::eLess; Require("DepthAttachmentWrites", "depth write", target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eDepth, "write-enabled depth testing did not claim depth"); target.depth_test_enable = false; target.depth_write_enable = false; target.depth_load_clear_enable = true; Require("DepthAttachmentWrites", "depth clear", target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eDepth, "depth attachment clear did not claim depth"); target.depth_load_clear_enable = false; target.stencil_test_enable = true; target.stencil_front = {vk::StencilOp::eKeep, vk::StencilOp::eKeep, vk::StencilOp::eKeep, vk::CompareOp::eAlways, 0xff, 0xff, 0}; target.stencil_back = target.stencil_front; Require("DepthAttachmentWrites", "stencil keep", !target.AttachmentWriteAspects(), "KEEP-only stencil state claimed a stencil write"); target.stencil_front.failOp = vk::StencilOp::eZero; Require("DepthAttachmentWrites", "unreachable stencil fail", !target.AttachmentWriteAspects(), "ALWAYS comparison claimed an unreachable fail operation"); target.stencil_front.failOp = vk::StencilOp::eKeep; target.stencil_front.passOp = vk::StencilOp::eReplace; Require("DepthAttachmentWrites", "stencil pass write", target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eStencil, "write-capable stencil pass did not claim stencil"); target.stencil_front.writeMask = 0; Require("DepthAttachmentWrites", "back-face keep", !target.AttachmentWriteAspects(), "masked front write or KEEP-only back face claimed stencil"); target.stencil_test_enable = false; target.stencil_clear_enable = true; Require("DepthAttachmentWrites", "stencil clear", target.AttachmentWriteAspects() == vk::ImageAspectFlagBits::eStencil, "stencil attachment clear did not claim stencil"); target.desc.view_info.format = vk::Format::eD32Sfloat; Require("DepthAttachmentWrites", "missing stencil aspect", !target.AttachmentWriteAspects(), "depth-only format claimed a stencil write"); std::printf("[host] %-32s ok\n", "DepthAttachmentWrites"); } void CheckDynamicRenderingState() { RenderState first{}; first.width = 64; first.height = 32; first.num_layers = 2; first.num_color_attachments = 1; first.color_attachments[0].image_layout = vk::ImageLayout::eColorAttachmentOptimal; first.color_attachments[0].clear_value = {1, 2, 3, 4}; first.color_attachments[0].is_clear = true; auto changed = first; Require("DynamicRenderingState", "identical state", first == changed, "fieldwise RenderState identity rejected an identical scope"); changed.color_attachments[0].is_clear = false; Require("DynamicRenderingState", "clear identity", first != changed, "clear/load state did not participate in rendering identity"); changed = first; changed.num_layers = 1; Require("DynamicRenderingState", "layer identity", first != changed, "layer count did not participate in rendering identity"); changed = first; changed.color_attachments[0].image_layout = vk::ImageLayout::eGeneral; Require("DynamicRenderingState", "layout identity", first != changed, "attachment layout did not participate in rendering identity"); PipelineRenderingState rgba{}; rgba.color_count = 1; rgba.color_formats[0] = vk::Format::eR8G8B8A8Unorm; auto uint_color = rgba; uint_color.color_formats[0] = vk::Format::eR8G8B8A8Uint; Require("DynamicRenderingState", "pipeline color format identity", rgba != uint_color, "distinct dynamic-rendering formats can alias one pipeline key"); auto depth = rgba; depth.depth_format = vk::Format::eD32SfloatS8Uint; depth.stencil_format = vk::Format::eD32SfloatS8Uint; Require("DynamicRenderingState", "pipeline depth/stencil identity", rgba != depth, "depth/stencil formats did not participate in pipeline identity"); RenderDepthInfo attachment{}; attachment.desc.view_info.format = vk::Format::eD32SfloatS8Uint; Require("DynamicRenderingState", "read-only depth/stencil layout", depth_attachment_layout(attachment) == vk::ImageLayout::eDepthStencilReadOnlyOptimal, "fully read-only depth/stencil used a writable layout"); attachment.depth_load_clear_enable = true; Require("DynamicRenderingState", "read-only deferred depth clear", depth_attachment_layout(attachment) == vk::ImageLayout::eDepthStencilReadOnlyOptimal, "a load clear changed the guest depth-write layout"); attachment.depth_test_enable = true; attachment.depth_write_enable = true; Require("DynamicRenderingState", "depth-write stencil-read layout", depth_attachment_layout(attachment) == vk::ImageLayout::eDepthAttachmentStencilReadOnlyOptimal, "depth-only writes did not retain read-only stencil"); attachment.depth_load_clear_enable = false; attachment.depth_write_enable = false; attachment.stencil_clear_enable = true; Require("DynamicRenderingState", "depth-read stencil-write layout", depth_attachment_layout(attachment) == vk::ImageLayout::eDepthReadOnlyStencilAttachmentOptimal, "stencil-only writes did not retain read-only depth"); attachment.depth_write_enable = true; Require("DynamicRenderingState", "writable depth/stencil layout", depth_attachment_layout(attachment) == vk::ImageLayout::eDepthStencilAttachmentOptimal, "combined depth/stencil writes did not use the writable layout"); std::printf("[host] %-32s ok\n", "DynamicRenderingState"); } void CheckDepthTargetFootprints() { TileSizeAlign stencil{}; TileSizeAlign htile{}; TileSizeAlign depth{}; struct AttachmentFormatCase { const char *name; Prospero::DepthFormat depth_format; Prospero::StencilFormat stencil_format; vk::Format expected; }; constexpr AttachmentFormatCase attachment_cases[] = { {"Z16", Prospero::DepthFormat::kZ16, Prospero::StencilFormat::kInvalid, vk::Format::eD16Unorm}, {"Z16S8", Prospero::DepthFormat::kZ16, Prospero::StencilFormat::k8UInt, vk::Format::eD16UnormS8Uint}, {"Z32", Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::kInvalid, vk::Format::eD32Sfloat}, {"Z32S8", Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, vk::Format::eD32SfloatS8Uint}, {"invalid depth", static_cast(2u), Prospero::StencilFormat::k8UInt, vk::Format::eUndefined}, {"invalid stencil", Prospero::DepthFormat::kZ16, static_cast(2u), vk::Format::eUndefined}, }; for (const auto &test : attachment_cases) { Require("DepthTargetFootprints", test.name, DepthAttachmentFormat(test.depth_format, test.stencil_format) == test.expected, "PS5 depth/stencil attachment mapping changed"); } Require("DepthTargetFootprints", "1920x1080 Z16S8 without HTile", TileGetDepthSize(1920, 1080, 0, Prospero::DepthFormat::kZ16, Prospero::StencilFormat::k8UInt, false, stencil, htile, depth) && depth.size == 0x480000 && depth.align == 0x10000 && stencil.size == 0x280000 && stencil.align == 0x10000 && htile.size == 0, "captured Z16S8 footprint disagrees with Prospero block rules"); struct TargetFormatCase { const char *name; vk::Format host_format; Prospero::BufferFormat guest_format; uint32_t bytes_per_element; bool stencil; bool supported; bool readback; }; constexpr TargetFormatCase target_cases[] = { {"D16", vk::Format::eD16Unorm, Prospero::BufferFormat::k16UNorm, 2, false, true, true}, {"D16S8", vk::Format::eD16UnormS8Uint, Prospero::BufferFormat::k16UNorm, 2, true, true, true}, {"D16 via D24S8", vk::Format::eD24UnormS8Uint, Prospero::BufferFormat::k16UNorm, 2, true, true, true}, {"D16 via D32S8", vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k16UNorm, 2, true, true, true}, {"D32", vk::Format::eD32Sfloat, Prospero::BufferFormat::k32Float, 4, false, true, true}, {"D32S8", vk::Format::eD32SfloatS8Uint, Prospero::BufferFormat::k32Float, 4, true, true, true}, {"D16 plus stencil mismatch", vk::Format::eD16Unorm, Prospero::BufferFormat::k16UNorm, 2, true, false, false}, {"fallback without stencil", vk::Format::eD24UnormS8Uint, Prospero::BufferFormat::k16UNorm, 2, false, false, false}, {"D32 guest via D24", vk::Format::eD24UnormS8Uint, Prospero::BufferFormat::k32Float, 4, true, false, false}, {"D16 byte mismatch", vk::Format::eD16Unorm, Prospero::BufferFormat::k16UNorm, 4, false, false, false}, }; for (const auto &test : target_cases) { ImageInfo target{}; target.pixel_format = test.host_format; target.guest_format = test.guest_format; target.bytes_per_block = test.bytes_per_element; target.stencil = test.stencil ? GuestRange{0x10000, 0x10000} : GuestRange{}; Require("DepthTargetFootprints", test.name, IsSupportedDepthTargetFormat(target) == test.supported && IsSupportedDepthPlaneReadback(target) == test.readback, "host/guest depth format or exact readback policy changed"); } ImageInfo compressed_stencil{}; compressed_stencil.pixel_format = vk::Format::eD32SfloatS8Uint; compressed_stencil.guest_format = Prospero::BufferFormat::k32Float; compressed_stencil.bytes_per_block = 4; compressed_stencil.stencil = {0x10000, 0x10000}; compressed_stencil.metadata.stencil_compressed = true; Require("DepthTargetFootprints", "compressed stencil depth-plane readback", IsSupportedDepthTargetFormat(compressed_stencil) && IsSupportedDepthPlaneReadback(compressed_stencil), "stencil metadata incorrectly blocked independent depth-plane " "readback"); Require("DepthTargetFootprints", "lexicographic subresource comparison", ImageSubresources{2, 1} > ImageSubresources{1, 4} && ImageSubresources{1, 4} < ImageSubresources{2, 1}, "subresource ordering diverged from required overlap semantics"); struct TransferPlaneCase { const char *name; vk::Format attachment; vk::Format transfer; uint32_t bytes; }; constexpr TransferPlaneCase transfer_cases[] = { {"D16S8 transfer", vk::Format::eD16UnormS8Uint, vk::Format::eD16Unorm, 2}, {"D24S8 transfer", vk::Format::eD24UnormS8Uint, vk::Format::eX8D24UnormPack32, 4}, {"D32S8 transfer", vk::Format::eD32SfloatS8Uint, vk::Format::eD32Sfloat, 4}, {"invalid transfer", vk::Format::eR16Unorm, vk::Format::eUndefined, 0}, }; for (const auto &test : transfer_cases) { Require("DepthTargetFootprints", test.name, DepthAspectTransferFormat(test.attachment) == test.transfer && DepthAspectTransferBytes(test.attachment) == test.bytes, "combined depth transfer-plane layout changed"); } struct PromotionCase { const char *name; uint16_t source; uint32_t d24; uint32_t d32; }; constexpr PromotionCase promotion_cases[] = { {"zero", 0, 0, 0}, {"midpoint", 0x8000, 0x00800080, 0x3f000080}, {"maximum", 0xffff, 0x00ffffff, 0x3f800000}, }; for (const auto &test : promotion_cases) { Require("DepthTargetFootprints", test.name, EncodeD16AsD24(test.source) == test.d24 && EncodeD16AsD32(test.source) == test.d32, "D16 host promotion changed the represented depth value"); } Require("DepthTargetFootprints", "640x360 Z32S8 without HTile", TileGetDepthSize(640, 360, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, false, stencil, htile, depth), "valid non-HTile depth/stencil footprint was rejected"); Require( "DepthTargetFootprints", "640x360 Prospero block sizes", depth.size == 0xf0000 && depth.align == 0x10000 && stencil.size == 0x60000 && stencil.align == 0x10000 && htile.size == 0 && htile.align == 0, "non-HTile depth/stencil footprint disagrees with Prospero block rules"); const auto depth_pitch = TileGetTexturePitch(Prospero::BufferFormat::k32Float, 640, Prospero::TileMode::kDepth); TileSizeAlign texture_depth{}; TileGetTextureTotalSize(Prospero::BufferFormat::k32Float, 640, 360, 1, 1, Prospero::TileMode::kDepth, false, texture_depth); Require("DepthTargetFootprints", "640x360 generic depth tile", depth_pitch == 640 && texture_depth.size == 0xf0000 && texture_depth.align == 0x10000, "generic depth texture sizing bypassed 64 KiB block padding"); Require( "DepthTargetFootprints", "960x540 Z32S8 with HTile", TileGetDepthSize(960, 540, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth) && depth.size == 0x280000 && depth.align == 0x10000 && stencil.size == 0xc0000 && stencil.align == 0x10000 && htile.size == 0x10000 && htile.align == 0x8000, "generic Prospero HTile block calculation rejected the title footprint"); Require("DepthTargetFootprints", "known HTile extent", TileGetDepthSize(1280, 720, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth) && depth.size == 0x3c0000 && stencil.size == 0xf0000 && htile.size == 0x20000, "validated 1280x720 HTile footprint regressed"); Require("DepthTargetFootprints", "PPSA06228 3840x2160 Z32S8 with HTile", TileGetDepthSize(3840, 2160, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth) && depth.size == 0x1fe0000 && depth.align == 0x10000 && stencil.size == 0x870000 && stencil.align == 0x10000 && htile.size == 0xa0000 && htile.align == 0x8000, "captured 3840x2160 depth/stencil/HTile footprint disagrees with " "Prospero rules"); Require("DepthTargetFootprints", "invalid depth format", !TileGetDepthSize(960, 540, 0, static_cast(2u), Prospero::StencilFormat::k8UInt, true, stencil, htile, depth), "unsupported depth format was silently admitted"); Require("DepthTargetFootprints", "invalid stencil format", !TileGetDepthSize(960, 540, 0, Prospero::DepthFormat::kZ32F, static_cast(2u), true, stencil, htile, depth), "unsupported stencil format was silently admitted"); Require("DepthTargetFootprints", "invalid extent", !TileGetDepthSize(0, 540, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth) && !TileGetDepthSize(16385, 540, 0, Prospero::DepthFormat::kZ32F, Prospero::StencilFormat::k8UInt, true, stencil, htile, depth), "invalid HTile extent was silently admitted"); std::printf("[host] %-32s ok\n", "DepthTargetFootprints"); } struct SlotLifetimeProbe { explicit SlotLifetimeProbe(bool *destroyed) : destroyed(destroyed) { if (destroyed == nullptr || *destroyed) { EXIT("fence-lifetime probe has invalid construction state\n"); } } ~SlotLifetimeProbe() { *destroyed = true; } bool *destroyed = nullptr; }; void CheckSlotVectorLifetime() { bool destroyed = false; Common::SlotVector slots; const auto first = slots.insert(&destroyed); Require("SlotVectorLifetime", "inserted", slots.is_allocated(first) && slots.try_get(first) != nullptr && !destroyed, "slot insertion did not expose its cache-owned object"); slots.erase(first); const bool first_destroyed = destroyed; destroyed = false; const auto second = slots.insert(&destroyed); Require("SlotVectorLifetime", "generation", first_destroyed && !destroyed && first.index == second.index && first != second && !slots.is_allocated(first) && slots.is_allocated(second), "reused slot accepted a stale cache resource id"); std::printf("[host] %-32s ok\n", "SlotVectorLifetime"); } void CheckEmbeddedFetchLaneSpill() { std::vector code; code.push_back(EncodeSMovB32(0, InlineU32(0))); code.push_back(EncodeSmem0(0x02u, 20, 4)); code.push_back(EncodeSmem1(0)); // s_load_dwordx4 s[20:23], s[8:9] AppendVop3(&code, 0x361u, 6, 20, InlineU32(0)); // v_writelane_b32 v6, s20, 0 code.push_back(EncodeSMovB32(20, InlineU32(0))); AppendVop3(&code, 0x360u, 20, Vgpr(6), InlineU32(0)); // v_readlane_b32 s20, v6, 0 code.push_back(EncodeVop2(0x01u, 0, Vgpr(8), 5)); code.push_back(EncodeMubuf0(0x03u, 0, true)); code.push_back(EncodeMubuf1(9, 5, 0)); AppendEnd(&code); std::array user_data{}; ShaderVertexInputInfo vertex; vertex.fetch_embedded = true; vertex.fetch_buffer_reg = 0; vertex.resources_num = 1; vertex.resources[0].fields[3] = BufferFormat(Prospero::BufferFormat::k32_32_32_32Float) << 12u | DstSel(4, 5, 6, 7); vertex.resources_dst[0].attr_id = 0; vertex.resources_dst[0].registers_num = 4; ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Vertex; options.user_data_base = 8; options.user_data = user_data; options.input_info.vertex = &vertex; auto translated = ShaderRecompiler::TranslateProgram(code, options); auto resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program); ShaderRecompiler::IR::ResourceSnapshot resources; ShaderRecompiler::IR::ResourceSpecialization specialization; const ShaderRecompiler::IR::SrtRuntime runtime{ .user_data = options.user_data, .shader_base = reinterpret_cast(code.data()), }; Require("EmbeddedFetchLaneSpill", "resource materialization", ShaderRecompiler::IR::MaterializeResources( resource_plan, runtime, resources, specialization), "lane-spilled fetch resources could not be materialized"); auto result = ShaderRecompiler::CompileProgram( std::move(translated), options, specialization); Require("EmbeddedFetchLaneSpill", "fetch rewrite", result.program.info.vertex_fetch_components[0] == 4, "lane-spilled fetch descriptor was not recognized and rewritten"); std::printf("[host] %-32s ok\n", "EmbeddedFetchLaneSpill"); } void CheckReferenceClockScale() { uint64_t value = 0; Require("ReferenceClockScale", "zero", Sync::ScaleReferenceClock(0, 3000000000ull, value) && value == 0, "zero host tick did not produce a zero GPU clock"); Require("ReferenceClockScale", "fractional second", Sync::ScaleReferenceClock(1500000000ull, 3000000000ull, value) && value == 50000000ull, "host half-second did not scale to 50,000,000 ticks"); Require("ReferenceClockScale", "whole and fractional", Sync::ScaleReferenceClock(3750000000ull, 3000000000ull, value) && value == 125000000ull, "host 1.25 seconds did not scale to 125,000,000 ticks"); Require("ReferenceClockScale", "monotonic floor", Sync::ScaleReferenceClock(3750000001ull, 3000000000ull, value) && value == 125000000ull, "sub-reference-tick increment did not use a monotonic floor"); Require("ReferenceClockScale", "guards", !Sync::ScaleReferenceClock(1, 0, value) && !Sync::ScaleReferenceClock(UINT64_MAX, 1, value), "invalid frequency or overflow was accepted"); std::printf("[host] %-32s ok\n", "ReferenceClockScale"); } void CheckClipControlDepthClipState() { HW::ClipControl clip; Require("ClipControlDepthClipState", "default", clip.IsZClipEnabled(), "default paired Z clipping was not enabled"); clip.min_z_clip_disable = true; Require("ClipControlDepthClipState", "asymmetric near", !clip.IsZClipEnabled(), "near-plane disable did not disable paired host Z clipping"); clip.min_z_clip_disable = false; clip.max_z_clip_disable = true; Require("ClipControlDepthClipState", "asymmetric far", !clip.IsZClipEnabled(), "far-plane disable did not disable paired host Z clipping"); clip.min_z_clip_disable = true; Require("ClipControlDepthClipState", "both disabled", !clip.IsZClipEnabled(), "paired Z-clip disable was not represented"); std::printf("[host] %-32s ok\n", "ClipControlDepthClipState"); } void CheckVulkan13FeatureRequirements() { const auto features = WindowContext::RequiredVulkan13Features(); Require( "Vulkan13FeatureRequirements", "production requirements", features.sType == vk::StructureType::ePhysicalDeviceVulkan13Features && features.pNext == nullptr && features.dynamicRendering == VK_TRUE && features.synchronization2 == VK_TRUE, "production device creation did not require dynamic rendering and " "synchronization2 from one feature declaration"); std::printf("[host] %-32s ok\n", "Vulkan13FeatureRequirements"); } void CheckPm4AcquireMemNoOp(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); const std::array standard_payload{0x00400000u, 1u, 0u, 0u, 0u, 10u}; const std::array custom_payload{ 0xdeadbeefu, 0xffffffffu, 0x80000000u, 0x13579bdfu, 0x2468ace0u, 0xaaaaaaaau, 0x55555555u}; const auto standard_before = standard_payload; const auto custom_before = custom_payload; Require("Pm4AcquireMemNoOp", "recognized packets", CpOpAcquireMem(processor, 0xC0055800u, standard_payload.data(), 0, 0) == 6 && CpOpAcquireMem(processor, 0xc0061050u, custom_payload.data(), 0, 0) == 7 && standard_payload == standard_before && custom_payload == custom_before, "ACQUIRE_MEM did not consume both pinned packet forms as " "side-effect-free " "no-ops"); std::printf("[host] %-32s ok\n", "Pm4AcquireMemNoOp"); } void CheckPm4SyntheticOcclusionCounterDump(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); constexpr uint64_t untouched = 0x1122334455667788ull; constexpr uint64_t ready_bit = 1ull << 63u; alignas(16) std::array, 16> results; for (auto &pair : results) { pair.fill(untouched); } const auto dump = [&](uint64_t *address) { const auto raw_address = reinterpret_cast(address); std::array packet{ KYTY_PM4(4, Pm4::IT_EVENT_WRITE, Pm4::R_ZERO), 0x00000139u, static_cast(raw_address), static_cast(raw_address >> 32u)}; Pm4Execution execution; return processor.Process(execution, packet); }; const auto begin_result = dump(&results[0][0]); bool begin_written = begin_result == Pm4ProcessResult::Complete; for (const auto &pair : results) { begin_written &= pair[0] == ready_bit && pair[1] == untouched; } const auto end_result = dump(&results[0][1]); bool end_written = end_result == Pm4ProcessResult::Complete; uint64_t visible_samples = 0; for (const auto &pair : results) { end_written &= pair[0] == ready_bit && pair[1] == (ready_bit | 1u); visible_samples += pair[1] - pair[0]; } Require("Pm4SyntheticOcclusionCounterDump", "always-visible result", begin_written && end_written && visible_samples == results.size(), "EVENT_WRITE did not publish valid nonzero begin/end counters to " "every PS5 DB"); std::printf("[host] %-32s ok\n", "Pm4SyntheticOcclusionCounterDump"); } void CheckPm4Predication(RenderContext &renderer) { constexpr const char *name = "Pm4Predication"; constexpr uint64_t ready = 1ull << 63u; GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); processor.BufferInit(); alignas(16) std::array query{}; alignas(16) uint64_t boolean = 0; uint32_t predicated = 0; uint32_t unconditional = 0; const auto commands = [&](uint32_t op, uint32_t condition, uint32_t wait, const void *source) { const auto address = reinterpret_cast(source); const auto predicated_address = reinterpret_cast(&predicated); const auto unconditional_address = reinterpret_cast(&unconditional); return std::array{ // Captured AGC form: c0022000 00010100 . 0xc0022000u, (op << 16u) | (wait << 12u) | (condition << 8u), static_cast(address), static_cast(address >> 32u), KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0) | 1u, 0, static_cast(predicated_address), static_cast(predicated_address >> 32u), 11, KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0), 0, static_cast(unconditional_address), static_cast(unconditional_address >> 32u), 22}; }; const auto check = [&](const char *stage, uint32_t op, uint32_t condition, uint32_t wait, const void *source, bool skip) { predicated = unconditional = 0; const auto packet = commands(op, condition, wait, source); Pm4Execution execution; Require(name, stage, processor.Process(execution, packet) == Pm4ProcessResult::Complete && processor.ShouldSkipPredicatedPackets() == skip && predicated == (skip ? 0u : 11u) && unconditional == 22, "predicate polarity, tagged packet execution, or packet consumption is wrong"); }; struct Case { uint64_t delta; uint32_t condition; bool skip; }; constexpr Case cases[]{{1, 1, false}, {1, 0, true}, {0, 1, true}, {0, 0, false}}; for (const auto &test : cases) { query.fill(ready | 0x344u); for (size_t db = 0; db < 16; db++) { query[db * 2 + 1] += test.delta; } boolean = test.delta; for (uint32_t wait : {0u, 1u}) { check("ready Z-pass", 1, test.condition, wait, query.data(), test.skip); check("bool preservation", 3, test.condition, wait, &boolean, test.skip); } } query.fill(ready | 0x344u); query[31]++; check("only last DB visible", 1, 1, 0, query.data(), false); check("inverse last DB visibility", 1, 0, 0, query.data(), true); check("clear", 0, 0, 0, nullptr, false); // Every begin AND end counter must be ready; no-wait must discard old skip state. for (size_t missing = 0; missing < query.size(); missing++) { query.fill(ready | 0x344u); query[missing] &= ~ready; for (uint32_t condition : {0u, 1u}) { boolean = 0; processor.SetPredication(1, 3, 0, &boolean, 0); check("unavailable no-wait", 1, condition, 1, query.data(), false); } } // Retry the same packet when its last DB's begin or end arrives later. for (size_t missing : {30u, 31u}) { query.fill(ready | 0x344u); query[31]++; query[missing] &= ~ready; boolean = 0; processor.SetPredication(1, 3, 0, &boolean, 0); predicated = unconditional = 0; const auto packet = commands(1, 1, 0, query.data()); Pm4Execution execution; for (int retry = 0; retry < 2; retry++) { Require(name, "pending wait suspends", processor.Process(execution, packet) == Pm4ProcessResult::Blocked && !execution.MadeProgress() && predicated == 0 && unconditional == 0 && processor.ShouldSkipPredicatedPackets(), "pending query advanced its packet or changed the prior predicate"); } query[missing] |= ready; Require(name, "ready wait resumes", processor.Process(execution, packet) == Pm4ProcessResult::Complete && execution.MadeProgress() && predicated == 11 && unconditional == 22 && !processor.ShouldSkipPredicatedPackets(), "ready query did not reevaluate and execute the suspended packet's suffix"); } processor.BufferWait(); std::printf("[host] %-32s ok\n", name); } void CheckPm4StencilInfoValueLane(RenderContext &renderer) { CommandProcessor processor(renderer, 0); constexpr std::array payload{0x00100801u, 0x28000000u}; const auto consumed = HwCtxSetStencilInfo( processor, 0xC0016900u, Pm4::DB_STENCIL_INFO, payload.data(), 1); const auto &stencil = processor.GetCtx().GetDepthStencilInfo(); Require("Pm4StencilInfoValueLane", "standalone register value", consumed == 1 && stencil.format == Prospero::StencilFormat::k8UInt && stencil.texture_compatibility == Prospero::TextureCompatibleStencil::kEnable && !stencil.expclear_enabled && !stencil.htile_stencil_disabled, "standalone DB_STENCIL_INFO did not decode its sole payload value"); std::printf("[host] %-32s ok\n", "Pm4StencilInfoValueLane"); } void CheckPm4NativeTargetGeometryRegisters(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor packet(renderer, 0); CommandProcessor indirect(renderer, 0); const auto write_context = [](CommandProcessor &processor, uint32_t offset, uint32_t value) { std::array command{ KYTY_PM4(3, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), offset, value}; Pm4Execution execution; return processor.Process(execution, command) == Pm4ProcessResult::Complete; }; constexpr uint32_t color_slot = 7; constexpr uint32_t width_minus_one = 1919; constexpr uint32_t height_minus_one = 1079; constexpr uint32_t depth_minus_one = 5; constexpr uint32_t max_mip = 3; constexpr uint32_t dimension_3d = 2; constexpr uint32_t attrib2_value = height_minus_one | (width_minus_one << 14u) | (max_mip << 28u); constexpr uint32_t attrib3_value = depth_minus_one | (static_cast(Prospero::TileMode::kStandard64KB) << 14u) | (dimension_3d << 24u) | (1u << 26u) | (1u << 30u); const bool color_packets_complete = write_context(packet, Pm4::CB_COLOR7_ATTRIB2, attrib2_value) && write_context(packet, Pm4::CB_COLOR7_ATTRIB3, attrib3_value); g_hw_ctx_indirect_func[Pm4::CB_COLOR7_ATTRIB2]( indirect, Pm4::CB_COLOR7_ATTRIB2, attrib2_value); g_hw_ctx_indirect_func[Pm4::CB_COLOR7_ATTRIB3]( indirect, Pm4::CB_COLOR7_ATTRIB3, attrib3_value); const auto color_matches = [](const HW::RenderTarget &target) { return target.attrib2.width == width_minus_one && target.attrib2.height == height_minus_one && target.attrib2.num_mip_levels == max_mip && target.attrib3.depth == depth_minus_one && target.attrib3.tile_mode == Prospero::TileMode::kStandard64KB && target.attrib3.dimension == dimension_3d && target.attrib3.metadata_pipe_aligned; }; Require("Pm4NativeTargetGeometry", "sparse color geometry", color_packets_complete && color_matches(packet.GetCtx().GetRenderTarget(color_slot)) && color_matches(indirect.GetCtx().GetRenderTarget(color_slot)) && !color_matches(packet.GetCtx().GetRenderTarget(0)) && !color_matches(indirect.GetCtx().GetRenderTarget(0)), "ATTRIB2/ATTRIB3 did not provide native color extent and layout"); constexpr uint32_t depth_size_value = width_minus_one | (height_minus_one << 16u); const bool depth_packet_complete = write_context(packet, Pm4::DB_DEPTH_SIZE_XY, depth_size_value); g_hw_ctx_indirect_func[Pm4::DB_DEPTH_SIZE_XY](indirect, Pm4::DB_DEPTH_SIZE_XY, depth_size_value); const auto depth_matches = [](const HW::DepthRenderTarget &target) { return target.size.valid && target.size.x_max == width_minus_one && target.size.y_max == height_minus_one; }; Require("Pm4NativeTargetGeometry", "native depth geometry", depth_packet_complete && depth_matches(packet.GetCtx().GetDepthRenderTarget()) && depth_matches(indirect.GetCtx().GetDepthRenderTarget()), "DB_DEPTH_SIZE_XY did not provide the native depth extent"); bool legacy_slots_are_unhandled = true; for (uint32_t slot = 0; slot < 8; slot++) { const auto stride = slot * 15u; const std::array legacy_color_slots{ Pm4::CB_COLOR0_BASE + stride + 1u, Pm4::CB_COLOR0_BASE + stride + 2u, Pm4::CB_COLOR0_CMASK + stride + 1u, Pm4::CB_COLOR0_FMASK + stride + 1u, }; for (const auto offset : legacy_color_slots) { legacy_slots_are_unhandled &= g_hw_ctx_func[offset] == nullptr && g_hw_ctx_indirect_func[offset] == nullptr; } } // These offsets are the removed GCN DB_DEPTH_INFO, DB_DEPTH_SIZE, and // DB_DEPTH_SLICE holes. Keep them numeric so deleting their legacy PM4 names // does not weaken this dispatch-table regression check. constexpr std::array legacy_depth_slots{0x000fu, 0x0016u, 0x0017u}; for (const auto offset : legacy_depth_slots) { legacy_slots_are_unhandled &= g_hw_ctx_func[offset] == nullptr && g_hw_ctx_indirect_func[offset] == nullptr; } // Removed GCN PA_SC_NGG_MODE_CNTL. legacy_slots_are_unhandled &= g_hw_ctx_func[0x0314u] == nullptr && g_hw_ctx_indirect_func[0x0314u] == nullptr; // Removed GCN shader resource/checksum/queue registers. Numeric offsets keep // this check independent of the deleted legacy names. constexpr std::array legacy_shader_slots{ 0x000u, 0x001u, 0x002u, 0x003u, 0x030u, 0x0b0u, 0x0bcu, 0x130u, 0x14au, 0x14bu, 0x20eu, 0x20fu, 0x210u, 0x211u, 0x216u, 0x217u, 0x219u, 0x21au, 0x27du, }; for (const auto offset : legacy_shader_slots) { legacy_slots_are_unhandled &= g_hw_sh_func[offset] == nullptr && g_hw_sh_indirect_func[offset] == nullptr; } // Restoring shader state after Toolkit rendering still emits the compiler's // GS-front allocation metadata at 0xca. for (uint32_t offset = 0x0cbu; offset <= 0x0ebu; offset++) { legacy_slots_are_unhandled &= g_hw_sh_func[offset] == nullptr && g_hw_sh_indirect_func[offset] == nullptr; } std::array index_size_packet{ KYTY_PM4(3, Pm4::IT_SET_UCONFIG_REG_INDEX, Pm4::R_ZERO), 0x20000000u | Pm4::VGT_INDEX_TYPE, 0x441u, }; Pm4Execution index_size_execution; const bool native_index_size_packet_complete = packet.Process(index_size_execution, index_size_packet) == Pm4ProcessResult::Complete && g_hw_uc_func[Pm4::VGT_INDEX_TYPE] != nullptr && g_hw_uc_indirect_func[Pm4::VGT_INDEX_TYPE] != nullptr && g_hw_uc_func[Pm4::IA_MULTI_VGT_PARAM] != nullptr && g_hw_uc_indirect_func[Pm4::IA_MULTI_VGT_PARAM] != nullptr; const bool native_pace_slots_are_handled = g_hw_sh_indirect_func[Pm4::SPI_SHADER_PACE_ID_PS] != nullptr && g_hw_sh_indirect_func[Pm4::SPI_SHADER_PACE_ID_GS] != nullptr && g_hw_sh_indirect_func[Pm4::COMPUTE_PACE_ID] != nullptr; Require("Pm4NativeTargetGeometry", "legacy register holes", legacy_slots_are_unhandled && native_pace_slots_are_handled && native_index_size_packet_complete, "a removed GCN register has a handler or a native packet is not handled"); std::printf("[host] %-32s ok\n", "Pm4NativeTargetGeometry"); } void CheckPm4PrivateAgcShaderRegisters(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); std::array registers{ Pm4::SPI_SHADER_PGM_RSRC4_GS, 0x0badc0deu, Pm4::SPI_SHADER_PGM_CHKSUM_HS, 0x12345678u, Pm4::SPI_SHADER_PGM_RSRC4_HS, 0x87654321u, Pm4::SPI_SHADER_PGM_LO_ES, 0x050025e5u, Pm4::SPI_SHADER_PGM_HI_ES, 0u, Pm4::SPI_SHADER_PGM_RSRC1_GS, 0x60000002u, Pm4::SPI_SHADER_PGM_RSRC2_GS, 0x00030008u, 0x000000cau, 0x03000002u, Pm4::SPI_SHADER_PGM_LO_PS, 0x05000104u, Pm4::SPI_SHADER_PGM_HI_PS, 0u, Pm4::SPI_SHADER_PGM_RSRC1_PS, 0x022c018du, Pm4::SPI_SHADER_PGM_RSRC2_PS, 0x0000003cu, }; const auto address = reinterpret_cast(registers.data()); std::array command{ KYTY_PM4(5, Pm4::IT_SET_SH_REG_INDIRECT, Pm4::R_ZERO), static_cast(address), static_cast(address >> 32u), 0x80000000u, static_cast(registers.size() / 2u), }; Pm4Execution execution; const bool handlers_present = g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_RSRC4_GS] != nullptr && g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_CHKSUM_HS] != nullptr && g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_RSRC4_HS] != nullptr; Require("Pm4PrivateAgcShaderRegisters", "private shader register stream", handlers_present && processor.Process(execution, command) == Pm4ProcessResult::Complete, "private AGC shader registers were rejected by SET_SH_REG_INDIRECT"); const auto &vs = processor.GetShCtx().GetVs(); const auto &ps = processor.GetShCtx().GetPs().ps_regs; Require("Pm4PrivateAgcShaderRegisters", "native state around front metadata", vs.es_regs.data_addr == 0x50025e500ull && vs.gs_regs.rsrc1.vgprs == 2u && vs.gs_regs.rsrc1.gs_vgpr_component_count == 3u && vs.gs_regs.rsrc2.es_vgpr_component_count == 3u && vs.gs_regs.rsrc2.user_sgpr == 4u && ps.data_addr == 0x500010400ull && ps.rsrc1.vgprs == 13u && ps.rsrc2.user_sgpr == 30u, "GS-front metadata changed native GS state or prevented later PS writes"); std::printf("[host] %-32s ok\n", "Pm4PrivateAgcShaderRegisters"); } void CheckPm4PrivateAgcUconfigRegisters(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); std::array registers{ Pm4::UC_PARAMETER_OVERSUBSCRIPTION, 0x12345678u, }; const auto address = reinterpret_cast(registers.data()); std::array command{ KYTY_PM4(5, Pm4::IT_SET_UCONFIG_REG_INDIRECT, Pm4::R_ZERO), static_cast(address), static_cast(address >> 32u), 0x80000000u, 1u, }; Pm4Execution execution; const bool handler_present = g_hw_uc_indirect_func[Pm4::UC_PARAMETER_OVERSUBSCRIPTION] != nullptr; Require("Pm4PrivateAgcUconfigRegisters", "private UCONFIG register stream", handler_present && processor.Process(execution, command) == Pm4ProcessResult::Complete, "private AGC UCONFIG register was rejected by SET_UCONFIG_REG_INDIRECT"); std::printf("[host] %-32s ok\n", "Pm4PrivateAgcUconfigRegisters"); } void CheckPm4DirectShaderRegisterFallback(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); constexpr uint64_t shader_address = 0x0000123456789a00ull; std::array packet{ KYTY_PM4(4, Pm4::IT_SET_SH_REG, Pm4::R_ZERO), Pm4::SPI_SHADER_PGM_LO_ES, static_cast(shader_address >> 8u), static_cast(shader_address >> 40u), }; Pm4Execution execution; Require("Pm4DirectShaderRegisterFallback", "ES program base", processor.Process(execution, packet) == Pm4ProcessResult::Complete && processor.GetShCtx().GetVs().es_regs.data_addr == shader_address, "direct SET_SH_REG did not route its ES base pair through the " "register handlers"); std::printf("[host] %-32s ok\n", "Pm4DirectShaderRegisterFallback"); } void CheckPm4GuardBandRegisterRanges(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); processor.GetCtx().SetGuardBands(2.0f, 3.0f, 4.0f, 5.0f); std::array single{ KYTY_PM4(3, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::PA_CL_GB_VERT_CLIP_ADJ, std::bit_cast(6.0f), }; Pm4Execution single_execution; const auto single_result = processor.Process(single_execution, single); const auto &single_viewport = processor.GetCtx().GetScreenViewport(); Require("Pm4GuardBandRegisterRanges", "single register", single_result == Pm4ProcessResult::Complete && single_viewport.guard_band_horz_clip == 2.0f && single_viewport.guard_band_vert_clip == 6.0f && single_viewport.guard_band_horz_discard == 4.0f && single_viewport.guard_band_vert_discard == 5.0f, "single guard-band write changed the wrong context state"); std::array range{ KYTY_PM4(6, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::PA_CL_GB_VERT_CLIP_ADJ, std::bit_cast(7.0f), std::bit_cast(8.0f), std::bit_cast(9.0f), std::bit_cast(10.0f), }; Pm4Execution range_execution; const auto range_result = processor.Process(range_execution, range); const auto &range_viewport = processor.GetCtx().GetScreenViewport(); Require("Pm4GuardBandRegisterRanges", "full range", range_result == Pm4ProcessResult::Complete && range_viewport.guard_band_horz_clip == 9.0f && range_viewport.guard_band_vert_clip == 7.0f && range_viewport.guard_band_horz_discard == 10.0f && range_viewport.guard_band_vert_discard == 8.0f, "full guard-band range did not preserve hardware register order"); std::printf("[host] %-32s ok\n", "Pm4GuardBandRegisterRanges"); } void CheckPm4BlendColorRegisterRanges(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); processor.GetCtx().SetBlendColor({1.0f, 2.0f, 3.0f, 4.0f}); constexpr std::array, 4> singleton_values{{ {Pm4::CB_BLEND_RED, 5.0f}, {Pm4::CB_BLEND_BLUE, 7.0f}, {Pm4::CB_BLEND_GREEN, 6.0f}, {Pm4::CB_BLEND_ALPHA, 8.0f}, }}; bool singletons_complete = true; for (const auto &[offset, value] : singleton_values) { std::array packet{ KYTY_PM4(3, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), offset, std::bit_cast(value), }; Pm4Execution execution; singletons_complete &= processor.Process(execution, packet) == Pm4ProcessResult::Complete; } const auto &singletons = processor.GetCtx().GetBlendColor(); Require("Pm4BlendColorRegisterRanges", "single registers", singletons_complete && singletons.red == 5.0f && singletons.green == 6.0f && singletons.blue == 7.0f && singletons.alpha == 8.0f, "single blend-color writes did not follow native PS5 register order"); std::array partial{ KYTY_PM4(4, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::CB_BLEND_BLUE, std::bit_cast(9.0f), std::bit_cast(10.0f), }; Pm4Execution partial_execution; const auto partial_result = processor.Process(partial_execution, partial); const auto &partial_color = processor.GetCtx().GetBlendColor(); Require("Pm4BlendColorRegisterRanges", "partial range", partial_result == Pm4ProcessResult::Complete && partial_color.red == 5.0f && partial_color.green == 10.0f && partial_color.blue == 9.0f && partial_color.alpha == 8.0f, "partial blend-color range changed the wrong channels"); std::array full{ KYTY_PM4(6, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::CB_BLEND_RED, std::bit_cast(11.0f), std::bit_cast(13.0f), std::bit_cast(12.0f), std::bit_cast(14.0f), }; Pm4Execution full_execution; const auto full_result = processor.Process(full_execution, full); const auto &full_color = processor.GetCtx().GetBlendColor(); Require("Pm4BlendColorRegisterRanges", "full range", full_result == Pm4ProcessResult::Complete && full_color.red == 11.0f && full_color.green == 12.0f && full_color.blue == 13.0f && full_color.alpha == 14.0f, "full blend-color range did not preserve native PS5 register order"); std::printf("[host] %-32s ok\n", "Pm4BlendColorRegisterRanges"); } void CheckPm4PolygonOffsetRegisters(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); const auto &defaults = processor.GetCtx().GetPolyOffset(); Require("Pm4PolygonOffset", "format defaults", defaults.neg_num_db_bits == -23 && defaults.db_is_float_fmt, "polygon offset Z format did not default to D32F"); constexpr std::array payload{0x000000f0u, std::bit_cast(2.5f), std::bit_cast(16.0f), std::bit_cast(-3.25f), std::bit_cast(32.0f), std::bit_cast(4.5f)}; const auto consumed = HwCtxSetPolyOffsetRegisters( processor, KYTY_PM4(8, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::PA_SU_POLY_OFFSET_DB_FMT_CNTL, payload.data(), 0); const auto &direct = processor.GetCtx().GetPolyOffset(); Require("Pm4PolygonOffset", "direct registers", consumed == payload.size() && direct.neg_num_db_bits == -16 && !direct.db_is_float_fmt && direct.clamp == 2.5f && direct.front_scale == 16.0f && direct.front_offset == -3.25f && direct.back_scale == 32.0f && direct.back_offset == 4.5f, "direct polygon offset register packet was decoded incorrectly"); g_hw_ctx_indirect_func[Pm4::PA_SU_POLY_OFFSET_DB_FMT_CNTL]( processor, Pm4::PA_SU_POLY_OFFSET_DB_FMT_CNTL, 0x000001e9u); g_hw_ctx_indirect_func[Pm4::PA_SU_POLY_OFFSET_FRONT_SCALE]( processor, Pm4::PA_SU_POLY_OFFSET_FRONT_SCALE, std::bit_cast(24.0f)); const auto &indirect = processor.GetCtx().GetPolyOffset(); Require("Pm4PolygonOffset", "indirect registers", indirect.neg_num_db_bits == -23 && indirect.db_is_float_fmt && indirect.front_scale == 24.0f && indirect.front_offset == -3.25f, "indirect polygon offset register write was decoded incorrectly"); std::printf("[host] %-32s ok\n", "Pm4PolygonOffset"); } void CheckPm4DepthControlHighBits(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); std::array direct{ KYTY_PM4(3, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::DB_DEPTH_CONTROL, 0x80000072u, }; Pm4Execution direct_execution; const bool direct_complete = processor.Process(direct_execution, direct) == Pm4ProcessResult::Complete; const auto &direct_control = processor.GetCtx().GetDepthControl(); const bool direct_decoded = !direct_control.stencil_enable && direct_control.z_enable && !direct_control.z_write_enable && !direct_control.depth_bounds_enable && direct_control.zfunc == 7u && !direct_control.backface_enable && direct_control.stencilfunc == 0u && direct_control.stencilfunc_bf == 0u; std::array registers{ Pm4::DB_DEPTH_CONTROL, 0x4070078du, }; const auto address = reinterpret_cast(registers.data()); std::array indirect{ KYTY_PM4(5, Pm4::IT_SET_CONTEXT_REG_INDIRECT, Pm4::R_ZERO), static_cast(address), static_cast(address >> 32u), 0x80000000u, 1u, }; Pm4Execution indirect_execution; const bool indirect_complete = processor.Process(indirect_execution, indirect) == Pm4ProcessResult::Complete; const auto &indirect_control = processor.GetCtx().GetDepthControl(); const bool indirect_decoded = indirect_control.stencil_enable && !indirect_control.z_enable && indirect_control.z_write_enable && indirect_control.depth_bounds_enable && indirect_control.zfunc == 0u && indirect_control.backface_enable && indirect_control.stencilfunc == 7u && indirect_control.stencilfunc_bf == 7u; Require("Pm4DepthControlHighBits", "preserved high bits", direct_complete && direct_decoded && indirect_complete && indirect_decoded, "DB_DEPTH_CONTROL high bits rejected or changed decoded PS5 state"); std::printf("[host] %-32s ok\n", "Pm4DepthControlHighBits"); } void CheckPm4DepthRenderOverride(RenderContext &renderer) { constexpr const char *name = "Pm4DepthRenderOverride"; GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); struct Case { uint32_t value; HW::DepthRenderOverride expected; }; constexpr std::array cases{Case{0x28000000u, {true, true, false, false}}, Case{0x50000000u, {false, false, true, true}}, Case{0x18000000u, {false, true, false, true}}, Case{0x60000000u, {true, false, true, false}}, Case{0xf8000000u, {true, true, true, true}}, Case{0x82001a0fu, {false, false, false, false}}}; for (const bool indirect : {false, true}) { for (const auto &test : cases) { std::array registers{Pm4::DB_RENDER_OVERRIDE, test.value}; const auto address = reinterpret_cast(registers.data()); const std::array direct_packet{ KYTY_PM4(3, Pm4::IT_SET_CONTEXT_REG, Pm4::R_ZERO), Pm4::DB_RENDER_OVERRIDE, test.value}; const std::array indirect_packet{ KYTY_PM4(5, Pm4::IT_SET_CONTEXT_REG_INDIRECT, Pm4::R_ZERO), static_cast(address), static_cast(address >> 32u), 0x80000000u, 1u}; Pm4Execution execution; const auto result = processor.Process( execution, indirect ? std::span{indirect_packet} : std::span{direct_packet}); const auto &actual = processor.GetCtx().GetDepthRenderOverride(); Require( name, indirect ? "indirect register" : "direct register", result == Pm4ProcessResult::Complete && actual.force_z_valid == test.expected.force_z_valid && actual.force_z_dirty == test.expected.force_z_dirty && actual.force_stencil_valid == test.expected.force_stencil_valid && actual.force_stencil_dirty == test.expected.force_stencil_dirty, "DB_RENDER_OVERRIDE lost, exchanged, or retained force flags"); } } std::printf("[host] %-32s ok\n", name); } void CheckAgcShaderFusion() { struct Case { bool gs; uint32_t front1, front2, back1, back2, fused1, fused2, older2; }; // Register results from both exports in the supplied libSceAgc.sprx. constexpr Case cases[]{ {true, 0x600c0007, 0x00030010, 0x600c0007, 0x008b0000, 0x600c0007, 0x008b0010, 0x008b0010}, // Captured volume ES + GS. {true, 0x20ac0003, 0xf805004a, 0x40ec0007, 0x028b003e, 0x40ec0007, 0x1a8f000a, 0xfa8f000a}, {false, 0x20ac0003, 0xf805004a, 0x10ec0007, 0x028b003e, 0x20ec0007, 0x1a8b000a, 0xfa8b000a}, }; ShaderUserData user_data{}; ShaderSpecialRegs specials{}; for (const auto &c : cases) { const auto checksum = c.gs ? Pm4::SPI_SHADER_PGM_CHKSUM_GS : Pm4::SPI_SHADER_PGM_CHKSUM_HS; const auto rsrc1 = c.gs ? Pm4::SPI_SHADER_PGM_RSRC1_GS : Pm4::SPI_SHADER_PGM_RSRC1_HS; const auto rsrc2 = c.gs ? Pm4::SPI_SHADER_PGM_RSRC2_GS : Pm4::SPI_SHADER_PGM_RSRC2_HS; const auto lo = c.gs ? Pm4::SPI_SHADER_PGM_LO_ES : Pm4::SPI_SHADER_PGM_LO_LS; std::array front_regs{{ {checksum, 0x1234}, {checksum, 0x5678}, {rsrc1, c.front1}, {rsrc2, c.front2}, {lo, 0}, {lo + 1u, 0}}}; const std::array back_regs{{ {checksum, 0}, {checksum, 0}, {rsrc1, c.back1}, {rsrc2, c.back2}, {lo, 0}, {lo + 1u, 0xabcdef00}}}; Shader front{}; front.user_data = &user_data; front.code = reinterpret_cast(0x12580e91c00ull); front.sh_registers = front_regs.data(); front.specials = &specials; front.type = static_cast(c.gs ? Prospero::ShaderBinaryType::kGsFront : Prospero::ShaderBinaryType::kHsFront); front.num_sh_registers = front_regs.size(); for (const bool older : {false, true}) { for (const bool in_place : {false, true}) { auto registers = back_regs; std::array scratch{}; Shader back = front; back.sh_registers = registers.data(); back.type = static_cast(c.gs ? Prospero::ShaderBinaryType::kGsBack : Prospero::ShaderBinaryType::kHsBack); Shader fused{}; const auto fuse = older ? Gen5::AgcUnknownNApJjpKNBl4 : Gen5::AgcUnknownFuseShaderHalves; const auto result = fuse(&fused, &front, &back, in_place ? nullptr : scratch.data()); const auto *regs = fused.sh_registers; Require("AgcShaderFusion", "native fused registers", result == 0 && regs == (in_place ? registers.data() : scratch.data()) && regs[0].value == 0x1234 && regs[1].value == 0x5678 && regs[2].value == c.fused1 && regs[3].value == (older ? c.older2 : c.fused2) && regs[4].value == 0x2580e91c && regs[5].value == 0xabcdef01 && fused.type == static_cast(c.gs ? Prospero::ShaderBinaryType::kGs : Prospero::ShaderBinaryType::kHs) && fused.user_data == (older ? &user_data : nullptr) && (in_place || std::memcmp(registers.data(), back_regs.data(), sizeof(registers)) == 0), "AGC lost front resources or changed the export's allocation/metadata behavior"); } } } std::printf("[host] %-32s ok\n", "AgcShaderFusion"); } struct AgcCommandBufferLayout { using Callback = KYTY_SYSV_ABI bool (*)(Gen5::CommandBuffer *, uint32_t, void *); uint32_t *bottom; uint32_t *top; uint32_t *cursor_up; uint32_t *cursor_down; Callback callback; void *user_data; uint32_t reserved_dw; }; static_assert(offsetof(AgcCommandBufferLayout, cursor_up) == 0x10); static_assert(offsetof(AgcCommandBufferLayout, reserved_dw) == 0x30); void CheckAgcWaitPackets(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); constexpr auto packet_mismatch = static_cast(0x8a6c000cu); const auto execute = [&](uint32_t *packet, uint32_t size_dw) { Pm4Execution execution; return processor.Process(execution, {packet, size_dw}) == Pm4ProcessResult::Complete; }; alignas(8) uint32_t label32 = 0x11223344u; std::array packet32{}; AgcCommandBufferLayout dcb32{packet32.data(), packet32.data() + packet32.size(), packet32.data(), packet32.data() + packet32.size(), nullptr, nullptr, 0}; const auto address32 = reinterpret_cast(&label32); auto *emitted32 = Gen5::AgcDcbWaitRegMem(reinterpret_cast(&dcb32), 0, 3, 0, 0, &label32, label32, UINT32_MAX, 400); Require("AgcWaitPackets", "native 32-bit packet", Gen5::AgcDcbWaitOnAddressGetSize(0) == 56 && emitted32 == packet32.data() && dcb32.cursor_up == packet32.data() + 14 && packet32[0] == 0xc0027904u && packet32[1] == 0x342u && packet32[2] == (0xc8010000u | (static_cast(address32 >> 32u) & 0xffffu)) && packet32[3] == static_cast(address32) && packet32[4] == 0xc0053c00u && packet32[5] == 0x13u && packet32[6] == (static_cast(address32) & ~0x3u) && packet32[7] == (static_cast(address32 >> 32u) & 0x3ffffu) && packet32[8] == label32 && packet32[9] == UINT32_MAX && packet32[10] == 25u && packet32[11] == 0xc0017904u && packet32[12] == 0x342u && packet32[13] == 0xc8000000u && execute(packet32.data(), 14), "32-bit waitOnAddress stream differs from native AGC"); alignas(8) uint32_t patched_label32 = 0xaabbccddu; const auto patched_address32 = reinterpret_cast(&patched_label32); packet32[6] |= 0x2u; packet32[7] |= 0xa5a40000u; Require( "AgcWaitPackets", "native 32-bit patch", Gen5::AgcWaitRegMemPatchAddress(packet32.data(), &patched_label32) == 0 && Gen5::AgcWaitRegMemPatchReference(packet32.data(), patched_label32) == 0 && packet32[2] == (0xc8010000u | (static_cast(patched_address32 >> 32u) & 0xffffu)) && packet32[3] == static_cast(patched_address32) && packet32[6] == ((static_cast(patched_address32) & ~0x3u) | 0x2u) && packet32[7] == ((static_cast(patched_address32 >> 32u) & 0x3ffffu) | 0xa5a40000u) && packet32[8] == patched_label32 && execute(packet32.data(), 14), "32-bit native wait patch did not update metadata and hardware packet"); alignas(8) uint64_t label64 = 0x1122334455667788ull; std::array packet64{}; AgcCommandBufferLayout dcb64{packet64.data(), packet64.data() + packet64.size(), packet64.data(), packet64.data() + packet64.size(), nullptr, nullptr, 0}; const auto address64 = reinterpret_cast(&label64); auto *emitted64 = Gen5::AgcDcbWaitRegMem(reinterpret_cast(&dcb64), 1, 3, 0, 0, &label64, label64, UINT64_MAX, 400); Require("AgcWaitPackets", "native 64-bit packet", Gen5::AgcDcbWaitOnAddressGetSize(1) == 64 && Gen5::AgcDcbWaitOnAddressGetSize(2) == 0 && emitted64 == packet64.data() && dcb64.cursor_up == packet64.data() + packet64.size() && packet64[0] == 0xc0027904u && packet64[1] == 0x342u && packet64[2] == (0xc8020000u | (static_cast(address64 >> 32u) & 0xffffu)) && packet64[3] == static_cast(address64) && packet64[4] == 0xc0079300u && packet64[5] == 0x13u && packet64[6] == (static_cast(address64) & ~0x7u) && packet64[7] == (static_cast(address64 >> 32u) & 0x3ffffu) && packet64[8] == static_cast(label64) && packet64[9] == static_cast(label64 >> 32u) && packet64[10] == UINT32_MAX && packet64[11] == UINT32_MAX && packet64[12] == 25u && packet64[13] == 0xc0017904u && packet64[14] == 0x342u && packet64[15] == 0xc8000000u && execute(packet64.data(), packet64.size()), "64-bit waitOnAddress stream differs from native AGC"); alignas(8) uint64_t patched_label64 = 0x11223344aabbccddull; const auto patched_address64 = reinterpret_cast(&patched_label64); packet64[6] |= 0x5u; packet64[7] |= 0x5a580000u; Require( "AgcWaitPackets", "native 64-bit patch", Gen5::AgcWaitRegMemPatchAddress(packet64.data(), &patched_label64) == 0 && Gen5::AgcWaitRegMemPatchReference(packet64.data(), 0xdeadbeefaabbccddull) == 0 && packet64[2] == (0xc8020000u | (static_cast(patched_address64 >> 32u) & 0xffffu)) && packet64[3] == static_cast(patched_address64) && packet64[6] == ((static_cast(patched_address64) & ~0x7u) | 0x5u) && packet64[7] == ((static_cast(patched_address64 >> 32u) & 0x3ffffu) | 0x5a580000u) && packet64[8] == 0xaabbccddu && packet64[9] == 0x11223344u && execute(packet64.data(), packet64.size()), "64-bit native wait patch did not preserve the native high reference " "DWORD"); std::array invalid{}; const uint32_t short_payload = 0x342u; Require( "AgcWaitPackets", "invalid packet", Gen5::AgcWaitRegMemPatchAddress(invalid.data(), &label32) == packet_mismatch && Gen5::AgcWaitRegMemPatchReference(invalid.data(), label32) == packet_mismatch && !Gen5::AgcIsInternalDataPacket( KYTY_PM4(2, Pm4::IT_SET_UCONFIG_REG, 1), &short_payload), "native wait patch accepted a packet without the AGC metadata prefix"); std::printf("[host] %-32s ok\n", "AgcWaitPackets"); } void CheckAgcDrawIndirectMultiPacket(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); const auto execute = [&](uint32_t *packet, uint32_t size_dw) { Pm4Execution execution; return processor.Process(execution, {packet, size_dw}) == Pm4ProcessResult::Complete; }; constexpr uint64_t modifier = 0x0000000069380b1dull; constexpr auto packet_mismatch = static_cast(0x8a6c000cu); const auto *count_address = reinterpret_cast( static_cast(0x123456789abcdef3ull)); constexpr std::array expected{ 0xc0017904u, 0x00000342u, 0xc6000008u, 0xc0082c00u, 0x11223344u, 0x00000111u, 0x00000113u, 0xc8000115u, 0x55667788u, 0x9abcdef0u, 0x12345678u, 0xaabbccddu, 0x00000022u, 0xc0017904u, 0x00000342u, 0xc6000000u}; std::array packet{}; AgcCommandBufferLayout dcb{packet.data(), packet.data() + packet.size(), packet.data(), packet.data() + packet.size(), nullptr, nullptr, 0}; auto *emitted = Gen5::AgcDcbDrawIndirectMulti( reinterpret_cast(&dcb), 0x11223344u, 1u, 0x55667788u, count_address, 0xaabbccddu, modifier); const uint32_t invalid_payload[]{0x342u, 0xc6000010u}; Require("AgcDrawIndirectMulti", "native packet", emitted == packet.data() && dcb.cursor_up == packet.data() + packet.size() && packet == expected && execute(packet.data(), 3u) && execute(packet.data() + 13u, 3u) && Gen5::AgcWaitRegMemPatchAddress(packet.data(), count_address) == packet_mismatch && Gen5::AgcWaitRegMemPatchReference(packet.data(), 0x12345678u) == packet_mismatch && !Gen5::AgcIsInternalDataPacket(0xc0017904u, invalid_payload), "drawIndirectMulti stream differs from native AGC"); std::printf("[host] %-32s ok\n", "AgcDrawIndirectMulti"); } void CheckPm4ContextStateOperations(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); constexpr auto primitive_type = static_cast(0x35u); HW::RenderControl original_control{}; original_control.depth_clear_enable = true; original_control.stencil_clear_enable = true; processor.GetCtx().SetRenderControl(original_control); processor.GetCtx().SetRenderTargetMask(0x1234abcd); processor.GetUcfg().SetPrimitiveType(primitive_type); g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_LO_PS]( processor, Pm4::SPI_SHADER_PGM_LO_PS, 0x00123456); const auto shader_address = processor.GetShCtx().GetPs().ps_regs.data_addr; const auto invoke = [&](ContextStateOperation operation) { std::array packet{}; std::array segment_sizes{}; AgcCommandBufferLayout dcb{packet.data(), packet.data() + packet.size(), packet.data(), packet.data() + packet.size(), nullptr, nullptr, 0}; const auto operation_value = static_cast(operation); size_t segment_count = 0; switch (operation) { case ContextStateOperation::Clear: segment_sizes = {5}; segment_count = 1; break; case ContextStateOperation::Push: segment_sizes = {5, 8, 9, 3, 2}; segment_count = 5; break; case ContextStateOperation::Pop: segment_sizes = {3, 5, 8, 9, 2}; segment_count = 5; break; case ContextStateOperation::PushClear: segment_sizes = {5, 8, 9, 3, 2, 5}; segment_count = 6; break; } const auto size_dw = static_cast( Gen5::AgcDcbContextStateOpGetSize(operation_value) / 4); auto *emitted = Gen5::AgcDcbContextStateOp( reinterpret_cast(&dcb), operation_value); bool packet_matches = true; size_t segment_offset = 0; for (size_t i = 0; i < segment_count; i++) { const auto segment_size = segment_sizes[i]; const auto selector = (i == 0 ? Pm4::R_CONTEXT_STATE : Pm4::R_ZERO); packet_matches &= packet[segment_offset] == KYTY_PM4(segment_size, Pm4::IT_NOP, selector); for (size_t j = 1; j < segment_size; j++) { packet_matches &= packet[segment_offset + j] == (i == 0 && j == 1 ? operation_value : 0u); } segment_offset += segment_size; } Require("Pm4ContextState", "HLE packet", emitted == packet.data() && dcb.cursor_up == packet.data() + size_dw && segment_offset == size_dw && packet_matches, "context-state HLE packet does not match its real allocation size"); Pm4Execution execution; return processor.Process(execution, {packet.data(), size_dw}); }; Require("Pm4ContextState", "push-clear", invoke(ContextStateOperation::PushClear) == Pm4ProcessResult::Complete && !processor.GetCtx().GetRenderControl().depth_clear_enable && !processor.GetCtx().GetRenderControl().stencil_clear_enable && processor.GetCtx().GetRenderTargetMask() == HW::Context{}.GetRenderTargetMask() && processor.GetUcfg().GetPrimType() == primitive_type && processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address, "push-clear did not save and clear only Cx state"); processor.GetCtx().SetRenderTargetMask(0x55aa55aa); Require("Pm4ContextState", "pop", invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderControl().depth_clear_enable && processor.GetCtx().GetRenderControl().stencil_clear_enable && processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, "pop did not restore the complete saved Cx state"); Require("Pm4ContextState", "push", invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, "push changed live Cx state"); processor.GetCtx().SetRenderTargetMask(0x01020304); Require("Pm4ContextState", "push-pop restore", invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, "ordinary push/pop did not restore Cx state"); Require("Pm4ContextState", "clear", invoke(ContextStateOperation::Clear) == Pm4ProcessResult::Complete && !processor.GetCtx().GetRenderControl().depth_clear_enable && processor.GetCtx().GetRenderTargetMask() == HW::Context{}.GetRenderTargetMask() && processor.GetUcfg().GetPrimType() == primitive_type && processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address, "clear reset state outside the Cx register domain"); processor.GetCtx().SetRenderTargetMask(0x0badc0de); Require("Pm4ContextState", "push before clear-state packet", invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete, "push before CLEAR_STATE failed"); processor.GetCtx().SetRenderTargetMask(0xfeedface); std::array clear_state_packet{0xc0001200, 0}; Pm4Execution clear_state_execution; Require("Pm4ContextState", "clear-state packet", processor.Process(clear_state_execution, clear_state_packet) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderTargetMask() == HW::Context{}.GetRenderTargetMask(), "CLEAR_STATE did not clear the current Cx state"); Require("Pm4ContextState", "pop after clear-state packet", invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderTargetMask() == 0x0badc0de, "CLEAR_STATE discarded the pushed Cx state"); // AGC CLEAR_STATE restores the viewport, scissor, and guard-band register defaults. processor.GetCtx().SetScreenScissor(1, 2, 3, 4); processor.GetCtx().SetGuardBands(2.0f, 3.0f, 4.0f, 5.0f); Pm4Execution scissor_reset; Require("Pm4ContextState", "scissor reset packet", processor.Process(scissor_reset, clear_state_packet) == Pm4ProcessResult::Complete, "CLEAR_STATE could not reset the rasterization state"); const auto &viewport = processor.GetCtx().GetScreenViewport(); const auto defaults = calc_final_scissor(viewport, {}, {20000, 20000}, 0); Require("Pm4ContextState", "Rasterization defaults", defaults.left == 0 && defaults.top == 0 && defaults.right == 16384 && defaults.bottom == 16384 && viewport.guard_band_horz_clip == 1.0f && viewport.guard_band_vert_clip == 1.0f && viewport.guard_band_horz_discard == 1.0f && viewport.guard_band_vert_discard == 1.0f && std::ranges::all_of(viewport.viewports, [](const auto &slot) { return slot.xscale == 1.0f && slot.yscale == 1.0f && slot.zscale == 1.0f && slot.viewport_scissor_right == 16384 && slot.viewport_scissor_bottom == 16384 && !slot.viewport_scissor_window_offset_enable; }), "CLEAR_STATE did not restore scissor and guard-band defaults"); const std::array scissor_registers{ std::pair{Pm4::PA_SC_SCREEN_SCISSOR_TL, Pm4::PA_SC_SCREEN_SCISSOR_BR}, std::pair{Pm4::PA_SC_WINDOW_SCISSOR_TL, Pm4::PA_SC_WINDOW_SCISSOR_BR}, std::pair{Pm4::PA_SC_GENERIC_SCISSOR_TL, Pm4::PA_SC_GENERIC_SCISSOR_BR}, std::pair{Pm4::PA_SC_VPORT_SCISSOR_0_TL, Pm4::PA_SC_VPORT_SCISSOR_0_BR}}; for (const auto &[tl, br] : scissor_registers) { processor.GetCtx().Reset(); g_hw_ctx_indirect_func[tl](processor, tl, 0); g_hw_ctx_indirect_func[br](processor, br, 0); HW::ScanModeControl scan{}; scan.vport_scissor_enable = tl == Pm4::PA_SC_VPORT_SCISSOR_0_TL; const auto empty = calc_final_scissor(processor.GetCtx().GetScreenViewport(), scan, {3840, 2160}, 0); Require("Pm4ContextState", "explicit empty scissor", empty.right == empty.left && empty.bottom == empty.top, "an explicitly empty scissor was expanded or ignored"); } processor.GetCtx().SetRenderTargetMask(0xabcdef01); Require("Pm4ContextState", "push before processor reset", invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete, "push before reset failed"); processor.Reset(); processor.GetCtx().SetRenderTargetMask(0x76543210); Require("Pm4ContextState", "processor reset discards push", invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete && invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && processor.GetCtx().GetRenderTargetMask() == 0x76543210, "processor reset retained an invalid saved Cx state"); Require("Pm4ContextState", "HLE packet size", Gen5::AgcDcbContextStateOpGetSize(0) == 20 && Gen5::AgcDcbContextStateOpGetSize(1) == 108 && Gen5::AgcDcbContextStateOpGetSize(2) == 108 && Gen5::AgcDcbContextStateOpGetSize(3) == 128 && Gen5::AgcDcbContextStateOpGetSize(4) == 0, "context-state HLE sizes do not match libSceAgc"); std::printf("[host] %-32s ok\n", "Pm4ContextState"); } void CheckPm4IndirectControlFlow(RenderContext &renderer) { constexpr const char *name = "Pm4IndirectControlFlow"; GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); uint32_t selected = 0; uint32_t returned = 0; const auto address = [](const void *value) { return reinterpret_cast(value); }; const auto write = [&](uint32_t *destination, uint32_t value) { return std::array{ KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0), 0, static_cast(address(destination)), static_cast(address(destination) >> 32u), value}; }; const auto indirect = [&](std::span target, bool chain) { uint32_t control = 0x0f200000u | static_cast(target.size()); if (chain) { control |= 1u << 20u; } return std::array{ KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0), static_cast(address(target.data())), static_cast(address(target.data()) >> 32u), control}; }; const auto then_commands = write(&selected, 11); const auto else_commands = write(&selected, 33); const auto branch_suffix = write(&selected, 44); const auto caller_suffix = write(&returned, 22); alignas(8) uint64_t condition = 0; std::array branch{ KYTY_PM4(14, Pm4::IT_INDIRECT_BUFFER, 0), 0, static_cast(address(&condition)), static_cast(address(&condition) >> 32u), UINT32_MAX, UINT32_MAX, 1, 0, static_cast(address(then_commands.data())), static_cast(address(then_commands.data()) >> 32u), static_cast(then_commands.size()), static_cast(address(else_commands.data())), static_cast(address(else_commands.data()) >> 32u), static_cast(else_commands.size())}; std::copy(branch_suffix.begin(), branch_suffix.end(), branch.begin() + 14); std::array caller{}; const auto call_branch = indirect(branch, false); std::copy(call_branch.begin(), call_branch.end(), caller.begin()); std::copy(caller_suffix.begin(), caller_suffix.end(), caller.begin() + 4); for (uint32_t mode : {1u, 2u}) { branch[1] = mode | (3u << 8u); for (uint64_t value : {0ull, 1ull}) { condition = value; selected = returned = 0; uint32_t expected = 44; if (value == 1) { expected = 11; } else if (mode == 2) { expected = 33; } Pm4Execution execution; Require(name, "conditional fetcher replacement", processor.Process(execution, caller) == Pm4ProcessResult::Complete && selected == expected && returned == 22, "taken branch resumed its discarded suffix or lost the caller's return"); } } // This stream exceeds native recursion capacity, while chains need one fetcher cursor. std::vector> links(65536); std::span target = then_commands; for (auto &link : links) { link = indirect(target, true); target = link; } const auto call_chain = indirect(target, false); std::copy(call_chain.begin(), call_chain.end(), caller.begin()); selected = returned = 0; Pm4Execution execution; Require(name, "long chain and call return", processor.Process(execution, caller) == Pm4ProcessResult::Complete && selected == 11 && returned == 22, "long chain did not complete and return to the original caller"); std::printf("[host] %-32s ok\n", name); } void CheckPm4WaitResume(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); uint32_t label = 0; uint32_t prefix = 0; uint32_t child_observation = UINT32_MAX; uint32_t suffix = 0; const auto address = [](const void *value) { return reinterpret_cast(value); }; std::array child{}; child[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); child[1] = 0; child[2] = static_cast(address(&child_observation)); child[3] = static_cast(address(&child_observation) >> 32u); child[4] = 0; child[5] = KYTY_PM4(7, Pm4::IT_WAIT_REG_MEM, 0); child[6] = 0x10u | 3u; child[7] = static_cast(address(&label)); child[8] = static_cast(address(&label) >> 32u); child[9] = 1; child[10] = UINT32_MAX; std::array nested{}; nested[0] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0); nested[1] = static_cast(address(child.data())); nested[2] = static_cast(address(child.data()) >> 32u); nested[3] = 0x0f300000u | static_cast(child.size()); std::array commands{}; commands[0] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[1] = 0; commands[2] = static_cast(address(&prefix)); commands[3] = static_cast(address(&prefix) >> 32u); commands[4] = 11; commands[5] = KYTY_PM4(4, Pm4::IT_INDIRECT_BUFFER, 0); commands[6] = static_cast(address(nested.data())); commands[7] = static_cast(address(nested.data()) >> 32u); commands[8] = 0x0f200000u | static_cast(nested.size()); commands[9] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[10] = 0; commands[11] = static_cast(address(&suffix)); commands[12] = static_cast(address(&suffix) >> 32u); commands[13] = 22; Pm4Execution execution; Require("Pm4WaitResume", "suspend", processor.Process(execution, commands) == Pm4ProcessResult::Blocked && prefix == 11 && child_observation == 0 && suffix == 0, "blocked indirect wait did not preserve its command position"); Require("Pm4WaitResume", "still blocked", processor.Process(execution, commands) == Pm4ProcessResult::Blocked && !execution.MadeProgress() && suffix == 0, "blocked wait advanced or replayed its caller"); label = 1; child[4] = 1; Require( "Pm4WaitResume", "resume", processor.Process(execution, commands) == Pm4ProcessResult::Complete && prefix == 11 && child_observation == 0 && suffix == 22, "resumed indirect wait replayed a child or did not finish its parent"); std::printf("[host] %-32s ok\n", "Pm4WaitResume"); } void CheckPm4RewindResume(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); uint32_t suffix = 0; const auto address = reinterpret_cast(&suffix); std::array commands{}; commands[0] = KYTY_PM4(2, Pm4::IT_REWIND, 0); commands[1] = 0; commands[2] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[3] = 0; commands[4] = static_cast(address); commands[5] = static_cast(address >> 32u); commands[6] = 33; Pm4Execution execution; Require("Pm4RewindResume", "pending", processor.Process(execution, commands) == Pm4ProcessResult::Blocked && suffix == 0, "pending rewind did not preserve its command position"); Require("Pm4RewindResume", "patch valid", Gen5::AgcRewindPatchSetRewindState(commands.data(), 1) == 0 && commands[1] == 0x80000000u, "rewind state patch did not set the valid bit"); Require("Pm4RewindResume", "resume", processor.Process(execution, commands) == Pm4ProcessResult::Complete && suffix == 33, "valid rewind did not resume at the following packet"); std::printf("[host] %-32s ok\n", "Pm4RewindResume"); } void CheckPm4CeCompletion(RenderContext &renderer) { GraphicsInitJmpTables(); CommandProcessor processor(renderer, 0); uint32_t suffix = 0; const auto address = reinterpret_cast(&suffix); std::array commands{}; commands[0] = 0xc0008600u; commands[1] = 1; commands[2] = 0xc0008500u; commands[3] = 0; commands[4] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0); commands[5] = 0; commands[6] = static_cast(address); commands[7] = static_cast(address >> 32u); commands[8] = 33; processor.ResetDeCe(); Pm4Execution execution; Require("Pm4CeCompletion", "wait", processor.Process(execution, commands) == Pm4ProcessResult::Blocked && suffix == 0, "DE did not wait for an active CE stream"); std::array ce_increment{0xc0008400u, 1}; Pm4Execution ce_execution; Require("Pm4CeCompletion", "CE increment", processor.Process(ce_execution, ce_increment) == Pm4ProcessResult::Complete, "CE counter increment did not complete"); Require( "Pm4CeCompletion", "counter gate", processor.Process(execution, commands) == Pm4ProcessResult::Complete && suffix == 33, "DE did not resume after CE advanced or failed to increment its counter"); suffix = 0; Pm4Execution balanced_execution; Require("Pm4CeCompletion", "balanced wait", processor.Process(balanced_execution, commands) == Pm4ProcessResult::Blocked && suffix == 0, "balanced CE/DE counters did not gate the next DE packet"); processor.SetCeComplete(true); Require("Pm4CeCompletion", "stream complete", processor.Process(balanced_execution, commands) == Pm4ProcessResult::Complete && suffix == 33, "DE remained blocked after the CE stream completed"); std::printf("[host] %-32s ok\n", "Pm4CeCompletion"); } } // namespace } // namespace Libs::Graphics int main(int argc, char **argv) { using namespace Libs::Graphics; std::setvbuf(stdout, nullptr, _IONBF, 0); EnsureConfigInitialized(); CheckLeastRecentlyUsedCacheOrdering(); if (argc == 2 && std::strcmp(argv[1], "--packed-integer-neg-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop3pIntegerNegationCapturedAndSelectedHalves()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--dpp-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorDpp8Captured(false)); RunCase(&vulkan, VectorDpp8Captured(true)); RunCase(&vulkan, VectorDppQuadPermuteReverse()); RunCase(&vulkan, VectorDppRowXmask()); RunCase(&vulkan, VectorDppBankMaskPreservesDestination()); RunCase(&vulkan, VectorDppBoundsControlZeroPreservesDestination()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--buffer-snorm-store-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, BufferStoreFormatXyzwSnorm16CapturedSkinningVectors()); RunCase(&vulkan, BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords()); RunCase(&vulkan, BufferStoreFormatXResource16UintWritesHalfword()); RunCase(&vulkan, BufferStoreFormatXResource16UintPreservesAdjacentLanes()); RunCase(&vulkan, BufferStoreFormatXyResource88UintWritesBytes()); RunCase(&vulkan, BufferStoreFormatXyzwDropsPartialRecord()); RunCase(&vulkan, BufferStoreFormatXChecksOnlyTransferredComponent()); RunCase(&vulkan, BufferFormatStoreVariants()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--s-ashr-i64-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, ScalarAshrI64Edges(false)); RunCase(&vulkan, ScalarAshrI64Edges(true)); RunCase(&vulkan, ScalarAshrI64OperandsAndAliases()); RunCase(&vulkan, ScalarAshrI64Masks(32)); RunCase(&vulkan, ScalarAshrI64Masks(64)); return 0; } if (argc == 2 && std::strcmp(argv[1], "--tessellation-only") == 0) { CheckTessellationPrograms(); CheckEmbeddedFetchVertexOffset(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--cmpx-lt-u16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorVopcCmpxLtU16CapturedSdwaExecMask()); RunCase(&vulkan, VectorVopcCmpxGtU16CapturedSdwaExecMask()); RunCase(&vulkan, VectorVopcSdwaCmpxWritesExecMask()); RunCase(&vulkan, VectorVop3CmpxWritesExecMask()); RunCase(&vulkan, VectorVopcSdwaCmpxClassF32CapturedExecMask()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--cmp-ngt-f16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorVopcCmpNgtF16CapturedSdwaAndEdges()); RunCase(&vulkan, VectorVopcCmpxNgtF16CapturedSdwaExecMask()); RunCase(&vulkan, VectorCompareF16Ops()); RunCase(&vulkan, Wave32VccMasksPreserveOtherHalf()); RunCase(&vulkan, VectorVop3FloatCompareNegSourceModifier()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--cmp-nlt-f16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorVopcCmpNltF16CapturedSdwaAndEdges()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--fract-f16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorFractF16CapturedAndEdges()); RunCase(&vulkan, VectorFractF16Modifiers()); RunCase(&vulkan, NativeAndSdwa16BitDestinationWrites()); RunCase(&vulkan, VectorSpecialF16Ops()); RunCase(&vulkan, VectorCosF16CapturedSdwaAndEdges()); RunCase(&vulkan, VectorSinF16SdwaAndEdges()); RunCase(&vulkan, VectorFloatConversionOps()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) { VulkanHarness vulkan; vulkan.CheckPackedTextureComponents(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--error-dialog-only") == 0) { CheckErrorDialogLifecycle(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--fmask-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, ImageLoadFmaskUsesNativeSampleMapping()); RunCase(&vulkan, ImageLoadR32UintUsesIntegerSampledImage()); return 0; } #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS if (argc == 2 && std::strcmp(argv[1], "--shader-fatal-only") == 0) { CheckShaderRecompilerFatalContracts(); return 0; } #endif if (argc == 2 && std::strcmp(argv[1], "--wave64-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Wave32VccMasksPreserveOtherHalf()); RunCase(&vulkan, DsBpermuteWave64UsesIndependentHalves()); RunCase(&vulkan, Wave64CrossHalfLaneAndLds()); RunCase(&vulkan, Wave64RawMasksAndScalarBranch()); RunCase(&vulkan, Wave64PartialMultidimensionalWorkgroup()); RunCase(&vulkan, Wave64AppendConsumeHighHalf()); RunCase(&vulkan, VectorAddcWritesPerLaneCarryOut()); RunCase(&vulkan, VectorVop3BCarryOutWritesSgprMask()); RunCase(&vulkan, VectorVop3BCarryOutUsesEncodedSdst()); RunCase(&vulkan, DispatcherIrreducibleControlFlow()); RunCase(&vulkan, ScalarSaveExecOps()); RunCase(&vulkan, ScalarOrn2SaveexecUsesSourceOrNotExec()); RunCase(&vulkan, ScalarOrn2SaveexecB32(32, 32)); RunCase(&vulkan, ScalarOrn2SaveexecB32(64, 64)); RunCase(&vulkan, ScalarOrn2SaveexecB32(32, 4)); RunCase(&vulkan, ScalarSubvectorLoops(32)); RunCase(&vulkan, ScalarSubvectorLoops(64)); RunCase(&vulkan, ScalarNotB64UpdatesScc()); RunCase(&vulkan, ScalarSelectB64PreservesMaskProvenance()); RunCase(&vulkan, ScalarConditionalMoveB64()); RunCase(&vulkan, ScalarConditionalMoveB64PreservesMasks()); RunCase(&vulkan, ScalarWqmB64SelectsSccDomain()); RunCase(&vulkan, ScalarWqmB64PreservesPartialMasks()); RunCase(&vulkan, ScalarWqmB32Masks(32)); RunCase(&vulkan, ScalarWqmB32Masks(64)); RunCase(&vulkan, ScalarMaskProvenanceOverlapAndMixedBinary()); RunCase(&vulkan, ScratchIsPrivatePerInvocation()); RunCase(&vulkan, Vop1MoveRelDestination()); RunCase(&vulkan, BranchVccnzUsesWaveMask()); RunCase(&vulkan, BranchVccnzUsesCarryProducedWaveMask()); RunCase(&vulkan, ImageSampleAndGather()); RunCase(&vulkan, ImageGatherLodApproximatesLevelZero()); RunCase(&vulkan, SharedReturnKeepsSelectedValues()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--position-w-only") == 0) { VulkanHarness vulkan; RunGraphicsCase(&vulkan, GraphicsPositionWExport()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--centroid-only") == 0) { VulkanHarness vulkan; RunGraphicsCase(&vulkan, GraphicsPackedHalfCentroid()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--pixel-alias-only") == 0) { CheckPixelParameterAliases(); CheckRectListShaders(); VulkanHarness vulkan; RunGraphicsCase(&vulkan, GraphicsPackedHalfCentroid()); RunGraphicsCase(&vulkan, GraphicsPackedHalfInputAlias(false)); RunGraphicsCase(&vulkan, GraphicsPackedHalfInputAlias(true)); RunGraphicsCase(&vulkan, GraphicsSmoothRawInputAlias()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--clip-control-only") == 0) { CheckClipControlDepthClipState(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--reference-clock-only") == 0) { CheckReferenceClockScale(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--scheduler-only") == 0) { VulkanHarness vulkan; vulkan.CheckSchedulerTimeline(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--host-image-allocation-only") == 0) { VulkanHarness vulkan; vulkan.CheckHostImageAllocation(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--occlusion-dump-only") == 0) { VulkanHarness vulkan; CheckPm4SyntheticOcclusionCounterDump(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--predication-only") == 0) { VulkanHarness vulkan; CheckPm4Predication(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--descriptor-heap-only") == 0) { VulkanHarness vulkan; vulkan.CheckDescriptorHeapLargeSet(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--push-constant-bank-only") == 0) { VulkanHarness vulkan; vulkan.CheckGraphicsPushConstantBank(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--shader-data-storage-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, BufferOffsetsUsePackedLaneAndStorageFallback()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--buffer-cmpswap-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, BufferAtomicCmpSwapExactRaw()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--ds-atomics-only") == 0) { VulkanHarness vulkan; for (const auto &test : MakeCases()) { if (std::string_view(test.name).starts_with("Ds") || std::string_view(test.name).starts_with("BufferAtomic")) { RunCase(&vulkan, test); } } return 0; } if (argc == 2 && std::strcmp(argv[1], "--mapped-range-only") == 0) { VulkanHarness vulkan; vulkan.CheckGpuMappedRangeLifecycle(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--stream-buffer-only") == 0) { VulkanHarness vulkan; vulkan.CheckStreamBufferRing(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--gpu-tiler-only") == 0) { VulkanHarness vulkan; vulkan.CheckGpuTilerCpuParity(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--gpu-command-lane-only") == 0) { VulkanHarness vulkan; vulkan.CheckGpuCommandLane(); CheckPm4IndirectControlFlow(vulkan.RuntimeRenderer()); CheckPm4WaitResume(vulkan.RuntimeRenderer()); CheckPm4RewindResume(vulkan.RuntimeRenderer()); CheckPm4CeCompletion(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--alignbyte-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorAlignByteUsesFiveBitByteOffset()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--sdwa-mov-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop1SdwaMovByteDestinations()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--sdwa-ashr-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop2SdwaAshrrevCapturedWord0SignExtends()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--sdwa-addc-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, VectorAddcWritesPerLaneCarryOut()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--cvt-pk-i16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop3CvtPkI16I32Captured()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--med3-i16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop3Med3I16Captured()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--mul-lo-u16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop3MulLoU16CapturedAndSelectors()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--mad-i16-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop3MadI16CapturedSelectorsAndSaturation()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--waitcnt-depctr-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, ScalarWaitcntDepctrCapturedVmVsrc()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--sff1-b64-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, ScalarBitfieldPack()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--context-state-only") == 0) { VulkanHarness vulkan; CheckAgcShaderFusion(); CheckPm4NativeTargetGeometryRegisters(vulkan.RuntimeRenderer()); CheckPm4PrivateAgcShaderRegisters(vulkan.RuntimeRenderer()); CheckPm4PrivateAgcUconfigRegisters(vulkan.RuntimeRenderer()); CheckPm4BlendColorRegisterRanges(vulkan.RuntimeRenderer()); CheckPm4PolygonOffsetRegisters(vulkan.RuntimeRenderer()); CheckPm4DepthControlHighBits(vulkan.RuntimeRenderer()); CheckPm4DepthRenderOverride(vulkan.RuntimeRenderer()); CheckPm4ContextStateOperations(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--agc-wait-only") == 0) { VulkanHarness vulkan; CheckAgcWaitPackets(vulkan.RuntimeRenderer()); CheckPm4WaitResume(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--agc-draw-multi-only") == 0) { VulkanHarness vulkan; CheckAgcDrawIndirectMultiPacket(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--rewind-only") == 0) { VulkanHarness vulkan; CheckPm4RewindResume(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--image-overlap-only") == 0) { CheckDepthAttachmentWrites(); CheckDynamicRenderingState(); VulkanHarness vulkan; vulkan.CheckRasterization(false); vulkan.CheckRenderExecutorColorDiscovery(); vulkan.CheckRenderExecutorColorVolumeDiscovery(); vulkan.CheckRenderExecutorDccFixedClearFloat(); vulkan.CheckSampledDccClear(); vulkan.CheckRenderExecutorColorDepthTileDiscovery(); vulkan.CheckRenderExecutorStencilBindingDiscovery(); vulkan.CheckUnifiedTextureCacheFlow(); vulkan.CheckBgra16Readback(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--draw-offset-only") == 0) { CheckEmbeddedFetchVertexOffset(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--polygon-mode-only") == 0) { VulkanHarness vulkan; vulkan.CheckRasterization(false); vulkan.CheckRasterization(false, true); return 0; } if (argc == 2 && std::strcmp(argv[1], "--htile-clear-only") == 0) { VulkanHarness vulkan; vulkan.CheckUnifiedTextureCacheFlow(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--dcc-clear-only") == 0) { VulkanHarness vulkan; vulkan.CheckRenderExecutorDccFixedClearFloat(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--compute-meta-clear-only") == 0) { CheckDynamicRenderingState(); VulkanHarness vulkan; vulkan.CheckComputeMetaClearClassification(); vulkan.CheckRenderExecutorColorVolumeDiscovery(); vulkan.CheckRenderExecutorDccFixedClearFloat(); vulkan.CheckSampledDccClear(); vulkan.CheckRenderExecutorStencilBindingDiscovery(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--layered-image-only") == 0) { VulkanHarness vulkan; vulkan.CheckUnifiedImageViewCache(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--image-view-cache-only") == 0) { VulkanHarness vulkan; vulkan.CheckUnifiedImageViewCache(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--storage-sampled-only") == 0) { VulkanHarness vulkan; vulkan.CheckUnifiedImageViewCache(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--depth-readback-only") == 0) { CheckDepthTargetFootprints(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-range-only") == 0) { VulkanHarness vulkan; vulkan.CheckUnifiedTextureCacheFlow(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-gc-only") == 0) { VulkanHarness vulkan; vulkan.CheckBufferCacheDirtyGarbageCollection(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--sampled-depth-resource-only") == 0) { VulkanHarness vulkan; CheckSampledDepthResource(); CheckDepthTextureEncoding(); vulkan.CheckComparisonDepthTexture(); vulkan.CheckRasterization(true); RunCase(nullptr, ImageSampleA16CompareBiasRdna2AddressOrder()); return 0; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-death") == 0) { RunReverseRenderTargetDeathCase(); } if (argc == 2 && std::strcmp(argv[1], "--reverse-rt-only") == 0) { CheckRenderTargetFormatContract(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--standard64-rt-only") == 0) { CheckStandard64RenderTargetTileRoundTrip(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--standard-tile-rt-only") == 0) { VulkanHarness vulkan; vulkan.CheckRenderExecutorColorStandardTileDiscovery(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--image-view-only") == 0) { VulkanHarness vulkan; CheckSampledColorViews(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--image-transition-only") == 0) { VulkanHarness vulkan; CheckImageTransitionState(vulkan.RuntimeRenderer()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--storage-bgra-only") == 0) { CheckSampledColorViews(); CheckBasicStorageTextureDescriptor(); VulkanHarness vulkan; vulkan.CheckUnifiedImageViewCache(); RunCase(&vulkan, ImageStoreBgraUsesInverseSwizzle()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) { CheckImageSamplerSpecialization(); CheckIndirectImageKeySwitch(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--storage-mip-host-only") == 0) { VulkanHarness vulkan; vulkan.CheckRenderExecutorStencilBindingDiscovery(); return 0; } if (argc == 2 && std::strcmp(argv[1], "--storage-mip-only") == 0) { RunCase(nullptr, ImageStoreMipSelectsPpsa01340Descriptor()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--storage-yzwx-only") == 0) { CheckSampledColorViews(); CheckBasicStorageTextureDescriptor(); CheckStorageTextureGpuOwnedRebindState(); VulkanHarness vulkan; RunCase(&vulkan, ImageStoreYzwxUsesInverseSwizzle()); return 0; } if (argc == 3 && std::strcmp(argv[1], "--image-view-death") == 0) { RunImageViewDeathCase(argv[2]); } if (argc == 3 && std::strcmp(argv[1], "--storage-texture-descriptor-death") == 0) { RunStorageTextureDescriptorDeathCase(argv[2]); } if (argc != 1) { std::fprintf(stderr, "unknown test selector: %s\n", argv[1]); return 2; } VulkanHarness vulkan; CheckRenderTargetFormatContract(); CheckSampledColorViews(); CheckImageTransitionState(vulkan.RuntimeRenderer()); CheckSampledDepthResource(); CheckDepthTextureEncoding(); vulkan.CheckComparisonDepthTexture(); vulkan.CheckRasterization(true); CheckBasicStorageTextureDescriptor(); CheckStorageTextureLinearUploadLayout(); CheckStorageTextureDepthTileUploadLayout(); CheckStandard64RenderTargetTileRoundTrip(); CheckStorageTextureVolumeUploadLayout(); CheckStorageTextureVolumeMipRegions(); CheckStorageTextureGpuOwnedRebindState(); CheckNativeMsaaState(); CheckPs5DepthRegisterDecoding(); CheckDepthHtileStencilCompatibility(); CheckDepthAttachmentWrites(); CheckDynamicRenderingState(); CheckDepthTargetFootprints(); CheckSlotVectorLifetime(); #else if (argc != 1) { std::fprintf(stderr, "unknown test selector: %s\n", argv[1]); return 2; } CheckShaderRecompilerFatalContracts(); VulkanHarness vulkan; #endif CheckImageSamplerSpecialization(); CheckNativeImageDescriptorTypes(); CheckClipControlDepthClipState(); CheckReferenceClockScale(); CheckErrorDialogLifecycle(); CheckVulkan13FeatureRequirements(); CheckPm4AcquireMemNoOp(vulkan.RuntimeRenderer()); CheckPm4SyntheticOcclusionCounterDump(vulkan.RuntimeRenderer()); CheckPm4Predication(vulkan.RuntimeRenderer()); CheckPm4StencilInfoValueLane(vulkan.RuntimeRenderer()); CheckPm4NativeTargetGeometryRegisters(vulkan.RuntimeRenderer()); CheckPm4PrivateAgcShaderRegisters(vulkan.RuntimeRenderer()); CheckPm4PrivateAgcUconfigRegisters(vulkan.RuntimeRenderer()); CheckPm4DirectShaderRegisterFallback(vulkan.RuntimeRenderer()); CheckPm4GuardBandRegisterRanges(vulkan.RuntimeRenderer()); CheckPm4BlendColorRegisterRanges(vulkan.RuntimeRenderer()); CheckPm4PolygonOffsetRegisters(vulkan.RuntimeRenderer()); CheckPm4DepthControlHighBits(vulkan.RuntimeRenderer()); CheckPm4DepthRenderOverride(vulkan.RuntimeRenderer()); CheckAgcShaderFusion(); CheckAgcWaitPackets(vulkan.RuntimeRenderer()); CheckAgcDrawIndirectMultiPacket(vulkan.RuntimeRenderer()); CheckPm4ContextStateOperations(vulkan.RuntimeRenderer()); CheckPm4IndirectControlFlow(vulkan.RuntimeRenderer()); CheckPm4WaitResume(vulkan.RuntimeRenderer()); CheckPm4RewindResume(vulkan.RuntimeRenderer()); CheckPm4CeCompletion(vulkan.RuntimeRenderer()); CheckEmbeddedFetchVertexOffset(); CheckEmbeddedFetchLaneSpill(); CheckTessellationPrograms(); CheckPixelParameterAliases(); CheckRectListShaders(); CheckIndirectImageKeySwitch(); CheckPs5GameExampleImageClearRuntimeShape(); vulkan.CheckSchedulerTimeline(); vulkan.CheckHostImageAllocation(); vulkan.CheckDescriptorHeapLargeSet(); vulkan.CheckGraphicsPushConstantBank(); vulkan.CheckGpuMappedRangeLifecycle(); vulkan.CheckStreamBufferRing(); vulkan.CheckGpuTilerCpuParity(); #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS vulkan.CheckRenderExecutorColorDiscovery(); vulkan.CheckRenderExecutorColorVolumeDiscovery(); vulkan.CheckRenderExecutorDccFixedClearFloat(); vulkan.CheckSampledDccClear(); vulkan.CheckRenderExecutorColorStandardTileDiscovery(); vulkan.CheckRenderExecutorColorDepthTileDiscovery(); vulkan.CheckRenderExecutorStencilBindingDiscovery(); vulkan.CheckUnifiedTextureCacheFlow(); vulkan.CheckBgra16Readback(); vulkan.CheckRasterization(false); vulkan.CheckRasterization(false, true); vulkan.CheckBufferCacheDirtyGarbageCollection(); #endif vulkan.CheckUnifiedImageViewCache(); vulkan.CheckPackedTextureComponents(); const auto tests = MakeCases(); const auto graphics_tests = MakeGraphicsCases(); CheckOpcodeCoverage(tests, graphics_tests); for (const auto &test : tests) { RunCase(&vulkan, test); } for (const auto &test : graphics_tests) { RunGraphicsCase(&vulkan, test); } vulkan.CheckGpuCommandLane(); std::printf("ShaderRecompilerComputeTests: all cases passed\n"); return 0; }