diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 9b92e9b8574..a859d883078 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -195,8 +195,8 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); if (baseArray == depth) depth += 5u; - if ((depth - baseArray + 1u) % 6u != 0) { - Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" + + if (depth - baseArray + 1u < 6u) { + Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + describe() + " (base array " + std::to_string(baseArray) + ")"); } break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 270d9ca0f85..302d815f44c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -445,7 +445,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -552,7 +552,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -667,7 +667,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -846,7 +846,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 34c28d4149b..798cd79410f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -252,9 +252,14 @@ void RunGuestTextureResourceTests() { Fields cubeBadArray = cube; cubeBadArray.baseArray = 6; rejectFields(cubeBadArray, "base array past its last array slice"); - Fields cubeNotMultiple = cube; - cubeNotMultiple.depth = 4; - rejectFields(cubeNotMultiple, "multiple of 6"); + Fields cubeExtraSlice = cube; + cubeExtraSlice.depth = 6; + result = DecodeTextureResource(pack(cubeExtraSlice)); + Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); + Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); + Fields cubePartial = cube; + cubePartial.depth = 4; + rejectFields(cubePartial, "fewer than 6 array slices"); Fields multisampled = base; multisampled.typeRaw = 14; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2e9cd49f46d..e5f9699ba09 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7572,6 +7572,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +void cubeExtraSliceTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); + const VkComponentMapping identity{}; + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0}; + auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it"); + std::vector memory(static_cast(geometry.guestBytes) + 256); + auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast(geometry.layerBytes)); + } + resource.baseAddress = reinterpret_cast(surface); + const std::span snapshot(surface, static_cast(geometry.guestBytes)); + Texture texture(context, detiler, resource, identity, snapshot); + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + Texture storageView(context, storage, resource, identity); + const auto wordCube = [&words](std::vector& backing, std::uint32_t format, const std::function& word) { + auto cubeWords = words; + cubeWords[1] = (format << 20u) | (3u << 30u); + auto described = DecodeTextureResource(cubeWords); + const auto layout = DescribeSurface(described); + backing.assign(static_cast(layout.guestBytes) + 256, std::byte{}); + auto* base = reinterpret_cast((reinterpret_cast(backing.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) { + const auto value = word(layer); + for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value)); + } + described.baseAddress = reinterpret_cast(base); + return described; + }; + std::vector depthMemory; + const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) { + const float value = static_cast(layer + 1) / 8.0f; + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }); + auto depthStorage = StorageTexture::Create(context, detiler, depth, 0); + recorder.Keep(depthStorage); + Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy"); + Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{}); + depthCopy.RefreshDepthCopy(); + constexpr std::array unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u}; + std::vector repackMemory; + const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; }); + auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0); + recorder.Keep(repackStorage); + Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy"); + Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{}); + repackCopy.RefreshDepthCopy(); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + const auto expected = 16.0f * (layer + 1) / 255.0f; + expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice"); + } +} + struct SampledViewProbe { Context& context; PFN_vkGetDeviceProcAddr resolver; @@ -8880,6 +8944,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } @@ -9108,6 +9173,7 @@ int main(int argc, char** argv) { firstLayerViewTests(device, recorder); sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); cmaskPassTests(device, recorder); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f692e267c0e..c23b0fe9934 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -119,7 +119,7 @@ - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more.