From 8c7fe539b8b862e4e39a5cfd9a5702e28a9f97d8 Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 14 Oct 2026 15:56:45 +0200 Subject: [PATCH] graphics: represent rebased contiguous mip-tail texture views Co-authored-by: fxpw --- .../host_gpu/renderer/image/imageInfo.cpp | 126 ++++++++++++++++++ .../host_gpu/renderer/image/imageInfo.h | 49 +------ .../renderer/pipeline/descriptors.cpp | 7 +- tests/ShaderRecompilerComputeTests.cpp | 30 +++++ tests/shaderCfgTests.cpp | 95 +++++++++++++ 5 files changed, 257 insertions(+), 50 deletions(-) diff --git a/src/graphics/host_gpu/renderer/image/imageInfo.cpp b/src/graphics/host_gpu/renderer/image/imageInfo.cpp index 780d2e447..2b4d91de7 100644 --- a/src/graphics/host_gpu/renderer/image/imageInfo.cpp +++ b/src/graphics/host_gpu/renderer/image/imageInfo.cpp @@ -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(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(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(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(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(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()); diff --git a/src/graphics/host_gpu/renderer/image/imageInfo.h b/src/graphics/host_gpu/renderer/image/imageInfo.h index a5dcc3452..44c5d7e3d 100644 --- a/src/graphics/host_gpu/renderer/image/imageInfo.h +++ b/src/graphics/host_gpu/renderer/image/imageInfo.h @@ -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 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(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(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(layer); - } + [[nodiscard]] int32_t SliceOf(const ImageInfo& container, int32_t mip) const noexcept; }; struct ImageViewInfo { diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index 61495c535..1e451c397 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -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(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(); } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 6962d5c2c..bf4730454 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -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 linear(view.LinearSize()); + fill(&linear, ++case_index); + std::vector 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 round_trip; + gpu_detile(observed, &round_trip, observed.size(), linear.size(), view, + nullptr, 0x800); + std::vector 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; diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index c66258d55..26431664d 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -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 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(0x1000, bytes) && + view.DataAlignment() == std::min(0x1000, + std::max(0x100, bytes)) && + view.MipOf(chain) == static_cast(level) && + view.SliceOf(chain, static_cast(level)) == + static_cast(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(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();