From 5bf643880eda5348ec5b051ba5bddd333b500019 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 03:27:15 +0300 Subject: [PATCH 01/11] feat(agc): clear resident depth image aspects --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/include/State.hpp | 4 +++ .../Graphics/src/DepthSurface.cpp | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 2e8ed728c80..bed95dfe68e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -12,6 +12,7 @@ namespace AgcDriver::Graphics { class Texture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +void RunDepthClearPass(const Context& context, const DepthClearPass& pass); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 753fa01ce5b..1bb1fd26c6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -110,6 +110,10 @@ struct ColorMetadataPass { std::vector targets; }; std::optional DecodeColorMetadataPass(const QueueState& queue); +struct DepthClearPass { + DepthTarget target; + VkImageAspectFlags aspects; +}; // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 33194fdb2fb..fd03093ef1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -80,6 +80,21 @@ class DepthSurface { DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; + void Clear(const DepthClearPass& pass) { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkClearDepthStencilValue clear{pass.target.clearDepth, pass.target.clearStencil}; + const VkImageSubresourceRange range{pass.aspects, 0, 1, 0, 1}; + context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::array key{}; std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); @@ -163,6 +178,20 @@ void ClearDepthSurfaces(VkDevice device) { std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); } +void RunDepthClearPass(const Context& context, const DepthClearPass& pass) { + Require(pass.aspects != 0 && (pass.aspects & ~(VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == 0, "invalid depth clear aspects"); + Require((pass.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) == 0 || pass.target.address != 0, "depth clear has no depth plane"); + Require((pass.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) == 0 || pass.target.stencilAddress != 0, "stencil clear has no stencil plane"); + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device != context.device || !sameSurface(surface->target, pass.target)) continue; + surface->Clear(pass); + return; + } + surfaces().push_back(std::make_unique(context, pass.target)); + surfaces().back()->Clear(pass); +} + std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); From b11a1a2483588ade17a05edd13ca20f9e922e64c Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 06:59:17 +0300 Subject: [PATCH 02/11] feat(agc): sample resident depth and stencil arrays Assemble 2D depth/stencil array views from the resident render-target slices, copying only slices changed by attachment use or explicit clears. Preserve BASE_ARRAY subviews and first-layer views, and reject missing resident layers. Record the remaining layout and guest-memory coherence limitations. Validation: agc_driver_graphics passed. GPU depth-array and depth-clear tests passed with Khronos core and synchronization validation enabled, covering D16, D32/S8, cached binding refresh, render-pass writes, stencil isolation, subviews, and missing layers. Match the test command pool to the runtime's reset flags. The Skyrim menu was observed through GPU captures; the desktop was locked before a new loading attempt, so gameplay remains unverified. AI-assisted implementation. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 21 ++ .../Graphics/include/Texture.hpp | 2 +- .../Graphics/src/DepthSurface.cpp | 142 ++++++++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 20 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 249 +++++++++++++++++- .../tests/shaders/SampleArray.comp | 14 + docs/dev/TechnicalDebt.md | 2 + 7 files changed, 414 insertions(+), 36 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/shaders/SampleArray.comp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 40c7b762965..13f8c58a9bb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -370,10 +370,31 @@ add_custom_command( VERBATIM ) +foreach(agcSampleAspect IN ITEMS Depth Stencil) + set(agcSampleArrayDefines) + if(agcSampleAspect STREQUAL "Stencil") + set(agcSampleArrayDefines -DSTENCIL=1) + endif() + set(agcSampleArraySource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/SampleArray.comp) + set(agcSampleArraySpv ${agcTestShaderGeneratedRoot}/Sample${agcSampleAspect}Array.spv) + set(agcSampleArrayHeader ${agcTestShaderGeneratedRoot}/Sample${agcSampleAspect}Array_spv.h) + add_custom_command( + OUTPUT "${agcSampleArrayHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleArrayDefines} -o "${agcSampleArraySpv}" "${agcSampleArraySource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleArraySpv} -DOUTPUT=${agcSampleArrayHeader} + -DSYMBOL=SAMPLE_${agcSampleAspect}_ARRAY_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleArraySource}" glslang-standalone + VERBATIM + ) + list(APPEND agcSampleArrayHeaders "${agcSampleArrayHeader}") +endforeach() + add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp tests/ResidentPresent.cpp ${agcSampleLodShaderHeader} + ${agcSampleArrayHeaders} ) target_include_directories(agc_driver_recorder_tests PRIVATE diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 05af6a3c5a1..2e16d6bd81a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -58,7 +58,7 @@ class Texture { // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t layers = 1); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); Texture(const Texture&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index fd03093ef1e..75bded588c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -18,7 +18,7 @@ namespace { class DepthSurface { public: - DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target) { + DepthSurface(const Context& context, const DepthTarget& target, std::uint32_t layers = 1) : context(context), target(target) { this->context.bufferPool.reset(); VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); @@ -32,10 +32,11 @@ class DepthSurface { info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; - info.arrayLayers = 1; + Require(layers != 0 && layers <= context.limits.maxImageArrayLayers, "depth array exceeds device limits"); + info.arrayLayers = layers; info.samples = VK_SAMPLE_COUNT_1_BIT; info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -63,7 +64,7 @@ class DepthSurface { toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.image = image; - toGeneral.subresourceRange = viewInfo.subresourceRange; + toGeneral.subresourceRange = {aspects, 0, 1, 0, layers}; const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); const VkClearDepthStencilValue clear{target.clearDepth, target.clearStencil}; @@ -81,6 +82,7 @@ class DepthSurface { DepthSurface& operator=(const DepthSurface&) = delete; void Clear(const DepthClearPass& pass) { + ++version; auto* recorder = Recorder::Active(); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -95,29 +97,71 @@ class DepthSurface { else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { - std::array key{}; - std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); - key[8] = components.r; - key[9] = components.g; - key[10] = components.b; - key[11] = components.a; - if (const auto found = textures.find(key); found != textures.end()) return found->second; + void ValidateSampled(std::span words, const GuestTextureResource& resource) const { + std::array key{}; + std::copy_n(words.begin(), std::min(words.size(), key.size()), key.begin()); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + if ((format != expected && !depthBits) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray) || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || (resource.dimension == TextureDimension::k2D && resource.baseArray != 0)) { char text[448]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); throw std::runtime_error(text); } - auto texture = std::make_shared(context, image, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components); - textures.emplace(key, texture); - return texture; + } + + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::span slices, bool stencil) { + std::array key{}; + std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); + key[8] = components.r; + key[9] = components.g; + key[10] = components.b; + key[11] = components.a; + key[12] = resource.baseArray; + key[13] = resource.depthOrLastArray; + key[14] = static_cast(resource.dimension); + key[15] = stencil; + auto found = textures.find(key); + if (found == textures.end()) { + SampledDepth sampled; + if (slices.size() > 1) sampled.array = std::make_unique(context, target, static_cast(slices.size())); + const auto source = sampled.array ? sampled.array->image : slices.front()->image; + const auto type = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + sampled.texture = std::make_shared(context, source, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, type, static_cast(slices.size())); + sampled.versions.resize(slices.size()); + found = textures.emplace(key, std::move(sampled)).first; + } + auto& sampled = found->second; + if (sampled.array) sampled.array->CopySlices(slices, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, sampled.versions); + return sampled.texture; + } + + void CopySlices(std::span slices, VkImageAspectFlags aspect, std::vector>& versions) { + bool changed = false; + for (std::size_t i = 0; i < slices.size(); ++i) changed |= versions[i] != std::pair{slices[i]->image, slices[i]->version}; + if (!changed) return; + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + for (std::uint32_t i = 0; i < slices.size(); ++i) { + if (versions[i] == std::pair{slices[i]->image, slices[i]->version}) continue; + VkImageCopy copy{}; + copy.srcSubresource = {aspect, 0, 0, 1}; + copy.dstSubresource = {aspect, 0, i, 1}; + copy.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, slices[i]->image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, access); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + for (std::size_t i = 0; i < slices.size(); ++i) versions[i] = {slices[i]->image, slices[i]->version}; } const Context context; @@ -125,9 +169,15 @@ class DepthSurface { VkImage image = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; + std::uint64_t version = 1; private: - std::map, std::shared_ptr> textures; + struct SampledDepth { + std::unique_ptr array; + std::shared_ptr texture; + std::vector> versions; + }; + std::map, SampledDepth> textures; void release() noexcept { textures.clear(); @@ -167,7 +217,10 @@ std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { - if (surface->context.device == context.device && sameSurface(surface->target, target)) return surface->view; + if (surface->context.device == context.device && sameSurface(surface->target, target)) { + ++surface->version; + return surface->view; + } } surfaces().push_back(std::make_unique(context, target)); return surfaces().back()->view; @@ -195,10 +248,55 @@ void RunDepthClearPass(const Context& context, const DepthClearPass& pass) { std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); - const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); - }); - return found == list.rend() ? nullptr : (*found)->Sampled(words, resource, components); + const auto find = [&](std::uint64_t address) -> DepthSurface* { + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); + }); + return found == list.rend() ? nullptr : found->get(); + }; + auto* base = find(resource.baseAddress); + auto matchedAddress = resource.baseAddress; + const bool array = resource.dimension == TextureDimension::k2DArray; + if (base == nullptr && array) { + for (auto it = list.rbegin(); it != list.rend(); ++it) { + const auto& surface = **it; + if (surface.context.device != context.device || surface.target.extent.width != resource.width || surface.target.extent.height != resource.height) continue; + const auto matches = [&](std::uint64_t address, std::uint32_t bytes) { + if (address == 0 || address < resource.baseAddress) return false; + const auto stride = DepthSliceBytes(surface.target.extent, bytes); + const auto offset = address - resource.baseAddress; + return offset % stride == 0 && offset / stride >= resource.baseArray && offset / stride <= resource.depthOrLastArray; + }; + const bool d16 = surface.target.format == VK_FORMAT_D16_UNORM || surface.target.format == VK_FORMAT_D16_UNORM_S8_UINT; + if (matches(surface.target.address, d16 ? 2u : 4u)) matchedAddress = surface.target.address; + else if (matches(surface.target.stencilAddress, 1u)) matchedAddress = surface.target.stencilAddress; + else continue; + base = it->get(); + break; + } + } + if (base == nullptr) return nullptr; + auto viewed = resource; + viewed.baseAddress = matchedAddress; + base->ValidateSampled(words, viewed); + const bool stencil = base->target.stencilAddress != 0 && matchedAddress == base->target.stencilAddress; + const auto last = array ? resource.depthOrLastArray : 0u; + Require(resource.baseArray <= last && last < 8192u && last - resource.baseArray < context.limits.maxImageArrayLayers, "depth array layer range is invalid"); + if (array) Require(resource.tileMode == TextureTileMode::kZ64KBX, "depth arrays outside the 64 KiB Z layout are not implemented"); + const bool d16 = base->target.format == VK_FORMAT_D16_UNORM || base->target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto stride = DepthSliceBytes(base->target.extent, stencil ? 1u : d16 ? 2u : 4u); + std::vector slices; + for (auto layer = resource.baseArray; layer <= last; ++layer) { + const auto address = resource.baseAddress + layer * stride; + auto* slice = find(address); + if (slice == nullptr || slice->target.format != base->target.format || slice->target.extent.width != resource.width || slice->target.extent.height != resource.height || (stencil ? slice->target.stencilAddress : slice->target.address) != address) { + char text[192]; + std::snprintf(text, sizeof(text), "AGC graphics: depth array 0x%llx layer %u at 0x%llx has no matching resident %s surface", static_cast(resource.baseAddress), layer, static_cast(address), stencil ? "stencil" : "depth"); + throw std::runtime_error(text); + } + slices.push_back(slice); + } + return base->Sampled(words, resource, components, slices, stencil); } bool DepthSurfaceAt(std::uint64_t address) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a5468427941..897c45d900f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -491,17 +491,27 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t layers) : context(context) { layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = depthImage; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = viewType; viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); - viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + viewInfo.subresourceRange = {aspect, 0, 1, 0, layers}; + viewRange = {viewType, 1, layers}; + try { + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); + if (viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY) { + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.layerCount = 1; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView depth first layer"); + } + } catch (...) { + release(); + throw; + } } Texture::~Texture() { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 61d55d29184..60cf4fa423a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -6,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" @@ -16,6 +18,8 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "ResidentPresent.hpp" #include "SampleLod_spv.h" +#include "SampleDepthArray_spv.h" +#include "SampleStencilArray_spv.h" #include #ifdef _WIN32 #ifndef NOMINMAX @@ -188,6 +192,7 @@ class Device { context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { @@ -2460,7 +2465,7 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { class SampleProgram { public: - SampleProgram(const Context& context, Recorder& recorder) : context(context), recorder(recorder), result(context, sizeof(float) * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) { + SampleProgram(const Context& context, Recorder& recorder, std::span code = SAMPLE_LOD_SPV, bool integer = false) : context(context), recorder(recorder), result(context, sizeof(float) * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) { VkDescriptorSetLayoutBinding bindings[2]{}; bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; @@ -2468,7 +2473,7 @@ class SampleProgram { layoutInfo.bindingCount = 2; layoutInfo.pBindings = bindings; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &setLayout), "vkCreateDescriptorSetLayout"); - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(float)}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(float) * 2}; VkPipelineLayoutCreateInfo pipelineLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &setLayout; @@ -2476,8 +2481,8 @@ class SampleProgram { pipelineLayoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(SAMPLE_LOD_SPV); - moduleInfo.pCode = SAMPLE_LOD_SPV; + moduleInfo.codeSize = code.size_bytes(); + moduleInfo.pCode = code.data(); Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, module, "main", nullptr}; @@ -2486,7 +2491,7 @@ class SampleProgram { VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; samplerInfo.magFilter = VK_FILTER_NEAREST; samplerInfo.minFilter = VK_FILTER_NEAREST; - samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.mipmapMode = integer ? VK_SAMPLER_MIPMAP_MODE_NEAREST : VK_SAMPLER_MIPMAP_MODE_LINEAR; samplerInfo.addressModeU = samplerInfo.addressModeV = samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.maxLod = VK_LOD_CLAMP_NONE; Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler"); @@ -2509,7 +2514,7 @@ class SampleProgram { SampleProgram(const SampleProgram&) = delete; SampleProgram& operator=(const SampleProgram&) = delete; - float Red(VkImageView view, VkImageLayout layout, float lod) { + float Red(VkImageView view, VkImageLayout layout, float lod, float layer = 0) { VkDescriptorSetAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; allocateInfo.descriptorPool = pool; allocateInfo.descriptorSetCount = 1; @@ -2534,7 +2539,8 @@ class SampleProgram { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); - context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(lod), &lod); + const float parameters[]{lod, layer}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(parameters), parameters); context.Function("vkCmdDispatch")(commands, 1, 1, 1); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); recorder.Submit(); @@ -2561,6 +2567,220 @@ void expectRed(float got, float want, const char* what) { if (std::abs(got - want) > 1.5f / 255.0f) throw std::runtime_error(std::string(what) + ": read " + std::to_string(got) + ", expected " + std::to_string(want)); } +void depthClearPassTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + DepthTarget target{0x10000, 0x20000, {64, 64}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 19}; + struct Release { + const Context& context; + ~Release() { ClearDepthSurfaces(context.device); } + } release{context}; + RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT}); + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.dimension = TextureDimension::k2D; + resource.format = 22; + const std::array words{}; + const VkComponentMapping mapping{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + const auto texture = DepthSurfaceTexture(context, words, resource, mapping); + Require(texture != nullptr, "depth clear did not create a sampleable surface"); + SampleProgram program(context, recorder); + expectRed(program.Red(texture->View(), texture->Layout(), 0.0f), 0.25f, "initial depth clear"); + target.clearDepth = 0.75f; + RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_DEPTH_BIT}); + expectRed(program.Red(texture->View(), texture->Layout(), 0.0f), 0.75f, "repeated depth clear kept old depth"); + target.clearDepth = 0.0f; + target.clearStencil = 47; + RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_STENCIL_BIT}); + expectRed(program.Red(texture->View(), texture->Layout(), 0.0f), 0.75f, "stencil-only clear changed depth"); + recorder.Sync(); +} + +void clearDepthAttachment(const Context& context, Recorder& recorder, const DepthTarget& target, float depth) { + const auto view = DepthSurfaceView(context, target); + VkAttachmentDescription attachment{}; + attachment.format = target.format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + const auto stages = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + const auto access = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + const VkSubpassDependency dependencies[]{ + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, stages, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, access, 0}, + {0, VK_SUBPASS_EXTERNAL, stages, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, 0}, + }; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + struct Release { + const Context& context; + VkRenderPass& pass; + VkFramebuffer& framebuffer; + ~Release() { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + } + } release{context, pass, framebuffer}; + Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &pass), "depth test render pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = target.extent.width; + framebufferInfo.height = target.extent.height; + framebufferInfo.layers = 1; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "depth test framebuffer"); + VkClearValue clear{}; + clear.depthStencil.depth = depth; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea.extent = target.extent; + begin.clearValueCount = 1; + begin.pClearValues = &clear; + const auto commands = recorder.Commands(); + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdEndRenderPass")(commands); + recorder.Sync(); +} + +void depthArrayTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + TextureCache cache(context); + context.textureCache = &cache; + struct Release { + const Context& context; + ~Release() { ClearDepthSurfaces(context.device); } + } release{context}; + SampleProgram arrayProgram(context, recorder, SAMPLE_Depth_ARRAY_SPV); + SampleProgram stencilProgram(context, recorder, SAMPLE_Stencil_ARRAY_SPV, true); + SampleProgram flatProgram(context, recorder); + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + const bool stencil = format == VK_FORMAT_D32_SFLOAT_S8_UINT; + DepthTarget target{stencil ? 0x2000000u : 0x1000000u, stencil ? 0x3000000u : 0u, {129, 137}, format, 0.0f, 0}; + const auto depthStride = DepthSliceBytes(target.extent, stencil ? 4u : 2u); + const auto stencilStride = DepthSliceBytes(target.extent, 1); + std::array targets; + for (std::uint32_t layer = 0; layer < targets.size(); ++layer) { + auto& slice = targets[layer]; + slice = target; + slice.address += layer * depthStride; + if (stencil) slice.stencilAddress += layer * stencilStride; + slice.clearDepth = (layer + 1) / 8.0f; + slice.clearStencil = static_cast(11 + layer); + RunDepthClearPass(context, {slice, VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)}); + } + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2DArray; + resource.depthOrLastArray = 3; + resource.format = stencil ? 22 : 7; + const auto descriptorWords = [](const GuestTextureResource& descriptor) { + std::array words{}; + words[0] = static_cast(descriptor.baseAddress >> 8); + words[1] = static_cast(descriptor.baseAddress >> 40) | (descriptor.format << 20) | (((descriptor.width - 1) & 3u) << 30); + words[2] = ((descriptor.width - 1) >> 2) | ((descriptor.height - 1) << 14); + words[3] = 0xd1800fac; + words[4] = descriptor.depthOrLastArray | (descriptor.baseArray << 16); + return words; + }; + const VkComponentMapping swizzle{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + const auto sample = [&](const GuestTextureResource& descriptor) { + return DepthSurfaceTexture(context, descriptorWords(descriptor), descriptor, swizzle); + }; + const auto texture = sample(resource); + Require(texture != nullptr && texture->FirstLayerView() != VK_NULL_HANDLE, "depth array views are missing"); + for (std::uint32_t layer = 0; layer < targets.size(); ++layer) expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, static_cast(layer)), targets[layer].clearDepth, "depth array slice contents"); + expectRed(flatProgram.Red(texture->FirstLayerView(), texture->Layout(), 0), targets[0].clearDepth, "depth array first-layer view"); + Require(sample(resource) == texture, "an unchanged depth array was not reused"); + ShaderRecompiler::RecompileResult compiled; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DArray; + const auto words = descriptorWords(resource); + binding.guestDescriptor.assign(words.begin(), words.end()); + compiled.bindings.push_back(binding); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compiled, 0}; + ShaderResources bindings(context, shader); + Require(bindings.Reusable(), "depth array bindings cannot be revalidated"); + targets[2].clearDepth = 0.875f; + RunDepthClearPass(context, {targets[2], VK_IMAGE_ASPECT_DEPTH_BIT}); + Require(bindings.ProveCurrent(shader), "depth array bindings failed revalidation after a clear"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.875f, "cached shader resources did not refresh a depth array"); + Require(sample(resource) == texture, "updating a depth array replaced its sampled view"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.875f, "depth array retained a cleared layer"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 1), targets[1].clearDepth, "depth array clear changed another layer"); + auto subset = resource; + subset.baseArray = 2; + auto subsetTexture = sample(subset); + expectRed(arrayProgram.Red(subsetTexture->View(), subsetTexture->Layout(), 0, 0), 0.875f, "depth array BASE_ARRAY is ignored"); + expectRed(arrayProgram.Red(subsetTexture->View(), subsetTexture->Layout(), 0, 1), targets[3].clearDepth, "depth array subset last layer"); + expectRed(flatProgram.Red(subsetTexture->FirstLayerView(), subsetTexture->Layout(), 0), 0.875f, "depth array first-layer view ignores BASE_ARRAY"); + subset.baseArray = 3; + subsetTexture = sample(subset); + expectRed(arrayProgram.Red(subsetTexture->View(), subsetTexture->Layout(), 0, 0), targets[3].clearDepth, "single-layer depth array"); + clearDepthAttachment(context, recorder, targets[2], 0.625f); + Require(bindings.ProveCurrent(shader), "depth array bindings failed revalidation after attachment use"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.625f, "depth array retained old attachment contents"); + auto shifted = resource; + shifted.baseAddress -= depthStride; + shifted.baseArray = 1; + shifted.depthOrLastArray = 4; + const auto shiftedTexture = sample(shifted); + Require(shiftedTexture != nullptr, "a depth array subview requires its unused base slice to be resident"); + expectRed(arrayProgram.Red(shiftedTexture->View(), shiftedTexture->Layout(), 0, 0), targets[0].clearDepth, "depth array shifted first layer"); + expectRed(arrayProgram.Red(shiftedTexture->View(), shiftedTexture->Layout(), 0, 2), 0.625f, "depth array shifted attachment layer"); + if (stencil) { + auto stencilResource = resource; + stencilResource.baseAddress = target.stencilAddress; + stencilResource.format = 5; + const auto stencilTexture = sample(stencilResource); + for (std::uint32_t layer = 0; layer < targets.size(); ++layer) expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, static_cast(layer)), targets[layer].clearStencil, "stencil array slice contents"); + targets[2].clearStencil = 37; + RunDepthClearPass(context, {targets[2], VK_IMAGE_ASPECT_STENCIL_BIT}); + Require(sample(stencilResource) == stencilTexture, "stencil array view changed after a clear"); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 2), 37, "stencil array retained a cleared layer"); + static_cast(sample(resource)); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.625f, "stencil array clear changed depth"); + } + auto missing = resource; + missing.depthOrLastArray = 4; + bool refused = false; + try { static_cast(sample(missing)); } catch (const std::runtime_error&) { refused = true; } + Require(refused, "depth array with an unbacked slice was silently sampled"); + shifted.baseArray = 0; + refused = false; + try { static_cast(sample(shifted)); } catch (const std::runtime_error&) { refused = true; } + Require(refused, "depth array with a missing first slice ignored its resident later layers"); + } + recorder.Sync(); +} + void minLodTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (!context.imageViewMinLod) { @@ -2812,13 +3032,23 @@ void keysFillTests(const Device& device, Recorder& recorder) { recorder.Sync(); } -int main() { +int main(int argc, char** argv) { try { Device device; std::lock_guard gpu(GpuMutex()); std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--depth-array-only") { + depthArrayTests(device, recorder); + std::cout << "Depth/stencil array sampling, subviews and refresh tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--depth-clear-only") { + depthClearPassTests(device, recorder); + std::cout << "Resident depth clear sampling and aspect preservation tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); @@ -2855,6 +3085,9 @@ int main() { firstLayerViewTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); + cmaskPassTests(device, recorder); + depthClearPassTests(device, recorder); + depthArrayTests(device, recorder); pendingKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); std::cout << "Recorder read tracking and label tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleArray.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleArray.comp new file mode 100644 index 00000000000..0e2543f99ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleArray.comp @@ -0,0 +1,14 @@ +#version 450 + +layout(local_size_x = 1) in; +#ifdef STENCIL +layout(set = 0, binding = 0) uniform usampler2DArray source; +#else +layout(set = 0, binding = 0) uniform sampler2DArray source; +#endif +layout(set = 0, binding = 1, std430) buffer Result { vec4 texel; }; +layout(push_constant) uniform Parameters { float lod; float layer; } parameters; + +void main() { + texel = vec4(textureLod(source, vec3(0.5, 0.5, parameters.layer), parameters.lod)); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1787c8127e9..e5623b71030 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,6 +231,8 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings +- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Every viewed layer must already have a matching resident depth/stencil target; missing layers throw. Uploads from guest memory, CPU-write coherence, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. + - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done From cb41a1ee7bcc952fe45c961ed69eab62a2eaadd3 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 07:18:26 +0300 Subject: [PATCH 03/11] fix(agc): upload missing depth array layers from guest memory Detile nonresident layers using their absolute guest slice index while preserving resident depth and stencil images. Refresh guest-backed layers after writes and switch to rendered contents when a resident layer appears. Reject unbacked and compressed guest layers. Validation: depth-array and depth-clear GPU regressions pass with Khronos core and synchronization validation. Tests cover mixed D16 and D32/S8 arrays, guest writes, slice swizzles, subviews and newly rendered layers. Graphics CTest passes. AI-assisted implementation. --- .../Graphics/src/DepthSurface.cpp | 105 +++++++++++++++--- .../prx/libSceAgcDriver/tests/Recorder.cpp | 94 ++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 186 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 75bded588c4..b361ef3d147 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -3,10 +3,13 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include +#include #include #include #include @@ -16,6 +19,11 @@ namespace AgcDriver::Graphics { namespace { +struct GuestDepthSlice { + std::vector bytes; + std::uint64_t generation = 0; +}; + class DepthSurface { public: DepthSurface(const Context& context, const DepthTarget& target, std::uint32_t layers = 1) : context(context), target(target) { @@ -128,40 +136,108 @@ class DepthSurface { auto found = textures.find(key); if (found == textures.end()) { SampledDepth sampled; - if (slices.size() > 1) sampled.array = std::make_unique(context, target, static_cast(slices.size())); + if (slices.size() > 1 || slices.front() == nullptr) sampled.array = std::make_unique(context, target, static_cast(slices.size())); const auto source = sampled.array ? sampled.array->image : slices.front()->image; const auto type = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; sampled.texture = std::make_shared(context, source, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, type, static_cast(slices.size())); sampled.versions.resize(slices.size()); + sampled.guest.resize(slices.size()); found = textures.emplace(key, std::move(sampled)).first; } auto& sampled = found->second; - if (sampled.array) sampled.array->CopySlices(slices, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, sampled.versions); + if (sampled.array) sampled.array->CopySlices(resource, slices, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, sampled.versions, sampled.guest); return sampled.texture; } - void CopySlices(std::span slices, VkImageAspectFlags aspect, std::vector>& versions) { - bool changed = false; - for (std::size_t i = 0; i < slices.size(); ++i) changed |= versions[i] != std::pair{slices[i]->image, slices[i]->version}; - if (!changed) return; + void CopySlices(const GuestTextureResource& resource, std::span slices, VkImageAspectFlags aspect, std::vector>& versions, std::vector& guest) { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? 1u : d16 ? 2u : 4u; + const auto stride = DepthSliceBytes(target.extent, bytes); + std::vector changed(slices.size()); + std::vector pending(slices.size()); + for (std::size_t i = 0; i < slices.size(); ++i) { + if (slices[i] != nullptr) { + changed[i] = versions[i] != std::pair{slices[i]->image, slices[i]->version}; + continue; + } + Require(resource.dccAddress == 0, "guest depth array layers with compression metadata are not implemented"); + const auto address = resource.baseAddress + (resource.baseArray + i) * stride; + StorageTexture::FlushPending(address, stride, nullptr, "depth array layer"); + GuestMemory::FlushGpuWrites(address, stride); + const auto generation = GuestMemory::CollectWrites(address, stride); + if (!guest[i].bytes.empty() && (GuestMemory::UnchangedSince(address, stride, guest[i].generation) || GuestMemory::CompareMapped(address, guest[i].bytes) == GuestMemory::Compare::Equal)) { + guest[i].generation = generation; + continue; + } + pending[i].bytes.resize(stride); + GuestMemory::Read(address, pending[i].bytes); + pending[i].generation = generation; + changed[i] = true; + } + if (std::none_of(changed.begin(), changed.end(), [](bool value) { return value; })) return; + if (std::any_of(pending.begin(), pending.end(), [](const auto& slice) { return !slice.bytes.empty(); })) { + Require(context.detiler != nullptr, "depth array upload requires a texture detiler"); + context.detiler->BeginBatch(); + } auto* recorder = Recorder::Active(); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + std::vector> staging; + std::vector> scratch; for (std::uint32_t i = 0; i < slices.size(); ++i) { - if (versions[i] == std::pair{slices[i]->image, slices[i]->version}) continue; - VkImageCopy copy{}; - copy.srcSubresource = {aspect, 0, 0, 1}; - copy.dstSubresource = {aspect, 0, i, 1}; - copy.extent = {target.extent.width, target.extent.height, 1}; - context.Function("vkCmdCopyImage")(commands, slices[i]->image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + if (!changed[i]) continue; + if (slices[i] == nullptr) { + Require(context.detiler != nullptr, "depth array upload requires a texture detiler"); + const auto mips = ComputeElementMipLayout(resource.tileMode, bytes, resource.width, resource.height, 1); + Require(mips.size() == 1 && mips[0].tiledSize == stride && mips[0].tiledOffset == 0 && !mips[0].tail, "depth array upload layout disagrees with the attachment slice stride"); + const auto& mip = mips[0]; + auto upload = std::make_shared(context, stride, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + auto tiled = std::make_shared(context, stride, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + auto linear = std::make_shared(context, mip.linearSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload->Bytes().data(), pending[i].bytes.data(), pending[i].bytes.size()); + if (recorder != nullptr) { + recorder->Keep(upload); + recorder->Keep(tiled); + recorder->Keep(linear); + } + staging.push_back(upload); + scratch.push_back(tiled); + scratch.push_back(linear); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, upload->Handle(), 0, tiled->Handle(), 0, stride); + 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); + context.detiler->Dispatch(commands, resource.tileMode, bytes, tiled->Handle(), 0, linear->Handle(), 0, mip, false, resource.baseArray + i); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.bufferRowLength = mip.pitchBytes / bytes; + copy.imageSubresource = {aspect, 0, i, 1}; + copy.imageExtent = {resource.width, resource.height, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + } else { + VkImageCopy copy{}; + copy.srcSubresource = {aspect, 0, 0, 1}; + copy.dstSubresource = {aspect, 0, i, 1}; + copy.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, slices[i]->image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } } RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, access); if (batch) batch->SubmitAndWait(); else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); - for (std::size_t i = 0; i < slices.size(); ++i) versions[i] = {slices[i]->image, slices[i]->version}; + for (std::size_t i = 0; i < slices.size(); ++i) { + if (!changed[i]) continue; + if (slices[i] != nullptr) { + versions[i] = {slices[i]->image, slices[i]->version}; + guest[i] = {}; + } else { + versions[i] = {}; + guest[i] = std::move(pending[i]); + } + } } const Context context; @@ -176,6 +252,7 @@ class DepthSurface { std::unique_ptr array; std::shared_ptr texture; std::vector> versions; + std::vector guest; }; std::map, SampledDepth> textures; @@ -289,7 +366,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spantarget.format != base->target.format || slice->target.extent.width != resource.width || slice->target.extent.height != resource.height || (stencil ? slice->target.stencilAddress : slice->target.address) != address) { + if (slice != nullptr && (slice->target.format != base->target.format || slice->target.extent.width != resource.width || slice->target.extent.height != resource.height || (stencil ? slice->target.stencilAddress : slice->target.address) != address)) { char text[192]; std::snprintf(text, sizeof(text), "AGC graphics: depth array 0x%llx layer %u at 0x%llx has no matching resident %s surface", static_cast(resource.baseAddress), layer, static_cast(address), stencil ? "stencil" : "depth"); throw std::runtime_error(text); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 60cf4fa423a..e7e98bdd96f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -32,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -2781,6 +2783,96 @@ void depthArrayTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void depthArrayGuestTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram depthProgram(context, recorder, SAMPLE_Depth_ARRAY_SPV); + SampleProgram stencilProgram(context, recorder, SAMPLE_Stencil_ARRAY_SPV, true); + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + const bool stencil = format == VK_FORMAT_D32_SFLOAT_S8_UINT; + const VkExtent2D extent{129, 137}; + const auto depthBytes = stencil ? 4u : 2u; + const auto depthStride = DepthSliceBytes(extent, depthBytes); + const auto stencilStride = DepthSliceBytes(extent, 1); + std::vector memory(3 * (depthStride + stencilStride) + 256); + const auto address = (reinterpret_cast(memory.data()) + 255) & ~std::uint64_t{255}; + DepthTarget target{address, stencil ? address + 3 * depthStride : 0, extent, format, 0.25f, 19}; + struct Release { + const Context& context; + Recorder& recorder; + ~Release() { recorder.Sync(); ClearDepthSurfaces(context.device); } + } release{context, recorder}; + RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)}); + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = extent.width; + resource.height = extent.height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2DArray; + resource.depthOrLastArray = 2; + resource.format = stencil ? 22 : 7; + const std::array words{}; + const VkComponentMapping mapping{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + const auto sample = [&](const GuestTextureResource& descriptor) { + auto texture = DepthSurfaceTexture(context, words, descriptor, mapping); + Require(texture != nullptr, "mixed depth array was not recognized"); + return texture; + }; + const auto writeCenter = [&](std::uint64_t base, std::uint32_t bytes, std::uint32_t layer, std::uint32_t value) { + const auto block = ThinBlockLayout(TextureTileMode::kZ64KBX, bytes); + const auto* equation = FindTextureSwizzleEquation(24, bytes); + Require(equation != nullptr, "missing depth swizzle equation"); + const auto x = extent.width / 2; + const auto y = extent.height / 2; + const auto coordinates = x | (y << 12) | (layer << 24); + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) offset |= std::uint64_t{std::popcount(coordinates & equation->bits[bit]) & 1u} << bit; + offset += ((y / block[2]) * ((extent.width + block[1] - 1) / block[1]) + x / block[1]) * block[0]; + const auto destination = base + layer * DepthSliceBytes(extent, bytes) + offset; + AgcDriver::GuestMemory::Write(destination, std::span(reinterpret_cast(&value), bytes)); + }; + const auto depthValue = [&](float value) { return stencil ? std::bit_cast(value) : static_cast(value * 65535.0f); }; + writeCenter(address, depthBytes, 1, depthValue(0.5f)); + writeCenter(address, depthBytes, 2, depthValue(0.75f)); + auto texture = sample(resource); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 0), 0.25f, "mixed array lost its resident slice"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 1), 0.5f, "mixed array did not detile guest layer one"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.75f, "mixed array did not detile guest layer two"); + Require(sample(resource) == texture, "unchanged mixed array was replaced"); + writeCenter(address, depthBytes, 1, depthValue(0.875f)); + Require(sample(resource) == texture, "guest write replaced the mixed array view"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 1), 0.875f, "mixed array retained old guest bytes"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.75f, "guest write changed an unrelated layer"); + auto subset = resource; + subset.baseArray = 2; + auto subview = sample(subset); + expectRed(depthProgram.Red(subview->View(), subview->Layout(), 0), 0.75f, "guest-only subview lost its absolute slice swizzle"); + if (stencil) { + auto plane = resource; + plane.baseAddress = target.stencilAddress; + plane.format = 5; + writeCenter(plane.baseAddress, 1, 1, 37); + writeCenter(plane.baseAddress, 1, 2, 59); + auto stencilTexture = sample(plane); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 0), 19, "mixed stencil array lost its resident slice"); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 1), 37, "mixed stencil array lost guest layer one"); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 2), 59, "mixed stencil array lost guest layer two"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 1), 0.875f, "guest stencil upload changed depth"); + } + auto rendered = target; + rendered.address += 2 * depthStride; + if (stencil) rendered.stencilAddress += 2 * stencilStride; + rendered.clearDepth = 0.125f; + RunDepthClearPass(context, {rendered, VK_IMAGE_ASPECT_DEPTH_BIT}); + Require(sample(resource) == texture, "rendering a guest layer replaced the mixed array view"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.125f, "mixed array ignored a newly rendered layer"); + Require(sample(subset) == subview, "rendering replaced a guest-only subview"); + expectRed(depthProgram.Red(subview->View(), subview->Layout(), 0), 0.125f, "guest-only subview ignored a newly rendered layer"); + } +} + void minLodTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (!context.imageViewMinLod) { @@ -3041,6 +3133,7 @@ int main(int argc, char** argv) { recorder.Activate(); if (argc == 2 && std::string_view(argv[1]) == "--depth-array-only") { depthArrayTests(device, recorder); + depthArrayGuestTests(device, recorder); std::cout << "Depth/stencil array sampling, subviews and refresh tests passed\n"; return 0; } @@ -3088,6 +3181,7 @@ int main(int argc, char** argv) { cmaskPassTests(device, recorder); depthClearPassTests(device, recorder); depthArrayTests(device, recorder); + depthArrayGuestTests(device, recorder); pendingKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); std::cout << "Recorder read tracking and label tests passed\n"; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e5623b71030..3620f935e3b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,7 +231,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings -- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Every viewed layer must already have a matching resident depth/stencil target; missing layers throw. Uploads from guest memory, CPU-write coherence, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. +- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries From 3a0b6b48d7ed17cc153f5a4868d09b3e630e221d Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 07:39:48 +0300 Subject: [PATCH 04/11] fix(agc): track depth writes after recording commands Version depth and stencil independently after draws and clears. Keep attachment lookup read-only so a gather during resource binding cannot hide a later attachment write. Validated GPU sampling, aspect isolation and copy counts with Vulkan synchronization validation; graphics tests pass. Written with AI assistance. --- .../Graphics/include/DepthSurface.hpp | 2 + .../Graphics/src/DepthSurface.cpp | 40 +++++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 96 ++++++++++++++++++- 4 files changed, 133 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index bed95dfe68e..9d8868e4c54 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -12,6 +12,8 @@ namespace AgcDriver::Graphics { class Texture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target, VkImageAspectFlags aspects); +void NoteDepthSurfaceWrite(const Context& context, const State& state); void RunDepthClearPass(const Context& context, const DepthClearPass& pass); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b361ef3d147..29d26950def 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -90,7 +90,6 @@ class DepthSurface { DepthSurface& operator=(const DepthSurface&) = delete; void Clear(const DepthClearPass& pass) { - ++version; auto* recorder = Recorder::Active(); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -101,10 +100,20 @@ class DepthSurface { const VkImageSubresourceRange range{pass.aspects, 0, 1, 0, 1}; context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); + NoteWrite(pass.aspects); if (batch) batch->SubmitAndWait(); else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + void NoteWrite(VkImageAspectFlags aspects) { + if (aspects & VK_IMAGE_ASPECT_DEPTH_BIT) ++depthVersion; + if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) ++stencilVersion; + } + + std::uint64_t Version(VkImageAspectFlags aspect) const { + return aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? stencilVersion : depthVersion; + } + void ValidateSampled(std::span words, const GuestTextureResource& resource) const { std::array key{}; std::copy_n(words.begin(), std::min(words.size(), key.size()), key.begin()); @@ -157,7 +166,7 @@ class DepthSurface { std::vector pending(slices.size()); for (std::size_t i = 0; i < slices.size(); ++i) { if (slices[i] != nullptr) { - changed[i] = versions[i] != std::pair{slices[i]->image, slices[i]->version}; + changed[i] = versions[i] != std::pair{slices[i]->image, slices[i]->Version(aspect)}; continue; } Require(resource.dccAddress == 0, "guest depth array layers with compression metadata are not implemented"); @@ -231,7 +240,7 @@ class DepthSurface { for (std::size_t i = 0; i < slices.size(); ++i) { if (!changed[i]) continue; if (slices[i] != nullptr) { - versions[i] = {slices[i]->image, slices[i]->version}; + versions[i] = {slices[i]->image, slices[i]->Version(aspect)}; guest[i] = {}; } else { versions[i] = {}; @@ -245,9 +254,10 @@ class DepthSurface { VkImage image = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; - std::uint64_t version = 1; private: + std::uint64_t depthVersion = 1; + std::uint64_t stencilVersion = 1; struct SampledDepth { std::unique_ptr array; std::shared_ptr texture; @@ -295,7 +305,6 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { if (surface->context.device == context.device && sameSurface(surface->target, target)) { - ++surface->version; return surface->view; } } @@ -303,6 +312,27 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) return surfaces().back()->view; } +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target, VkImageAspectFlags aspects) { + if (aspects == 0) return; + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device != context.device || !sameSurface(surface->target, target)) continue; + surface->NoteWrite(aspects); + return; + } + throw std::runtime_error("AGC graphics: recording a write to an unknown depth surface"); +} + +void NoteDepthSurfaceWrite(const Context& context, const State& state) { + if (!state.depth) return; + VkImageAspectFlags aspects = state.depthTest && state.depthWrite ? VK_IMAGE_ASPECT_DEPTH_BIT : 0u; + const auto writesStencil = [](const VkStencilOpState& stencil) { + return stencil.writeMask != 0 && (stencil.failOp != VK_STENCIL_OP_KEEP || stencil.passOp != VK_STENCIL_OP_KEEP || stencil.depthFailOp != VK_STENCIL_OP_KEEP); + }; + if (state.stencilTest && (writesStencil(state.stencilFront) || writesStencil(state.stencilBack))) aspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + NoteDepthSurfaceWrite(context, *state.depth, aspects); +} + void ClearDepthSurfaces(VkDevice device) { std::lock_guard lock(surfacesMutex()); std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index f9e21abd9e9..a4eb69b38dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1412,6 +1412,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + NoteDepthSurfaceWrite(context, state); if (meshArguments != nullptr) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); auto checkRecords = indirectRecordCheck(record.indirect); @@ -1883,6 +1884,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + NoteDepthSurfaceWrite(context, state); if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); auto checkRecords = indirectRecordCheck(args != nullptr ? &indirect : nullptr); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index e7e98bdd96f..638823b183b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -2600,8 +2601,55 @@ void depthClearPassTests(const Device& device, Recorder& recorder) { recorder.Sync(); } -void clearDepthAttachment(const Context& context, Recorder& recorder, const DepthTarget& target, float depth) { +class DepthOperations { +public: + explicit DepthOperations(Context& context) : context(context), resolver(context.deviceProc), + createImage(context.Function("vkCreateImage")), + copyImage(context.Function("vkCmdCopyImage")), + uploadImage(context.Function("vkCmdCopyBufferToImage")) { + Require(active == nullptr, "nested depth operation counters"); + active = this; + context.deviceProc = Resolve; + } + ~DepthOperations() { + context.deviceProc = resolver; + active = nullptr; + } + std::size_t images = 0; + std::size_t copies = 0; + std::size_t uploads = 0; + +private: + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast(CreateImage); + if (std::strcmp(name, "vkCmdCopyImage") == 0) return reinterpret_cast(CopyImage); + if (std::strcmp(name, "vkCmdCopyBufferToImage") == 0) return reinterpret_cast(UploadImage); + return active->resolver(device, name); + } + static VKAPI_ATTR VkResult VKAPI_CALL CreateImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* allocator, VkImage* image) { + const auto result = active->createImage(device, info, allocator, image); + if (result == VK_SUCCESS) ++active->images; + return result; + } + static VKAPI_ATTR void VKAPI_CALL CopyImage(VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage destination, VkImageLayout destinationLayout, std::uint32_t count, const VkImageCopy* regions) { + active->copies += count; + active->copyImage(commands, source, sourceLayout, destination, destinationLayout, count, regions); + } + static VKAPI_ATTR void VKAPI_CALL UploadImage(VkCommandBuffer commands, VkBuffer source, VkImage destination, VkImageLayout layout, std::uint32_t count, const VkBufferImageCopy* regions) { + active->uploads += count; + active->uploadImage(commands, source, destination, layout, count, regions); + } + Context& context; + PFN_vkGetDeviceProcAddr resolver; + PFN_vkCreateImage createImage; + PFN_vkCmdCopyImage copyImage; + PFN_vkCmdCopyBufferToImage uploadImage; + static inline DepthOperations* active = nullptr; +}; + +void clearDepthAttachment(const Context& context, Recorder& recorder, const DepthTarget& target, float depth, const std::function& beforeWrite = {}) { const auto view = DepthSurfaceView(context, target); + if (beforeWrite) beforeWrite(); VkAttachmentDescription attachment{}; attachment.format = target.format; attachment.samples = VK_SAMPLE_COUNT_1_BIT; @@ -2658,11 +2706,13 @@ void clearDepthAttachment(const Context& context, Recorder& recorder, const Dept const auto commands = recorder.Commands(); context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); context.Function("vkCmdEndRenderPass")(commands); + NoteDepthSurfaceWrite(context, target, VK_IMAGE_ASPECT_DEPTH_BIT); recorder.Sync(); } void depthArrayTests(const Device& device, Recorder& recorder) { auto context = device.GetContext(); + DepthOperations operations(context); TextureDetiler detiler(context); context.detiler = &detiler; TextureCache cache(context); @@ -2716,6 +2766,25 @@ void depthArrayTests(const Device& device, Recorder& recorder) { for (std::uint32_t layer = 0; layer < targets.size(); ++layer) expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, static_cast(layer)), targets[layer].clearDepth, "depth array slice contents"); expectRed(flatProgram.Red(texture->FirstLayerView(), texture->Layout(), 0), targets[0].clearDepth, "depth array first-layer view"); Require(sample(resource) == texture, "an unchanged depth array was not reused"); + const auto readOnlyCopies = operations.copies; + static_cast(DepthSurfaceView(context, targets[2])); + State readOnly{}; + readOnly.depth = targets[2]; + readOnly.depthTest = true; + readOnly.stencilTest = stencil; + readOnly.stencilFront.writeMask = 0xff; + readOnly.stencilBack.passOp = VK_STENCIL_OP_REPLACE; + NoteDepthSurfaceWrite(context, readOnly); + static_cast(sample(resource)); + Require(operations.copies == readOnlyCopies, "read-only attachment use copied a depth array"); + auto disabled = readOnly; + disabled.depthTest = false; + disabled.depthWrite = true; + disabled.stencilTest = false; + disabled.stencilFront.passOp = VK_STENCIL_OP_REPLACE; + NoteDepthSurfaceWrite(context, disabled); + static_cast(sample(resource)); + Require(operations.copies == readOnlyCopies, "disabled depth testing copied a depth array"); ShaderRecompiler::RecompileResult compiled; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; @@ -2746,7 +2815,10 @@ void depthArrayTests(const Device& device, Recorder& recorder) { subset.baseArray = 3; subsetTexture = sample(subset); expectRed(arrayProgram.Red(subsetTexture->View(), subsetTexture->Layout(), 0, 0), targets[3].clearDepth, "single-layer depth array"); - clearDepthAttachment(context, recorder, targets[2], 0.625f); + clearDepthAttachment(context, recorder, targets[2], 0.625f, [&] { + Require(sample(resource) == texture, "attachment setup replaced a depth array view"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.875f, "depth array changed before the attachment write"); + }); Require(bindings.ProveCurrent(shader), "depth array bindings failed revalidation after attachment use"); expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.625f, "depth array retained old attachment contents"); auto shifted = resource; @@ -2763,12 +2835,32 @@ void depthArrayTests(const Device& device, Recorder& recorder) { stencilResource.format = 5; const auto stencilTexture = sample(stencilResource); for (std::uint32_t layer = 0; layer < targets.size(); ++layer) expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, static_cast(layer)), targets[layer].clearStencil, "stencil array slice contents"); + const auto stencilCopies = operations.copies; + NoteDepthSurfaceWrite(context, readOnly); + static_cast(sample(stencilResource)); + Require(operations.copies == stencilCopies, "read-only attachment use copied a stencil array"); targets[2].clearStencil = 37; RunDepthClearPass(context, {targets[2], VK_IMAGE_ASPECT_STENCIL_BIT}); Require(sample(stencilResource) == stencilTexture, "stencil array view changed after a clear"); expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 2), 37, "stencil array retained a cleared layer"); + Require(operations.copies == stencilCopies + 1, "a stencil clear copied unchanged slices"); static_cast(sample(resource)); + Require(operations.copies == stencilCopies + 1, "a stencil-only clear copied depth"); expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.625f, "stencil array clear changed depth"); + RunDepthClearPass(context, {targets[2], VK_IMAGE_ASPECT_DEPTH_BIT}); + static_cast(sample(stencilResource)); + Require(operations.copies == stencilCopies + 1, "a depth-only clear copied stencil"); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 2), 37, "depth array clear changed stencil"); + static_cast(sample(resource)); + Require(operations.copies == stencilCopies + 2, "a depth clear copied unchanged slices"); + expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), targets[2].clearDepth, "depth-only clear did not refresh depth"); + auto writableStencil = readOnly; + writableStencil.stencilBack.writeMask = 0xff; + NoteDepthSurfaceWrite(context, writableStencil); + static_cast(sample(resource)); + Require(operations.copies == stencilCopies + 2, "a stencil-writing state copied depth"); + static_cast(sample(stencilResource)); + Require(operations.copies == stencilCopies + 3, "a stencil-writing state did not refresh its slice"); } auto missing = resource; missing.depthOrLastArray = 4; From 0c3feb69c509bb64686a4223385773aa5434a4f9 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 07:44:57 +0300 Subject: [PATCH 05/11] perf(agc): share depth array backing across texture views Cache gathered depth storage and guest snapshots separately from component views. Keep gathered images alive through surviving views and preserve independent aspect, layer and metadata identities. Add allocation/copy/upload checks, view lifetime sampling and pending-copy exception cleanup coverage. Correct test fixture destruction order after an old-code failing assertion caused an RX6600 reset. Remaining validation used CPU-only lavapipe; depth and graphics checks pass. Written with AI assistance. --- .../Graphics/src/DepthSurface.cpp | 62 +++++++++------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 73 ++++++++++++++++--- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 101 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 29d26950def..92887e26d6f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -131,31 +131,35 @@ class DepthSurface { } } - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::span slices, bool stencil) { - std::array key{}; - std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); - key[8] = components.r; - key[9] = components.g; - key[10] = components.b; - key[11] = components.a; - key[12] = resource.baseArray; - key[13] = resource.depthOrLastArray; - key[14] = static_cast(resource.dimension); - key[15] = stencil; - auto found = textures.find(key); - if (found == textures.end()) { + std::shared_ptr Sampled(const GuestTextureResource& resource, VkComponentMapping components, std::span slices, bool stencil) { + const VkImageAspectFlags aspect = stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + const BackingKey backingKey{resource.baseAddress, resource.baseArray, slices.size(), static_cast(resource.tileMode), aspect, resource.dccAddress, resource.dccAlphaOnMsb, resource.format}; + auto backing = arrays.find(backingKey); + if (backing == arrays.end() && (slices.size() > 1 || slices.front() == nullptr)) { SampledDepth sampled; - if (slices.size() > 1 || slices.front() == nullptr) sampled.array = std::make_unique(context, target, static_cast(slices.size())); - const auto source = sampled.array ? sampled.array->image : slices.front()->image; - const auto type = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - sampled.texture = std::make_shared(context, source, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, type, static_cast(slices.size())); + sampled.array = std::make_shared(context, target, static_cast(slices.size())); sampled.versions.resize(slices.size()); sampled.guest.resize(slices.size()); - found = textures.emplace(key, std::move(sampled)).first; + backing = arrays.emplace(backingKey, std::move(sampled)).first; } - auto& sampled = found->second; - if (sampled.array) sampled.array->CopySlices(resource, slices, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, sampled.versions, sampled.guest); - return sampled.texture; + const auto key = std::pair{backingKey, std::array{components.r, components.g, components.b, components.a, static_cast(resource.dimension)}}; + auto found = textures.find(key); + if (found == textures.end()) { + const auto type = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + std::shared_ptr texture; + if (backing == arrays.end()) { + texture = std::make_shared(context, slices.front()->image, target.format, aspect, components, type, 1); + } else { + auto owned = std::make_shared(backing->second.array, aspect, components, type, static_cast(slices.size())); + texture = std::shared_ptr(owned, &owned->texture); + } + found = textures.emplace(key, std::move(texture)).first; + } + if (backing != arrays.end()) { + auto& sampled = backing->second; + sampled.array->CopySlices(resource, slices, aspect, sampled.versions, sampled.guest); + } + return found->second; } void CopySlices(const GuestTextureResource& resource, std::span slices, VkImageAspectFlags aspect, std::vector>& versions, std::vector& guest) { @@ -258,16 +262,24 @@ class DepthSurface { private: std::uint64_t depthVersion = 1; std::uint64_t stencilVersion = 1; + using BackingKey = std::array; struct SampledDepth { - std::unique_ptr array; - std::shared_ptr texture; + std::shared_ptr array; std::vector> versions; std::vector guest; }; - std::map, SampledDepth> textures; + struct SampledView { + std::shared_ptr array; + Texture texture; + SampledView(std::shared_ptr source, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType type, std::uint32_t layers) + : array(std::move(source)), texture(array->context, array->image, array->target.format, aspect, components, type, layers) {} + }; + std::map arrays; + std::map>, std::shared_ptr> textures; void release() noexcept { textures.clear(); + arrays.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); @@ -403,7 +415,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanSampled(words, resource, components, slices, stencil); + return base->Sampled(resource, components, slices, stencil); } bool DepthSurfaceAt(std::uint64_t address) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 638823b183b..13b592dcda5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -190,6 +190,7 @@ class Device { function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Vulkan device: " << properties.deviceName << '\n'; context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -2573,10 +2574,12 @@ void expectRed(float got, float want, const char* what) { void depthClearPassTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); DepthTarget target{0x10000, 0x20000, {64, 64}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 19}; + SampleProgram program(context, recorder); struct Release { const Context& context; - ~Release() { ClearDepthSurfaces(context.device); } - } release{context}; + Recorder& recorder; + ~Release() { recorder.Sync(); ClearDepthSurfaces(context.device); } + } release{context, recorder}; RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT}); GuestTextureResource resource{}; resource.baseAddress = target.address; @@ -2589,7 +2592,6 @@ void depthClearPassTests(const Device& device, Recorder& recorder) { const VkComponentMapping mapping{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; const auto texture = DepthSurfaceTexture(context, words, resource, mapping); Require(texture != nullptr, "depth clear did not create a sampleable surface"); - SampleProgram program(context, recorder); expectRed(program.Red(texture->View(), texture->Layout(), 0.0f), 0.25f, "initial depth clear"); target.clearDepth = 0.75f; RunDepthClearPass(context, {target, VK_IMAGE_ASPECT_DEPTH_BIT}); @@ -2681,11 +2683,13 @@ void clearDepthAttachment(const Context& context, Recorder& recorder, const Dept const Context& context; VkRenderPass& pass; VkFramebuffer& framebuffer; + Recorder& recorder; ~Release() { + recorder.Sync(); if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); } - } release{context, pass, framebuffer}; + } release{context, pass, framebuffer, recorder}; Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &pass), "depth test render pass"); VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = pass; @@ -2710,20 +2714,21 @@ void clearDepthAttachment(const Context& context, Recorder& recorder, const Dept recorder.Sync(); } -void depthArrayTests(const Device& device, Recorder& recorder) { +void depthArrayTests(const Device& device, Recorder& recorder, bool failBeforeSync = false) { auto context = device.GetContext(); DepthOperations operations(context); TextureDetiler detiler(context); context.detiler = &detiler; TextureCache cache(context); context.textureCache = &cache; - struct Release { - const Context& context; - ~Release() { ClearDepthSurfaces(context.device); } - } release{context}; SampleProgram arrayProgram(context, recorder, SAMPLE_Depth_ARRAY_SPV); SampleProgram stencilProgram(context, recorder, SAMPLE_Stencil_ARRAY_SPV, true); SampleProgram flatProgram(context, recorder); + struct Release { + const Context& context; + Recorder& recorder; + ~Release() { recorder.Sync(); ClearDepthSurfaces(context.device); } + } release{context, recorder}; for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) { const bool stencil = format == VK_FORMAT_D32_SFLOAT_S8_UINT; DepthTarget target{stencil ? 0x2000000u : 0x1000000u, stencil ? 0x3000000u : 0u, {129, 137}, format, 0.0f, 0}; @@ -2766,6 +2771,24 @@ void depthArrayTests(const Device& device, Recorder& recorder) { for (std::uint32_t layer = 0; layer < targets.size(); ++layer) expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, static_cast(layer)), targets[layer].clearDepth, "depth array slice contents"); expectRed(flatProgram.Red(texture->FirstLayerView(), texture->Layout(), 0), targets[0].clearDepth, "depth array first-layer view"); Require(sample(resource) == texture, "an unchanged depth array was not reused"); + const auto variantImages = operations.images; + const auto variantCopies = operations.copies; + auto oneMapping = swizzle; + oneMapping.r = VK_COMPONENT_SWIZZLE_ONE; + const auto oneTexture = DepthSurfaceTexture(context, descriptorWords(resource), resource, oneMapping); + expectRed(arrayProgram.Red(oneTexture->View(), oneTexture->Layout(), 0, 2), 1.0f, "depth array view ignored its component mapping"); + auto alphaMapping = swizzle; + alphaMapping.a = VK_COMPONENT_SWIZZLE_R; + const auto alphaTexture = DepthSurfaceTexture(context, descriptorWords(resource), resource, alphaMapping); + if (failBeforeSync) { + auto extra = resource; + extra.baseArray = 1; + static_cast(sample(extra)); + throw std::runtime_error("intentional depth test failure with pending copies"); + } + recorder.Sync(); + Require(operations.images == variantImages, "a component mapping allocated another depth array"); + Require(operations.copies == variantCopies, "a component mapping copied unchanged depth slices"); const auto readOnlyCopies = operations.copies; static_cast(DepthSurfaceView(context, targets[2])); State readOnly{}; @@ -2793,6 +2816,7 @@ void depthArrayTests(const Device& device, Recorder& recorder) { binding.binding = 0; binding.count = 1; binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DArray; + binding.imageSamplers = {0}; const auto words = descriptorWords(resource); binding.guestDescriptor.assign(words.begin(), words.end()); compiled.bindings.push_back(binding); @@ -2803,6 +2827,7 @@ void depthArrayTests(const Device& device, Recorder& recorder) { RunDepthClearPass(context, {targets[2], VK_IMAGE_ASPECT_DEPTH_BIT}); Require(bindings.ProveCurrent(shader), "depth array bindings failed revalidation after a clear"); expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.875f, "cached shader resources did not refresh a depth array"); + expectRed(arrayProgram.Red(alphaTexture->View(), alphaTexture->Layout(), 0, 2), 0.875f, "component mappings do not share depth refreshes"); Require(sample(resource) == texture, "updating a depth array replaced its sampled view"); expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.875f, "depth array retained a cleared layer"); expectRed(arrayProgram.Red(texture->View(), texture->Layout(), 0, 1), targets[1].clearDepth, "depth array clear changed another layer"); @@ -2877,6 +2902,7 @@ void depthArrayTests(const Device& device, Recorder& recorder) { void depthArrayGuestTests(const Device& device, Recorder& recorder) { auto context = device.GetContext(); + DepthOperations operations(context); TextureDetiler detiler(context); context.detiler = &detiler; SampleProgram depthProgram(context, recorder, SAMPLE_Depth_ARRAY_SPV); @@ -2933,9 +2959,19 @@ void depthArrayGuestTests(const Device& device, Recorder& recorder) { expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 1), 0.5f, "mixed array did not detile guest layer one"); expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.75f, "mixed array did not detile guest layer two"); Require(sample(resource) == texture, "unchanged mixed array was replaced"); + const auto variantImages = operations.images; + const auto variantCopies = operations.copies; + const auto variantUploads = operations.uploads; + auto alphaMapping = mapping; + alphaMapping.a = VK_COMPONENT_SWIZZLE_R; + const auto alphaTexture = DepthSurfaceTexture(context, words, resource, alphaMapping); + recorder.Sync(); + Require(operations.images == variantImages, "a mixed array mapping allocated another image"); + Require(operations.copies == variantCopies && operations.uploads == variantUploads, "a mixed array mapping copied or uploaded unchanged slices"); writeCenter(address, depthBytes, 1, depthValue(0.875f)); Require(sample(resource) == texture, "guest write replaced the mixed array view"); expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 1), 0.875f, "mixed array retained old guest bytes"); + expectRed(depthProgram.Red(alphaTexture->View(), alphaTexture->Layout(), 0, 1), 0.875f, "mixed array mappings do not share guest uploads"); expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.75f, "guest write changed an unrelated layer"); auto subset = resource; subset.baseArray = 2; @@ -2962,6 +2998,11 @@ void depthArrayGuestTests(const Device& device, Recorder& recorder) { expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.125f, "mixed array ignored a newly rendered layer"); Require(sample(subset) == subview, "rendering replaced a guest-only subview"); expectRed(depthProgram.Red(subview->View(), subview->Layout(), 0), 0.125f, "guest-only subview ignored a newly rendered layer"); + recorder.Sync(); + ClearDepthSurfaces(context.device); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0, 2), 0.125f, "a surviving depth array view lost its backing"); + expectRed(depthProgram.Red(alphaTexture->View(), alphaTexture->Layout(), 0, 1), 0.875f, "a surviving component view lost its backing"); + expectRed(depthProgram.Red(subview->View(), subview->Layout(), 0), 0.125f, "a surviving single-layer array view lost its backing"); } } @@ -3223,6 +3264,19 @@ int main(int argc, char** argv) { std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--depth-array-failure-only") { + bool failed = false; + try { + depthArrayTests(device, recorder, true); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "intentional depth test failure with pending copies") throw; + failed = true; + } + Require(failed, "the depth test did not exercise failure cleanup"); + depthArrayTests(device, recorder); + std::cout << "Depth array failure cleanup and subsequent sampling tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--depth-array-only") { depthArrayTests(device, recorder); depthArrayGuestTests(device, recorder); @@ -3270,7 +3324,6 @@ int main(int argc, char** argv) { firstLayerViewTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); - cmaskPassTests(device, recorder); depthClearPassTests(device, recorder); depthArrayTests(device, recorder); depthArrayGuestTests(device, recorder); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3620f935e3b..0fda6360baa 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,7 +231,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings -- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. +- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries From 7bd0496632f348a1131215f9c28c98c3952edb1a Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 10:19:41 +0300 Subject: [PATCH 06/11] test(agc): reserve inaccessible depth array fixture addresses Avoid assuming low virtual addresses remain unmapped after driver activity. Reserve guard storage for the unbacked-slice checks and release it after GPU cleanup. AI-assisted: yes. --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 13b592dcda5..2a90da4e6ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2715,6 +2715,25 @@ void clearDepthAttachment(const Context& context, Recorder& recorder, const Dept } void depthArrayTests(const Device& device, Recorder& recorder, bool failBeforeSync = false) { + constexpr std::size_t reservedBytes = 12 * 1024 * 1024; +#ifdef _WIN32 + void* reserved = VirtualAlloc(nullptr, reservedBytes, MEM_RESERVE, PAGE_NOACCESS); + Require(reserved != nullptr, "cannot reserve depth array test addresses"); +#else + void* reserved = mmap(nullptr, reservedBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(reserved != MAP_FAILED, "cannot reserve depth array test addresses"); +#endif + struct Reservation { + void* address; + ~Reservation() { +#ifdef _WIN32 + VirtualFree(address, 0, MEM_RELEASE); +#else + munmap(address, reservedBytes); +#endif + } + } reservation{reserved}; + const auto reservedAddress = reinterpret_cast(reserved); auto context = device.GetContext(); DepthOperations operations(context); TextureDetiler detiler(context); @@ -2731,7 +2750,7 @@ void depthArrayTests(const Device& device, Recorder& recorder, bool failBeforeSy } release{context, recorder}; for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) { const bool stencil = format == VK_FORMAT_D32_SFLOAT_S8_UINT; - DepthTarget target{stencil ? 0x2000000u : 0x1000000u, stencil ? 0x3000000u : 0u, {129, 137}, format, 0.0f, 0}; + DepthTarget target{reservedAddress + (stencil ? 0x500000u : 0x100000u), stencil ? reservedAddress + 0x900000u : 0u, {129, 137}, format, 0.0f, 0}; const auto depthStride = DepthSliceBytes(target.extent, stencil ? 4u : 2u); const auto stencilStride = DepthSliceBytes(target.extent, 1); std::array targets; From 80979c992b51585a1b65364b761d75f0220bc049 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 11:26:26 +0300 Subject: [PATCH 07/11] docs(agc): keep texture limitations with graphics notes --- docs/dev/TechnicalDebt.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0fda6360baa..5ce0de4320d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,8 +231,6 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings -- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done @@ -283,6 +281,7 @@ Throughout the project, every function at every stage either **does exactly what - [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [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. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. +- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. From 3b9c2a980edb98c00fbf6e98d81ec2403b4be496 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Wed, 7 Oct 2026 06:18:05 +0300 Subject: [PATCH 08/11] fix(agc): honor disabled color-buffer formats Exclude COLOR_INVALID attachments from the effective write mask while preserving later MRT export indices and depth-only rendering. Validated with the graphics tests and Skyrim menu draws. AI-assisted implementation and testing by OpenAI Codex. --- .../Execution/include/QueueState.hpp | 5 ++- .../src/Driver/Draw/DrawPrecheck.cpp | 2 +- .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 17 ++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 33 +++++++++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index d66bdb19c03..b782a2ac76e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -156,7 +156,10 @@ inline Registers InitialContextRegisters() { {0x318, 0}, {0x31b, 0}, {0x31c, 0}, {0x31d, 0}, {0x390, 0}, {0x3b0, 0}, {0x3b8, 0} }; - for (std::uint32_t i = 0; i < 8; ++i) result.emplace(0x1e0 + i, 0x20010001); + for (std::uint32_t i = 0; i < 8; ++i) { + result.emplace(0x1e0 + i, 0x20010001); + result.emplace(0x31c + 0xfu * i, 0); + } for (std::uint32_t i = 0; i < 16; ++i) { result.emplace(0x94 + 2 * i, 0x80000000); result.emplace(0x95 + 2 * i, 0x40004000); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index f2842ad619a..9eecb5f8b39 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -29,7 +29,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S { const auto targetMask = queue.context.find(0x8e); const auto shaderMask = queue.context.find(0x8f); - const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0; + const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && Graphics::ColorWriteMask(queue.context) != 0; if (!colorWrites && !queue.shader.contains(0x8)) { const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 1bb1fd26c6a..97be2d451f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -103,6 +103,7 @@ State DecodeState(const QueueState& queue); std::array ExportMappings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); +std::uint32_t ColorWriteMask(const Registers& context); struct ColorMetadataPass { enum class Mode { EliminateFastClear, DccDecompress }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 959031d35b8..bb774db5739 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -514,7 +514,7 @@ State DecodeState(const QueueState& queue) { const auto shaderMask = read(cx, 0x8f); // Channels of targets the pixel shader does not export are never written, so the target mask only // matters where the shader exports. - const auto targetMask = read(cx, 0x8e) & shaderMask; + const auto targetMask = ColorWriteMask(cx); APS5_LOG_OUT_DEBUG("CB_TARGET_MASK=0x%x CB_SHADER_MASK=0x%x", targetMask, shaderMask); std::vector exportSlots; for (std::uint32_t slot = 0; slot < 8; ++slot) { @@ -639,6 +639,17 @@ std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height) { return ((width + metablockWidth - 1) / metablockWidth) * ((height + metablockHeight - 1) / metablockHeight) * metablockBytes; } +std::uint32_t ColorWriteMask(const Registers& context) { + auto mask = read(context, 0x8e) & read(context, 0x8f); + for (std::uint32_t slot = 0; slot < 8; ++slot) { + const auto channels = 0xfu << (slot * 4u); + if ((mask & channels) == 0) continue; + const auto info = find(context, 0x31c + slot * 0xfu); + if (info != context.end() && ((info->second >> 2u) & 0x1fu) == 0) mask &= ~channels; + } + return mask; +} + ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto stride = slot * 0xfu; ColorTarget color{}; @@ -805,7 +816,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; - if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word); + if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, ColorWriteMask(cx) != 0)) return colorControlMessage(word); if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason; if (PixelProgramSkipped(queue)) return NullPixelProgramRejection(queue); for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) { @@ -834,7 +845,7 @@ std::string NullPixelProgramRejection(const QueueState& queue) { const auto targetMask = find(queue.context, 0x8e); const auto shaderMask = find(queue.context, 0x8f); if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK"; - if ((targetMask->second & shaderMask->second) != 0) return "AGC graphics: a draw without a pixel program writes color"; + if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; return {}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 29e3ea5ccd9..5d4d7b2fdc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -84,6 +84,14 @@ void stateTests() { initial.context[0xdead] = 1; initial.ClearContext(); Require(initial.context.at(0x200) == 0 && !initial.context.contains(0xdead), "context reset did not restore defaults"); + for (std::uint32_t slot = 0; slot < 8; ++slot) { + const auto info = 0x31cu + 0xfu * slot; + Require(initial.context.at(info) == 0, "reset color target was not disabled"); + initial.context[info] = 10u << 2u; + } + initial.ClearContext(); + initial.context[0x8e] = initial.context[0x8f] = 0xffffffffu; + Require(AgcDriver::Graphics::ColorWriteMask(initial.context) == 0, "context clear kept old color targets enabled"); Require(initial.userConfig.at(0x24b) == 0, "primitive restart must be disabled in initial queue state"); initial.userConfig[0x24b] = 1; initial.ClearContext(); @@ -541,6 +549,27 @@ void DisabledColorTests() { Require(partial.hasColorTarget && partial.blend.colorWriteMask == 3, "partial color write mask changed"); queue.context.erase(0x31c); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); + queue = makeState(); + queue.context[0x31c] = 0; + for (const auto offset : {0x31bu, 0x31du, 0x3b0u, 0x3b8u, 0x390u, 0x318u, 0x1e0u}) queue.context.erase(offset); + const auto disabled = AgcDriver::Graphics::DecodeState(queue); + Require(!disabled.hasColorTarget && disabled.colors.empty() && disabled.blends.empty(), "COLOR_INVALID retained an attachment despite the disabled buffer"); + Require(disabled.renderExtent.width == 64 && disabled.renderExtent.height == 4, "COLOR_INVALID lost the attachment-free render extent"); + queue.shader[0x008] = 0; + queue.shader[0x009] = 0; + Require(AgcDriver::Graphics::NullPixelProgramRejection(queue).empty(), "COLOR_INVALID rejected a draw without a pixel shader"); + queue.context[0x200] = 0x36; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x010] = 0x80000181; + queue.context[0x011] = 0x20000180; + queue.context[0x012] = 0x100; + queue.context[0x014] = 0x100; + queue.context[0x007] = 0x003f003f; + queue.context[0x00a] = 0; + queue.context[0x00b] = std::bit_cast(1.0f); + const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); + Require(!depthOnly.hasColorTarget && depthOnly.depth && depthOnly.depthTest && depthOnly.depthWrite && depthOnly.renderExtent.height == 64, "COLOR_INVALID discarded a depth-only draw"); } void CompactedExportTests() { @@ -562,6 +591,10 @@ void CompactedExportTests() { Require(state.colors[1].slot == 4 && state.colors[1].exportIndex == 1 && state.colors[1].address == slotFour, "export 1 did not reach MRT slot 4, the second slot CB_SHADER_MASK enables"); Require(!state.blends[0].blendEnable && state.blends[1].blendEnable && state.blends[1].colorWriteMask == (VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT), "export 1 did not take MRT slot 4's blend control and target mask"); Require(AgcDriver::Graphics::ExportMappings(state)[1] == state.colors[1].componentMapping, "export 1 did not take MRT slot 4's component mapping"); + auto disabledFirst = queue; + disabledFirst.context[0x31c] = 0; + const auto withHole = AgcDriver::Graphics::DecodeState(disabledFirst); + Require(withHole.colors.size() == 1 && withHole.colors[0].slot == 4 && withHole.colors[0].exportIndex == 1 && withHole.blends.size() == 2 && withHole.blends[0].colorWriteMask == 0, "COLOR_INVALID shifted a later MRT export"); queue.context[0x1c5] = 0x90009u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue.context[0x8e] = 0xf000fu; From 806a595f52c6f291aebc1477f97d7c024b2b7303 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 03:39:05 +0300 Subject: [PATCH 09/11] fix(agc): reject depth maintenance before shader compilation --- .../src/Driver/Draw/DrawPrecheck.cpp | 2 ++ .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 8 ++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 9eecb5f8b39..e75ed53eead 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -23,6 +23,8 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + rejected = Graphics::DepthMaintenanceRejection(queue); + if (!rejected.empty()) return DrawVerdict::Rejected; static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; traceIndirect = traceIndirectEnabled; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 97be2d451f5..3b07fac78ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -115,6 +115,7 @@ struct DepthClearPass { DepthTarget target; VkImageAspectFlags aspects; }; +std::string DepthMaintenanceRejection(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index bb774db5739..06189e70d1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -431,7 +431,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { return result; } +std::string DepthMaintenanceRejection(const QueueState& queue) { + const auto control = find(queue.context, 0x000); + if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; + return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress"); +} + State DecodeState(const QueueState& queue) { + if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason); const auto& cx = queue.context; State result{}; result.stages = DecodeShaderStages(queue); @@ -770,6 +777,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue } std::string DrawRejection(const QueueState& queue, bool indexed) { + if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) return reason; const auto& cx = queue.context; // A register a rule needs that is absent gives no verdict here: DecodeState reports it. const auto value = [&](const Registers& registers, std::uint32_t offset, std::uint32_t& out) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 5d4d7b2fdc7..db66371eaf5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -681,6 +681,31 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); } +void depthMaintenanceTests() { + for (const auto mode : {0x4u, 0x8u, 0x10u, 0x80u, 0x100u, 0x1000u, 0x4000u}) { + for (const auto clear : {0u, 1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x000] = mode | clear; + queue.context[0x200] = 0; + queue.context[0x8e] = 0; + queue.context[0x8f] = 0; + queue.shader.erase(0x8); + const auto reason = AgcDriver::Graphics::DepthMaintenanceRejection(queue); + Require(reason.find("DB_RENDER_CONTROL") != std::string::npos, "depth maintenance passed without depth tests or color writes"); + Require(AgcDriver::Graphics::DrawRejection(queue, false) == reason, "draw precheck did not reject depth maintenance first"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); + } + } + for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x2000u, 0x2063u}) { + auto queue = makeState(); + queue.context[0x000] = control; + Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "ordinary depth controls were mistaken for maintenance"); + } + auto absent = makeState(); + absent.context.erase(0x000); + Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); +} + // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a // format with a depth channel (1, 2, 3 or 32_ABGR 9), the sample mask needs 32_ABGR, and the // formats the export path does not lay out are refused. A missing 0x1c4 gives no verdict here. @@ -2322,6 +2347,7 @@ int main() { AgcDriver::Graphics::ValidateDepthBounds(unrestricted, bounded); } stateTests(); + depthMaintenanceTests(); hardwareScreenOffsetTests(); srgb8TargetTests(); DepthClipTests(); From a27e8e19f0e4fe42a7790aaaae2b56e77f4a0f82 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 04:47:37 +0300 Subject: [PATCH 10/11] feat(agc): add experimental depth clear and copy recognition Extract the held draw decoders and resident-image copy path for review. Register bounds do not prove rasterized coverage; preserve this as an explicit draft blocker in TechnicalDebt. CPU decoding and Vulkan image-operation tests pass on RX 6600 and llvmpipe, without establishing arbitrary geometry or console equivalence. AI-assisted: yes. --- .../Execution/include/VulkanDevice.hpp | 2 + .../src/Driver/Draw/DrawPrecheck.cpp | 20 +++ .../Execution/src/VulkanDevice.cpp | 8 ++ .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/include/State.hpp | 7 ++ .../Graphics/src/DepthSurface.cpp | 48 +++++++ .../libSceAgcDriver/Graphics/src/State.cpp | 108 +++++++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 117 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 62 ++++++++++ docs/dev/TechnicalDebt.md | 4 + 10 files changed, 373 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 8828e54a3a2..d7766c8357a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -280,6 +280,8 @@ class VulkanDevice { // built for its draw-cache entry (design_cpu_final M8); null otherwise. void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); std::optional KnownDrawRejection(const Graphics::State& graphics, std::span shaders) const; + void DepthClearPass(const Graphics::DepthClearPass& pass); + void DepthCopyPass(const Graphics::DepthCopyPass& pass); void ColorMetadataPass(const Graphics::ColorMetadataPass& pass); // A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after // the packet's labels; Rebuild when the recipe's device is not this one or DrawWithRecipe diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index e75ed53eead..2461b427bc7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -25,6 +25,26 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S } rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; + if (const auto pass = Graphics::DecodeDepthCopyPass(queue)) { + require(!drawParameters.indirect, "indirect depth copies are unsupported"); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (device == nullptr) device = std::make_shared(); + const std::shared_ptr localDevice = device; + recordLabelsForPacket(localDevice.get(), submission.queue); + localDevice->DepthCopyPass(*pass); + return DrawVerdict::Drawn; + } + if (const auto pass = Graphics::DecodeDepthClearPass(queue)) { + require(!drawParameters.indirect, "indirect depth clears are unsupported"); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (device == nullptr) device = std::make_shared(); + const std::shared_ptr localDevice = device; + recordLabelsForPacket(localDevice.get(), submission.queue); + localDevice->DepthClearPass(*pass); + return DrawVerdict::Drawn; + } static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; traceIndirect = traceIndirectEnabled; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 755ba86979a..771dd6aef9a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2488,6 +2488,14 @@ std::optional VulkanDevice::KnownDrawRejection(const Graphics::Stat return Graphics::KnownValidationFailure(graphicsContext(), shaders, graphics); } +void VulkanDevice::DepthClearPass(const Graphics::DepthClearPass& pass) { + Graphics::RunDepthClearPass(graphicsContext(), pass); +} + +void VulkanDevice::DepthCopyPass(const Graphics::DepthCopyPass& pass) { + Graphics::RunDepthCopyPass(graphicsContext(), pass); +} + void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 9d8868e4c54..656c7939d06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -15,6 +15,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target, VkImageAspectFlags aspects); void NoteDepthSurfaceWrite(const Context& context, const State& state); void RunDepthClearPass(const Context& context, const DepthClearPass& pass); +void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 3b07fac78ec..8742682be52 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -115,6 +115,13 @@ struct DepthClearPass { DepthTarget target; VkImageAspectFlags aspects; }; +std::optional DecodeDepthClearPass(const QueueState& queue); +struct DepthCopyPass { + DepthTarget source; + DepthTarget destination; + VkImageAspectFlags aspects; +}; +std::optional DecodeDepthCopyPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 92887e26d6f..65bcde0c262 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -110,6 +110,29 @@ class DepthSurface { if (aspects & VK_IMAGE_ASPECT_STENCIL_BIT) ++stencilVersion; } + void CopyFrom(const DepthSurface& source, VkImageAspectFlags aspects) { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + std::array copies{}; + std::uint32_t count = 0; + for (const auto aspect : {VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_ASPECT_STENCIL_BIT}) { + if ((aspects & aspect) == 0) continue; + auto& copy = copies[count++]; + copy.srcSubresource = {static_cast(aspect), 0, 0, 1}; + copy.dstSubresource = copy.srcSubresource; + copy.extent = {target.extent.width, target.extent.height, 1}; + } + context.Function("vkCmdCopyImage")(commands, source.image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, count, copies.data()); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, access); + NoteWrite(aspects); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } + std::uint64_t Version(VkImageAspectFlags aspect) const { return aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? stencilVersion : depthVersion; } @@ -364,6 +387,31 @@ void RunDepthClearPass(const Context& context, const DepthClearPass& pass) { surfaces().back()->Clear(pass); } +void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass) { + Require(pass.aspects != 0 && (pass.aspects & ~(VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == 0, "invalid depth copy aspects"); + Require(pass.source.format == pass.destination.format && pass.source.extent.width == pass.destination.extent.width && pass.source.extent.height == pass.destination.extent.height, "depth copy surfaces have incompatible formats or extents"); + Require((pass.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) == 0 || (pass.source.address != 0 && pass.destination.address != 0), "depth copy has an absent depth plane"); + Require((pass.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) == 0 || (pass.source.stencilAddress != 0 && pass.destination.stencilAddress != 0), "depth copy has an absent stencil plane"); + std::lock_guard lock(surfacesMutex()); + DepthSurface* source = nullptr; + DepthSurface* destination = nullptr; + for (const auto& surface : surfaces()) { + if (surface->context.device != context.device) continue; + if (sameSurface(surface->target, pass.source)) source = surface.get(); + if (sameSurface(surface->target, pass.destination)) destination = surface.get(); + } + Require(source != nullptr, "depth copy source has no resident surface"); + if (source == destination) return; + Require(pass.source.address == 0 || pass.source.address != pass.destination.address, "depth copies with a shared depth plane are unsupported"); + Require(pass.source.stencilAddress == 0 || pass.source.stencilAddress != pass.destination.stencilAddress, "depth copies with a shared stencil plane are unsupported"); + if (destination == nullptr) { + Require(((pass.aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || pass.destination.address == 0) && ((pass.aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 || pass.destination.stencilAddress == 0), "partial depth copy destination has no resident surface"); + surfaces().push_back(std::make_unique(context, pass.destination)); + destination = surfaces().back().get(); + } + destination->CopyFrom(*source, pass.aspects); +} + std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 06189e70d1e..1d1fbc3ba51 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -155,8 +155,8 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { - zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); +void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result, bool clearing = false, bool copying = false) { + zero(cx, 0x000, clearing ? 0x00001f9cu : 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const auto view = read(cx, 0x002); zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); @@ -175,8 +175,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) DepthTarget depth{}; depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; - Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); - Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); + Require(copying || zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); + Require(copying || !stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; if (const auto slice = view & 0x1fffu; slice != 0) { @@ -776,6 +776,106 @@ std::optional DecodeColorMetadataPass(const QueueState& queue return pass; } +std::optional DecodeDepthClearPass(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + if (control == cx.end()) return std::nullopt; + Require((control->second & ~0x2063u) == 0, "depth copy, resummarize or decompress is unsupported"); + if ((control->second & 3u) == 0) return std::nullopt; + Require(ColorWriteMask(cx) == 0, "depth clear with color writes is unsupported"); + zero(cx, 0x1c4, ~0u, "depth clear with shader depth or sample-mask export"); + zero(cx, 0x2f8, ~0u, "depth clear with multisampling or coverage conversion"); + zero(cx, 0x080, ~0u, "depth clear with a window offset"); + const auto view = read(cx, 0x002); + const auto lastSlice = ((view >> 13u) & 0x7ffu) | ((view >> 19u) & 0x1800u); + Require((view & 0x1fffu) == lastSlice, "depth clear over multiple slices is unsupported"); + const auto depthControl = read(cx, 0x200); + Require((depthControl & 8u) == 0, "depth clear with depth bounds is unsupported"); + const bool depthUnchanged = (depthControl & 2u) == 0 || ((depthControl & 4u) == 0 && ((depthControl >> 4u) & 7u) == 7u); + Require((control->second & 1u) != 0 || depthUnchanged, "stencil clear with ordinary depth work is unsupported"); + Require((control->second & 2u) != 0 || (depthControl & 1u) == 0, "depth clear with ordinary stencil work is unsupported"); + VkImageAspectFlags aspects = 0; + if ((control->second & 1u) != 0) { + Require((depthControl & 6u) == 6u && (view & 0x01000000u) == 0 && (read(cx, 0x010) & 3u) != 0, "depth clear needs a writable enabled depth plane"); + aspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + if ((control->second & 2u) != 0) { + Require((depthControl & 1u) != 0 && (view & 0x02000000u) == 0 && (read(cx, 0x011) & 1u) != 0, "stencil clear needs a writable enabled stencil plane"); + aspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + } + State decoded{}; + decodeDepth(cx, depthControl, decoded, true); + const auto& target = *decoded.depth; + if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) Require(std::isfinite(target.clearDepth) && target.clearDepth >= 0.0f && target.clearDepth <= 1.0f, "depth clear value is outside [0, 1]"); + Require(read(cx, 0x206) == 0x43fu, "unsupported viewport transform for depth clear"); + const auto xs = std::fabs(readFloat(cx, 0x10f)); + const auto xo = readFloat(cx, 0x110); + const auto ys = std::fabs(readFloat(cx, 0x111)); + const auto yo = readFloat(cx, 0x112); + Require(std::isfinite(xs) && std::isfinite(xo) && std::isfinite(ys) && std::isfinite(yo), "non-finite depth clear viewport"); + VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}}; + intersect(covered, cx, 0x00c, true); + intersect(covered, cx, 0x081, false); + intersect(covered, cx, 0x090, false); + if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x094, false); + const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= target.extent.width && yo - ys <= 0.0f && yo + ys >= target.extent.height; + const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height; + Require(viewportCovers && scissorCovers, "depth clear over part of a surface is unsupported"); + return DepthClearPass{target, aspects}; +} + +std::optional DecodeDepthCopyPass(const QueueState& queue) { + const auto& cx = queue.context; + const auto overrideRegister = find(cx, 0x003); + if (overrideRegister == cx.end()) return std::nullopt; + const auto overrides = overrideRegister->second; + const bool depth = (overrides & 0x28000000u) == 0x28000000u && (read(cx, 0x010) & 3u) != 0; + const bool stencil = (overrides & 0x50000000u) == 0x50000000u && (read(cx, 0x011) & 1u) != 0; + if ((!depth && !stencil) || ((read(cx, 0x202) >> 4u) & 7u) != 0) return std::nullopt; + const auto base = [&](std::uint32_t low, std::uint32_t high) { + const auto upper = find(cx, high); + return (upper == cx.end() ? 0ull : static_cast(upper->second & 0xffu) << 40u) | (static_cast(read(cx, low)) << 8u); + }; + const auto depthWrite = base(0x014, 0x01c); + const auto stencilWrite = base(0x015, 0x01d); + const bool copyDepth = depth && depthWrite != base(0x012, 0x01a); + const bool copyStencil = stencil && stencilWrite != base(0x013, 0x01b); + if (!copyDepth && !copyStencil) return std::nullopt; + Require((read(cx, 0x000) & ~0x2060u) == 0, "depth override copy with clear, color copy or resummarization is unsupported"); + Require((read(cx, 0x200) & DepthControlMask) == 0, "depth override copy with depth or stencil tests is unsupported"); + zero(cx, 0x1c4, ~0u, "depth override copy with shader depth or sample-mask export"); + zero(cx, 0x2f8, ~0u, "depth override copy with multisampling or coverage conversion"); + zero(cx, 0x080, ~0u, "depth override copy with a window offset"); + const auto view = read(cx, 0x002); + const auto slice = view & 0x1fffu; + const auto lastSlice = ((view >> 13u) & 0x7ffu) | ((view >> 19u) & 0x1800u); + Require(slice == lastSlice, "depth override copy over multiple slices is unsupported"); + Require(!copyDepth || (view & 0x01000000u) == 0, "depth override copy has a read-only depth destination"); + Require(!copyStencil || (view & 0x02000000u) == 0, "depth override copy has a read-only stencil destination"); + State decoded{}; + decodeDepth(cx, 0, decoded, false, true); + const auto& source = *decoded.depth; + auto destination = source; + if (source.address != 0) destination.address = depthWrite + slice * DepthSliceBytes(source.extent, (read(cx, 0x010) & 3u) == 1 ? 2u : 4u); + if (source.stencilAddress != 0) destination.stencilAddress = stencilWrite + slice * DepthSliceBytes(source.extent, 1u); + Require(!copyDepth || (depthWrite != 0 && source.address != 0), "depth override copy has an absent depth plane"); + Require(!copyStencil || (stencilWrite != 0 && source.stencilAddress != 0), "depth override copy has an absent stencil plane"); + Require(read(cx, 0x206) == 0x43fu, "unsupported viewport transform for depth override copy"); + const auto xs = std::fabs(readFloat(cx, 0x10f)); + const auto xo = readFloat(cx, 0x110); + const auto ys = std::fabs(readFloat(cx, 0x111)); + const auto yo = readFloat(cx, 0x112); + VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}}; + intersect(covered, cx, 0x00c, true); + intersect(covered, cx, 0x081, false); + intersect(covered, cx, 0x090, false); + if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x094, false); + const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= source.extent.width && yo - ys <= 0.0f && yo + ys >= source.extent.height; + const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= source.extent.width && covered.extent.height >= source.extent.height; + Require(viewportCovers && scissorCovers, "depth override copy over part of a surface is unsupported"); + return DepthCopyPass{source, destination, (copyDepth ? VK_IMAGE_ASPECT_DEPTH_BIT : 0u) | (copyStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)}; +} + std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) return reason; const auto& cx = queue.context; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index db66371eaf5..d0a04042fe6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -784,6 +785,120 @@ void DepthBoundsBiasTests() { alignas(256) std::array dccKeys{}; +void depthClearPassTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x000] = 0; + Require(!DecodeDepthClearPass(queue), "ordinary draw was decoded as a depth clear"); + for (const auto mode : {0x4u, 0x8u, 0x10u, 0x1000u}) { + auto maintenance = queue; + maintenance.context[0x000] = mode; + maintenance.context[0x200] = 0; + maintenance.context[0x8e] = maintenance.context[0x8f] = 0; + expectFailure([&] { DecodeDepthClearPass(maintenance); }, "copy, resummarize or decompress"); + } + queue.context[0x000] = 0x2060; + Require(!DecodeDepthClearPass(queue), "ordinary compression and invocation controls were decoded as a clear"); + queue.context[0x000] = 3; + queue.context[0x002] = 0; + queue.context[0x200] = 0x777; + queue.context[0x8e] = 0; + queue.context[0x010] = 0x80000183; + queue.context[0x011] = 0x20000181; + queue.context[0x012] = queue.context[0x014] = 0x100; + queue.context[0x013] = queue.context[0x015] = 0x200; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 37; + queue.context[0x00b] = std::bit_cast(0.75f); + queue.context[0x10b] = 0x333; + queue.context[0x10c] = queue.context[0x10d] = 0xffffffff; + const auto pass = DecodeDepthClearPass(queue); + Require(pass && pass->aspects == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) && pass->target.clearDepth == 0.75f && pass->target.clearStencil == 37, "explicit depth/stencil clear lost its aspects or values"); + Require(!queue.context.contains(0x1b3), "clear test accidentally supplies pixel inputs"); + auto invalid = queue; + invalid.context[0x8e] = 0xf; + expectFailure([&] { DecodeDepthClearPass(invalid); }, "color writes"); + invalid = queue; + invalid.context[0x91] = 0x40020; + expectFailure([&] { DecodeDepthClearPass(invalid); }, "part of a surface"); + invalid = queue; + invalid.context[0x2] = 1u << 24u; + expectFailure([&] { DecodeDepthClearPass(invalid); }, "writable enabled depth plane"); + invalid = queue; + invalid.context[0x2] = 1u << 13u; + expectFailure([&] { DecodeDepthClearPass(invalid); }, "multiple slices"); + queue.context[0x000] = 1; + queue.context[0x200] = 0x76; + Require(DecodeDepthClearPass(queue)->aspects == VK_IMAGE_ASPECT_DEPTH_BIT, "depth-only clear included stencil"); + queue.context[0x000] = 2; + queue.context[0x200] = 0x701; + Require(DecodeDepthClearPass(queue)->aspects == VK_IMAGE_ASPECT_STENCIL_BIT, "stencil-only clear included depth"); + queue.context[0x200] = 0x773; + Require(DecodeDepthClearPass(queue)->aspects == VK_IMAGE_ASPECT_STENCIL_BIT, "an always-pass read-only depth test rejected a stencil clear"); + queue.context[0x200] = 0x777; + expectFailure([&] { DecodeDepthClearPass(queue); }, "ordinary depth work"); + queue.context[0x200] = 0x733; + expectFailure([&] { DecodeDepthClearPass(queue); }, "ordinary depth work"); + queue.context[0x200] = 0x77b; + expectFailure([&] { DecodeDepthClearPass(queue); }, "depth bounds"); +} + +void depthCopyPassTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + Require(!DecodeDepthCopyPass(queue), "unconfigured depth override became a copy"); + queue.context[0x000] = 0x60; + queue.context[0x00a] = 0; + queue.context[0x00b] = std::bit_cast(1.0f); + queue.context[0x003] = 0xf8000000; + queue.context[0x002] = 0; + queue.context[0x200] = 0; + queue.context[0x202] = 0xcc0000; + queue.context[0x8e] = queue.context[0x8f] = 0; + queue.context[0x010] = 0xa0000183; + queue.context[0x011] = 0x20000181; + queue.context[0x012] = 0x100; + queue.context[0x013] = 0x200; + queue.context[0x014] = 0x300; + queue.context[0x015] = 0x400; + queue.context[0x007] = (3u << 16u) | 63u; + const auto pass = DecodeDepthCopyPass(queue); + Require(pass && pass->source.address == 0x10000 && pass->destination.address == 0x30000 && pass->source.stencilAddress == 0x20000 && pass->destination.stencilAddress == 0x40000 && pass->aspects == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT), "depth override copy lost its read/write bases or aspects"); + auto changed = queue; + changed.context[0x003] = 0x28000000; + Require(DecodeDepthCopyPass(changed)->aspects == VK_IMAGE_ASPECT_DEPTH_BIT, "depth-only override copied stencil"); + changed.context[0x003] = 0x50000000; + Require(DecodeDepthCopyPass(changed)->aspects == VK_IMAGE_ASPECT_STENCIL_BIT, "stencil-only override copied depth"); + changed.context[0x003] = 0x18000000; + Require(!DecodeDepthCopyPass(changed), "dirty without valid requested a depth copy"); + changed.context[0x003] = 0x60000000; + Require(!DecodeDepthCopyPass(changed), "valid without dirty requested a depth copy"); + changed = queue; + changed.context[0x202] = 0xcc0010; + Require(!DecodeDepthCopyPass(changed), "ordinary color rendering became a depth copy"); + changed = queue; + changed.context[0x014] = changed.context[0x012]; + changed.context[0x015] = changed.context[0x013]; + Require(!DecodeDepthCopyPass(changed), "in-place expansion became a copy"); + changed = queue; + changed.context[0x002] = 2u | (2u << 13u); + changed.context[0x01a] = changed.context[0x01c] = 1; + const auto sliced = DecodeDepthCopyPass(changed); + Require(sliced->source.address == (1ull << 40u) + 0x10000 + 2 * DepthSliceBytes({64, 4}, 4) && sliced->destination.address == (1ull << 40u) + 0x30000 + 2 * DepthSliceBytes({64, 4}, 4), "depth copy lost slice or high address bits"); + changed = queue; + changed.context[0x200] = 2; + expectFailure([&] { DecodeDepthCopyPass(changed); }, "tests"); + changed = queue; + changed.context[0x000] = 0x61; + expectFailure([&] { DecodeDepthCopyPass(changed); }, "clear"); + changed = queue; + changed.context[0x002] = 1u << 13u; + expectFailure([&] { DecodeDepthCopyPass(changed); }, "multiple slices"); + changed = queue; + changed.context[0x091] = 0x40020; + expectFailure([&] { DecodeDepthCopyPass(changed); }, "part of a surface"); +} + void metadataPassTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; @@ -2359,6 +2474,8 @@ int main() { CompactedExportTests(); metadataPassTests(); cmaskTests(); + depthClearPassTests(); + depthCopyPassTests(); ShaderStageTests(); PixelInputLayoutTests(); InitialContextTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2a90da4e6ec..a2ae9661741 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2603,6 +2603,62 @@ void depthClearPassTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void depthCopyPassTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + SampleProgram depthProgram(context, recorder); + SampleProgram stencilProgram(context, recorder, SAMPLE_Stencil_ARRAY_SPV, true); + struct Release { + const Context& context; + Recorder& recorder; + ~Release() { recorder.Sync(); ClearDepthSurfaces(context.device); } + } release{context, recorder}; + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + const bool stencil = format == VK_FORMAT_D32_SFLOAT_S8_UINT; + const VkImageAspectFlags both = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + DepthTarget source{stencil ? 0x50000u : 0x10000u, stencil ? 0x60000u : 0u, {64, 64}, format, 0.25f, 19}; + auto destination = source; + destination.address += 0x20000; + if (stencil) destination.stencilAddress += 0x20000; + RunDepthClearPass(context, {source, both}); + const auto submissions = recorder.Submissions(); + RunDepthCopyPass(context, {source, destination, both}); + Require(recorder.Submissions() == submissions, "depth copy submitted or waited instead of recording"); + GuestTextureResource resource{}; + resource.baseAddress = destination.address; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.dimension = TextureDimension::k2D; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.format = stencil ? 22 : 7; + const std::array words{}; + const VkComponentMapping mapping{}; + const auto texture = DepthSurfaceTexture(context, words, resource, mapping); + Require(texture != nullptr, "depth copy did not create a sampleable destination"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0), 0.25f, "copied depth contents"); + source.clearDepth = 0.75f; + RunDepthClearPass(context, {source, VK_IMAGE_ASPECT_DEPTH_BIT}); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0), 0.25f, "copy destination aliases its source"); + RunDepthCopyPass(context, {source, destination, VK_IMAGE_ASPECT_DEPTH_BIT}); + Require(DepthSurfaceTexture(context, words, resource, mapping) == texture, "repeated depth copy replaced the destination view"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0), 0.75f, "repeated depth copy kept stale contents"); + if (stencil) { + auto stencilResource = resource; + stencilResource.baseAddress = destination.stencilAddress; + stencilResource.format = 5; + stencilResource.dimension = TextureDimension::k2DArray; + const auto stencilTexture = DepthSurfaceTexture(context, words, stencilResource, mapping); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 0), 19, "depth-only copy changed destination stencil"); + source.clearStencil = 47; + RunDepthClearPass(context, {source, VK_IMAGE_ASPECT_STENCIL_BIT}); + RunDepthCopyPass(context, {source, destination, VK_IMAGE_ASPECT_STENCIL_BIT}); + Require(DepthSurfaceTexture(context, words, stencilResource, mapping) == stencilTexture, "stencil copy replaced the destination view"); + expectRed(stencilProgram.Red(stencilTexture->View(), stencilTexture->Layout(), 0, 0), 47, "stencil copy did not refresh its sampled view"); + expectRed(depthProgram.Red(texture->View(), texture->Layout(), 0), 0.75f, "stencil-only copy changed destination depth"); + } + } +} + class DepthOperations { public: explicit DepthOperations(Context& context) : context(context), resolver(context.deviceProc), @@ -3307,6 +3363,11 @@ int main(int argc, char** argv) { std::cout << "Resident depth clear sampling and aspect preservation tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--depth-copy-only") { + depthCopyPassTests(device, recorder); + std::cout << "Resident depth/stencil copy, refresh and aspect preservation tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); @@ -3344,6 +3405,7 @@ int main(int argc, char** argv) { atomicViewTests(device, recorder); metadataPassTests(device, recorder); depthClearPassTests(device, recorder); + depthCopyPassTests(device, recorder); depthArrayTests(device, recorder); depthArrayGuestTests(device, recorder); pendingKeyStoreTests(device, recorder); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 5ce0de4320d..78531b8ca02 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,6 +231,10 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings +- [Explicit depth/stencil clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` and `STENCIL_CLEAR_ENABLE` before pixel-shader input validation, following the clear selection in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/video_core/renderer_vulkan/vk_rasterizer.cpp). Direct clear draws with a full viewport and scissor clear the selected resident image aspects to `DB_DEPTH_CLEAR` and `DB_STENCIL_CLEAR`. Stencil-only clears allow an enabled read-only depth test with `ALWAYS` comparison, which cannot reject fragments or modify depth; depth bounds and depth writes still throw. They are assumed to cover the viewport; arbitrary clear-draw geometry has not been verified. Partial viewport/scissor clears, multisampling, multiple slices, simultaneous color or ordinary depth/stencil work, and indirect clears throw. HTILE resummarization and metadata-driven clears remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. +- [Depth/stencil override copies](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize paired `FORCE_Z_DIRTY`/`FORCE_Z_VALID` and `FORCE_STENCIL_DIRTY`/`FORCE_STENCIL_VALID` flags with distinct read/write bases and disabled color/depth/stencil tests, following [AMD CIK 3D Registers](https://www.x.org/docs/AMD/old/CIK_3D_registers_v2.pdf), pp. 215 and 225. Direct draws with a full viewport and scissor copy selected resident image aspects on the GPU; derived sampled arrays refresh after copies. Like explicit clears, these draws are assumed to cover the viewport. Arbitrary geometry and console equivalence have not been measured. Partial viewport/scissor coverage, multiple slices, multisampling, indirect draws, shared source/destination planes, nonresident sources and partial copies into nonresident destinations throw. Guest-memory coherence and HTILE resummarization remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. +- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. + - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done From cedec6e1d5765800c2ffb039d8675d864d27cd57 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Thu, 8 Oct 2026 11:27:42 +0300 Subject: [PATCH 11/11] docs(agc): keep texture limitations with graphics notes --- docs/dev/TechnicalDebt.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 78531b8ca02..25b371df4c3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -231,10 +231,6 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcstod`, `wcstof`, `wcstold`, `wcstol`, `wcstoll`, `wcstoul`, `wcstoull`, `wcscoll`, `wcsxfrm`, `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings -- [Explicit depth/stencil clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` and `STENCIL_CLEAR_ENABLE` before pixel-shader input validation, following the clear selection in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/video_core/renderer_vulkan/vk_rasterizer.cpp). Direct clear draws with a full viewport and scissor clear the selected resident image aspects to `DB_DEPTH_CLEAR` and `DB_STENCIL_CLEAR`. Stencil-only clears allow an enabled read-only depth test with `ALWAYS` comparison, which cannot reject fragments or modify depth; depth bounds and depth writes still throw. They are assumed to cover the viewport; arbitrary clear-draw geometry has not been verified. Partial viewport/scissor clears, multisampling, multiple slices, simultaneous color or ordinary depth/stencil work, and indirect clears throw. HTILE resummarization and metadata-driven clears remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. -- [Depth/stencil override copies](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize paired `FORCE_Z_DIRTY`/`FORCE_Z_VALID` and `FORCE_STENCIL_DIRTY`/`FORCE_STENCIL_VALID` flags with distinct read/write bases and disabled color/depth/stencil tests, following [AMD CIK 3D Registers](https://www.x.org/docs/AMD/old/CIK_3D_registers_v2.pdf), pp. 215 and 225. Direct draws with a full viewport and scissor copy selected resident image aspects on the GPU; derived sampled arrays refresh after copies. Like explicit clears, these draws are assumed to cover the viewport. Arbitrary geometry and console equivalence have not been measured. Partial viewport/scissor coverage, multiple slices, multisampling, indirect draws, shared source/destination planes, nonresident sources and partial copies into nonresident destinations throw. Guest-memory coherence and HTILE resummarization remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. -- [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done @@ -285,6 +281,8 @@ Throughout the project, every function at every stage either **does exactly what - [CMASK fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - only a CMASK whose nibbles are all fast-cleared (0) or all expanded (0xF) is modeled, checked over the target's whole CMASK, 4 KiB per 1024x512-pixel metablock (the size PS5 titles clear, for every color format seen); the addressing within a metablock is not decoded. The first draw or fast-clear eliminate over a fast-cleared CMASK fills the whole target with `CB_COLOR_CLEAR_WORD` and stores those bytes as expanded, so a draw that covers only part of the target reads the clear value in the rest as the eliminate would leave it. On a DCC target the clear is in the DCC keys, so its CMASK must be all expanded. A mixed CMASK throws, as does a DCC target's CMASK that is not all expanded and a fast clear of a mipmapped, 3D or array color target - [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. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. +- [Explicit depth/stencil clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` and `STENCIL_CLEAR_ENABLE` before pixel-shader input validation, following the clear selection in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/video_core/renderer_vulkan/vk_rasterizer.cpp). Direct clear draws with a full viewport and scissor clear the selected resident image aspects to `DB_DEPTH_CLEAR` and `DB_STENCIL_CLEAR`. Stencil-only clears allow an enabled read-only depth test with `ALWAYS` comparison, which cannot reject fragments or modify depth; depth bounds and depth writes still throw. They are assumed to cover the viewport; arbitrary clear-draw geometry has not been verified. Partial viewport/scissor clears, multisampling, multiple slices, simultaneous color or ordinary depth/stencil work, and indirect clears throw. HTILE resummarization and metadata-driven clears remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. +- [Depth/stencil override copies](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) recognize paired `FORCE_Z_DIRTY`/`FORCE_Z_VALID` and `FORCE_STENCIL_DIRTY`/`FORCE_STENCIL_VALID` flags with distinct read/write bases and disabled color/depth/stencil tests, following [AMD CIK 3D Registers](https://www.x.org/docs/AMD/old/CIK_3D_registers_v2.pdf), pp. 215 and 225. Direct draws with a full viewport and scissor copy selected resident image aspects on the GPU; derived sampled arrays refresh after copies. Like explicit clears, these draws are assumed to cover the viewport. Arbitrary geometry and console equivalence have not been measured. Partial viewport/scissor coverage, multiple slices, multisampling, indirect draws, shared source/destination planes, nonresident sources and partial copies into nonresident destinations throw. Guest-memory coherence and HTILE resummarization remain unimplemented. Viewport and scissor bounds do not prove rasterized coverage: vertex/index geometry, clipping and culling are not evaluated. A matching partial draw can overwrite untouched pixels. General use requires a coverage proof or the original rasterized operation. - [Depth/stencil texture arrays](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) assemble resident single-slice targets with GPU copies and refresh changed slices before sampling. Only single-mip, single-sample 2D arrays in the 64 KiB Z layout are implemented. The slice stride uses the same padded block dimensions as depth attachment selection; console layout equivalence has not been measured. Viewed layers without resident targets are detiled from readable guest memory with their absolute slice index, and refreshed after guest writes; compressed guest layers and unbacked memory throw. Component mappings share array storage, but distinct layer ranges remain cached until device teardown; eviction is unimplemented. CPU-write coherence for resident targets, storage-image aliases, other tile modes, cubes and depth mip chains remain unimplemented. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none.