shader: read raw D16 depth bits through kDepth k16UInt image loads

Co-authored-by: Artyom Zabroda <artyomzabroda@proton.me>
This commit is contained in:
nmzik
2026-10-22 18:56:26 +02:00
parent a358b847c1
commit aafe014d0c
6 changed files with 192 additions and 10 deletions
+2
View File
@@ -678,6 +678,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
add_test(NAME wait_until_safe_for_rendering
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --wait-until-safe-for-rendering-only)
add_test(NAME depth_bits
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --depth-bits-only)
add_test(NAME pm4_context_state
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
@@ -356,7 +356,7 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
}
view.usage = storage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlagBits::eSampled;
view.mapping =
storage || surface_format.conversion_format != Prospero::BufferFormat::kInvalid
storage || resource.conversion_format != Prospero::BufferFormat::kInvalid
? vk::ComponentMapping {}
: TextureGetComponentMapping(descriptor.DstSelXYZW(), surface_format.host_to_storage);
switch (resource.dimension) {
@@ -515,7 +515,9 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
}
const auto samples = multisampled ? 1u << last_level : 1u;
const auto depth = static_cast<uint32_t>(descriptor.Depth()) + 1u;
const auto format = descriptor.Format();
const auto format = ShaderRecompiler::IR::ReadsDepthBits(resource)
? Prospero::BufferFormat::k16UNorm
: descriptor.Format();
const auto surface_format = TextureGetSurfaceFormatInfo(format);
const bool shader_conversion =
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
@@ -389,6 +389,7 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
const auto format = image.conversion_format;
if (format == Prospero::BufferFormat::kInvalid) return {};
const auto info = Format::GetFormatInfo(format);
if (IR::ReadsDepthBits(image)) return info;
const bool raw_signed = IR::RawSignedStorageFormat(image, format) != Prospero::BufferFormat::kInvalid;
EXIT_IF((Prospero::RemapTextureFormat(format) == format && !raw_signed) || info.component_count == 0u ||
info.component_count > 4u);
@@ -424,6 +425,7 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
const auto numeric_class = ctx.state.program.info.images[mem.resource].numeric_class;
const auto scalar_type = ImageScalarType(ctx.state, numeric_class);
const bool integer = info.packed_bitfield || info.type == Format::ComponentType::Uint;
uint32_t packed = 0;
if (info.packed_bitfield) {
packed = ctx.state.builder.AllocateId();
@@ -441,8 +443,16 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
components[component], texel, component);
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
const auto unorm_max = static_cast<float>((1u << info.component_bits[component]) - 1u);
components[component] = Binary(ctx.state, spv::OpFMul, scalar_type, components[component],
ConstantF32Value(ctx.state, 255.0f));
ConstantF32Value(ctx.state, unorm_max));
if (integer) {
// Depth bits: round back to the stored UNorm code.
components[component] =
Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state),
Binary(ctx.state, spv::OpFAdd, scalar_type, components[component],
ConstantF32Value(ctx.state, 0.5f)));
}
}
}
for (uint32_t component = info.component_count; component < 4u; component++) {
@@ -454,20 +464,19 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
for (uint32_t component = 0; component < 4u; component++) {
const auto selector = (swizzle >> (component * 3u)) & 7u;
if (selector == 1u) {
selected[component] = info.packed_bitfield
selected[component] = integer
? ConstantU32(ctx.state, Format::FormattedConstantBits(info, Format::FormattedSourceKind::One))
: ConstantF32Value(ctx.state, 1.0f);
} else if (selector >= 4u) {
selected[component] = components[selector - 4u];
} else {
selected[component] = info.packed_bitfield ? ConstantU32(ctx.state, 0u)
: ZeroF32(ctx.state);
selected[component] = integer ? ConstantU32(ctx.state, 0u) : ZeroF32(ctx.state);
}
}
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct,
ImageVectorType(ctx.state, numeric_class, 4), result, selected[0],
selected[1], selected[2], selected[3]);
integer ? TypeU32Vector(ctx.state, 4) : TypeF32Vector(ctx.state, 4),
result, selected[0], selected[1], selected[2], selected[3]);
return info.type == Format::ComponentType::Unorm
? Unary(ctx.state, spv::OpBitcast, TypeF32Vector(ctx.state, 4), result) : result;
}
@@ -188,6 +188,12 @@ RawSignedStorageFormat(const ImageResource& image, Prospero::BufferFormat format
return Prospero::BufferFormat::kInvalid;
}
// A raw load of a kDepth k16UInt T# reads a D16 surface's codes. Vulkan cannot view D16 as
// R16Uint, so the host binds it as UNorm and the shader converts back.
[[nodiscard]] constexpr bool ReadsDepthBits(const ImageResource& image) {
return image.conversion_format == Prospero::BufferFormat::k16UInt;
}
struct SamplerResource {
static constexpr uint32_t NoIndirectSampler = UINT32_MAX;
@@ -932,7 +932,12 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
return SpecializationFail("FMASK requires a direct image load");
}
}
image.conversion_format = ImageConversionFormat(format);
// See ReadsDepthBits; raw loads only, since filtering would blend the codes.
const bool depth_bits =
!storage && base.unfiltered_access_only && format == Prospero::BufferFormat::k16UInt &&
static_cast<Prospero::TileMode>((descriptor.dwords[3] >> 20u) & 0x1fu) ==
Prospero::TileMode::kDepth;
image.conversion_format = depth_bits ? format : ImageConversionFormat(format);
if (storage || image.conversion_format != Prospero::BufferFormat::kInvalid) {
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
}
@@ -940,7 +945,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
const bool raw_sint_storage = raw_sint_format != Prospero::BufferFormat::kInvalid;
if (raw_sint_format == Prospero::BufferFormat::k16UInt) image.conversion_format = format;
image.numeric_class = Prospero::SampledTextureNumericClass(
Prospero::RemapTextureFormat(format));
depth_bits ? Prospero::BufferFormat::k16UNorm : Prospero::RemapTextureFormat(format));
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
if (image.indirect_root != ImageResource::NoIndirectImage) {
return SpecializationFail("packed UNorm sampling requires a direct image");
+158
View File
@@ -12478,6 +12478,123 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
// Cyberpunk 2077 copies its HTile D16 depth target through a kDepth k16UInt image load.
void CheckDepthBitsCopy() {
constexpr const char *name = "DepthBitsCopy";
constexpr uintptr_t base = 0x0000000205000000ull;
constexpr uint64_t allocation_size = 0x30000;
constexpr uint64_t htile_address = base + 0x10000;
constexpr uint64_t copy_address = base + 0x20000;
constexpr u32 depth_code = 0x2000u;
std::vector<u32> copy{EncodeMimg0(0x00, 0x1), EncodeMimg1(4, 0, 2),
EncodeMimg0(0x08, 0x1), EncodeMimg1(4, 0, 0)};
AppendEnd(&copy);
ShaderMapUserData(reinterpret_cast<uint64_t>(copy.data()),
{.type = Prospero::ShaderBinaryType::kCs,
.code_size_bytes = static_cast<u32>(copy.size() * sizeof(u32))});
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, 0x10000, 0, &direct_offset) == 0,
"depth allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset, 0x10000) == 0 &&
mapped == reinterpret_cast<void *>(base),
"depth mapping failed");
std::memset(mapped, 0, allocation_size);
RenderContext context(m_runtime_context);
context.InitializeGpu(nullptr);
LibKernel::Memory::InstallGpuResources(&context);
context.GetGpu().SendCommandSync([&] {
CommandProcessor processor(context, 0);
processor.BufferInit();
auto &scheduler = context.GetCommandScheduler();
auto &textures = context.GetTextureCache();
auto &shaders = processor.GetShCtx();
context.MapMemory(base, allocation_size);
ImageDesc depth{};
depth.type = BindingType::DepthTarget;
depth.info.data = {base, 0x10000};
depth.info.pixel_format = vk::Format::eD16Unorm;
depth.info.guest_format = Prospero::BufferFormat::k16UNorm;
depth.info.type = Prospero::ImageType::kColor2D;
depth.info.extent = {256, 128, 1};
depth.info.resources = {1, 1};
depth.info.pitch = 256;
depth.info.bytes_per_block = 2;
depth.info.samples = 1;
depth.info.tile_mode = Prospero::TileMode::kDepth;
depth.info.UpdateSize();
depth.info.metadata.kind = ImageMetadataKind::Htile;
depth.info.metadata.range = {htile_address, 0x8000};
depth.info.htile_initial_clear = false;
depth.view_info.format = vk::Format::eD16Unorm;
depth.view_info.type = vk::ImageViewType::e2D;
depth.view_info.aspect = vk::ImageAspectFlagBits::eDepth;
depth.view_info.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment;
const auto depth_id = textures.FindImage(depth);
Require(name, "HTile depth target", textures.FindDepthTarget(depth_id, depth) != nullptr,
"the D16 depth target was not created");
vk::ClearValue clear{};
clear.depthStencil.depth = static_cast<float>(depth_code) / 65535.0f;
TextureCacheTestAccess::ClearImage(textures, scheduler.Current(), depth_id,
{vk::ImageAspectFlagBits::eDepth, 0, 1, 0, 1}, clear);
// The captured T# with this depth target's size and HTile; the copy writes 2x2 texels.
const ShaderTextureResource depth_bits{{static_cast<u32>(base >> 8u),
(static_cast<u32>(Prospero::BufferFormat::k16UInt) << 20u) | (3u << 30u),
0x3fu | (127u << 14u),
DstSel(4, 0, 0, 1) | (static_cast<u32>(Prospero::TileMode::kDepth) << 20u) |
(static_cast<u32>(Prospero::ImageType::kColor2D) << 28u),
0, 0x00700000u, 0x00280000u | (static_cast<u32>(htile_address >> 8u) << 24u),
static_cast<u32>(htile_address >> 16u)}};
const ShaderTextureResource target{{static_cast<u32>(copy_address >> 8u),
(static_cast<u32>(Prospero::BufferFormat::k16UInt) << 20u) | (1u << 30u), 1u << 14u,
DstSel(4, 5, 6, 7) | (static_cast<u32>(Prospero::TileMode::kLinear) << 20u) |
(static_cast<u32>(Prospero::ImageType::kColor2D) << 28u),
0, 0x00700000u, 0, 0}};
shaders.SetCsShader({.data_addr = reinterpret_cast<uint64_t>(copy.data()),
.num_thread_x = 2, .num_thread_y = 2, .num_thread_z = 1,
.wave_size = 64, .user_sgpr = 16, .tidig_comp_cnt = 1});
for (u32 i = 0; i < 8; i++) {
shaders.SetCsUserSgpr(i, target.fields[i], HW::UserSgprType::Unknown);
shaders.SetCsUserSgpr(8u + i, depth_bits.fields[i], HW::UserSgprType::Unknown);
}
processor.DispatchDirect(1, 1, 1, 0x41u);
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(target.fields, 8, value.dwords.begin());
ShaderRecompiler::IR::ImageResource resource{};
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage;
resource.numeric_class = Prospero::TextureNumericClass::Uint;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.written = true;
const auto binding =
RenderExecutorTestAccess::ResolveTexture(context.GetRenderExecutor(), resource, value);
Require(name, "raw depth bits",
ReadCachedTexel(name, context, binding.image_id, {}, {2, 2, 1}) ==
std::vector<u32>(2, depth_code | depth_code << 16u),
"the copy did not read the stored D16 codes");
Require(name, "depth target retained", TextureCacheTestAccess::Contains(textures, depth_id),
"reading depth bits replaced the HTile depth target");
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
});
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
Require(name, "unmap", Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"depth mapping release failed");
Require(name, "release",
Libs::LibKernel::Memory::KernelReleaseDirectMemory(direct_offset, allocation_size) == 0,
"depth allocation release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckSampledDccClear() {
constexpr const char *name = "SampledDccClear";
constexpr uintptr_t base = 0x0000000205000000ull;
@@ -37773,6 +37890,39 @@ template <bool rg, bool snorm = false> TestCase ImageSamplePacked8() {
return test;
}
// A kDepth k16UInt image load returns the stored D16 codes and integer swizzle constants.
TestCase ImageLoadDepthBits() {
using O = ShaderOpcode;
TestCase test;
test.name = "ImageLoadDepthBits";
constexpr std::array<u32, 4> depth{0x2000u, 0xc000u, 0xffffu, 0x0001u};
for (u32 texel = 0; texel < depth.size(); ++texel) {
AppendVMovU32(&test.code, 20, texel % 2u);
AppendVMovU32(&test.code, 21, texel / 2u);
test.code.push_back(EncodeMimg0(0x00, 0xf));
test.code.push_back(EncodeMimg1(0, 20));
for (u32 component = 0; component < 4; ++component) {
AppendStoreVgpr(&test.code, component, texel * 4u + component);
}
test.expected.insert(test.expected.end(), {depth[texel], 0u, 0u, 1u});
}
AppendEnd(&test.code);
test.image_width = test.image_height = 2;
test.sampled_image_rgba = {depth[0] | depth[1] << 16u, depth[2] | depth[3] << 16u};
test.sampled_image_format = vk::Format::eR16Unorm;
test.sampled_image_dwords_per_pixel = 1;
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k16UInt);
test.user_data[1] |= 1u << 30u;
test.user_data[2] = 1u << 14u;
test.user_data[3] |= static_cast<u32>(Prospero::TileMode::kDepth) << 20u;
test.user_data[50] = test.expected.size() * sizeof(u32);
test.user_data[51] = 3u << 28u;
test.has_user_data = true;
test.image_descriptor_swizzle = DstSel(4, 0, 0, 1);
test.opcodes = {O::V_MOV_B32, O::IMAGE_LOAD, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
return test;
}
TestCase ImageLoadR128IgnoresAdjacentMaskSgprs() {
using O = ShaderOpcode;
@@ -43679,6 +43829,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageSamplePacked8<false>);
AddCase(ImageSamplePacked8<true>);
AddCase(ImageSamplePacked8<false, true>);
AddCase(ImageLoadDepthBits);
AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs);
AddCase(ImageLoad1DUsesScalarCoordinate);
AddCase(ImageGather2DInstructionWith1DDescriptor);
@@ -49719,6 +49870,12 @@ int main(int argc, char **argv) {
RunCase(&vulkan, ImageSamplePacked8<false, true>());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--depth-bits-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckDepthBitsCopy();
RunCase(&vulkan, ImageLoadDepthBits());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageLoadSintUsesSignedSampledImage<false>());
@@ -50610,6 +50767,7 @@ int main(int argc, char **argv) {
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorColorMetadataClear();
vulkan.CheckSampledDccClear();
vulkan.CheckDepthBitsCopy();
vulkan.CheckRenderExecutorColorStandardTileDiscovery();
vulkan.CheckRenderExecutorColorDepthTileDiscovery();
vulkan.CheckRenderExecutorStencilBindingDiscovery();