graphics: represent rebased contiguous mip-tail texture views

Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
nmzik
2026-10-14 15:56:45 +02:00
parent c88f05f7b5
commit 8c7fe539b8
5 changed files with 257 additions and 50 deletions
@@ -21,6 +21,54 @@ bool IsDepthPlane(const ImageInfo& info) {
info.tile_mode == Prospero::TileMode::kLinear);
}
GuestRange ContiguousMipRange(const ImageInfo& info, uint32_t level) {
const auto& block = info.tiling.block;
if (info.tile_mode != Prospero::TileMode::kStandard4KB ||
block.family != TileBlockFamily::Standard4KB || info.IsVolume() || info.samples != 1 ||
info.HasMetadata() || level >= info.resources.levels || level >= info.mip_layout.size() ||
info.tiling.texel_width == 0 || info.tiling.texel_height == 0) {
return {};
}
const auto extent = info.MipExtent(level);
const auto width = (extent.width + info.tiling.texel_width - 1u) / info.tiling.texel_width;
const auto height = (extent.height + info.tiling.texel_height - 1u) / info.tiling.texel_height;
const auto& mip = info.mip_layout[level];
uint32_t last = 0, origin = 0;
if (!std::has_single_bit(width) || !std::has_single_bit(height) ||
width > block.block_width || height > block.block_height ||
mip.tail_x > block.block_width - width || mip.tail_y > block.block_height - height ||
mip.tail_x % width != 0 || mip.tail_y % height != 0 ||
!TileGetBlockOffset(block, width - 1u, height - 1u, 0, last) ||
!TileGetBlockOffset(block, mip.tail_x, mip.tail_y, 0, origin)) {
return {};
}
// Standard4KB interleaves monotonically ordered X/Y bits. A dense rectangle
// fills exactly this power-of-two interval, including when rebased into a tail.
const uint64_t size = uint64_t {last} + block.bytes_per_element;
if (!std::has_single_bit(size) || size != uint64_t {width} * height * block.bytes_per_element ||
(origin & (size - 1u)) != 0 || size > block.block_size - origin ||
mip.offset > UINT64_MAX - origin) {
return {};
}
return {mip.offset + origin, size};
}
uint32_t CompactMipSize(const ImageInfo& info) {
if (info.resources.levels != 1 || info.resources.layers != 1) {
return 0;
}
const auto range = ContiguousMipRange(info, 0);
if (range.size == 0 || range.address != 0) {
return 0;
}
const auto alignment = std::max<uint64_t>(256, range.size);
const auto in_block = info.data.address & (info.tiling.block.block_size - 1u);
return (info.data.address & (alignment - 1u)) == 0 &&
range.size <= info.tiling.block.block_size - in_block
? static_cast<uint32_t>(range.size)
: 0;
}
} // namespace
vk::Extent3D ImageInfo::MipExtent(uint32_t level) const {
@@ -34,6 +82,7 @@ void ImageInfo::UpdateSize() {
extent.height == 0 || extent.depth == 0 || resources.levels == 0 ||
resources.levels > mip_layout.size() || resources.layers == 0 || samples == 0 ||
!std::has_single_bit(samples));
const bool auto_size = data.size == 0;
TileTextureElementLayout element {};
EXIT_NOT_IMPLEMENTED(!TileGetTextureElementLayout(guest_format, element));
if (bytes_per_block == 0) {
@@ -164,9 +213,86 @@ void ImageInfo::UpdateSize() {
linear_offset = Add(mip.linear_offset, Multiply(mip.linear_size, active.depth));
mip_pitch = std::max(mip_pitch >> 1u, 1u);
}
const auto compact_size = CompactMipSize(*this);
if (compact_size != 0 && (auto_size || data.size == compact_size)) {
data.size = mip_layout[0].size = compact_size;
}
}
}
uint32_t ImageInfo::DataAlignment() const {
if (!IsTiled()) {
return 256;
}
const auto compact_size = CompactMipSize(*this);
if (compact_size != 0 && data.size == compact_size && mip_layout[0].size == compact_size) {
return std::max(256u, compact_size);
}
return tiling.block.block_size;
}
int32_t ImageInfo::SliceOf(const ImageInfo& container, int32_t mip) const noexcept {
if (!IsCompatible(container) || tile_mode != container.tile_mode || type != container.type ||
IsVolume() || resources.levels != 1 || resources.layers == 0 ||
container.resources.layers == 0 || mip < 0 ||
static_cast<uint32_t>(mip) >= container.resources.levels ||
container.resources.levels > container.mip_layout.size() ||
!data.Valid() || !container.data.Valid() || data.address < container.data.address ||
data.End() > container.data.End()) {
return -1;
}
if (HasStencil() != container.HasStencil() ||
(HasStencil() && (stencil.address < container.stencil.address ||
stencil.End() > container.stencil.End()))) {
return -1;
}
const auto level = static_cast<uint32_t>(mip);
const auto& layout = container.mip_layout[level];
if (extent.width != std::max(container.extent.width >> level, 1u) ||
extent.height != std::max(container.extent.height >> level, 1u) ||
extent.depth != container.extent.depth || mip_layout[0].pitch != layout.pitch ||
mip_layout[0].height != layout.height || layout.size == 0 ||
layout.size % container.resources.layers != 0 ||
container.data.size % container.resources.layers != 0 ||
data.size % resources.layers != 0) {
return -1;
}
// PS5 block slices contain the entire reversed mip chain. A rebased tail
// descriptor covers only its active bytes within the shared tail block.
const auto slice_stride = container.data.size / container.resources.layers;
const auto child_stride = data.size / resources.layers;
auto mip_offset = layout.offset;
auto mip_size = layout.size / container.resources.layers;
const auto compact_size = CompactMipSize(*this);
if (compact_size != 0 && child_stride == compact_size && mip_layout[0].size == compact_size) {
const auto range = ContiguousMipRange(container, level);
if (range.size != compact_size) {
return -1;
}
mip_offset = range.address;
mip_size = range.size;
} else if (mip_layout[0].tail_x != layout.tail_x || mip_layout[0].tail_y != layout.tail_y) {
return -1;
}
if (child_stride != mip_size || mip_offset >= slice_stride ||
child_stride > slice_stride - mip_offset ||
(resources.layers > 1 && child_stride != slice_stride)) {
return -1;
}
const auto address_delta = data.address - container.data.address;
if (address_delta < mip_offset || (address_delta - mip_offset) % slice_stride != 0) {
return -1;
}
const auto layer = (address_delta - mip_offset) / slice_stride;
if (layer > INT32_MAX || layer >= container.resources.layers ||
resources.layers > container.resources.layers - layer) {
return -1;
}
return static_cast<int32_t>(layer);
}
uint64_t ImageInfo::LinearSize(uint32_t levels) const {
levels = levels == 0 ? resources.levels : levels;
EXIT_IF(levels == 0 || levels > resources.levels || levels > mip_layout.size());
@@ -88,6 +88,7 @@ struct ImageInfo {
uint32_t first_tail_level = 16;
void UpdateSize();
[[nodiscard]] uint32_t DataAlignment() const;
[[nodiscard]] uint64_t LinearSize(uint32_t levels = 0) const;
[[nodiscard]] vk::Extent3D MipExtent(uint32_t level) const;
[[nodiscard]] std::vector<vk::BufferImageCopy> BufferCopies(uint64_t buffer_offset = 0,
@@ -136,53 +137,7 @@ struct ImageInfo {
}
return -1;
}
[[nodiscard]] int32_t SliceOf(const ImageInfo& container, int32_t mip) const noexcept {
if (!IsCompatible(container) || tile_mode != container.tile_mode || type != container.type ||
IsVolume() || resources.levels != 1 || resources.layers == 0 ||
container.resources.layers == 0 || mip < 0 ||
static_cast<uint32_t>(mip) >= container.resources.levels ||
container.resources.levels > container.mip_layout.size() ||
!data.Valid() || !container.data.Valid() || data.address < container.data.address ||
data.End() > container.data.End()) {
return -1;
}
if (HasStencil() != container.HasStencil() ||
(HasStencil() && (stencil.address < container.stencil.address ||
stencil.End() > container.stencil.End()))) {
return -1;
}
const auto level = static_cast<uint32_t>(mip);
const auto& layout = container.mip_layout[level];
if (extent.width != std::max(container.extent.width >> level, 1u) ||
extent.height != std::max(container.extent.height >> level, 1u) ||
extent.depth != container.extent.depth || mip_layout[0].pitch != layout.pitch ||
mip_layout[0].height != layout.height || layout.size == 0 ||
layout.size % container.resources.layers != 0 ||
container.data.size % container.resources.layers != 0 ||
data.size % resources.layers != 0) {
return -1;
}
// PS5 block slices contain the entire reversed mip chain.
const auto slice_stride = container.data.size / container.resources.layers;
const auto child_stride = data.size / resources.layers;
if (child_stride != layout.size / container.resources.layers ||
layout.offset >= slice_stride || child_stride > slice_stride - layout.offset ||
(resources.layers > 1 && child_stride != slice_stride)) {
return -1;
}
const auto address_delta = data.address - container.data.address;
if (address_delta < layout.offset || (address_delta - layout.offset) % slice_stride != 0) {
return -1;
}
const auto layer = (address_delta - layout.offset) / slice_stride;
if (layer > INT32_MAX || layer >= container.resources.layers ||
resources.layers > container.resources.layers - layer) {
return -1;
}
return static_cast<int32_t>(layer);
}
[[nodiscard]] int32_t SliceOf(const ImageInfo& container, int32_t mip) const noexcept;
};
struct ImageViewInfo {
@@ -608,6 +608,9 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
block_bytes != 0 ? block_bytes : Prospero::NumBytesPerElement(format);
desc.info.samples = samples;
desc.info.tile_mode = tile;
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
desc.info.metadata.kind = ImageMetadataKind::Dcc;
}
if (levels > physical_levels) {
if (!ResolveTextureMipView(desc.info, physical_levels,
!resource.r128 && descriptor.MetaCompress(), view_levels,
@@ -620,8 +623,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
} else {
desc.info.UpdateSize();
}
const auto alignment =
tile == Prospero::TileMode::kLinear ? 256u : desc.info.tiling.block.block_size;
const auto alignment = desc.info.DataAlignment();
EXIT_NOT_IMPLEMENTED(desc.info.data.size == 0 || alignment == 0 ||
(address & (static_cast<uint64_t>(alignment) - 1u)) != 0);
if (storage) {
@@ -632,7 +634,6 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
std::countr_zero(samples));
desc.info.metadata.kind = ImageMetadataKind::Dcc;
desc.info.metadata.range = {descriptor.MetaAddr() << 8u, metadata_size.size};
desc.info.metadata.dcc_alpha_msb = descriptor.DccAlphaPos();
}
+30
View File
@@ -18673,6 +18673,36 @@ public:
check_round_trip("odd multi-mip", total.size, layout);
}
{
constexpr auto format = Prospero::BufferFormat::k16_16_16_16Float;
constexpr auto tile = Prospero::TileMode::kStandard4KB;
auto view = MakeTilingImage(format, 16, 16, 1, 1, tile, 0, false);
view.data.address = 0x5032b23800;
view.UpdateSize();
Require(name, "rebased mip footprint",
view.data.size == 0x800 && view.DataAlignment() == 0x800 &&
view.mip_layout[0].pitch == 32 &&
view.mip_layout[0].height == 16 &&
view.tiled_slice_stride == 0x1000,
"a compact view changed its allocation geometry");
check_round_trip("rebased mip exact range", view.data.size, view);
check_round_trip("rebased mip adjacent canary", 0x1000, view);
std::vector<uint8_t> linear(view.LinearSize());
fill(&linear, ++case_index);
std::vector<uint8_t> expected(0x1000, 0xab), observed;
convert_reference(true, &expected, linear, view);
gpu_tile(linear, &observed, 0x1000, linear.size(), view, nullptr, 0,
0x100, 0x800);
compare("rebased mip prefix and neighboring mip", expected, observed);
std::vector<uint8_t> round_trip;
gpu_detile(observed, &round_trip, observed.size(), linear.size(), view,
nullptr, 0x800);
std::vector<uint8_t> expected_linear(linear.size(), 0);
convert_reference(false, &expected_linear, expected, view);
compare("rebased mip nonzero source", expected_linear, round_trip);
}
struct TailMode {
Prospero::TileMode tile;
TileBlockFamily family;
+95
View File
@@ -1296,6 +1296,100 @@ void TestNormalizedImageContracts() {
"Vulkan image-view compatibility classes diverged from production");
}
void TestRebasedTextureMipViews() {
constexpr auto format = Prospero::BufferFormat::k16_16_16_16Float;
constexpr auto tile = Prospero::TileMode::kStandard4KB;
TileSizeAlign allocation{};
TileGetTextureSize(format, 16, 16, 1, tile, &allocation, nullptr, nullptr);
Check(allocation.size == 0x1000 && allocation.align == 0x1000,
"rebased texture views changed guest allocation size/alignment");
ImageInfo chain{};
chain.data.address = 0x5032b23000;
chain.pixel_format = vk::Format::eR16G16B16A16Sfloat;
chain.guest_format = format;
chain.extent = {256, 256, 1};
chain.resources = {9, 2};
chain.tile_mode = tile;
chain.UpdateSize();
Check(chain.tiled_slice_stride == 0xab000 && chain.data.size == 0x156000 &&
chain.first_tail_level == 4 && chain.DataAlignment() == 0x1000,
"full texture chain lost its guest allocation layout");
const std::array<uint64_t, 9> offsets{
0x2b000, 0xb000, 0x3000, 0x1000, 0x800, 0x600, 0x500, 0x400, 0x300};
const auto make_view = [&](uint32_t level, uint32_t layer) {
auto view = chain;
view.data = {chain.data.address + layer * chain.tiled_slice_stride +
offsets[level], 0};
view.extent = {256u >> level, 256u >> level, 1};
view.resources = {1, 1};
view.pitch = 0;
view.UpdateSize();
return view;
};
for (uint32_t layer = 0; layer < 2; ++layer) {
for (uint32_t level = 0; level < offsets.size(); ++level) {
auto view = make_view(level, layer);
const uint64_t bytes = uint64_t{view.extent.width} * view.extent.height * 8;
Check(view.data.size == bytes && view.mip_layout[0].size == bytes &&
view.tiled_slice_stride == std::max<uint64_t>(0x1000, bytes) &&
view.DataAlignment() == std::min<uint64_t>(0x1000,
std::max<uint64_t>(0x100, bytes)) &&
view.MipOf(chain) == static_cast<int32_t>(level) &&
view.SliceOf(chain, static_cast<int32_t>(level)) ==
static_cast<int32_t>(layer),
"rebased mip footprint/origin or parent layer stride was incorrect");
view.UpdateSize();
Check(view.data.size == bytes && view.mip_layout[0].size == bytes &&
view.MipOf(chain) == static_cast<int32_t>(level),
"resizing a normalized view lost its compact footprint");
}
}
auto view = make_view(4, 0);
Check(view.data.address == 0x5032b23800 && view.mip_layout[0].pitch == 32 &&
view.mip_layout[0].height == 16 &&
view.mip_layout[0].linear_pitch == 32 && view.LinearSize() == 0x1000,
"captured 16x16 view changed its padded tiling or linear transfer size");
view.data.address += 0x800;
Check(view.MipOf(chain) == -1, "tail view accepted the following mip's address");
view = make_view(4, 0);
view.mip_layout[0].pitch /= 2;
Check(view.MipOf(chain) == -1, "tail view accepted a different padded pitch");
view = make_view(4, 0);
view.resources.layers = 2;
view.data.size *= 2;
view.mip_layout[0].size *= 2;
Check(view.MipOf(chain) == -1, "tail array view ignored the parent chain stride");
view = make_view(4, 0);
view.data = {chain.data.address, 0x1000};
view.UpdateSize();
Check(view.MipOf(chain) == -1,
"full-block child ignored the parent mip's nonzero tail origin");
for (uint32_t variant = 0; variant < 7; ++variant) {
view = make_view(4, 0);
view.data = {chain.data.address, 0};
view.pitch = 0;
switch (variant) {
case 0: view.extent = {8, 16, 1}; break; // Holes in the Standard4KB span.
case 1: view.resources.levels = 2; break;
case 2: view.resources.layers = 2; break;
case 3: view.type = Prospero::ImageType::kColor3D; break;
case 4: view.tile_mode = Prospero::TileMode::kStandard64KB; break;
case 5: view.metadata.kind = ImageMetadataKind::Dcc; break;
case 6: view.data = {0x5032b23800, 0x1000}; break;
}
view.UpdateSize();
Check(view.data.size >= 0x1000 && view.DataAlignment() >= 0x1000,
"unsupported or explicitly sized texture became a compact tail view");
}
view = make_view(4, 0);
view.data = {0x5032b23900, 0};
view.UpdateSize();
Check(view.data.size == 0x1000 && view.DataAlignment() == 0x1000,
"misaligned descriptor received compact tail alignment");
}
void TestShaderFeatureCollection() {
using namespace ShaderRecompiler::IR;
@@ -15725,6 +15819,7 @@ int main() {
TestShaderBufferResourceSize();
TestNativeShaderResourceDependencies();
TestNormalizedImageContracts();
TestRebasedTextureMipViews();
TestShaderFeatureCollection();
TestTypedSpirvSerialization();
TestDeferredSpirvPhiPatching();