Merge pull request #131 from oneandonlydean/fix/agc-2d-over-array-storage

fix(agc): bind a 2D storage access to a 2D array surface as its first layer
This commit is contained in:
boykopovar 2026-10-01 10:40:20 +03:00 committed by GitHub
commit 00a18dc10f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 7 deletions

View File

@ -390,6 +390,7 @@ private:
std::vector<std::shared_ptr<Texture>> textures;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<bool> storageFirstLayer;
std::vector<bool> storageWritten;
std::vector<std::shared_ptr<Sampler>> samplers;
bool reusable = false;

View File

@ -109,6 +109,7 @@ public:
// The image holds every mip of the surface; one storage view per written mip is made on demand,
// so successive mip writes of a chain share one image and one write-back.
VkImageView View(std::uint32_t mip);
VkImageView FirstLayerView(std::uint32_t mip);
// Render targets live in the same images: draws attach mip 0 through a view of the color
// buffer's format and mark the image dirty like a storage write.
bool Attachable() const { return attachable; }
@ -363,7 +364,7 @@ private:
void forgetBorrowed(std::uint32_t first, std::uint32_t count);
bool overlaps(std::uint64_t address, std::size_t bytes) const;
bool pendingUnitInside(std::uint64_t address, std::size_t bytes) const;
VkImageView createView(std::uint32_t mip) const;
VkImageView createView(std::uint32_t mip, bool firstLayer = false) const;
void release() noexcept;
Context context;
@ -403,6 +404,7 @@ private:
VkImageView view = VK_NULL_HANDLE;
std::uint32_t defaultMip = 0;
std::map<std::uint32_t, VkImageView> extraViews;
std::map<std::uint32_t, VkImageView> firstLayerViews;
bool attachable = false;
std::map<std::pair<VkFormat, std::uint32_t>, VkImageView> attachmentViews;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;

View File

@ -974,7 +974,7 @@ void ShaderResources::buildComplete() {
break;
case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL});
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageFirstLayer[index] ? storageTextures[index]->FirstLayerView(storageMips[index]) : storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL});
break;
case VK_DESCRIPTOR_TYPE_SAMPLER:
write.pImageInfo = images.data() + images.size();
@ -2391,7 +2391,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
const auto* record = nextRecord();
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
Require(MatchesGuestDimension(*binding.imageShape, resource.dimension), "guest texture dimension disagrees with the shader's declared image shape");
if (!MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)};
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
std::shared_ptr<Texture> texture;
@ -2418,7 +2418,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
else mipOffset = 0;
const auto* record = nextRecord();
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
if (binding.imageShape.has_value()) Require(MatchesGuestDimension(*binding.imageShape, resource.dimension), "guest storage texture dimension disagrees with the shader's declared image shape");
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
if (binding.imageShape.has_value() && !firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest storage texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u);
Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented");
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
@ -2426,6 +2427,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back());
else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes));
storageMips.push_back(mip);
storageFirstLayer.push_back(firstLayer);
// Images the shader only reads have nothing to store back.
storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]);
describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});

View File

@ -879,13 +879,14 @@ bool AdjacentGenerationEnabled() {
}
VkImageView StorageTexture::createView(std::uint32_t mip) const {
VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer) const {
Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture");
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface");
const auto viewLayerCount = firstLayer ? 1u : geometry.imageLayers - descriptor.baseArray;
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
// Storage views address one mip; cube faces are written as array layers.
viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : descriptor.dimension == TextureDimension::k2D ? VK_IMAGE_VIEW_TYPE_2D : descriptor.dimension == TextureDimension::k3D ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
viewInfo.viewType = firstLayer ? VK_IMAGE_VIEW_TYPE_2D : descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : descriptor.dimension == TextureDimension::k2D ? VK_IMAGE_VIEW_TYPE_2D : descriptor.dimension == TextureDimension::k3D ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
viewInfo.format = storageFormat;
viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, descriptor.baseArray, viewLayerCount};
@ -923,6 +924,14 @@ VkImageView StorageTexture::View(std::uint32_t mip) {
return created;
}
VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) {
const auto found = firstLayerViews.find(mip);
if (found != firstLayerViews.end()) return found->second;
const auto created = createView(mip, true);
firstLayerViews.emplace(mip, created);
return created;
}
const char* LookupOutcomes::Name(Kind kind) {
static constexpr const char* names[Count] = {"sampled fast hit", "sampled fast miss", "sampled hit view", "sampled hit cleared view", "sampled hit snapshot", "sampled made view", "sampled made snapshot", "storage hit", "storage made", "refresh unchanged", "refresh compared", "upload direct", "upload cpu", "upload clear", "dcc scan", "pending flush"};
return kind < Count ? names[kind] : "?";
@ -3217,6 +3226,8 @@ StorageTexture::~StorageTexture() {
void StorageTexture::release() noexcept {
for (const auto& [mip, extra] : extraViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, extra, nullptr);
extraViews.clear();
for (const auto& [mip, extra] : firstLayerViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, extra, nullptr);
firstLayerViews.clear();
for (const auto& [format, attachment] : attachmentViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, attachment, nullptr);
attachmentViews.clear();
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);

View File

@ -1721,6 +1721,36 @@ void minLodTests(const Device& device, Recorder& recorder) {
expectRed(sample(0x100, 0.0f, 1), unorm(1), "minimum LOD clamp: MIN_LOD at the view's base level reads its base level");
}
void firstLayerViewTests(const Device& device, Recorder& recorder) {
const auto& context = device.GetContext();
TextureDetiler detiler(context);
auto withDetiler = context;
withDetiler.detiler = &detiler;
GuestTextureResource resource{};
resource.width = 64;
resource.height = 4;
resource.depthOrLastArray = 2;
resource.baseArray = 1;
resource.mipCount = 1;
resource.tileMode = TextureTileMode::kLinear;
resource.dimension = TextureDimension::k2DArray;
resource.format = 56;
resource.dstSelX = 4;
resource.dstSelY = 5;
resource.dstSelZ = 6;
resource.dstSelW = 7;
const auto geometry = DescribeSurface(resource);
std::vector<std::uint8_t> memory(static_cast<std::size_t>(geometry.guestBytes) + 256);
auto* surface = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(memory.data()) + 255) & ~std::uintptr_t{255});
for (std::uint32_t layer = 0; layer < 3; ++layer) std::memset(surface + geometry.GuestLayerOffset(layer), 0x20 * (layer + 1), static_cast<std::size_t>(geometry.layerBytes));
resource.baseAddress = reinterpret_cast<std::uint64_t>(surface);
auto image = std::make_shared<StorageTexture>(withDetiler, detiler, resource, 0);
recorder.Keep(image);
SampleProgram program(context, recorder);
expectRed(program.Red(image->FirstLayerView(0), VK_IMAGE_LAYOUT_GENERAL, 0.0f), 0x40 / 255.0f, "a first-layer view does not read the BASE_ARRAY layer");
Require(image->FirstLayerView(0) == image->FirstLayerView(0), "first-layer views are not reused");
}
int main() {
try {
Device device;
@ -1748,6 +1778,7 @@ int main() {
dataWordPositionsTests();
dataRefreshTests(device, recorder);
minLodTests(device, recorder);
firstLayerViewTests(device, recorder);
std::cout << "Recorder read tracking and label tests passed\n";
return 0;
} catch (const std::exception& error) {