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) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-10-02 19:50:00 +02:00
parent 9f9a8f5a53
commit efde918a62
4 changed files with 95 additions and 37 deletions

View File

@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURETILING_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <vector>
@ -34,17 +35,18 @@ std::array<std::uint32_t, 3> ThickBlockExtent(TextureTileMode tileMode, std::uin
// row (TileMipLayout), except the tail mips, which share one block. Throws for linear tiling.
std::array<std::uint32_t, 3> 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<TileMipLayout> 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<std::uint64_t>(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<std::int32_t>(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);

View File

@ -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<std::uint64_t>(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<bool>* 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<bool>* 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::SliceWindow> 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<std::pair<std::uint64_t, std::uint64_t>> 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<VkBufferImageCopy> StorageTexture::CopyRegions(const std::vector<boo
for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) {
if (layers != nullptr && !(*layers)[layer]) continue;
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 = layer * sliceLinearBytes + mip.linearOffset;
@ -2836,6 +2846,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
std::vector<VkImageCopy> 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<bool>& layers) {
context.Function<PFN_vkCmdPipelineBarrier>("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<bool>& 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<PFN_vkCmdPipelineBarrier>("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);
}
}

View File

@ -255,35 +255,54 @@ std::array<std::uint32_t, 3> 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<std::uint64_t>(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<std::uint64_t>(mip.blocksPerRow) * (paddedHeight / block[1]) * (tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u);
mip.tiledSize = result.slabBytes;
result.mips.resize(mipCount);
std::array<std::uint32_t, 3> 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::uint32_t>(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<std::uint64_t>(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<std::uint64_t>(mip.pitchBytes) * mip.height;
} else {
mip.blocksPerRow = paddedWidth / block[0];
mip.tiledSize = static_cast<std::uint64_t>(mip.blocksPerRow) * (AlignUp(mip.height, block[1]) / block[1]) * blockBytes;
}
mip.linearSize = static_cast<std::uint64_t>(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;

View File

@ -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");
}