From efde918a628bf746e64b7084024b5ca43d4e8aad Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Fri, 2 Oct 2026 19:50:00 +0200 Subject: [PATCH] feat(graphics): mipmapped 3D textures 3D textures with more than one mip level were refused. Each slab of blockDepth slices of a thick (SW_4KB_S / SW_64KB_S) volume now holds the whole mip chain, smallest level first and level 0 last, as 2D chains are laid out. A level's depth halves per level, so the texture loops skip the slices a level does not have. Thin 3D (D/R 64KB_X) chains use the 2D mip layout per depth slice. Thick mip tails still throw. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/TextureTiling.hpp | 11 ++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 25 +++++-- .../Graphics/src/TextureTiling.cpp | 73 ++++++++++++------- .../libSceAgcDriver/tests/TextureTiling.cpp | 23 ++++++ 4 files changed, 95 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index aad8c0ddc..691baa8aa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURETILING_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include #include #include #include @@ -34,17 +35,18 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin // row (TileMipLayout), except the tail mips, which share one block. Throws for linear tiling. std::array ThinBlockLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement); -// Single-mip 3D surface. Each depth slice z is detiled from slab z / blockDepth (slabBytes apart) with the -// swizzle's slice input set to z, into linear slices sliceLinearBytes apart. +// 3D surface. Each slab holds the whole mip chain, smallest level first. Depth slice z of a level is detiled +// from slab z / blockDepth (slabBytes apart) with the swizzle's slice input set to z, into linear +// slices sliceLinearBytes apart. struct ThickLayout { - TileMipLayout mip; + std::vector mips; std::uint32_t depth; std::uint32_t blockDepth; std::uint64_t slabBytes; std::uint64_t sliceLinearBytes; std::uint64_t guestBytes; }; -ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth); +ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth, std::uint32_t mipCount); // Guest surface as the texture upload/write-back loops walk it: array layers of thin surfaces, or the // depth slices of a volume (which Vulkan holds as one layer with extent depth). @@ -63,6 +65,7 @@ struct SurfaceGeometry { std::uint64_t LinearLayerOffset(std::uint32_t layer) const { return static_cast(layer) * sliceLinearBytes; } std::uint32_t CopyLayer(std::uint32_t layer) const { return imageDepth > 1 ? 0u : layer; } std::int32_t CopyDepth(std::uint32_t layer) const { return imageDepth > 1 ? static_cast(layer) : 0; } + bool HasLayer(std::uint32_t level, std::uint32_t layer) const { return imageDepth <= 1 || layer < std::max(imageDepth >> level, 1u); } }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 89c02d55a..962cbccef 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -326,7 +326,9 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { const auto guestLayerOffset = geometry.GuestLayerOffset(layer); const auto linearLayerOffset = geometry.LinearLayerOffset(layer); - for (const auto& mip : mips) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick); } } @@ -356,6 +358,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { const auto linearLayerOffset = static_cast(layer) * sliceLinearBytes; for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; VkBufferImageCopy region{}; region.bufferOffset = linearLayerOffset + mip.linearOffset; @@ -1435,7 +1438,9 @@ void StorageTexture::upload(const std::vector* layers) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { if (layers != nullptr && !(*layers)[layer]) continue; - for (const auto& mip : mips) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; detiler.Dispatch(commands, descriptor.tileMode, elementBytes, import->buffer, importOffset + geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick); } } @@ -1484,7 +1489,9 @@ void StorageTexture::upload(const std::vector* layers) { CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { - for (const auto& mip : mips) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, mip, false, layer, geometry.thick); } } @@ -1534,6 +1541,7 @@ std::vector StorageTexture::sliceWindows(std::span< for (const auto& [runBegin, runEnd] : runs) { for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; const auto sliceBegin = geometry.GuestLayerOffset(layer) + mip.tiledOffset; const auto sliceEnd = sliceBegin + mip.tiledSize; @@ -1614,6 +1622,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< std::vector> tailBlocks; for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; if (!mip.tail) continue; const auto sliceBegin = geometry.GuestLayerOffset(layer) + mip.tiledOffset; @@ -1912,6 +1921,7 @@ std::vector StorageTexture::CopyRegions(const std::vector regions; for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + if (!geometry.HasLayer(level, layer)) continue; VkImageCopy region{}; region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, geometry.CopyLayer(layer), 1}; region.srcOffset = {0, 0, geometry.CopyDepth(layer)}; @@ -3258,7 +3269,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 1, &linearRead, 0, nullptr); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { if (storedLayers != nullptr && !(*storedLayers)[layer]) continue; - for (const auto& mip : mips) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiledScratch->Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick); } } @@ -3349,7 +3362,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const VkBufferMemoryBarrier toShader[] = {WholeBufferBarrier(linear.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT), WholeBufferBarrier(tiled.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT)}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 2, toShader, 0, nullptr); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { - for (const auto& mip : mips) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto& mip = mips[level]; detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 656239486..847432717 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -255,35 +255,54 @@ std::array ThinBlockLayout(TextureTileMode tileMode, std::uint return {block.blockSize, block.blockWidth, block.blockHeight}; } -ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth) { +ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth, std::uint32_t mipCount) { Require(width != 0 && height != 0 && depth != 0, "cannot compute layout for a zero-sized 3D texture"); + Require(mipCount != 0 && mipCount <= 16u, "3D texture mip count is out of range"); const auto bytesPerElement = BytesPerElement(format); Require(BlockWidth(format) == 1u && BlockHeight(format) == 1u, "block-compressed 3D textures are not implemented"); ThickLayout result{}; result.depth = depth; - auto& mip = result.mip; - mip.width = width; - mip.height = height; - mip.tail = false; - if (tileMode == TextureTileMode::kLinear) { - const auto paddedWidth = AlignUp(width, CalcLinearBlockWidth(bytesPerElement)); - mip.blocksPerRow = paddedWidth; - mip.pitchBytes = paddedWidth * bytesPerElement; - mip.tiledSize = static_cast(mip.pitchBytes) * height; - result.blockDepth = 1; - result.slabBytes = mip.tiledSize; - } else { - const auto block = ThickBlockExtent(tileMode, bytesPerElement); - const auto paddedWidth = AlignUp(width, block[0]); - const auto paddedHeight = AlignUp(height, block[1]); - mip.blocksPerRow = paddedWidth / block[0]; - mip.pitchBytes = paddedWidth * bytesPerElement; - result.blockDepth = block[2]; - result.slabBytes = static_cast(mip.blocksPerRow) * (paddedHeight / block[1]) * (tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u); - mip.tiledSize = result.slabBytes; + result.mips.resize(mipCount); + std::array block{CalcLinearBlockWidth(bytesPerElement), 1u, 1u}; + std::uint32_t blockBytes = 0; + if (tileMode != TextureTileMode::kLinear) { + block = ThickBlockExtent(tileMode, bytesPerElement); + blockBytes = tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u; + const auto log2BlockBytes = static_cast(std::countr_zero(blockBytes)); + const auto tailWidth = log2BlockBytes % 3u == 1u ? block[0] >> 1u : block[0]; + const auto tailHeight = log2BlockBytes % 3u == 0u ? block[1] >> 1u : block[1]; + const auto effectiveLog2 = log2BlockBytes - (log2BlockBytes - 8u) / 3u; + const auto maxTailLevels = effectiveLog2 <= 11u ? 1u + (1u << (effectiveLog2 - 9u)) : effectiveLog2 - 4u; + for (std::uint32_t level = 0; mipCount > 1 && level < mipCount; ++level) + Require(std::max(width >> level, 1u) > tailWidth || std::max(height >> level, 1u) > tailHeight || mipCount - level > maxTailLevels, "3D texture mip tails are not implemented"); } - mip.linearSize = static_cast(mip.pitchBytes) * height; - result.sliceLinearBytes = mip.linearSize; + result.blockDepth = block[2]; + std::uint64_t tiledOffset = 0; + std::uint64_t linearOffset = 0; + for (auto level = mipCount; level-- > 0;) { + auto& mip = result.mips[level]; + mip.width = std::max(width >> level, 1u); + mip.height = std::max(height >> level, 1u); + mip.tail = false; + mip.tailX = 0; + mip.tailY = 0; + const auto paddedWidth = AlignUp(mip.width, block[0]); + mip.pitchBytes = paddedWidth * bytesPerElement; + if (tileMode == TextureTileMode::kLinear) { + mip.blocksPerRow = paddedWidth; + mip.tiledSize = static_cast(mip.pitchBytes) * mip.height; + } else { + mip.blocksPerRow = paddedWidth / block[0]; + mip.tiledSize = static_cast(mip.blocksPerRow) * (AlignUp(mip.height, block[1]) / block[1]) * blockBytes; + } + mip.linearSize = static_cast(mip.pitchBytes) * mip.height; + mip.tiledOffset = tiledOffset; + mip.linearOffset = linearOffset; + tiledOffset += mip.tiledSize; + linearOffset += mip.linearSize; + } + result.slabBytes = tiledOffset; + result.sliceLinearBytes = linearOffset; result.guestBytes = result.slabBytes * ((depth + result.blockDepth - 1u) / result.blockDepth); return result; } @@ -291,9 +310,8 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { - Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; - geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u); + geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); geometry.layers = depth; geometry.imageDepth = depth; geometry.guestBytes = ComputeSurfaceSize(geometry.mips, depth); @@ -302,10 +320,9 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { return geometry; } if (descriptor.dimension == TextureDimension::k3D) { - Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; - const auto thick = ComputeThickLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, depth); - geometry.mips = {thick.mip}; + const auto thick = ComputeThickLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, depth, descriptor.mipCount); + geometry.mips = thick.mips; geometry.layers = depth; geometry.imageDepth = depth; geometry.guestBytes = thick.guestBytes; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index afda8cfc4..841b57298 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -119,6 +119,29 @@ void RunTextureTilingTests() { reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 0); }, "mip count is out of range"); reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 17); }, "mip count is out of range"); + { + GuestTextureResource volume{}; + volume.width = 64; + volume.height = 64; + volume.depthOrLastArray = 31; + volume.mipCount = 3; + volume.tileMode = TextureTileMode::kStandard4KB; + volume.dimension = TextureDimension::k3D; + volume.format = 56; + const auto geometry = DescribeSurface(volume); + Require(geometry.thick && geometry.blockDepth == 8 && geometry.mips.size() == 3, "mipmapped 3D texture geometry changed"); + Require(geometry.mips[2].tiledOffset == 0 && geometry.mips[2].tiledSize == 8192, "3D mip 2 must lead each slab"); + Require(geometry.mips[1].tiledOffset == 8192 && geometry.mips[1].tiledSize == 32768, "3D mip 1 offset or size changed"); + Require(geometry.mips[0].tiledOffset == 40960 && geometry.mips[0].tiledSize == 131072, "3D mip 0 must end each slab"); + Require(geometry.layerBytes == 172032 && geometry.guestBytes == 172032ull * 4, "3D slabs must hold the whole mip chain"); + Require(geometry.HasLayer(1, 15) && !geometry.HasLayer(1, 16) && !geometry.HasLayer(2, 8), "3D mips must halve their depth"); + volume.width = 8; + volume.height = 8; + volume.depthOrLastArray = 7; + volume.mipCount = 2; + reject([&] { DescribeSurface(volume); }, "3D texture mip tails are not implemented"); + } + reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain"); reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers"); }