feat(agc): sample with DISABLE_CUBE_WRAP through non-seamless cube samplers

Guest samplers with DISABLE_CUBE_WRAP (S# word 0 bit 28) are created with
VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT when the device enables
VK_EXT_non_seamless_cube_map; RADV maps the flag to the same bit. The bit
only changes cube filtering, so without the extension the sampler is still
created and only a cube texture sampled through it throws.
This commit is contained in:
AlxcBTW authored and Dean Galvin committed 2026-10-10 21:20:28 +13:00
1 parent f6af6d11d3
commit bf18420007
9 files changed
+67 -6

No files matched your search

@@ -223,6 +223,7 @@ struct VulkanDevice::State {
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
bool samplerFilterMinmax = false;
bool nonSeamlessCubeMap = false;
bool fragmentShaderPixelInterlock = false;
bool conservativeRasterization = false;
bool provokingVertexLast = false;
@@ -954,6 +955,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->depthClipControl = depthClipFeatures.depthClipControl == VK_TRUE;
if (state->depthClipControl) deviceExtensions.push_back(VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME);
}
VkPhysicalDeviceNonSeamlessCubeMapFeaturesEXT nonSeamlessCubeFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT};
if (hasExtension(VK_EXT_NON_SEAMLESS_CUBE_MAP_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &nonSeamlessCubeFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->nonSeamlessCubeMap = nonSeamlessCubeFeatures.nonSeamlessCubeMap == VK_TRUE;
if (state->nonSeamlessCubeMap) deviceExtensions.push_back(VK_EXT_NON_SEAMLESS_CUBE_MAP_EXTENSION_NAME);
}
nonSeamlessCubeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT};
nonSeamlessCubeFeatures.nonSeamlessCubeMap = VK_TRUE;
if (state->meshShader) {
deviceExtensions.insert(deviceExtensions.end(), meshExtensions.begin(), meshExtensions.end());
state->capabilities.push_back(5283);
@@ -1075,6 +1085,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
maintenance8Features.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &maintenance8Features;
}
if (state->nonSeamlessCubeMap) {
nonSeamlessCubeFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &nonSeamlessCubeFeatures;
}
byteFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
if (state->fragmentShaderBarycentric) {
barycentricFeatures.pNext = byteFeatures.pNext;
@@ -2588,6 +2602,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.depthBounds = state->depthBounds;
context.depthBiasClamp = state->depthBiasClamp;
context.samplerFilterMinmax = state->samplerFilterMinmax;
context.nonSeamlessCubeMap = state->nonSeamlessCubeMap;
context.conservativeRasterization = state->conservativeRasterization;
context.provokingVertexLast = state->provokingVertexLast;
context.provokingVertexModePerPipeline = state->provokingVertexModePerPipeline;
@@ -126,6 +126,7 @@ struct Context {
bool depthBounds = false;
bool depthBiasClamp = false;
bool samplerFilterMinmax = false;
bool nonSeamlessCubeMap = false;
bool conservativeRasterization = false;
VkBuffer emptyBuffer = VK_NULL_HANDLE;
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
@@ -28,6 +28,7 @@ struct GuestSamplerResource {
bool compareEnable = false;
VkCompareOp compareOp = VK_COMPARE_OP_NEVER;
bool unnormalizedCoordinates = false;
bool nonSeamlessCube = false;
};
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words, bool unnormalizedProven = false, bool forceDegammaPaired = false);
@@ -23,6 +23,7 @@ public:
VkSampler Handle() const;
bool RequiresFilterMinmax() const;
bool ReadsOpaqueBlackBorder() const;
bool RequiresNonSeamlessCube() const;
private:
void release() noexcept;
@@ -32,6 +33,7 @@ private:
VkSampler sampler = VK_NULL_HANDLE;
bool requiresFilterMinmax = false;
bool opaqueBlackBorder = false;
bool requiresNonSeamlessCube = false;
};
// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the
@@ -66,6 +68,7 @@ private:
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
}
@@ -107,7 +107,6 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words,
static_cast<void>(truncCoord);
static_cast<void>(anisoThreshold);
static_cast<void>(anisoBias);
Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented");
const auto reductionMode = toVkReductionMode(filterMode);
Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented");
Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented");
@@ -162,6 +161,7 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words,
const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS};
result.compareOp = compareOps.at(depthCompareFunc);
result.forceDegamma = forceSrgb;
result.nonSeamlessCube = disableCubeWrap;
if (forceUnormCoords) {
result.unnormalizedCoordinates = true;
result.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
@@ -38,6 +38,10 @@ namespace AgcDriver::Graphics {
const bool clampsToBorder = descriptor.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
opaqueBlackBorder = clampsToBorder && (descriptor.borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK || descriptor.borderColor == VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
info.unnormalizedCoordinates = descriptor.unnormalizedCoordinates ? VK_TRUE : VK_FALSE;
if (descriptor.nonSeamlessCube) {
if (context.nonSeamlessCubeMap) info.flags |= VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT;
else requiresNonSeamlessCube = true;
}
Check(context.Function<PFN_vkCreateSampler>("vkCreateSampler")(context.device, &info, nullptr, &sampler), "vkCreateSampler");
}
@@ -105,6 +109,10 @@ namespace AgcDriver::Graphics {
return forcesDegamma;
}
bool Sampler::RequiresNonSeamlessCube() const {
return requiresNonSeamlessCube;
}
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) {
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented");
@@ -122,4 +130,11 @@ namespace AgcDriver::Graphics {
}
}
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
if (!cube) return;
for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) {
if (((samplerMask >> element) & 1u) != 0u && samplers[element]->RequiresNonSeamlessCube()) Require(false, "a cube texture is sampled through a sampler that disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map");
}
}
}
@@ -2980,6 +2980,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
}
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers);
RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers);
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
const VkComponentMapping components = ViewComponents(resource);
@@ -93,6 +93,7 @@ struct SamplerCapture {
bool created = false;
const void* next = nullptr;
VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM;
VkSamplerCreateFlags flags = 0;
};
SamplerCapture samplerCapture;
@@ -105,6 +106,7 @@ THandle fakeHandle(std::uint64_t value) {
VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
samplerCapture.created = true;
samplerCapture.next = info->pNext;
samplerCapture.flags = info->flags;
const auto* reduction = static_cast<const VkSamplerReductionModeCreateInfoEXT*>(info->pNext);
if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode;
*sampler = fakeHandle<VkSampler>(0x5a);
@@ -235,6 +237,32 @@ void RunSamplerReductionTests(const Fields& base) {
reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering");
}
void RunNonSeamlessCubeTests(const Fields& base) {
Fields nonSeamless = base;
nonSeamless.disableCubeWrap = true;
const auto decoded = DecodeSamplerResource(pack(nonSeamless));
Require(decoded.nonSeamlessCube, "DISABLE_CUBE_WRAP was not decoded as a non-seamless cube sampler");
Require(!DecodeSamplerResource(pack(base)).nonSeamlessCube, "a sampler without DISABLE_CUBE_WRAP decoded as non-seamless");
Context context{};
context.limits.maxSamplerAnisotropy = 1.0f;
context.deviceProc = captureProc;
context.nonSeamlessCubeMap = true;
Require((createSampler(context, decoded).flags & VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT) != 0u, "a non-seamless cube sampler was created without VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT");
Require(createSampler(context, DecodeSamplerResource(pack(base))).flags == 0u, "a seamless sampler was created with flags");
const std::array honored{std::make_shared<Sampler>(context, decoded)};
Require(!honored[0]->RequiresNonSeamlessCube(), "a sampler created with the non-seamless flag still restricts cube textures");
RequireNonSeamlessCube(true, 0b1u, honored);
context.nonSeamlessCubeMap = false;
Require(createSampler(context, decoded).flags == 0u, "a sampler set the non-seamless flag without the device feature");
const std::array unsupported{std::make_shared<Sampler>(context, DecodeSamplerResource(pack(base))), std::make_shared<Sampler>(context, decoded)};
Require(unsupported[1]->RequiresNonSeamlessCube(), "a non-seamless sampler without the device feature does not restrict cube textures");
RequireNonSeamlessCube(false, 0b11u, unsupported);
RequireNonSeamlessCube(true, 0b01u, unsupported);
reject([&] { RequireNonSeamlessCube(true, 0b10u, unsupported); }, "disables seamless cube filtering, which needs VK_EXT_non_seamless_cube_map");
}
void RunSamplerCacheDegammaTests(const Fields& base) {
Context context{};
context.limits.maxSamplerAnisotropy = 1.0f;
@@ -406,10 +434,7 @@ void RunGuestSamplerResourceTests() {
truncated.anisoBias = 1;
static_cast<void>(DecodeSamplerResource(pack(truncated)));
Fields badCubeWrap = base;
badCubeWrap.disableCubeWrap = true;
rejectFields(badCubeWrap, "seamless cube filtering");
RunNonSeamlessCubeTests(base);
RunSamplerReductionTests(base);
Fields badDegamma = base;
+1 -1
View File
@@ -370,7 +370,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) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, codes other than 0 and 0xF, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. When the clear runs as a compute pass (a resident target whose CMASK is host-imported), an invalid code throws at the next fast-clear materialization, not at the one that read it
- [HTILE depth fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source.
- [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.
- [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. PS5 equivalence has not been measured on console hardware.
- [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware.
- [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw.
- [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB; larger ones keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value.
- [DCC key stores](../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes.