From 3a925efddf1e094539a6a9d27ad564fad1c50534 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 14 Oct 2026 16:07:49 +0200 Subject: [PATCH] graphics: preserve sparse spans in rebased mip-tail views --- .../host_gpu/renderer/image/imageInfo.cpp | 21 ++++----- tests/ShaderRecompilerComputeTests.cpp | 15 +++--- tests/shaderCfgTests.cpp | 46 ++++++++++++++++++- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/graphics/host_gpu/renderer/image/imageInfo.cpp b/src/graphics/host_gpu/renderer/image/imageInfo.cpp index 2b4d91de7..df1310025 100644 --- a/src/graphics/host_gpu/renderer/image/imageInfo.cpp +++ b/src/graphics/host_gpu/renderer/image/imageInfo.cpp @@ -21,7 +21,7 @@ bool IsDepthPlane(const ImageInfo& info) { info.tile_mode == Prospero::TileMode::kLinear); } -GuestRange ContiguousMipRange(const ImageInfo& info, uint32_t level) { +GuestRange TiledMipRange(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 || @@ -42,11 +42,10 @@ GuestRange ContiguousMipRange(const ImageInfo& info, uint32_t level) { !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. + // Standard4KB interleaves monotonically ordered X/Y bits, so the last + // corner bounds every active texel. Tiling preserves any holes in this span. 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 || + if ((origin & (std::bit_ceil(size) - 1u)) != 0 || size > block.block_size - origin || mip.offset > UINT64_MAX - origin) { return {}; } @@ -57,11 +56,11 @@ 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) { + const auto range = TiledMipRange(info, 0); + if (range.size == 0 || range.size % 4 != 0 || range.address != 0) { return 0; } - const auto alignment = std::max(256, range.size); + const auto alignment = std::max(256, std::bit_ceil(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 @@ -226,7 +225,7 @@ uint32_t ImageInfo::DataAlignment() const { } 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 std::max(256u, std::bit_ceil(compact_size)); } return tiling.block.block_size; } @@ -260,14 +259,14 @@ int32_t ImageInfo::SliceOf(const ImageInfo& container, int32_t mip) const noexce } // PS5 block slices contain the entire reversed mip chain. A rebased tail - // descriptor covers only its active bytes within the shared tail block. + // descriptor covers its addressed span 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); + const auto range = TiledMipRange(container, level); if (range.size != compact_size) { return -1; } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index bf4730454..df8bd2c39 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -18673,15 +18673,18 @@ public: check_round_trip("odd multi-mip", total.size, layout); } - { - constexpr auto format = Prospero::BufferFormat::k16_16_16_16Float; + for (const bool sparse : {false, true}) { + const auto format = sparse ? Prospero::BufferFormat::k32_32_32_32UInt + : 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; + const uint32_t extent = sparse ? 2 : 16; + auto view = MakeTilingImage(format, extent, extent, 1, 1, tile, 0, false); + view.data.address = sparse ? 0x50322e3500 : 0x5032b23800; view.UpdateSize(); Require(name, "rebased mip footprint", - view.data.size == 0x800 && view.DataAlignment() == 0x800 && - view.mip_layout[0].pitch == 32 && + view.data.size == (sparse ? 0x60 : 0x800) && + view.DataAlignment() == (sparse ? 0x100 : 0x800) && + view.mip_layout[0].pitch == (sparse ? 16 : 32) && view.mip_layout[0].height == 16 && view.tiled_slice_stride == 0x1000, "a compact view changed its allocation geometry"); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 9ea9586d2..aca9dbe11 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -1366,18 +1366,26 @@ void TestRebasedTextureMipViews() { Check(view.MipOf(chain) == -1, "full-block child ignored the parent mip's nonzero tail origin"); - for (uint32_t variant = 0; variant < 7; ++variant) { + for (uint32_t variant = 0; variant < 9; ++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 0: view.extent = {7, 16, 1}; break; // Unproven non-power-of-two view. 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; + case 7: + case 8: + view.guest_format = variant == 7 ? Prospero::BufferFormat::k8UNorm + : Prospero::BufferFormat::k16UNorm; + view.pixel_format = variant == 7 ? vk::Format::eR8Unorm : vk::Format::eR16Unorm; + view.bytes_per_block = 0; + view.extent = {1, 1, 1}; + break; } view.UpdateSize(); Check(view.data.size >= 0x1000 && view.DataAlignment() >= 0x1000, @@ -1388,6 +1396,40 @@ void TestRebasedTextureMipViews() { view.UpdateSize(); Check(view.data.size == 0x1000 && view.DataAlignment() == 0x1000, "misaligned descriptor received compact tail alignment"); + + view = make_view(4, 0); + view.data = {chain.data.address, 0}; + view.extent.width = 8; + view.UpdateSize(); + Check(view.data.size == 0x600 && view.DataAlignment() == 0x800, + "sparse rectangle confused addressed span with active byte count"); + + auto sparse_chain = chain; + sparse_chain.data = {0x503225f000, 0}; + sparse_chain.pixel_format = vk::Format::eR32G32B32A32Uint; + sparse_chain.guest_format = Prospero::BufferFormat::k32_32_32_32UInt; + sparse_chain.extent = {64, 64, 1}; + sparse_chain.resources = {6, 7}; + sparse_chain.pitch = sparse_chain.bytes_per_block = 0; + sparse_chain.UpdateSize(); + auto sparse = sparse_chain; + sparse.data = {0x50322e3500, 0}; + sparse.extent = {2, 2, 1}; + sparse.resources = {1, 1}; + sparse.pitch = 0; + sparse.UpdateSize(); + Check(sparse_chain.tiled_slice_stride == 0x16000 && + sparse.data.size == 0x60 && sparse.mip_layout[0].size == 0x60 && + sparse.DataAlignment() == 0x100 && sparse.LinearSize() == 0x200 && + sparse.MipOf(sparse_chain) == 5 && sparse.SliceOf(sparse_chain, 5) == 6, + "captured sparse 2x2 view lost its byte span, tail origin, or array layer"); + sparse.UpdateSize(); + Check(sparse.data.size == 0x60 && sparse.mip_layout[0].size == 0x60 && + sparse.DataAlignment() == 0x100, + "repeated sparse view sizing changed its span or alignment"); + sparse.data.address -= 0x100; + Check(sparse.MipOf(sparse_chain) == -1, + "sparse view accepted a different mip-tail origin"); } void TestShaderFeatureCollection() {