graphics: share packed texture and render-target channel layouts

Resolve PS5 5551 textures with alpha in bit 15 and compose host channel order with guest selectors. Share the existing render-target mapping with sampled textures so packed layouts cannot diverge.

Add native GPU readback coverage for 5551 and 4444, arbitrary selectors, constants, and existing 1555 render-target mapping. Verified PPSA24515 through launch.json with Vulkan validation, advancing scenes and over 12,700 guest flips. Independent correctness and cleanliness audits passed.
This commit is contained in:
nmzik
2026-09-10 13:45:08 +04:00
parent 635fb8882b
commit a305a6c1c6
5 changed files with 181 additions and 38 deletions
@@ -28,13 +28,13 @@ constexpr Prospero::ColorComponentMapping kRenderTargetColorMappings[4][4] = {
Prospero::ColorMappingArgb},
};
struct RenderTargetHostFormatInfo {
struct HostFormatInfo {
vk::Format format = vk::Format::eUndefined;
Prospero::ColorComponentMapping host_to_storage;
};
RenderTargetHostFormatInfo ResolveRenderTargetHostFormat(Prospero::BufferFormat guest_format,
Prospero::ChannelOrder order) {
HostFormatInfo ResolveHostFormat(Prospero::BufferFormat guest_format,
Prospero::ChannelOrder order) {
if (order == Prospero::ChannelOrder::kAlt) {
switch (guest_format) {
case Prospero::BufferFormat::k8_8_8_8UNorm:
@@ -48,14 +48,12 @@ RenderTargetHostFormatInfo ResolveRenderTargetHostFormat(Prospero::BufferFormat
default: break;
}
}
const auto format = VulkanFormat(guest_format);
switch (guest_format) {
case Prospero::BufferFormat::k5_5_5_1UNorm:
return {vk::Format::eA1R5G5B5UnormPack16, Prospero::ColorMappingBgra};
case Prospero::BufferFormat::k5_5_5_1UNorm: return {format, Prospero::ColorMappingBgra};
case Prospero::BufferFormat::k1_5_5_5UNorm:
return {vk::Format::eR5G5B5A1UnormPack16, Prospero::ColorMappingAbgr};
case Prospero::BufferFormat::k4_4_4_4UNorm:
return {vk::Format::eR4G4B4A4UnormPack16, Prospero::ColorMappingAbgr};
default: return {VulkanFormat(guest_format), {}};
case Prospero::BufferFormat::k4_4_4_4UNorm: return {format, Prospero::ColorMappingAbgr};
default: return {format, {}};
}
}
@@ -66,7 +64,7 @@ RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layo
Prospero::ChannelOrder order) {
const auto encoding = Prospero::ResolveRenderTargetFormat(layout, type);
if (encoding.IsValid() && encoding.SupportsOrder(order)) {
const auto host = ResolveRenderTargetHostFormat(encoding.buffer_format, order);
const auto host = ResolveHostFormat(encoding.buffer_format, order);
const auto bytes = Prospero::RenderTargetBytesPerElement(encoding.buffer_format);
if (host.format != vk::Format::eUndefined && bytes != 0) {
const auto order_mapping =
@@ -92,7 +90,11 @@ static size_t GetTextureRegionCount(uint32_t depth, uint64_t levels, bool volume
return count;
}
vk::ComponentSwizzle TextureGetComponentSwizzle(uint8_t s) {
vk::ComponentSwizzle TextureGetComponentSwizzle(uint8_t s,
Prospero::ColorComponentMapping storage_to_host) {
if (s >= 4u && s <= 7u) {
s = static_cast<uint8_t>(4u + storage_to_host.Map(s - 4u));
}
switch (static_cast<Prospero::CompSwizzle>(s)) {
case Prospero::CompSwizzle::kZero: return vk::ComponentSwizzle::eZero;
case Prospero::CompSwizzle::kOne: return vk::ComponentSwizzle::eOne;
@@ -102,30 +104,33 @@ vk::ComponentSwizzle TextureGetComponentSwizzle(uint8_t s) {
case Prospero::CompSwizzle::kAlpha: return vk::ComponentSwizzle::eA;
default: EXIT("unknown swizzle: %d\n", static_cast<int>(s));
}
return vk::ComponentSwizzle::eIdentity;
}
} // namespace
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle) {
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle,
Prospero::ColorComponentMapping host_to_storage) {
Prospero::ColorComponentMapping storage_to_host {0u};
for (uint32_t host = 0; host < 4u; ++host) {
storage_to_host.packed |= static_cast<uint8_t>(host << (host_to_storage.Map(host) * 2u));
}
vk::ComponentMapping components {};
components.r = TextureGetComponentSwizzle(GetDstSel(swizzle, 0));
components.g = TextureGetComponentSwizzle(GetDstSel(swizzle, 1));
components.b = TextureGetComponentSwizzle(GetDstSel(swizzle, 2));
components.a = TextureGetComponentSwizzle(GetDstSel(swizzle, 3));
components.r = TextureGetComponentSwizzle(GetDstSel(swizzle, 0), storage_to_host);
components.g = TextureGetComponentSwizzle(GetDstSel(swizzle, 1), storage_to_host);
components.b = TextureGetComponentSwizzle(GetDstSel(swizzle, 2), storage_to_host);
components.a = TextureGetComponentSwizzle(GetDstSel(swizzle, 3), storage_to_host);
return components;
}
SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format) {
const auto backing_format = Prospero::RemapTextureFormat(format);
const auto vk_format = VulkanFormat(backing_format);
const auto backing_format = Prospero::RemapTextureFormat(format);
const auto host = ResolveHostFormat(backing_format, Prospero::ChannelOrder::kStandard);
const auto conversion_format =
backing_format != format ? format : Prospero::BufferFormat::kInvalid;
if (vk_format != vk::Format::eUndefined) {
return SurfaceFormatInfo(vk_format, conversion_format);
if (host.format != vk::Format::eUndefined) {
return {host.format, conversion_format, host.host_to_storage};
}
EXIT("unknown format: fmt = %u\n", static_cast<uint32_t>(format));
return SurfaceFormatInfo(vk::Format::eUndefined, Prospero::BufferFormat::kInvalid);
}
namespace {
@@ -20,12 +20,9 @@ struct RenderTargetFormatInfo {
};
struct SurfaceFormatInfo {
explicit constexpr SurfaceFormatInfo(vk::Format format,
Prospero::BufferFormat conversion_format)
: vk_format(format), conversion_format(conversion_format) {}
vk::Format vk_format;
Prospero::BufferFormat conversion_format;
vk::Format vk_format;
Prospero::BufferFormat conversion_format;
Prospero::ColorComponentMapping host_to_storage;
};
struct TextureUploadMipLayout {
@@ -43,7 +40,8 @@ struct TextureUploadLayout {
TextureUploadMipLayout mips[16] = {};
};
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle);
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle,
Prospero::ColorComponentMapping host_to_storage);
SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format);
RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layout,
Prospero::ChannelType type,
@@ -483,17 +483,18 @@ static void PopulateTextureMipLayout(ImageInfo& info) {
static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, vk::Format format,
bool shader_conversion, bool storage, uint32_t view_levels,
uint32_t image_layers) {
const SurfaceFormatInfo& surface_format, bool storage,
uint32_t view_levels, uint32_t image_layers) {
ImageViewInfo view {};
view.format = format;
view.aspect = vk::ImageAspectFlagBits::eColor;
view.base_level = descriptor.BaseLevel();
view.level_count = view_levels;
view.usage = storage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlagBits::eSampled;
view.mapping = storage || shader_conversion
? vk::ComponentMapping {}
: TextureGetComponentMapping(descriptor.DstSelXYZW());
view.usage = storage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlagBits::eSampled;
view.mapping =
storage || surface_format.conversion_format != Prospero::BufferFormat::kInvalid
? vk::ComponentMapping {}
: TextureGetComponentMapping(descriptor.DstSelXYZW(), surface_format.host_to_storage);
switch (resource.dimension) {
case ShaderRecompiler::Decoder::ImageDimension::Dim1D:
view.type = vk::ImageViewType::e1D;
@@ -672,7 +673,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
} else {
PopulateTextureMipLayout(desc.info);
}
desc.view_info = TextureViewInfo(resource, descriptor, view_format, shader_conversion, storage,
desc.view_info = TextureViewInfo(resource, descriptor, view_format, surface_format, storage,
view_levels, desc.info.resources.layers);
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
+2 -1
View File
@@ -78,7 +78,8 @@ constexpr FormatMapping kFormatMappings[] = {
{Prospero::BufferFormat::k8_8_8_8Srgb, vk::Format::eR8G8B8A8Srgb},
{Prospero::BufferFormat::k9_9_9_5Float, vk::Format::eE5B9G9R9UfloatPack32},
{Prospero::BufferFormat::k5_6_5UNorm, vk::Format::eB5G6R5UnormPack16},
{Prospero::BufferFormat::k5_5_5_1UNorm, vk::Format::eR5G5B5A1UnormPack16},
{Prospero::BufferFormat::k5_5_5_1UNorm, vk::Format::eA1R5G5B5UnormPack16},
{Prospero::BufferFormat::k1_5_5_5UNorm, vk::Format::eR5G5B5A1UnormPack16},
{Prospero::BufferFormat::k4_4_4_4UNorm, vk::Format::eR4G4B4A4UnormPack16},
{Prospero::BufferFormat::kBc1UNorm, vk::Format::eBc1RgbaUnormBlock},
{Prospero::BufferFormat::kBc1Srgb, vk::Format::eBc1RgbaSrgbBlock},
+139 -1
View File
@@ -8506,6 +8506,138 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
void CheckPackedTextureComponents() {
constexpr const char *name = "PackedTextureComponents";
constexpr uintptr_t base = 0x0000000204200000ull;
constexpr uint64_t allocation_size = 0x10000;
struct FormatCase {
Prospero::BufferFormat format;
std::array<uint16_t, 4> words;
std::array<std::array<uint32_t, 4>, 4> channels;
std::array<uint32_t, 4> maxima;
};
// Packed format channels start at the least significant bit.
// PPSA24515's blue, white, and masked black exposed the 5551 reversal.
constexpr std::array cases{
FormatCase{Prospero::BufferFormat::k5_5_5_1UNorm,
{0x7c00, 0x7fff, 0x8000, 0x8c41},
{{{0, 0, 31, 0}, {31, 31, 31, 0}, {0, 0, 0, 1}, {1, 2, 3, 1}}},
{31, 31, 31, 1}},
FormatCase{Prospero::BufferFormat::k4_4_4_4UNorm,
{0x000f, 0x00f0, 0x0f00, 0x4321},
{{{15, 0, 0, 0}, {0, 15, 0, 0}, {0, 0, 15, 0}, {1, 2, 3, 4}}},
{15, 15, 15, 15}},
};
const auto argb = TextureGetComponentMapping(DstSel(5, 1, 7, 0),
Prospero::ColorMappingArgb);
Require(name, "inverse mapping",
argb == vk::ComponentMapping{vk::ComponentSwizzle::eB,
vk::ComponentSwizzle::eOne, vk::ComponentSwizzle::eR,
vk::ComponentSwizzle::eZero},
"cyclic host mapping reordered output slots or descriptor constants");
const auto target = TextureGetRenderTargetFormat(
Prospero::ChannelLayout::k1_5_5_5, Prospero::ChannelType::kUNorm,
Prospero::ChannelOrder::kStandard);
Require(name, "1555 render target",
target.format == vk::Format::eR5G5B5A1UnormPack16 &&
target.export_mapping == Prospero::ColorMappingAbgr,
"shared format resolution changed the existing 1555 render target");
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_size, 0, &direct_offset) == 0,
"packed texture allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_size) == 0 && mapped == reinterpret_cast<void *>(base),
"packed texture mapping failed");
for (const auto &format : cases) {
std::memset(mapped, 0, allocation_size);
std::memcpy(mapped, format.words.data(), sizeof(format.words));
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
auto &resources = context.GetGpuResources();
auto &cache = resources.GetTextureCache();
auto &executor = context.GetRenderExecutor();
resources.MapMemory(base, allocation_size);
for (const auto swizzle : {DstSel(4, 5, 6, 7), DstSel(7, 1, 4, 0)}) {
ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
(static_cast<uint32_t>(format.format) << 20u) | (3u << 30u), 0,
swizzle | (static_cast<uint32_t>(Prospero::ImageType::kColor2D) << 28u),
0, 0x00700000u, 0, 0}};
TestCase test;
test.name = name;
test.has_user_data = true;
test.image_descriptor_swizzle = swizzle;
std::copy_n(descriptor.fields, 8, test.user_data.begin());
test.user_data[50] = 16 * sizeof(uint32_t);
test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_LOAD,
ShaderOpcode::BUFFER_STORE_DWORD, ShaderOpcode::S_ENDPGM};
for (uint32_t pixel = 0; pixel < format.words.size(); ++pixel) {
AppendVMovU32(&test.code, 20, pixel);
AppendVMovU32(&test.code, 21, 0);
test.code.push_back(EncodeMimg0(0x00, 0xf));
test.code.push_back(EncodeMimg1(0, 20));
for (uint32_t channel = 0; channel < 4; ++channel) {
AppendStoreVgpr(&test.code, channel, pixel * 4 + channel);
}
}
AppendEnd(&test.code);
const auto compiled = CompileCase(test, SubgroupSize());
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
const auto binding = RenderExecutorTestAccess::ResolveTexture(
executor, compiled.program.info.images.at(0), value);
Image sampled;
sampled.view = cache.FindTexture(binding.image_id, binding.desc);
sampled.layout = cache.GetImage(binding.image_id).backing.state.layout;
scheduler.Finish();
auto output = CreateStorageBuffer(name, {}, 16);
Dispatch(test, compiled, output, nullptr, &sampled);
const auto actual = ReadBuffer(name, output, 16);
for (uint32_t pixel = 0; pixel < format.words.size(); ++pixel) {
for (uint32_t channel = 0; channel < 4; ++channel) {
const auto selector = GetDstSel(swizzle, channel);
const auto maximum = selector < 4 ? 1u : format.maxima[selector - 4];
const auto expected = selector < 4 ? selector : format.channels[pixel][selector - 4];
const float observed = std::bit_cast<float>(actual[pixel * 4 + channel]);
// Recover the source bits without testing the host's UNORM rounding precision.
const bool matches = maximum == 1 ? observed == expected
: observed >= 0.0f && observed <= 1.0f &&
std::round(observed * maximum) == expected;
if (!matches) {
std::ostringstream message;
message << "format=" << static_cast<uint32_t>(format.format)
<< " swizzle=" << swizzle << " word=" << format.words[pixel]
<< " channel=" << channel << " expected=" << expected
<< " actual=" << observed;
Fail(name, "GPU components", message.str());
}
}
}
DestroyBuffer(&output);
}
RenderExecutorTestAccess::ResetBindings(executor);
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
Require(name, "release",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0 &&
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"packed texture backing release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckComparisonDepthTexture() {
constexpr const char *name = "ComparisonDepthTexture";
constexpr uintptr_t base = 0x0000000204200000ull;
@@ -25465,7 +25597,7 @@ void CheckSampledColorViews() {
"valid PS5 sampled mappings were rejected or reserved selectors were "
"admitted");
const auto arbitrary = DstSel(5, 1, 7, 0);
const auto components = TextureGetComponentMapping(arbitrary);
const auto components = TextureGetComponentMapping(arbitrary, {});
Require("SampledColorViews", "generic Vulkan component mapping",
SelectSampledColorView(vk::Format::eR8G8B8A8Unorm,
vk::Format::eR8G8B8A8Unorm,
@@ -29073,6 +29205,11 @@ int main(int argc, char **argv) {
std::setvbuf(stdout, nullptr, _IONBF, 0);
EnsureConfigInitialized();
CheckLeastRecentlyUsedCacheOrdering();
if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckPackedTextureComponents();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--error-dialog-only") == 0) {
CheckErrorDialogLifecycle();
return 0;
@@ -29494,6 +29631,7 @@ int main(int argc, char **argv) {
vulkan.CheckBufferCacheDirtyGarbageCollection();
#endif
vulkan.CheckUnifiedImageViewCache();
vulkan.CheckPackedTextureComponents();
const auto tests = MakeCases();
const auto graphics_tests = MakeGraphicsCases();
CheckOpcodeCoverage(tests, graphics_tests);