Compare commits

...
Author SHA1 Message Date
nmzik 307fdcf7ad add self updater (#1352)
* add self updater

* remove updater tests after validation
2026-10-10 05:42:32 +02:00
Mac 679be25c37 shader: recognize remaining disabled debug branches 2026-10-10 04:31:55 +02:00
fxpw fa56c5eae9 shader: add scalar zero-bit count and search operations 2026-10-10 04:31:55 +02:00
foufouadi 3b9e176b70 shader: support VOP1/VOP2 DPP8 fetch-inactive 2026-10-10 04:31:55 +02:00
Bartlomiej Wolynczyk 053f5fd3cc shader: emit DPP row_share instead of an identity move 2026-10-10 04:31:55 +02:00
HamsterCL f3836bb831 tests: fix the Linux build of ResourceMaterializationTests (#1333)
tests: avoid empty optional::emplace() on IndirectDescriptor (fixes Linux build)

clang with libstdc++ 14 does not treat std::optional<DescriptorSource::IndirectDescriptor>
as default-constructible (nested struct with default member initializers), so the
argument-less emplace() fails to resolve. Pass an explicit value-initialized
IndirectDescriptor, matching the form already used elsewhere in this file.
2026-10-10 03:47:38 +02:00
popcoder565andClaude Opus 5.5 2f4d0f383e shader: add SDWA support for V_MIN_I32 (#1342)
WWE 2K26 stops building a shader CFG with "unsupported family=VOP2
opcode=0x11 raw=[0x220404f9 0x00060605] reason=VOP2 SDWA modifier is not
supported for opcode".

V_MIN_I32 takes the same integer path as V_MAX_I32, so give it the same
full-destination SDWA profile. The test runs the game's encoding, which
takes the signed minimum with the zero-extended low byte of a register.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:43:08 +02:00
TheCruZ d247d3d60c shader: complete signed 64-bit integer comparisons 2026-10-10 03:36:25 +02:00
nmzik 93452b24e3 renderer: cache dynamic graphics state 2026-10-10 03:28:02 +02:00
nmzik 5559cfcbb2 renderer: optimize tiling and reuse centralized image layouts 2026-10-10 03:27:13 +02:00
nmzik 1c301c9aab Revert "shader: keep scalar payload and terminal conditions on the GPU" 2026-10-09 21:10:15 +02:00
nmzikandHector Mödinger S. 59183dbfa8 shader: add missing F64 VOPC compares
Co-authored-by: Hector Mödinger S. <hectormodinger@gmail.com>
2026-10-09 21:01:51 +02:00
nmzik c1926f915d update screenshot 2026-10-09 19:07:22 +02:00
nmzik 7da3c4ed21 launcher: add resizable game grid/improve controller setup 2026-10-09 19:03:35 +02:00
nmzik 3e9f88bf79 shader: isolate SWAPPC work from ordinary shader paths 2026-10-09 17:27:07 +02:00
nmzik 8c81386568 shader: share finite descriptor planning with GPU-selected inline samplers 2026-10-09 15:06:39 +00:00
nmzik bb5edbd490 shader: expose graphics compute queue fields to GETREG 2026-10-09 15:06:39 +00:00
nmzikandBehzad 1818aae562 shader: link scalar leaf calls only for SWAPPC programs
Co-authored-by: Behzad <behzadkhan1736@gmail.com>
2026-10-09 15:06:39 +00:00
nmzik 165cd884ca agc: initialize function table in tracked driver memory 2026-10-09 15:06:39 +00:00
nmzik 973809cde0 shader: retain bounded scalar material image selectors 2026-10-09 15:06:39 +00:00
nmzik 71c7261740 shader: share indirect texture dispatch with image loads 2026-10-09 15:06:39 +00:00
nmzik 5b68a26cce shader: keep scalar payload and terminal conditions on the GPU 2026-10-09 15:06:39 +00:00
nmzik 3b1a94707a shader: capture only memory-dependent resource predicates 2026-10-09 15:06:39 +00:00
nmzik 488c0acae9 shader: resolve writable descriptors selected by immutable branches 2026-10-09 15:06:39 +00:00
nmzik 4c8fc53a85 agc: support one-word NOP reservations and share packet sizing 2026-10-09 14:02:06 +02:00
nmzik 3ae5a9b70a agc: implement branch target patching and packet size queries 2026-10-09 13:01:19 +02:00
nmzik a31e2cd361 shader: BUFFER_LOAD_SHORT_D16_HI 2026-10-09 12:57:04 +02:00
nmzik 1bd282ec0a shader: BUFFER_LOAD_SHORT_D16 2026-10-09 12:57:04 +02:00
nmzik 1fc520cd62 shader: FLAT_STORE_DWORD 2026-10-09 12:57:04 +02:00
92 changed files with 6900 additions and 2533 deletions

No files matched your search

+5 -1
View File
@@ -257,6 +257,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
file "$binary"
lipo "$binary" -verify_arch x86_64
@@ -266,6 +267,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
codesign --verify --strict "$binary"
done
@@ -407,6 +409,7 @@ jobs:
run: |
file _Build/linux/install/launcher
file _Build/linux/install/kyty_emulator
test -x _Build/linux/install/kyty_updater
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
ldd _Build/linux/install/kyty_emulator > /dev/null
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
@@ -495,7 +498,8 @@ jobs:
test -d "$linux_dir"
chmod a+x \
"$linux_dir/launcher" \
"$linux_dir/kyty_emulator"
"$linux_dir/kyty_emulator" \
"$linux_dir/kyty_updater"
tar -C "$linux_dir" -czf \
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .
+2
View File
@@ -657,6 +657,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-tiler-only)
add_test(NAME image_transfer_layout
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-layout-only)
add_test(NAME texture_cache_layered_image
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --layered-image-only)
+10 -2
View File
@@ -52,8 +52,8 @@ or graphical glitches, so please include the version you tested when reporting a
<img src="docs/screenshots/ps5-01.png" width="300" alt="Astro Bot running in KytyPS5">
</td>
<td align="center">
<strong>Dreaming Sarah</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Dreaming Sarah running in KytyPS5">
<strong>Saros</strong><br>
<img src="docs/screenshots/ps5-03.png" width="300" alt="Saros running in KytyPS5">
</td>
</tr>
<tr>
@@ -338,6 +338,14 @@ folders recursively for game directories containing `eboot.bin` and ZArchive (`.
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
Official release builds include a built-in updater. Leave **Check for updates on startup** enabled,
or click **Check for updates**, then choose **Install Update**. The launcher downloads the package
for your platform from GitHub, checks its SHA-256 checksum, and restarts after installation. Close
any running games first. Settings, saves, patches, and other files outside the release package are
preserved; replacement failures restore the previous files. The installation folder must be writable.
If a package cannot be installed automatically, the updater offers the GitHub release page.
Source and fork builds do not replace themselves with official releases.
The emulator can also be started directly with a legally obtained game directory, ELF file, or
ZArchive dump:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+4 -3
View File
@@ -689,7 +689,7 @@ void CommandProcessor::ProcessPm4(Pm4Execution& execution) {
const auto opcode = (packet_header >> 8u) & 0xffu;
EXIT_NOT_IMPLEMENTED(remaining_dw > total_dw);
if (packet_header == 0x80000000u) {
if (packet_header == 0x80000000u || Pm4::PacketSizeDw(packet_header) == 1u) {
cursor.offset_dw++;
execution.m_made_progress = true;
continue;
@@ -1003,7 +1003,7 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
}
m_renderer.GetRenderExecutor().DispatchDirect(m_submit_id, CurrentBuffer(), thread_group_x,
thread_group_y, thread_group_z, mode);
thread_group_y, thread_group_z, mode, IsAsyncComputeQueue());
}
}
@@ -1015,7 +1015,8 @@ void CommandProcessor::DispatchIndirect(uint64_t args_addr, uint32_t mode) {
return;
}
m_sh_ctx.SetCsWaveSize(Pm4::ComputeWaveSize(mode));
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode);
m_renderer.GetRenderExecutor().DispatchIndirect(m_submit_id, CurrentBuffer(), args_addr, mode,
IsAsyncComputeQueue());
}
void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
+3 -3
View File
@@ -120,15 +120,15 @@ void DumpPm4PacketStream(Common::File* file, const uint32_t* cmd_buffer, uint32_
uint32_t len = 0;
const auto packet_type = static_cast<PacketType>(cmd_id >> 30u);
// Type-2 packets are header-only padding; every other packet type requires a body.
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2);
EXIT_NOT_IMPLEMENTED(dw < 2 && packet_type != PacketType::Type2 &&
PacketSizeDw(cmd_id) != 1u);
switch (packet_type) {
case PacketType::Type3: {
const bool sh_gx = (cmd_id & 0x2u) == 0;
const uint8_t op = ((cmd_id >> 8u) & 0xffu);
const auto r = KYTY_PM4_R(cmd_id);
len = ((cmd_id >> 16u) & 0x3fffu) + 1;
len = PacketSizeDw(cmd_id) - 1u;
EXIT_NOT_IMPLEMENTED(len >= dw);
+4
View File
@@ -19,6 +19,10 @@ class File;
namespace Libs::Graphics::Pm4 {
constexpr uint32_t PacketSizeDw(uint32_t header) {
return (header & 0x3fffff00u) == 0x3fff1000u ? 1u : KYTY_PM4_LEN(header);
}
constexpr uint32_t IT_NOP = 0x10;
constexpr uint32_t IT_SET_BASE = 0x11;
constexpr uint32_t IT_CLEAR_STATE = 0x12;
+138 -265
View File
@@ -110,37 +110,6 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
return true;
}
[[nodiscard]] std::vector<vk::BufferImageCopy> BuildDepthCopies(const ImageInfo& info,
uint64_t slice_stride,
vk::ImageAspectFlags aspect) {
std::vector<vk::BufferImageCopy> copies(info.resources.layers);
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
auto& copy = copies[layer];
copy.bufferOffset = slice_stride * layer;
copy.bufferRowLength = info.pitch;
copy.bufferImageHeight = info.extent.height;
copy.imageSubresource = {aspect, 0, layer, 1};
copy.imageExtent = {info.extent.width, info.extent.height, 1};
}
return copies;
}
[[nodiscard]] std::vector<GpuTileInfo> BuildDepthTiles(const ImageInfo& info) {
TileBlockLayout block {};
EXIT_NOT_IMPLEMENTED(
!TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_block, block));
const auto full_slice_size = info.data.size / info.resources.layers;
std::vector<GpuTileInfo> tiles;
tiles.reserve(info.resources.layers);
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
const auto offset = full_slice_size * layer;
tiles.push_back({block.family, block.bytes_per_element, offset, full_slice_size, offset,
full_slice_size, 0, info.extent.width, info.extent.height, 1, info.pitch});
tiles.back().surface_z = layer;
}
return tiles;
}
} // namespace
TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler,
@@ -570,20 +539,20 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
const auto depth_size = depth_slice * layers;
const auto color_size = color_slice * layers;
std::vector<vk::BufferImageCopy> depth_copies(layers);
std::vector<vk::BufferImageCopy> color_copies(layers);
for (uint32_t layer = 0; layer < layers; layer++) {
depth_copies[layer].bufferOffset = depth_slice * layer;
depth_copies[layer].bufferRowLength = depth.info.pitch;
depth_copies[layer].bufferImageHeight = depth.info.extent.height;
depth_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1};
depth_copies[layer].imageExtent = depth.info.extent;
color_copies[layer].bufferOffset = color_slice * layer;
color_copies[layer].bufferRowLength = color.info.pitch;
color_copies[layer].bufferImageHeight = color.info.extent.height;
color_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, layer, 1};
color_copies[layer].imageExtent = color.info.extent;
}
const std::array depth_copies {
vk::BufferImageCopy {0,
depth.info.pitch,
depth.info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
depth.info.extent}};
const std::array color_copies {
vk::BufferImageCopy {0,
color.info.pitch,
color.info.extent.height,
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
{},
color.info.extent}};
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
@@ -740,7 +709,11 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
if (retain_cached_layout) {
info.data = cached.info.data;
info.resources = cached.info.resources;
info.mip_layout = cached.info.mip_layout;
info.mip_layout = cached.info.mip_layout;
info.tiling = cached.info.tiling;
info.tiled_slice_stride = cached.info.tiled_slice_stride;
info.linear_slice_stride = cached.info.linear_slice_stride;
info.first_tail_level = cached.info.first_tail_level;
}
info.htile_clear_mask = 0;
const auto replacement_id = InsertImage(info);
@@ -801,7 +774,14 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
// a level into or out of the mip tail. These are separate guest layouts.
if (requested.tile_mode != cached.info.tile_mode ||
(requested.resources == cached.info.resources &&
requested.mip_layout != cached.info.mip_layout)) {
!std::equal(requested.mip_layout.begin(), requested.mip_layout.end(),
cached.info.mip_layout.begin(),
[](const ImageMipInfo& left, const ImageMipInfo& right) {
return left.offset == right.offset && left.size == right.size &&
left.pitch == right.pitch && left.height == right.height &&
left.tail_x == right.tail_x && left.tail_y == right.tail_y &&
left.surface_z == right.surface_z;
}))) {
if (safe_to_delete) {
FreeImage(cached_id);
}
@@ -913,168 +893,55 @@ ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) {
return expanded_id;
}
struct TextureCache::TextureTransfer {
TextureUploadLayout layout;
std::vector<vk::BufferImageCopy> regions;
std::vector<GpuTileInfo> tiles;
ColorTransform color_transform = ColorTransform::None;
bool valid = false;
[[nodiscard]] uint64_t LinearSize() const {
uint64_t size = 0;
for (const auto& tile: tiles) {
size = std::max(size, tile.linear_offset + tile.linear_size);
}
return size;
bool TextureCache::CanDownload(const Image& image) const {
const auto& info = image.info;
if (info.samples != 1 || image.backing.samples != 1 || info.resources.layers == 0 ||
info.data.size % info.resources.layers != 0) {
return false;
}
};
struct TextureCache::ImageDownload {
TextureTransfer texture;
bool depth_target = false;
bool valid = false;
};
TextureCache::TextureTransfer
TextureCache::BuildTextureTransfer(const Image& image, BindingType binding,
TransferDirection direction) const {
const auto& info = image.info;
const bool upload = direction == TransferDirection::Upload;
const bool render_target = binding == BindingType::RenderTarget;
const bool video_out = binding == BindingType::VideoOut;
uint32_t layers = info.TransferLayers();
bool volume = info.IsVolume();
const char* owner = "TextureCache readback";
TextureTransfer transfer;
transfer.color_transform = info.GetColorTransform();
if (upload) {
if ((render_target || video_out) &&
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1)) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
owner = "TextureCache";
if (render_target) {
owner = "RenderTarget";
} else if (binding == BindingType::Storage) {
owner = "StorageTextureCache";
} else if (video_out) {
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
layers = info.resources.layers;
volume = false;
owner = "VideoOut";
}
if (UploadBinding(image) == BindingType::DepthTarget) {
return IsSupportedDepthPlaneReadback(info) &&
Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block;
}
transfer.layout = TextureCalcUploadLayout(info.guest_format, info.extent.width,
info.extent.height, info.resources.levels, layers,
info.tile_mode, info.data.size, volume, owner);
transfer.regions = TextureBuildImageCopies(transfer.layout);
if (info.IsDepth()) {
for (auto& region: transfer.regions) {
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth;
}
}
if (transfer.layout.surface.description.tile_mode != Prospero::TileMode::kLinear) {
if (!TextureBuildGpuTileInfos(info.data.size, transfer.regions, transfer.layout,
info.resources.levels, transfer.tiles)) {
return transfer;
}
}
transfer.valid = true;
return transfer;
}
TextureCache::ImageDownload TextureCache::BuildDownload(const Image& image) const {
const auto& info = image.info;
const auto binding = UploadBinding(image);
ImageDownload transfer {.depth_target = binding == BindingType::DepthTarget};
if (info.samples != 1 || image.backing.samples != 1) {
return transfer;
}
if (transfer.depth_target) {
transfer.valid = IsSupportedDepthPlaneReadback(info) && info.resources.layers != 0 &&
info.data.size % info.resources.layers == 0 &&
Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block;
return transfer;
}
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
return transfer;
}
transfer.texture = BuildTextureTransfer(image, binding, TransferDirection::Download);
transfer.valid = transfer.texture.valid;
return transfer;
return info.metadata.compression == VideoOutCompression::Uncompressed &&
(!info.IsTiled() || !Prospero::IsFmaskTextureFormat(info.guest_format)) &&
info.LinearSize() != 0;
}
void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_offset) {
auto& destination = image.depth_id ? m_slot_images[image.depth_id] : image;
const auto binding = image.depth_id ? BindingType::DepthTarget : UploadBinding(image);
const auto upload = [&](std::vector<vk::BufferImageCopy>& copies, TileManager::Result linear) {
for (auto& copy: copies) {
copy.bufferOffset += linear.offset;
}
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
};
const auto& info = image.info;
const auto binding = image.depth_id ? BindingType::DepthTarget : UploadBinding(image);
if (binding != BindingType::DepthTarget) {
const auto& info = image.info;
auto transfer = BuildTextureTransfer(image, binding, TransferDirection::Upload);
if (!transfer.valid) {
EXIT("TextureCache: invalid texture upload: binding=%u addr=0x%016" PRIx64
" size=0x%016" PRIx64 " format=%u tile=%u family=%u extent=%ux%ux%u "
"pitch=%u levels=%u layers=%u samples=%u\n",
static_cast<uint32_t>(binding), info.data.address, info.data.size,
static_cast<uint32_t>(info.guest_format), static_cast<uint32_t>(info.tile_mode),
static_cast<uint32_t>(transfer.layout.surface.texture.block.family), info.extent.width,
info.extent.height, info.extent.depth, info.pitch, info.resources.levels,
info.resources.layers, info.samples);
if ((binding == BindingType::RenderTarget || binding == BindingType::VideoOut) &&
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1)) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
TileManager::Result linear {source.Handle(), source_offset, info.data.size};
if (!transfer.tiles.empty()) {
linear = m_tiler.Detile(source.Handle(), source_offset, info.data.size,
transfer.LinearSize(), transfer.tiles, transfer.color_transform);
} else if (transfer.color_transform != ColorTransform::None) {
linear = m_tiler.TransformColor(linear, transfer.color_transform, true);
if (binding == BindingType::VideoOut &&
info.metadata.compression != VideoOutCompression::Uncompressed) {
EXIT("TextureCache: invalid video-out upload\n");
}
upload(transfer.regions, linear);
const auto linear = m_tiler.Detile(source.Handle(), source_offset, info);
const auto copies = info.BufferCopies(linear.offset);
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
return;
}
// The stencil plane has its own row pitch and shares the native image
// with the depth plane.
auto info = destination.info;
if (image.depth_id) {
info.data = image.info.data;
info.resources = {image.stencil_subresources.level_count,
image.stencil_subresources.layer_count};
info.guest_format = Prospero::BufferFormat::k8UInt;
info.bytes_per_block = 1;
if (info.IsTiled()) info.pitch = TileGetDepthPitch(info.extent.width, 1, 0);
}
if (info.samples != 1 || destination.backing.samples != 1 ||
info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
Prospero::NumBytesPerElement(info.guest_format) != info.bytes_per_block) {
EXIT("TextureCache: invalid depth upload\n");
}
const auto layers = info.resources.layers;
const auto full_slice_size = info.data.size / layers;
auto copies = BuildDepthCopies(info, full_slice_size, image.depth_id
? vk::ImageAspectFlagBits::eStencil
: vk::ImageAspectFlagBits::eDepth);
const auto layers = info.resources.layers;
const auto full_slice_size = info.linear_slice_stride;
auto linear = m_tiler.Detile(source.Handle(), source_offset, info);
auto copies = info.BufferCopies(linear.offset);
if (image.depth_id) {
for (auto& copy: copies) {
copy.imageSubresource.baseArrayLayer += image.stencil_subresources.base_layer;
}
}
TileManager::Result linear {source.Handle(), source_offset, source.Size() - source_offset};
if (info.IsTiled()) {
const auto tiles = BuildDepthTiles(info);
linear =
m_tiler.Detile(source.Handle(), source_offset, info.data.size, info.data.size, tiles);
}
const auto transfer_bytes = image.depth_id ? 1u : DepthAspectTransferBytes(info.pixel_format);
if (transfer_bytes != info.bytes_per_block) {
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
@@ -1095,11 +962,14 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off
.source_slice_stride = full_slice_size,
.target_slice_stride = transfer_slice});
linear = promoted;
for (uint32_t layer = 0; layer < layers; layer++) {
copies[layer].bufferOffset = transfer_slice * layer;
}
copies.resize(1);
copies[0].bufferOffset = linear.offset;
copies[0].bufferRowLength = info.pitch;
copies[0].bufferImageHeight = info.extent.height;
copies[0].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, 0, layers};
copies[0].imageExtent = info.extent;
}
upload(copies, linear);
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
}
void TextureCache::InitializeImage(ImageId id) {
@@ -1257,8 +1127,25 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
}
auto& record = m_slot_images[association];
TouchImage(record);
record.depth_id = depth_id;
record.stencil_subresources = depth.stencil_subresources;
if (record.depth_id != depth_id || record.stencil_subresources != depth.stencil_subresources) {
ImageInfo plane {};
plane.data = stencil;
plane.pixel_format = vk::Format::eS8Uint;
plane.guest_format = Prospero::BufferFormat::k8UInt;
plane.type = depth.info.type;
plane.extent = depth.info.extent;
plane.resources = {depth.stencil_subresources.level_count,
depth.stencil_subresources.layer_count};
plane.tile_mode = depth.info.tile_mode;
plane.pitch =
plane.IsTiled() ? TileGetDepthPitch(plane.extent.width, 1, 0) : depth.info.pitch;
plane.bytes_per_block = 1;
plane.UpdateSize();
record.info = plane;
record.depth_id = depth_id;
record.stencil_subresources = depth.stencil_subresources;
}
return association;
}
@@ -1282,6 +1169,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
for (const auto id: candidates) {
const auto& image = m_slot_images[id];
// Stencil descriptors name the associated plane; its depth owner supplies
// the native image and the canonical guest tiling.
if ((desc.type == BindingType::Texture || desc.type == BindingType::Storage) &&
image.depth_id && image.info.data == desc.info.data &&
m_slot_images[image.depth_id].info.samples == desc.info.samples &&
image.info.extent == desc.info.extent && image.info.type == desc.info.type &&
image.info.resources.levels >= desc.info.resources.levels &&
image.info.resources.layers >= desc.info.resources.layers &&
desc.info.bytes_per_block == 1 &&
desc.info.guest_format == Prospero::BufferFormat::k8UInt) {
result = id;
break;
}
if (SameBacking(image.info, desc.info, exact_format)) {
result = id;
}
@@ -1303,7 +1203,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
}
if (result) {
if (result && !m_slot_images[result].depth_id) {
auto& resolved = m_slot_images[result];
if (exact_format && resolved.info.pixel_format != desc.info.pixel_format) {
result = {};
@@ -1685,7 +1585,7 @@ void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) {
void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset) {
const auto& info = image.info;
const auto layers = info.resources.layers;
const auto full_slice_size = info.data.size / layers;
const auto full_slice_size = info.linear_slice_stride;
const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format);
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
EXIT_NOT_IMPLEMENTED(transfer_bytes == 0 || texels_per_slice > UINT32_MAX ||
@@ -1696,31 +1596,31 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers);
const uint64_t transfer_size = transfer_slice * layers;
EXIT_NOT_IMPLEMENTED(guest_slice > full_slice_size);
auto copies = BuildDepthCopies(info, full_slice_size, vk::ImageAspectFlagBits::eDepth);
if (transfer_bytes == info.bytes_per_block) {
if (!info.IsTiled()) {
for (auto& copy: copies) {
copy.bufferOffset += destination_offset;
}
const auto copies = info.BufferCopies(destination_offset);
image.Download(copies, destination.Handle(), destination_offset, info.data.size);
return;
} else {
const auto copies = info.BufferCopies();
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset,
info.data.size);
}
const auto tiles = BuildDepthTiles(info);
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, info.data.size,
info.data.size, tiles);
return;
}
EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) ||
transfer_bytes != sizeof(uint32_t));
for (uint32_t layer = 0; layer < layers; layer++) {
copies[layer].bufferOffset = transfer_slice * layer;
}
const std::array copies {vk::BufferImageCopy {0,
info.pitch,
info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
info.extent}};
auto host_linear = m_tiler.GetScratchBuffer(transfer_size);
image.Download(copies, host_linear.buffer, 0, host_linear.size);
const bool tiled = info.IsTiled();
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size, host_linear.buffer)
: TileManager::Result {destination.Handle(), destination_offset,
destination.Size() - destination_offset};
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.LinearSize(), host_linear.buffer)
: TileManager::Result {destination.Handle(), destination_offset,
destination.Size() - destination_offset};
m_tiler.ConvertD16(host_linear, guest_linear, TileManager::D16Direction::Demote,
DepthAspectTransferFormat(info.pixel_format) == vk::Format::eD32Sfloat,
{.width = info.extent.width,
@@ -1730,47 +1630,39 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
.target_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint16_t),
.source_slice_stride = transfer_slice,
.target_slice_stride = full_slice_size});
if (!tiled) {
return;
if (tiled) {
m_tiler.Tile(guest_linear.buffer, guest_linear.offset, guest_linear.size,
destination.Handle(), destination_offset, info.data.size, info);
}
const auto tiles = BuildDepthTiles(info);
m_tiler.Tile(guest_linear.buffer, guest_linear.offset, info.data.size, destination.Handle(),
destination_offset, info.data.size, tiles);
}
void TextureCache::DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
uint64_t destination_size, ImageDownload transfer) {
if (!transfer.valid) {
EXIT("TextureCache: invalid image download transfer\n");
uint64_t destination_size, uint32_t levels) {
if (!CanDownload(image)) {
EXIT("TextureCache: invalid image download\n");
}
if (transfer.depth_target) {
if (destination_size != image.info.data.size) {
EXIT("TextureCache: partial depth image download is unsupported\n");
}
if (UploadBinding(image) == BindingType::DepthTarget) {
EXIT_IF(destination_size != image.info.data.size || levels != image.info.resources.levels);
DownloadDepth(image, destination, destination_offset);
return;
}
auto& texture = transfer.texture;
const auto transform = texture.color_transform;
if (texture.tiles.empty()) {
if (transform != ColorTransform::None) {
auto linear = m_tiler.GetScratchBuffer(destination_size);
image.Download(texture.regions, linear.buffer, 0, linear.size);
m_tiler.TransformColor(linear,
{destination.Handle(), destination_offset, destination_size},
transform, false);
return;
}
for (auto& copy: texture.regions) {
copy.bufferOffset += destination_offset;
}
image.Download(texture.regions, destination.Handle(), destination_offset, destination_size);
return;
const auto& info = image.info;
const auto transform = info.GetColorTransform();
if (info.IsTiled()) {
const auto copies = info.BufferCopies(0, levels);
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, destination_size,
levels);
} else if (transform != ColorTransform::None) {
auto linear = m_tiler.GetScratchBuffer(destination_size);
const auto copies = info.BufferCopies(0, levels);
image.Download(copies, linear.buffer, 0, linear.size);
m_tiler.TransformColor(linear, {destination.Handle(), destination_offset, destination_size},
transform, false);
} else {
const auto copies = info.BufferCopies(destination_offset, levels);
image.Download(copies, destination.Handle(), destination_offset, destination_size);
}
m_tiler.TileImage(image, texture.regions, destination.Handle(), destination_offset,
destination_size, texture.LinearSize(), texture.tiles, transform);
}
bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size) {
@@ -1810,30 +1702,12 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
if (copy_size == 0) {
return false;
}
auto transfer = m_texture_cache.BuildDownload(image);
if (!transfer.valid) {
if (!m_texture_cache.CanDownload(image) ||
(TextureCache::UploadBinding(image) == TextureCache::BindingType::DepthTarget &&
copy_size != image.info.data.size)) {
return false;
}
if (transfer.depth_target && copy_size != image.info.data.size) {
return false;
}
if (!transfer.depth_target && levels < image.info.resources.levels) {
auto& texture = transfer.texture;
std::erase_if(texture.regions, [levels](const vk::BufferImageCopy& region) {
return region.imageSubresource.mipLevel >= levels;
});
if (texture.regions.empty()) {
return false;
}
if (!texture.tiles.empty()) {
texture.tiles.clear();
if (!TextureBuildGpuTileInfos(copy_size, texture.regions, texture.layout, levels,
texture.tiles)) {
return false;
}
}
}
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, std::move(transfer));
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, levels);
return true;
}
@@ -1842,8 +1716,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
if (image.depth_id) {
return false;
}
auto transfer = BuildDownload(image);
if (!transfer.valid || !image.SafeToDownload()) {
if (!image.SafeToDownload() || !CanDownload(image)) {
return false;
}
const auto range = image.info.data;
@@ -1859,7 +1732,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
}
download.Flush(offset, range.size);
DownloadImage(image, download, offset, range.size, std::move(transfer));
DownloadImage(image, download, offset, range.size, image.info.resources.levels);
vk::BufferMemoryBarrier barrier {};
barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
+2 -8
View File
@@ -73,10 +73,6 @@ public:
void RunGarbageCollector();
private:
enum class TransferDirection { Upload, Download };
struct TextureTransfer;
struct ImageDownload;
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
@@ -141,12 +137,10 @@ private:
void MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer);
void InitializeImage(ImageId id);
[[nodiscard]] TextureTransfer
BuildTextureTransfer(const Image& image, BindingType binding, TransferDirection direction) const;
[[nodiscard]] ImageDownload BuildDownload(const Image& image) const;
[[nodiscard]] bool CanDownload(const Image& image) const;
void UploadImage(Image& image, Buffer& source, uint64_t source_offset);
void DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
uint64_t destination_size, ImageDownload transfer);
uint64_t destination_size, uint32_t levels);
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
void CommitGpuWrite(Image& image);
// Caller holds m_lock. Volume layer ranges select depth slices.
@@ -139,9 +139,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// Nonlinear clear values are still stored as normalized components.
// Metadata clears are materialized during image discovery; render-pass loads preserve contents.
uint32_t width = 0;
uint32_t height = 0;
uint32_t pitch = 0;
uint64_t size = 0;
uint32_t height = 0;
bool tile = false;
const bool standard4 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard4KB;
const bool standard64 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard64KB;
@@ -164,6 +162,9 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
if (texture_tile && samples != 1) {
EXIT("texture-tiled color render targets do not support multisampling\n");
}
if (volume && !tile) {
EXIT("linear 3D render targets are unsupported\n");
}
width = rt.attrib2.width + 1;
height = rt.attrib2.height + 1;
@@ -198,80 +199,25 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
}
if (samples == 1) {
pitch = TileGetTexturePitch(transfer_format, width, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
}
if (pitch == 0) {
EXIT("unsupported render-target pitch: width=%u bytes=%u\n", width, bytes_per_element);
}
TileSizeOffset mip_sizes[16] {};
TilePaddedSize mip_padded[16] {};
TileSurfaceLayout volume_layout {};
uint64_t backing_size = 0;
if (volume) {
const TileSurfaceDescription description {transfer_format,
rt.attrib3.tile_mode,
TileSurfaceDimension::Dim3D,
width,
height,
depth,
levels,
1};
if (!tile || !TileGetTiledTextureLayout(description, volume_layout)) {
EXIT("unsupported 3D render-target layout: %ux%ux%u levels=%u tile=%u\n", width, height,
depth, levels, static_cast<uint32_t>(rt.attrib3.tile_mode));
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else {
TileSizeAlign layout {};
bool valid_layout = false;
if (samples == 1) {
TileGetTextureSize(transfer_format, width, height, levels, rt.attrib3.tile_mode,
&layout, mip_sizes, mip_padded);
valid_layout = layout.size != 0 && layout.align != 0 &&
(rt.attrib3.tile_mode != Prospero::TileMode::kRenderTarget ||
levels <= std::bit_width(std::max(width, height)));
} else {
valid_layout = TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments);
mip_sizes[0] = {layout.size, 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (!valid_layout) {
EXIT("unsupported render-target layout: %ux%u pitch=%u bytes=%u levels=%u\n", width,
height, pitch, bytes_per_element, levels);
}
size = layout.size;
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
}
if (!volume) {
backing_size = size * view.image_layers;
}
if (backing_size == 0) {
EXIT("render-target backing is empty\n");
}
if (!GuestRange {rt.base.addr, backing_size}.Valid()) {
EXIT("render-target backing range is invalid\n");
}
auto& desc = r.desc;
desc.type = TextureCache::BindingType::RenderTarget;
desc.info.data = {rt.base.addr, backing_size};
desc.info.data = {rt.base.addr, 0};
desc.info.pixel_format = target_format.format;
desc.info.guest_format = target_format.guest_format;
desc.info.type = image_type;
desc.info.extent = {width, height, depth};
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.pitch = pitch;
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.bytes_per_block = bytes_per_element;
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
if (!volume && rt.attrib3.tile_mode == Prospero::TileMode::kRenderTarget &&
levels > std::bit_width(std::max(width, height))) {
EXIT("unsupported render-target mip count\n");
}
desc.info.UpdateSize();
if (!desc.info.data.Valid()) {
EXIT("render-target backing range is invalid\n");
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
@@ -293,30 +239,6 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
desc.info.metadata.clear_word = rt.clear_word0.word0;
desc.info.metadata.clear_register_valid = true;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
const auto& mip = volume_layout.mips[level];
desc.info.mip_layout[level] = {
mip.offset,
mip.size,
mip.padded_width,
mip.padded_height,
};
continue;
}
const auto level_offset =
mip_sizes[level].src_size != 0 ? mip_sizes[level].src_offset : mip_sizes[level].offset;
const auto level_size =
static_cast<uint64_t>(mip_sizes[level].src_size != 0 ? mip_sizes[level].src_size
: mip_sizes[level].size) *
view.image_layers;
desc.info.mip_layout[level] = {
level_offset,
level_size,
mip_padded[level].width,
mip_padded[level].height,
};
}
desc.view_info.format = target_format.format;
if (is_1d) {
desc.view_info.type =
@@ -334,6 +334,7 @@ CommandBuffer& CommandScheduler::BeginCommand() {
EXIT_IF(!m_command.IsInvalid());
m_command.m_buffer = m_command_pool.Commit();
m_command.Begin();
m_dynamic_state.Invalidate();
return m_command;
}
@@ -3,14 +3,13 @@
#include "common/common.h"
#include "common/uniqueFunction.h"
#include "graphics/host_gpu/renderer/dynamicState.h"
#include "graphics/host_gpu/renderer/masterSemaphore.h"
#include "graphics/host_gpu/renderer/render.h"
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
@@ -50,6 +49,7 @@ public:
[[nodiscard]] uint64_t CurrentTick() const noexcept { return m_master.CurrentTick(); }
[[nodiscard]] bool IsFree(uint64_t tick);
[[nodiscard]] MasterSemaphore& GetMasterSemaphore() noexcept { return m_master; }
[[nodiscard]] DynamicState& GetDynamicState() noexcept { return m_dynamic_state; }
[[nodiscard]] RenderContext& Context() const noexcept { return m_context; }
[[nodiscard]] GraphicContext& Graphics() const noexcept { return m_graphics; }
@@ -92,6 +92,7 @@ private:
GraphicContext& m_graphics;
CommandPool m_command_pool;
CommandBuffer m_command;
DynamicState m_dynamic_state;
std::queue<PendingOperation> m_pending_operations;
std::queue<PendingOperation> m_priority_operations;
std::mutex m_operation_mutex;
@@ -239,7 +239,7 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
desc.info.bytes_per_block = bytes;
desc.info.samples = samples;
desc.info.tile_mode = Prospero::TileMode::kDepth;
desc.info.mip_layout[0] = {0, depth_backing_size, pitch, height};
desc.info.UpdateSize();
desc.info.metadata.range =
has_htile ? GuestRange {z.htile_data_base_addr, htile_backing_size} : GuestRange {};
desc.info.metadata.kind = has_htile ? ImageMetadataKind::Htile : ImageMetadataKind::None;
@@ -0,0 +1,150 @@
#include "graphics/host_gpu/renderer/dynamicState.h"
#include "graphics/host_gpu/graphicContext.h"
namespace Libs::Graphics {
void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdbuf) {
if (dirty_state.viewports) {
dirty_state.viewports = false;
cmdbuf.setViewportWithCount(num_viewports, viewports.data());
}
if (dirty_state.scissors) {
dirty_state.scissors = false;
cmdbuf.setScissorWithCount(num_scissors, scissors.data());
}
if (dirty_state.depth_test_enabled) {
dirty_state.depth_test_enabled = false;
cmdbuf.setDepthTestEnable(depth_test_enabled);
}
if (dirty_state.depth_write_enabled) {
dirty_state.depth_write_enabled = false;
// Note that this must be set in a command buffer even if depth test is disabled.
cmdbuf.setDepthWriteEnable(depth_write_enabled);
}
if (depth_test_enabled && dirty_state.depth_compare_op) {
dirty_state.depth_compare_op = false;
cmdbuf.setDepthCompareOp(depth_compare_op);
}
if (dirty_state.depth_bounds_test_enabled) {
dirty_state.depth_bounds_test_enabled = false;
#if !defined(__APPLE__)
cmdbuf.setDepthBoundsTestEnable(depth_bounds_test_enabled);
#endif
}
if (depth_bounds_test_enabled && dirty_state.depth_bounds) {
dirty_state.depth_bounds = false;
#if !defined(__APPLE__)
cmdbuf.setDepthBounds(depth_bounds_min, depth_bounds_max);
#endif
}
if (dirty_state.depth_bias_enabled) {
dirty_state.depth_bias_enabled = false;
cmdbuf.setDepthBiasEnable(depth_bias_enabled);
}
if (depth_bias_enabled && dirty_state.depth_bias) {
dirty_state.depth_bias = false;
cmdbuf.setDepthBias(depth_bias_constant, depth_bias_clamp, depth_bias_slope);
}
if (dirty_state.stencil_test_enabled) {
dirty_state.stencil_test_enabled = false;
cmdbuf.setStencilTestEnable(stencil_test_enabled);
}
if (stencil_test_enabled) {
if (dirty_state.stencil_front_ops && dirty_state.stencil_back_ops &&
stencil_front_ops == stencil_back_ops) {
dirty_state.stencil_front_ops = false;
dirty_state.stencil_back_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eFrontAndBack, stencil_front_ops.fail_op,
stencil_front_ops.pass_op, stencil_front_ops.depth_fail_op,
stencil_front_ops.compare_op);
} else {
if (dirty_state.stencil_front_ops) {
dirty_state.stencil_front_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eFront, stencil_front_ops.fail_op,
stencil_front_ops.pass_op, stencil_front_ops.depth_fail_op,
stencil_front_ops.compare_op);
}
if (dirty_state.stencil_back_ops) {
dirty_state.stencil_back_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eBack, stencil_back_ops.fail_op,
stencil_back_ops.pass_op, stencil_back_ops.depth_fail_op,
stencil_back_ops.compare_op);
}
}
if (dirty_state.stencil_front_reference && dirty_state.stencil_back_reference &&
stencil_front_reference == stencil_back_reference) {
dirty_state.stencil_front_reference = false;
dirty_state.stencil_back_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_reference);
} else {
if (dirty_state.stencil_front_reference) {
dirty_state.stencil_front_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eFront,
stencil_front_reference);
}
if (dirty_state.stencil_back_reference) {
dirty_state.stencil_back_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eBack, stencil_back_reference);
}
}
if (dirty_state.stencil_front_write_mask && dirty_state.stencil_back_write_mask &&
stencil_front_write_mask == stencil_back_write_mask) {
dirty_state.stencil_front_write_mask = false;
dirty_state.stencil_back_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_write_mask);
} else {
if (dirty_state.stencil_front_write_mask) {
dirty_state.stencil_front_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eFront,
stencil_front_write_mask);
}
if (dirty_state.stencil_back_write_mask) {
dirty_state.stencil_back_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eBack, stencil_back_write_mask);
}
}
if (dirty_state.stencil_front_compare_mask && dirty_state.stencil_back_compare_mask &&
stencil_front_compare_mask == stencil_back_compare_mask) {
dirty_state.stencil_front_compare_mask = false;
dirty_state.stencil_back_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_compare_mask);
} else {
if (dirty_state.stencil_front_compare_mask) {
dirty_state.stencil_front_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eFront,
stencil_front_compare_mask);
}
if (dirty_state.stencil_back_compare_mask) {
dirty_state.stencil_back_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eBack,
stencil_back_compare_mask);
}
}
}
if (dirty_state.blend_constants) {
dirty_state.blend_constants = false;
cmdbuf.setBlendConstants(blend_constants.data());
}
if (dirty_state.color_write_enables) {
dirty_state.color_write_enables = false;
#if !defined(__APPLE__)
if (num_color_write_enables != 0) {
cmdbuf.setColorWriteEnableEXT(num_color_write_enables, color_write_enables.data());
}
#endif
}
if (dirty_state.line_width) {
dirty_state.line_width = false;
cmdbuf.setLineWidth(line_width);
}
if (dirty_state.feedback_loop_aspects && graphics.attachment_feedback_loop_enabled) {
dirty_state.feedback_loop_aspects = false;
cmdbuf.setAttachmentFeedbackLoopEnableEXT(feedback_loop_aspects);
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,258 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
#include "common/assert.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <span>
namespace Libs::Graphics {
struct GraphicContext;
struct StencilOps {
vk::StencilOp fail_op {};
vk::StencilOp pass_op {};
vk::StencilOp depth_fail_op {};
vk::CompareOp compare_op {};
bool operator==(const StencilOps& other) const {
return fail_op == other.fail_op && pass_op == other.pass_op &&
depth_fail_op == other.depth_fail_op && compare_op == other.compare_op;
}
};
struct DynamicState {
struct {
bool viewports : 1;
bool scissors : 1;
bool depth_test_enabled : 1;
bool depth_write_enabled : 1;
bool depth_compare_op : 1;
bool depth_bounds_test_enabled : 1;
bool depth_bounds : 1;
bool depth_bias_enabled : 1;
bool depth_bias : 1;
bool stencil_test_enabled : 1;
bool stencil_front_ops : 1;
bool stencil_front_reference : 1;
bool stencil_front_write_mask : 1;
bool stencil_front_compare_mask : 1;
bool stencil_back_ops : 1;
bool stencil_back_reference : 1;
bool stencil_back_write_mask : 1;
bool stencil_back_compare_mask : 1;
bool blend_constants : 1;
bool color_write_enables : 1;
bool line_width : 1;
bool feedback_loop_aspects : 1;
} dirty_state {};
std::array<vk::Viewport, 16> viewports {};
std::array<vk::Rect2D, 16> scissors {};
uint32_t num_viewports {};
uint32_t num_scissors {};
bool depth_test_enabled {};
bool depth_write_enabled {};
vk::CompareOp depth_compare_op {};
bool depth_bounds_test_enabled {};
float depth_bounds_min {};
float depth_bounds_max {};
bool depth_bias_enabled {};
float depth_bias_constant {};
float depth_bias_clamp {};
float depth_bias_slope {};
bool stencil_test_enabled {};
StencilOps stencil_front_ops {};
uint32_t stencil_front_reference {};
uint32_t stencil_front_write_mask {};
uint32_t stencil_front_compare_mask {};
StencilOps stencil_back_ops {};
uint32_t stencil_back_reference {};
uint32_t stencil_back_write_mask {};
uint32_t stencil_back_compare_mask {};
std::array<float, 4> blend_constants {};
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
uint32_t num_color_write_enables {};
float line_width {};
vk::ImageAspectFlags feedback_loop_aspects {};
/// Commits the dynamic state to the provided command buffer.
void Commit(const GraphicContext& graphics, vk::CommandBuffer cmdbuf);
/// Invalidates all dynamic state to be flushed into the next command buffer.
void Invalidate() { std::memset(&dirty_state, 0xFF, sizeof(dirty_state)); }
void SetViewports(std::span<const vk::Viewport> viewports_) {
EXIT_IF(viewports_.size() > viewports.size());
if (!std::ranges::equal(std::span(viewports).first(num_viewports), viewports_)) {
std::ranges::copy(viewports_, viewports.begin());
num_viewports = static_cast<uint32_t>(viewports_.size());
dirty_state.viewports = true;
}
}
void SetScissors(std::span<const vk::Rect2D> scissors_) {
EXIT_IF(scissors_.size() > scissors.size());
if (!std::ranges::equal(std::span(scissors).first(num_scissors), scissors_)) {
std::ranges::copy(scissors_, scissors.begin());
num_scissors = static_cast<uint32_t>(scissors_.size());
dirty_state.scissors = true;
}
}
void SetDepthTestEnabled(const bool enabled) {
if (depth_test_enabled != enabled) {
depth_test_enabled = enabled;
dirty_state.depth_test_enabled = true;
}
}
void SetDepthWriteEnabled(const bool enabled) {
if (depth_write_enabled != enabled) {
depth_write_enabled = enabled;
dirty_state.depth_write_enabled = true;
}
}
void SetDepthCompareOp(const vk::CompareOp compare_op) {
if (depth_compare_op != compare_op) {
depth_compare_op = compare_op;
dirty_state.depth_compare_op = true;
}
}
void SetDepthBoundsTestEnabled(const bool enabled) {
if (depth_bounds_test_enabled != enabled) {
depth_bounds_test_enabled = enabled;
dirty_state.depth_bounds_test_enabled = true;
}
}
void SetDepthBounds(const float min, const float max) {
if (depth_bounds_min != min || depth_bounds_max != max) {
depth_bounds_min = min;
depth_bounds_max = max;
dirty_state.depth_bounds = true;
}
}
void SetDepthBiasEnabled(const bool enabled) {
if (depth_bias_enabled != enabled) {
depth_bias_enabled = enabled;
dirty_state.depth_bias_enabled = true;
}
}
void SetDepthBias(const float constant, const float clamp, const float slope) {
if (depth_bias_constant != constant || depth_bias_clamp != clamp ||
depth_bias_slope != slope) {
depth_bias_constant = constant;
depth_bias_clamp = clamp;
depth_bias_slope = slope;
dirty_state.depth_bias = true;
}
}
void SetStencilTestEnabled(const bool enabled) {
if (stencil_test_enabled != enabled) {
stencil_test_enabled = enabled;
dirty_state.stencil_test_enabled = true;
}
}
void SetStencilOps(const StencilOps& front_ops, const StencilOps& back_ops) {
if (stencil_front_ops != front_ops) {
stencil_front_ops = front_ops;
dirty_state.stencil_front_ops = true;
}
if (stencil_back_ops != back_ops) {
stencil_back_ops = back_ops;
dirty_state.stencil_back_ops = true;
}
}
void SetStencilReferences(const uint32_t front_reference, const uint32_t back_reference) {
if (stencil_front_reference != front_reference) {
stencil_front_reference = front_reference;
dirty_state.stencil_front_reference = true;
}
if (stencil_back_reference != back_reference) {
stencil_back_reference = back_reference;
dirty_state.stencil_back_reference = true;
}
}
void SetStencilWriteMasks(const uint32_t front_write_mask, const uint32_t back_write_mask) {
if (stencil_front_write_mask != front_write_mask) {
stencil_front_write_mask = front_write_mask;
dirty_state.stencil_front_write_mask = true;
}
if (stencil_back_write_mask != back_write_mask) {
stencil_back_write_mask = back_write_mask;
dirty_state.stencil_back_write_mask = true;
}
}
void SetStencilCompareMasks(const uint32_t front_compare_mask,
const uint32_t back_compare_mask) {
if (stencil_front_compare_mask != front_compare_mask) {
stencil_front_compare_mask = front_compare_mask;
dirty_state.stencil_front_compare_mask = true;
}
if (stencil_back_compare_mask != back_compare_mask) {
stencil_back_compare_mask = back_compare_mask;
dirty_state.stencil_back_compare_mask = true;
}
}
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
if (blend_constants != blend_constants_) {
blend_constants = blend_constants_;
dirty_state.blend_constants = true;
}
}
void SetColorWriteEnables(std::span<const vk::Bool32> enables) {
EXIT_IF(enables.size() > color_write_enables.size());
if (!std::ranges::equal(std::span(color_write_enables).first(num_color_write_enables),
enables)) {
std::ranges::copy(enables, color_write_enables.begin());
num_color_write_enables = static_cast<uint32_t>(enables.size());
dirty_state.color_write_enables = true;
}
}
void SetLineWidth(const float width) {
if (line_width != width) {
line_width = width;
dirty_state.line_width = true;
}
}
void SetAttachmentFeedbackLoopAspects(const vk::ImageAspectFlags aspects) {
if (feedback_loop_aspects != aspects) {
feedback_loop_aspects = aspects;
dirty_state.feedback_loop_aspects = true;
}
}
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
@@ -193,6 +193,8 @@ void BlitHelper::ReinterpretColorAsMsDepth(Image& source, Image& destination) {
command.setScissor(0, 1, &scissor);
command.draw(3, 1, 0, 0);
command.endRendering();
// The helper pipeline has static state that invalidates the guest draw's dynamic state.
m_scheduler.GetDynamicState().Invalidate();
}
} // namespace Libs::Graphics
@@ -0,0 +1,246 @@
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include "common/alignment.h"
namespace Libs::Graphics {
namespace {
uint64_t Multiply(uint64_t left, uint64_t right) {
EXIT_IF(right != 0 && left > UINT64_MAX / right);
return left * right;
}
uint64_t Add(uint64_t left, uint64_t right) {
EXIT_IF(right > UINT64_MAX - left);
return left + right;
}
bool IsDepthPlane(const ImageInfo& info) {
return (info.IsDepth() || info.pixel_format == vk::Format::eS8Uint) &&
(info.tile_mode == Prospero::TileMode::kDepth ||
info.tile_mode == Prospero::TileMode::kLinear);
}
} // namespace
vk::Extent3D ImageInfo::MipExtent(uint32_t level) const {
EXIT_IF(level >= resources.levels || level >= mip_layout.size());
return {std::max(extent.width >> level, 1u), std::max(extent.height >> level, 1u),
IsVolume() ? std::max(extent.depth >> level, 1u) : resources.layers};
}
void ImageInfo::UpdateSize() {
EXIT_IF(guest_format == Prospero::BufferFormat::kInvalid || extent.width == 0 ||
extent.height == 0 || extent.depth == 0 || resources.levels == 0 ||
resources.levels > mip_layout.size() || resources.layers == 0 || samples == 0 ||
!std::has_single_bit(samples));
TileTextureElementLayout element {};
EXIT_NOT_IMPLEMENTED(!TileGetTextureElementLayout(guest_format, element));
if (bytes_per_block == 0) {
bytes_per_block = element.bytes;
}
EXIT_IF(bytes_per_block != element.bytes);
mip_layout = {};
tiling = {};
tiling.texel_width = element.texel_width;
tiling.texel_height = element.texel_height;
tiling.block.bytes_per_element = element.bytes;
tiled_slice_stride = 0;
linear_slice_stride = 0;
first_tail_level = 16;
if (samples > 1) {
EXIT_NOT_IMPLEMENTED(IsVolume() || resources.levels != 1 ||
(tile_mode != Prospero::TileMode::kDepth &&
tile_mode != Prospero::TileMode::kRenderTarget));
const auto fragments = std::countr_zero(samples);
if (pitch == 0) {
pitch = tile_mode == Prospero::TileMode::kDepth
? TileGetDepthPitch(extent.width, bytes_per_block, fragments)
: TileGetRenderTargetPitch(extent.width, bytes_per_block, fragments);
}
EXIT_NOT_IMPLEMENTED(!TileGetTextureBlockLayout(guest_format, tile_mode, false, tiling));
if (data.size == 0) {
TileSizeAlign slice {};
EXIT_NOT_IMPLEMENTED(!TileGetRenderTargetSize(extent.width, extent.height, pitch,
bytes_per_block, slice, fragments));
data.size = Multiply(slice.size, resources.layers);
}
EXIT_IF(data.size % resources.layers != 0);
tiled_slice_stride = data.size / resources.layers;
mip_layout[0] = {0, data.size, pitch, extent.height};
return;
}
if (pitch == 0) {
pitch = TileGetTexturePitch(guest_format, extent.width, tile_mode);
}
EXIT_IF(pitch == 0);
if (IsDepthPlane(*this) && resources.levels == 1) {
EXIT_NOT_IMPLEMENTED(IsVolume());
if (IsTiled()) {
EXIT_NOT_IMPLEMENTED(
!TileGetTextureBlockLayout(guest_format, tile_mode, false, tiling));
}
if (data.size == 0) {
TileSizeAlign slice {};
TileGetTextureSize(guest_format, extent.width, extent.height, 1, tile_mode, &slice,
nullptr, nullptr);
data.size = Multiply(slice.size, resources.layers);
}
EXIT_IF(data.size % resources.layers != 0);
const auto slice = data.size / resources.layers;
EXIT_IF(Multiply(Multiply(pitch, extent.height), bytes_per_block) > slice);
linear_slice_stride = slice;
tiled_slice_stride = IsTiled() ? slice : 0;
mip_layout[0] = {0, data.size, pitch, extent.height, 0, slice, pitch};
return;
}
if (!IsTiled()) {
TileSizeAlign total {};
TileSizeOffset offsets[16] {};
TilePaddedSize padded[16] {};
TileGetTextureSize(guest_format, extent.width, extent.height, resources.levels, tile_mode,
&total, offsets, padded);
if (data.size == 0) {
data.size = Multiply(total.size, TransferLayers());
}
for (uint32_t level = 0; level < resources.levels; ++level) {
auto& mip = mip_layout[level];
mip.offset = offsets[level].offset;
mip.size = Multiply(offsets[level].size, MipExtent(level).depth);
mip.pitch = padded[level].width / element.texel_width;
mip.height = padded[level].height / element.texel_height;
mip.linear_offset = offsets[level].offset;
mip.linear_size = offsets[level].size;
mip.linear_pitch = mip.pitch;
linear_slice_stride =
std::max(linear_slice_stride, Add(mip.linear_offset, mip.linear_size));
}
if (TransferLayers() > 1 && data.size % TransferLayers() == 0) {
linear_slice_stride = std::max(linear_slice_stride, data.size / TransferLayers());
}
} else {
TileSurfaceLayout surface {};
const TileSurfaceDescription description {guest_format,
tile_mode,
IsVolume() ? TileSurfaceDimension::Dim3D
: TileSurfaceDimension::Dim2D,
extent.width,
extent.height,
IsVolume() ? extent.depth : 1u,
resources.levels,
resources.layers};
EXIT_NOT_IMPLEMENTED(!TileGetTiledTextureLayout(description, surface));
tiling = surface.texture;
first_tail_level = surface.first_tail_level;
tiled_slice_stride = surface.block_slice_size;
if (data.size == 0) {
data.size = surface.total_size;
}
if (!IsVolume() && resources.layers > 1 && data.size % resources.layers == 0) {
tiled_slice_stride = std::max(tiled_slice_stride, data.size / resources.layers);
}
uint64_t linear_offset = 0;
uint32_t mip_pitch = IsVolume() ? extent.width : pitch;
for (uint32_t level = 0; level < resources.levels; ++level) {
const auto& source = surface.mips[level];
auto& mip = mip_layout[level];
const auto active = MipExtent(level);
mip.offset = source.offset;
mip.size = Multiply(source.size, IsVolume() ? 1u : resources.layers);
mip.pitch = source.padded_width;
mip.height = source.padded_height;
mip.tail_x = source.tail_x;
mip.tail_y = source.tail_y;
mip.linear_pitch =
Common::AlignUp(mip_pitch, element.texel_width) / element.texel_width;
const auto rows =
Common::AlignUp(active.height, element.texel_height) / element.texel_height;
mip.linear_size = Multiply(Multiply(mip.linear_pitch, rows), element.bytes);
const auto alignment = std::max(element.bytes, 4u);
EXIT_IF(linear_offset > UINT64_MAX - (alignment - 1u));
mip.linear_offset = Common::AlignUp<uint64_t>(linear_offset, alignment);
linear_offset = Add(mip.linear_offset, Multiply(mip.linear_size, active.depth));
mip_pitch = std::max(mip_pitch >> 1u, 1u);
}
}
}
uint64_t ImageInfo::LinearSize(uint32_t levels) const {
levels = levels == 0 ? resources.levels : levels;
EXIT_IF(levels == 0 || levels > resources.levels || levels > mip_layout.size());
if (linear_slice_stride == 0) {
const auto& mip = mip_layout[levels - 1u];
return Add(mip.linear_offset, Multiply(mip.linear_size, MipExtent(levels - 1u).depth));
}
uint64_t size = 0;
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto slices = MipExtent(level).depth;
const auto stride = linear_slice_stride != 0 ? linear_slice_stride : mip.linear_size;
size = std::max(
size, Add(Add(mip.linear_offset, Multiply(slices - 1u, stride)), mip.linear_size));
}
return size;
}
std::vector<vk::BufferImageCopy> ImageInfo::BufferCopies(uint64_t buffer_offset,
uint32_t levels) const {
levels = levels == 0 ? resources.levels : levels;
EXIT_IF(levels > resources.levels || levels > mip_layout.size() || tiling.texel_width == 0 ||
tiling.texel_height == 0);
const auto aspect = IsDepth() ? vk::ImageAspectFlagBits::eDepth
: pixel_format == vk::Format::eS8Uint ? vk::ImageAspectFlagBits::eStencil
: vk::ImageAspectFlagBits::eColor;
std::vector<vk::BufferImageCopy> copies;
std::array<uint32_t, 16> image_heights {};
uint64_t count = 0;
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto active = MipExtent(level);
if (linear_slice_stride == 0) {
image_heights[level] = Common::AlignUp(active.height, tiling.texel_height);
} else {
const auto row_bytes = Multiply(mip.linear_pitch, tiling.block.bytes_per_element);
EXIT_IF(row_bytes == 0);
const auto rows = linear_slice_stride / row_bytes;
if (linear_slice_stride % row_bytes == 0 &&
rows >=
(uint64_t {active.height} + tiling.texel_height - 1u) / tiling.texel_height &&
rows <= UINT32_MAX / tiling.texel_height) {
image_heights[level] = static_cast<uint32_t>(rows) * tiling.texel_height;
}
}
count += image_heights[level] != 0 ? 1u : active.depth;
}
copies.reserve(static_cast<size_t>(count));
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto active = MipExtent(level);
vk::BufferImageCopy copy {};
copy.bufferOffset = Add(buffer_offset, mip.linear_offset);
copy.bufferRowLength = mip.linear_pitch * tiling.texel_width;
copy.bufferImageHeight = image_heights[level] != 0 ? image_heights[level]
: !IsTiled() ? mip.height * tiling.texel_height
: Common::AlignUp(active.height, tiling.texel_height);
copy.imageSubresource = {aspect, level, 0, 1};
copy.imageExtent = {active.width, active.height, 1};
if (image_heights[level] == 0) {
for (uint32_t slice = 0; slice < active.depth; ++slice) {
copy.bufferOffset = Add(Add(buffer_offset, mip.linear_offset),
Multiply(slice, linear_slice_stride));
copy.imageSubresource.baseArrayLayer = IsVolume() ? 0u : slice;
copy.imageOffset.z = IsVolume() ? static_cast<int32_t>(slice) : 0;
copies.push_back(copy);
}
} else {
copy.imageSubresource.layerCount = IsVolume() ? 1u : active.depth;
copy.imageExtent.depth = IsVolume() ? active.depth : 1u;
copies.push_back(copy);
}
}
return copies;
}
} // namespace Libs::Graphics
@@ -4,6 +4,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/regionDefinitions.h"
#include "graphics/host_gpu/vulkanCommon.h"
@@ -12,6 +13,7 @@
#include <bit>
#include <cmath>
#include <cstdint>
#include <vector>
namespace Libs::Graphics {
@@ -47,11 +49,19 @@ struct ImageSubresourceRange {
};
struct ImageMipInfo {
// Guest mip chains run backwards within each layer or volume block-slice.
// Tail levels share an offset and use tail_x/tail_y within that block.
uint64_t offset = 0;
uint64_t size = 0;
// Padded dimensions in storage elements (compressed blocks for BC formats).
uint32_t pitch = 0;
uint32_t height = 0;
uint64_t linear_offset = 0;
uint64_t linear_size = 0; // Per transfer slice.
uint32_t linear_pitch = 0; // Storage elements.
uint32_t tail_x = 0;
uint32_t tail_y = 0;
uint32_t surface_z = 0;
auto operator<=>(const ImageMipInfo&) const = default;
};
@@ -71,6 +81,17 @@ struct ImageInfo {
Prospero::TileMode tile_mode = Prospero::TileMode::kLinear;
bool bgra16 = false;
std::array<ImageMipInfo, 16> mip_layout {};
TileTextureBlockLayout tiling;
uint64_t tiled_slice_stride = 0;
// Nonzero for guest-linear and depth-plane transfers; tiled colors are mip-major.
uint64_t linear_slice_stride = 0;
uint32_t first_tail_level = 16;
void UpdateSize();
[[nodiscard]] uint64_t LinearSize(uint32_t levels = 0) const;
[[nodiscard]] vk::Extent3D MipExtent(uint32_t level) const;
[[nodiscard]] std::vector<vk::BufferImageCopy> BufferCopies(uint64_t buffer_offset = 0,
uint32_t levels = 0) const;
[[nodiscard]] ColorTransform GetColorTransform() const noexcept {
if (bgra16) return ColorTransform::SwapBgra16;
@@ -1,16 +1,13 @@
#include "graphics/host_gpu/renderer/image/textureCommon.h"
#include "common/alignment.h"
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/host_gpu/renderer/image/tiler.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/shader.h"
#include <algorithm>
#include <array>
#include <cinttypes>
namespace Libs::Graphics {
namespace {
@@ -60,68 +57,6 @@ HostFormatInfo ResolveHostFormat(Prospero::BufferFormat guest_format,
}
}
uint32_t GetTextureLevelDepth(uint32_t depth, uint32_t level, bool volume_texture) {
return volume_texture ? std::max(depth >> level, 1u) : depth;
}
size_t GetTextureRegionCount(uint32_t depth, uint32_t levels, bool volume_texture) {
size_t count = 0;
for (uint32_t level = 0; level < levels; level++) {
count += GetTextureLevelDepth(depth, level, volume_texture);
}
return count;
}
uint64_t CalcTextureSliceStride(const TextureUploadMipLayout* mips, uint32_t levels,
uint64_t total_size, uint32_t depth) {
uint64_t stride = 0;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
stride = std::max(stride, mips[mip_level].offset + mips[mip_level].size);
}
if (depth > 1 && total_size != 0 && total_size % depth == 0) {
stride = std::max(stride, total_size / depth);
}
return stride;
}
uint64_t CalcLinearUploadLevelSize(const TileTextureElementLayout& element, uint32_t pitch,
uint32_t height) {
const uint32_t element_columns =
std::max((pitch + element.texel_width - 1u) / element.texel_width, 1u);
const uint32_t element_rows =
std::max((height + element.texel_height - 1u) / element.texel_height, 1u);
return static_cast<uint64_t>(element_columns) * element_rows * element.bytes;
}
uint64_t SetLinearUploadLevels(TextureUploadMipLayout* mips,
const TileTextureElementLayout& element, uint32_t height,
uint32_t levels, uint32_t base_pitch) {
uint64_t offset = 0;
auto mip_pitch = base_pitch;
auto mip_height = height;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
const auto size = CalcLinearUploadLevelSize(element, mip_pitch, mip_height);
mips[mip_level] = {offset, size};
offset += size;
if (mip_pitch > 1) {
mip_pitch /= 2;
}
if (mip_height > 1) {
mip_height /= 2;
}
}
return offset;
}
bool FitsBufferRange(uint64_t offset, uint64_t size, uint64_t capacity) {
return offset <= capacity && size <= capacity - offset;
}
} // namespace
RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layout,
@@ -176,203 +111,4 @@ SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format) {
EXIT("unknown format: fmt = %u\n", static_cast<uint32_t>(format));
}
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool volume_texture, const char* owner) {
TextureUploadLayout layout {};
layout.surface.description = {
format,
tile_mode,
volume_texture ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width,
height,
volume_texture ? depth : 1u,
levels,
volume_texture ? 1u : depth,
};
const auto& description = layout.surface.description;
TileTextureElementLayout element {};
if (format == Prospero::BufferFormat::kInvalid) {
EXIT("%s: legacy texture upload format unsupported: fmt=0 tile=%u size=%" PRIu64
" extent=%ux%u levels=%u\n",
owner, static_cast<uint32_t>(description.tile_mode), upload_size, width, height,
levels);
}
if (tile_mode == Prospero::TileMode::kLinear) {
if (!TileGetTextureElementLayout(format, element)) {
EXIT("%s: unsupported linear texture format: fmt=%u\n", owner,
static_cast<uint32_t>(format));
}
} else {
if (!TileGetTiledTextureLayout(description, layout.surface)) {
EXIT("%s: unsupported typed tiled upload: fmt=%u tile=%u "
"size=%" PRIu64 " extent=%ux%u levels=%u\n",
owner, static_cast<uint32_t>(format), static_cast<uint32_t>(tile_mode),
upload_size, width, height, levels);
}
element = {layout.surface.texture.block.bytes_per_element,
layout.surface.texture.texel_width, layout.surface.texture.texel_height};
}
layout.pitch = TileGetTexturePitch(format, width, description.tile_mode);
if (tile_mode == Prospero::TileMode::kLinear) {
TileSizeOffset level_sizes[16] {};
TilePaddedSize padded_sizes[16] {};
TileGetTextureSize(format, width, height, levels, description.tile_mode, nullptr,
level_sizes, padded_sizes);
for (uint32_t level = 0; level < levels; ++level) {
layout.mips[level] = {level_sizes[level].offset, level_sizes[level].size,
padded_sizes[level].width, padded_sizes[level].height};
}
} else if (volume_texture) {
layout.slice_stride = SetLinearUploadLevels(layout.mips, element, height, levels, width);
} else {
layout.source_slice_stride = layout.surface.block_slice_size;
if (depth > 1 && upload_size != 0 && upload_size % depth == 0) {
layout.source_slice_stride = std::max(layout.source_slice_stride, upload_size / depth);
}
SetLinearUploadLevels(layout.mips, element, height, levels, layout.pitch);
}
if (!volume_texture || tile_mode == Prospero::TileMode::kLinear) {
layout.slice_stride = CalcTextureSliceStride(layout.mips, levels, upload_size, depth);
}
return layout;
}
std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayout& layout) {
const auto& description = layout.surface.description;
const bool volume_texture = description.dimension == TileSurfaceDimension::Dim3D;
const bool linear_texture = description.tile_mode == Prospero::TileMode::kLinear;
const auto depth = volume_texture ? description.depth : description.layers;
uint32_t mip_width = description.width;
uint32_t mip_height = description.height;
uint32_t mip_pitch = volume_texture && !linear_texture ? description.width : layout.pitch;
std::vector<vk::BufferImageCopy> regions;
regions.reserve(GetTextureRegionCount(depth, description.levels, volume_texture));
for (uint32_t mip_level = 0; mip_level < description.levels; ++mip_level) {
EXIT_NOT_IMPLEMENTED(layout.mips[mip_level].size == 0);
const auto mip_depth = GetTextureLevelDepth(depth, mip_level, volume_texture);
vk::BufferImageCopy region {};
region.imageSubresource = {vk::ImageAspectFlagBits::eColor, mip_level, 0, 1};
region.imageExtent = {mip_width, mip_height, 1};
if (linear_texture) {
region.bufferRowLength = layout.mips[mip_level].row_length;
region.bufferImageHeight = layout.mips[mip_level].image_height;
} else {
const auto texel_width = layout.surface.texture.texel_width;
const auto aligned_pitch = Common::AlignUp(mip_pitch, texel_width);
region.bufferRowLength =
aligned_pitch > Common::AlignUp(mip_width, texel_width) ? aligned_pitch : 0;
}
for (uint32_t slice_index = 0; slice_index < mip_depth; ++slice_index) {
region.bufferOffset = layout.mips[mip_level].offset + slice_index * layout.slice_stride;
region.imageSubresource.baseArrayLayer = volume_texture ? 0u : slice_index;
region.imageOffset.z = volume_texture ? static_cast<int>(slice_index) : 0;
regions.push_back(region);
}
if (mip_width > 1) {
mip_width /= 2;
}
if (mip_height > 1) {
mip_height /= 2;
}
if (mip_pitch > 1) {
mip_pitch /= 2;
}
}
return regions;
}
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t levels,
std::vector<GpuTileInfo>& out_tile_infos) {
const auto& description = layout.surface.description;
const bool volume_texture = description.dimension == TileSurfaceDimension::Dim3D;
const auto depth = volume_texture ? description.depth : description.layers;
if (tiled_size == 0 || levels == 0 || levels > 16 || depth == 0 ||
regions.size() != GetTextureRegionCount(depth, levels, volume_texture) ||
Prospero::IsFmaskTextureFormat(description.format)) {
return false;
}
const auto& surface = layout.surface;
const auto& texture = surface.texture;
const auto& block = texture.block;
if (surface.description.levels < levels || block.bytes_per_element == 0 ||
block.family == TileBlockFamily::Count) {
return false;
}
std::vector<GpuTileInfo> tile_infos;
tile_infos.reserve(regions.size());
const auto slices_per_copy = volume_texture ? block.block_depth : 1u;
size_t region_base = 0;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
const auto& mip = surface.mips[mip_level];
const auto mip_depth = GetTextureLevelDepth(depth, mip_level, volume_texture);
for (uint32_t slice_index = 0; slice_index < mip_depth; slice_index += slices_per_copy) {
const auto& region = regions[region_base + slice_index];
GpuTileInfo tile_info {};
tile_info.family = block.family;
tile_info.bytes_per_element = block.bytes_per_element;
tile_info.linear_offset = region.bufferOffset;
tile_info.linear_size = layout.mips[mip_level].size;
tile_info.tiled_size = mip.size;
if (volume_texture) {
tile_info.depth = std::min(slices_per_copy, mip_depth - slice_index);
tile_info.linear_slice_stride = layout.slice_stride;
tile_info.linear_size += (tile_info.depth - 1u) * layout.slice_stride;
tile_info.tiled_offset =
mip.offset +
static_cast<uint64_t>(slice_index / slices_per_copy) * surface.block_slice_size;
tile_info.surface_z =
block.block_depth == 1 ? static_cast<uint32_t>(region.imageOffset.z) : 0;
} else {
const auto source_stride = layout.source_slice_stride != 0
? layout.source_slice_stride
: surface.block_slice_size;
if (slice_index > (UINT64_MAX - mip.offset) / source_stride) {
return false;
}
tile_info.tiled_offset =
mip.offset + static_cast<uint64_t>(slice_index) * source_stride;
tile_info.surface_z = block.family == TileBlockFamily::RenderTarget64KB ||
block.family == TileBlockFamily::Depth64KB
? region.imageSubresource.baseArrayLayer
: 0;
}
if (!FitsBufferRange(tile_info.linear_offset, tile_info.linear_size, UINT64_MAX) ||
!FitsBufferRange(tile_info.tiled_offset, tile_info.tiled_size, tiled_size)) {
return false;
}
const auto row_length =
region.bufferRowLength != 0 ? region.bufferRowLength : region.imageExtent.width;
const auto image_height = region.bufferImageHeight != 0 ? region.bufferImageHeight
: region.imageExtent.height;
tile_info.width = std::max(
(region.imageExtent.width + texture.texel_width - 1u) / texture.texel_width, 1u);
tile_info.height =
std::max((image_height + texture.texel_height - 1u) / texture.texel_height, 1u);
tile_info.pitch =
std::max((row_length + texture.texel_width - 1u) / texture.texel_width, 1u);
tile_info.tail = mip_level >= surface.first_tail_level;
tile_info.tail_x = tile_info.tail ? mip.tail_x : 0;
tile_info.tail_y = tile_info.tail ? mip.tail_y : 0;
tile_info.tiled_width = mip.padded_width;
tile_info.tiled_height = mip.padded_height;
tile_infos.push_back(tile_info);
}
region_base += mip_depth;
}
out_tile_infos = std::move(tile_infos);
return true;
}
} // namespace Libs::Graphics
@@ -2,15 +2,10 @@
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_HOST_GPU_RENDERER_IMAGE_TEXTURECOMMON_H_
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <vector>
namespace Libs::Graphics {
struct GpuTileInfo;
struct RenderTargetFormatInfo {
vk::Format format = vk::Format::eUndefined;
uint32_t bytes_per_element = 0;
@@ -24,35 +19,12 @@ struct SurfaceFormatInfo {
Prospero::ColorComponentMapping host_to_storage;
};
struct TextureUploadMipLayout {
uint64_t offset = 0;
uint64_t size = 0;
uint32_t row_length = 0;
uint32_t image_height = 0;
};
struct TextureUploadLayout {
uint32_t pitch = 0;
uint64_t slice_stride = 0;
uint64_t source_slice_stride = 0;
TileSurfaceLayout surface;
TextureUploadMipLayout mips[16] = {};
};
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle,
Prospero::ColorComponentMapping host_to_storage);
SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format);
RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layout,
Prospero::ChannelType type,
Prospero::ChannelOrder order);
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool volume_texture, const char* owner);
std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayout& layout);
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t levels,
std::vector<GpuTileInfo>& out_tile_infos);
} // namespace Libs::Graphics
+155 -152
View File
@@ -31,7 +31,12 @@ TileManager::TileManager(GraphicContext& graphics, CommandScheduler& scheduler,
StreamBuffer& stream_buffer)
: m_graphics(graphics), m_scheduler(scheduler), m_stream_buffer(stream_buffer) {
static_assert(FamilyCount == 9);
static_assert(sizeof(Push) == 60);
static_assert(sizeof(Mip) == 48);
static_assert(offsetof(TilingParams, mips) == 16);
static_assert(sizeof(TilingParams) == 784);
static_assert(sizeof(TilingParams) <= UINT16_MAX && PipelineCount <= UINT16_MAX);
static_assert(sizeof(Tiling) == 16);
static_assert(sizeof(Conversion) == 32);
std::array<vk::DescriptorSetLayoutBinding, 3> bindings {};
for (uint32_t index = 0; index < 2; index++) {
bindings[index] = {index, vk::DescriptorType::eStorageBuffer, 1,
@@ -48,7 +53,8 @@ TileManager::TileManager(GraphicContext& graphics, CommandScheduler& scheduler,
&m_descriptor_layout),
"create TileManager descriptor layout");
const vk::PushConstantRange push_range {vk::ShaderStageFlagBits::eCompute, 0, sizeof(Push)};
const vk::PushConstantRange push_range {vk::ShaderStageFlagBits::eCompute, 0,
sizeof(Conversion)};
vk::PipelineLayoutCreateInfo layout_info {};
layout_info.setLayoutCount = 1;
layout_info.pSetLayouts = &m_descriptor_layout;
@@ -88,14 +94,25 @@ TileManager::~TileManager() {
}
}
void TileManager::Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos, uint64_t source_base,
uint64_t target_base, std::vector<Dispatch>& dispatches,
ColorTransform transform) {
EXIT_IF(infos.empty() || tiled_capacity == 0 || linear_capacity == 0);
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
TileManager::Tiling TileManager::Prepare(bool tile, uint64_t tiled_capacity,
uint64_t linear_capacity, const ImageInfo& info,
uint32_t levels, uint64_t source_base,
uint64_t target_base) {
levels = levels != 0 ? levels : info.resources.levels;
EXIT_IF(levels == 0 || levels > MaxMipLevels || levels > info.resources.levels ||
tiled_capacity == 0 || linear_capacity == 0);
EXIT_NOT_IMPLEMENTED(tiled_capacity > UINT32_MAX || linear_capacity > UINT32_MAX);
const auto& block = info.tiling.block;
const auto transform = info.GetColorTransform();
EXIT_NOT_IMPLEMENTED(
!info.IsTiled() || info.samples != 1 || Prospero::IsFmaskTextureFormat(info.guest_format) ||
block.family >= TileBlockFamily::Count || !std::has_single_bit(block.bytes_per_element) ||
block.bytes_per_element > 16 || block.block_width == 0 || block.block_height == 0 ||
block.block_depth == 0 || block.block_size == 0 || info.tiling.texel_width == 0 ||
info.tiling.texel_height == 0 || info.tiled_slice_stride == 0);
EXIT_IF((transform == ColorTransform::SwapBgra16 && block.bytes_per_element != 8u) ||
(transform == ColorTransform::Reverse10_11_11 && block.bytes_per_element != 4u));
const auto checked_multiply = [](uint64_t left, uint64_t right, uint64_t& result) {
return (left == 0 || right <= UINT64_MAX / left) && (result = left * right, true);
};
@@ -106,112 +123,96 @@ void TileManager::Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_ca
return size != 0 && offset <= capacity && size <= capacity - offset;
};
dispatches.clear();
dispatches.reserve(infos.size());
for (const auto& info: infos) {
EXIT_IF((transform == ColorTransform::SwapBgra16 && info.bytes_per_element != 8u) ||
(transform == ColorTransform::Reverse10_11_11 && info.bytes_per_element != 4u));
TileBlockLayout block {};
const uint32_t tiled_width = info.tiled_width != 0 ? info.tiled_width : info.pitch;
const uint32_t tiled_height = info.tiled_height != 0 ? info.tiled_height : info.height;
const uint64_t groups_x = (static_cast<uint64_t>(info.width) + 7u) / 8u;
const uint64_t groups_y = (static_cast<uint64_t>(info.height) + 7u) / 8u;
EXIT_NOT_IMPLEMENTED(
!TileGetBlockLayout(info.family, info.bytes_per_element, block) || info.width == 0 ||
info.height == 0 || info.depth == 0 || info.pitch < info.width ||
groups_x > limits.maxComputeWorkGroupCount[0] ||
groups_y > limits.maxComputeWorkGroupCount[1] ||
info.depth > limits.maxComputeWorkGroupCount[2] ||
(!info.tail && (tiled_width < info.width || tiled_height < info.height)) ||
!valid_range(info.linear_offset, info.linear_size, linear_capacity) ||
!valid_range(info.tiled_offset, info.tiled_size, tiled_capacity) ||
(block.block_depth == 1 && info.depth != 1));
// ImageInfo owns the layout. Upload only the header and active mip records.
TilingParams params;
params.color_transform = static_cast<uint32_t>(transform);
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = info.mip_layout[level];
const auto extent = info.MipExtent(level);
const auto width = static_cast<uint32_t>(
(uint64_t {extent.width} + info.tiling.texel_width - 1u) / info.tiling.texel_width);
const auto height = static_cast<uint32_t>(
(uint64_t {extent.height} + info.tiling.texel_height - 1u) / info.tiling.texel_height);
const auto depth = extent.depth;
const bool tail = level >= info.first_tail_level;
EXIT_NOT_IMPLEMENTED(width == 0 || height == 0 || depth == 0 || mip.linear_pitch < width ||
mip.linear_size == 0 || mip.size == 0 ||
(!tail && (mip.pitch < width || mip.height < height)) ||
(!info.IsVolume() && mip.size % depth != 0));
uint64_t pitch_bytes = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(info.pitch, info.bytes_per_element, pitch_bytes) ||
pitch_bytes > UINT32_MAX);
uint64_t slice_bytes = info.linear_slice_stride;
EXIT_NOT_IMPLEMENTED(
!checked_multiply(mip.linear_pitch, block.bytes_per_element, pitch_bytes) ||
pitch_bytes > UINT32_MAX);
const uint64_t slice_bytes =
info.linear_slice_stride != 0 ? info.linear_slice_stride : mip.linear_size;
uint64_t minimum_slice = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(pitch_bytes, info.height, minimum_slice));
if (slice_bytes == 0) {
slice_bytes = minimum_slice;
}
uint64_t linear_used = 0;
uint64_t bytes = 0;
EXIT_NOT_IMPLEMENTED((info.depth > 1 && slice_bytes < minimum_slice) ||
!checked_multiply(info.depth - 1u, slice_bytes, bytes) ||
uint64_t bytes = 0;
uint64_t linear_span = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(pitch_bytes, height, minimum_slice) ||
(depth > 1 && slice_bytes < minimum_slice) ||
!checked_multiply(height - 1u, pitch_bytes, linear_used) ||
!checked_multiply(width, block.bytes_per_element, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
!checked_multiply(info.height - 1u, pitch_bytes, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
!checked_multiply(info.width, info.bytes_per_element, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
linear_used > info.linear_size || slice_bytes > UINT32_MAX);
linear_used > mip.linear_size || slice_bytes > UINT32_MAX ||
!checked_multiply(depth - 1u, slice_bytes, linear_span) ||
!checked_add(linear_span, mip.linear_size, linear_span) ||
!valid_range(mip.linear_offset, linear_span, linear_capacity));
const uint64_t columns =
(static_cast<uint64_t>(tiled_width) + block.block_width - 1u) / block.block_width;
const uint64_t rows =
(static_cast<uint64_t>(tiled_height) + block.block_height - 1u) / block.block_height;
uint64_t blocks_per_slice = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(columns, rows, blocks_per_slice) ||
columns > UINT32_MAX || blocks_per_slice > UINT32_MAX);
if (info.tail) {
(uint64_t {mip.pitch} + block.block_width - 1u) / block.block_width;
if (tail) {
EXIT_NOT_IMPLEMENTED(
info.family == TileBlockFamily::Standard256B || info.depth > block.block_depth ||
info.tail_x >= block.block_width || info.width > block.block_width - info.tail_x ||
info.tail_y >= block.block_height ||
info.height > block.block_height - info.tail_y ||
info.tiled_size < block.block_size);
} else {
const uint64_t slices =
(static_cast<uint64_t>(info.depth) + block.block_depth - 1u) / block.block_depth;
uint64_t tiled_used = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(blocks_per_slice, slices, tiled_used) ||
!checked_multiply(tiled_used, block.block_size, tiled_used) ||
tiled_used > info.tiled_size);
block.family == TileBlockFamily::Standard256B || mip.tail_x >= block.block_width ||
width > block.block_width - mip.tail_x || mip.tail_y >= block.block_height ||
height > block.block_height - mip.tail_y);
}
const uint64_t tiled_plane = info.IsVolume() ? mip.size : mip.size / depth;
const uint64_t tiled_slice = info.tiled_slice_stride;
const uint64_t slices = (uint64_t {depth} + block.block_depth - 1u) / block.block_depth;
uint64_t tiled_span = 0;
EXIT_NOT_IMPLEMENTED(columns > UINT32_MAX || tiled_slice > UINT32_MAX ||
(slices > 1 && tiled_slice < tiled_plane) ||
!checked_multiply(slices - 1u, tiled_slice, tiled_span) ||
!checked_add(tiled_span, tiled_plane, tiled_span) ||
!valid_range(mip.offset, tiled_span, tiled_capacity));
const uint32_t alignment = std::min(info.bytes_per_element, 4u);
EXIT_NOT_IMPLEMENTED(((info.linear_offset | info.tiled_offset | pitch_bytes | slice_bytes) &
(alignment - 1u)) != 0);
const uint64_t src = source_base + (tile ? info.linear_offset : info.tiled_offset);
const uint64_t dst = target_base + (tile ? info.tiled_offset : info.linear_offset);
EXIT_NOT_IMPLEMENTED(src > UINT32_MAX || dst > UINT32_MAX);
const uint32_t family_index = static_cast<uint32_t>(info.family);
const uint32_t element_index = std::countr_zero(info.bytes_per_element);
EXIT_NOT_IMPLEMENTED(family_index >= FamilyCount || element_index >= BytesPerElementCount);
Dispatch dispatch {};
dispatch.pipeline_slot =
((tile ? FamilyCount : 0u) + family_index) * BytesPerElementCount + element_index;
dispatch.push.src_base = static_cast<uint32_t>(src);
dispatch.push.dst_base = static_cast<uint32_t>(dst);
dispatch.push.width = info.width;
dispatch.push.height = info.height;
dispatch.push.depth = info.depth;
dispatch.push.surface_z = info.surface_z;
dispatch.push.pitch_bytes = static_cast<uint32_t>(pitch_bytes);
dispatch.push.slice_bytes = static_cast<uint32_t>(slice_bytes);
dispatch.push.blocks_per_row = static_cast<uint32_t>(columns);
dispatch.push.blocks_per_slice = static_cast<uint32_t>(blocks_per_slice);
dispatch.push.tail_x = info.tail_x;
dispatch.push.tail_y = info.tail_y;
dispatch.push.tail = info.tail;
dispatch.push.color_transform = static_cast<uint32_t>(transform);
dispatches.push_back(dispatch);
const uint32_t alignment = std::min(block.bytes_per_element, 4u);
EXIT_NOT_IMPLEMENTED(
((mip.linear_offset | mip.offset | pitch_bytes | slice_bytes | tiled_slice) &
(alignment - 1u)) != 0);
uint64_t src = 0, dst = 0, texels = 0;
EXIT_NOT_IMPLEMENTED(
!checked_add(source_base, tile ? mip.linear_offset : mip.offset, src) ||
!checked_add(target_base, tile ? mip.offset : mip.linear_offset, dst) ||
src > UINT32_MAX || dst > UINT32_MAX || !checked_multiply(width, height, texels) ||
!checked_multiply(texels, depth, texels) || texels > UINT32_MAX - params.num_texels);
params.mips[level] = {static_cast<uint32_t>(src),
static_cast<uint32_t>(dst),
width,
height,
static_cast<uint32_t>(pitch_bytes),
static_cast<uint32_t>(slice_bytes),
static_cast<uint32_t>(tiled_slice),
static_cast<uint32_t>(columns),
mip.surface_z,
tail ? mip.tail_x : 0u,
tail ? mip.tail_y : 0u,
static_cast<uint32_t>(texels)};
params.num_texels += static_cast<uint32_t>(texels);
}
const uint64_t uniform_alignment =
std::max<uint64_t>(limits.minUniformBufferOffsetAlignment, 1);
const uint64_t stride = Common::AlignUp<uint64_t>(sizeof(Push), uniform_alignment);
EXIT_NOT_IMPLEMENTED(dispatches.size() > UINT64_MAX / stride);
const uint64_t bytes = dispatches.size() * stride;
auto [mapped, offset] = m_stream_buffer.Map(bytes, uniform_alignment);
EXIT_IF(mapped == nullptr);
for (size_t index = 0; index < dispatches.size(); index++) {
std::memcpy(mapped + index * stride, &dispatches[index].push, sizeof(Push));
dispatches[index].params_offset = offset + index * stride;
}
m_stream_buffer.Commit();
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const auto params_size =
static_cast<uint16_t>(offsetof(TilingParams, mips) + levels * sizeof(Mip));
const auto offset = m_stream_buffer.Copy(
&params, params_size,
std::max<uint64_t>(limits.minUniformBufferOffsetAlignment, alignof(TilingParams)));
const auto slot =
((tile ? FamilyCount : 0u) + static_cast<uint32_t>(block.family)) * BytesPerElementCount +
std::countr_zero(block.bytes_per_element);
return {offset, params.num_texels, params_size, static_cast<uint16_t>(slot)};
}
vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
@@ -257,10 +258,9 @@ vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
return m_pipelines[slot];
}
void TileManager::Record(vk::Buffer source, uint64_t source_offset,
uint64_t source_capacity, vk::Buffer target, uint64_t target_offset,
uint64_t target_capacity, std::span<Dispatch> dispatches,
bool clear_target) {
void TileManager::Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
const Tiling& tiling, bool clear_target) {
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
@@ -295,9 +295,8 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
barriers[2].srcAccessMask = vk::AccessFlagBits::eHostWrite;
barriers[2].dstAccessMask = vk::AccessFlagBits::eUniformRead;
barriers[2].buffer = m_stream_buffer.Handle();
barriers[2].offset = dispatches.front().params_offset;
barriers[2].size =
dispatches.back().params_offset - dispatches.front().params_offset + sizeof(Push);
barriers[2].offset = tiling.params_offset;
barriers[2].size = tiling.params_size;
command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer, {}, 0,
@@ -314,24 +313,26 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
const vk::DescriptorBufferInfo source_info {source, source_descriptor_offset, source_range};
const vk::DescriptorBufferInfo target_info {target, target_descriptor_offset, target_range};
for (auto& dispatch: dispatches) {
const vk::DescriptorBufferInfo params_info {m_stream_buffer.Handle(),
dispatch.params_offset, sizeof(Push)};
const vk::DescriptorBufferInfo infos[] {source_info, target_info, params_info};
std::array<vk::WriteDescriptorSet, 3> writes {};
for (uint32_t index = 0; index < writes.size(); index++) {
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType = index == 2 ? vk::DescriptorType::eUniformBuffer
: vk::DescriptorType::eStorageBuffer;
writes[index].pBufferInfo = &infos[index];
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
command.bindPipeline(vk::PipelineBindPoint::eCompute, GetPipeline(dispatch.pipeline_slot));
command.dispatch((dispatch.push.width + 7u) / 8u, (dispatch.push.height + 7u) / 8u,
dispatch.push.depth);
const vk::DescriptorBufferInfo params_info {m_stream_buffer.Handle(), tiling.params_offset,
tiling.params_size};
const vk::DescriptorBufferInfo infos[] {source_info, target_info, params_info};
std::array<vk::WriteDescriptorSet, 3> writes {};
for (uint32_t index = 0; index < writes.size(); index++) {
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType =
index == 2 ? vk::DescriptorType::eUniformBuffer : vk::DescriptorType::eStorageBuffer;
writes[index].pBufferInfo = &infos[index];
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
command.bindPipeline(vk::PipelineBindPoint::eCompute, GetPipeline(tiling.pipeline_slot));
const uint64_t groups = (uint64_t {tiling.num_texels} + 63u) / 64u;
const uint32_t groups_y = static_cast<uint32_t>(
(groups + limits.maxComputeWorkGroupCount[0] - 1u) / limits.maxComputeWorkGroupCount[0]);
const uint32_t groups_x = static_cast<uint32_t>((groups + groups_y - 1u) / groups_y);
EXIT_NOT_IMPLEMENTED(groups_y > limits.maxComputeWorkGroupCount[1]);
command.dispatch(groups_x, groups_y, 1);
barriers[1].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eMemoryRead;
@@ -341,51 +342,53 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
}
TileManager::Result TileManager::Detile(vk::Buffer tiled, uint64_t tiled_offset,
uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos,
ColorTransform transform) {
const ImageInfo& info) {
if (!info.IsTiled()) {
const Result source {tiled, tiled_offset, info.data.size};
return info.GetColorTransform() == ColorTransform::None
? source
: TransformColor(source, info.GetColorTransform(), true);
}
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t source_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
Prepare(false, tiled_capacity, linear_capacity, infos, source_base, 0, dispatches, transform);
const uint64_t source_base = tiled_offset & (descriptor_alignment - 1);
const auto linear_capacity = info.LinearSize();
const auto tiling = Prepare(false, info.data.size, linear_capacity, info, 0, source_base, 0);
auto scratch = GetScratchBuffer(linear_capacity, tiled);
Record(tiled, tiled_offset, tiled_capacity, scratch.buffer, 0, scratch.size, dispatches,
true);
Record(tiled, tiled_offset, info.data.size, scratch.buffer, 0, scratch.size, tiling, true);
return {scratch.buffer, 0, linear_capacity};
}
void TileManager::Tile(vk::Buffer linear, uint64_t linear_offset, uint64_t linear_capacity,
vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
std::span<const GpuTileInfo> infos) {
const ImageInfo& info, uint32_t levels) {
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t source_base = linear_offset & (descriptor_alignment - 1);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
Prepare(true, tiled_capacity, linear_capacity, infos, source_base, target_base, dispatches);
Record(linear, linear_offset, linear_capacity, tiled, tiled_offset, tiled_capacity,
dispatches, false);
const uint64_t source_base = linear_offset & (descriptor_alignment - 1);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
const auto tiling =
Prepare(true, tiled_capacity, linear_capacity, info, levels, source_base, target_base);
Record(linear, linear_offset, linear_capacity, tiled, tiled_offset, tiled_capacity, tiling,
false);
}
void TileManager::TileImage(Image& image, std::span<const vk::BufferImageCopy> regions,
vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
uint64_t linear_capacity, std::span<const GpuTileInfo> infos,
ColorTransform transform) {
uint32_t levels) {
EXIT_IF(regions.empty());
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
const auto linear_capacity = image.info.LinearSize(levels);
// Reserve stream parameters before recording the image download.
Prepare(true, tiled_capacity, linear_capacity, infos, 0, target_base, dispatches, transform);
const auto tiling =
Prepare(true, tiled_capacity, linear_capacity, image.info, levels, 0, target_base);
auto linear = GetScratchBuffer(linear_capacity, tiled);
image.Download(regions, linear.buffer, 0, linear.size);
Record(linear.buffer, 0, linear_capacity, tiled, tiled_offset, tiled_capacity,
dispatches, false);
Record(linear.buffer, 0, linear_capacity, tiled, tiled_offset, tiled_capacity, tiling, false);
}
TileManager::Result TileManager::GetScratchBuffer(uint64_t size, vk::Buffer input) {
@@ -563,13 +566,13 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
Push push {};
Conversion push {};
push.src_base = source_binding.base;
push.dst_base = target_binding.base;
push.width = layout.width;
push.height = rows;
push.pitch_bytes = static_cast<uint32_t>(layout.source_row_stride);
push.slice_bytes = static_cast<uint32_t>(layout.target_row_stride);
push.source_pitch = static_cast<uint32_t>(layout.source_row_stride);
push.target_pitch = static_cast<uint32_t>(layout.target_row_stride);
command.pushConstants(m_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0,
sizeof(push), &push);
command.dispatch(static_cast<uint32_t>(groups_x), rows, 1);
@@ -644,7 +647,7 @@ void TileManager::TransformColor(Result input, Result output, ColorTransform tra
command.bindPipeline(vk::PipelineBindPoint::eCompute, m_color_transform);
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
Push push {};
Conversion push {};
push.src_base = input_binding.base;
push.dst_base = output_binding.base;
push.width = pixels;
+35 -44
View File
@@ -9,7 +9,6 @@
#include <array>
#include <memory>
#include <span>
#include <vector>
namespace Libs::Graphics {
@@ -20,26 +19,6 @@ class StreamBuffer;
struct GraphicContext;
struct TileManagerTestAccess;
struct GpuTileInfo {
TileBlockFamily family = TileBlockFamily::Count;
uint32_t bytes_per_element = 0;
uint64_t linear_offset = 0;
uint64_t linear_size = 0;
uint64_t tiled_offset = 0;
uint64_t tiled_size = 0;
uint64_t linear_slice_stride = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t depth = 1;
uint32_t pitch = 0;
uint32_t tail_x = 0;
uint32_t tail_y = 0;
bool tail = false;
uint32_t tiled_width = 0;
uint32_t tiled_height = 0;
uint32_t surface_z = 0;
};
class TileManager final {
public:
enum class D16Direction { Promote, Demote };
@@ -64,15 +43,12 @@ public:
KYTY_CLASS_NO_COPY(TileManager);
// Consume scratch results before the next acquisition, or pass their buffer as input.
[[nodiscard]] Result Detile(vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
uint64_t linear_capacity, std::span<const GpuTileInfo> infos,
ColorTransform transform = ColorTransform::None);
[[nodiscard]] Result Detile(vk::Buffer tiled, uint64_t tiled_offset, const ImageInfo& info);
void Tile(vk::Buffer linear, uint64_t linear_offset, uint64_t linear_capacity, vk::Buffer tiled,
uint64_t tiled_offset, uint64_t tiled_capacity, std::span<const GpuTileInfo> infos);
uint64_t tiled_offset, uint64_t tiled_capacity, const ImageInfo& info,
uint32_t levels = 0);
void TileImage(Image& image, std::span<const vk::BufferImageCopy> regions, vk::Buffer tiled,
uint64_t tiled_offset, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos,
ColorTransform transform = ColorTransform::None);
uint64_t tiled_offset, uint64_t tiled_capacity, uint32_t levels = 0);
[[nodiscard]] Result GetScratchBuffer(uint64_t size, vk::Buffer input = nullptr);
void ConvertD16(Result source, Result target, D16Direction direction, bool d32,
const D16Layout& layout);
@@ -86,29 +62,44 @@ private:
static constexpr uint32_t BytesPerElementCount = 5;
static constexpr uint32_t DirectionCount = 2;
static constexpr uint32_t PipelineCount = FamilyCount * BytesPerElementCount * DirectionCount;
static constexpr uint32_t MaxMipLevels = 16;
struct Push {
struct alignas(16) Mip {
uint32_t src_base;
uint32_t dst_base;
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t surface_z;
uint32_t pitch_bytes;
uint32_t slice_bytes;
uint32_t tiled_slice_bytes;
uint32_t blocks_per_row;
uint32_t blocks_per_slice;
uint32_t surface_z;
uint32_t tail_x;
uint32_t tail_y;
uint32_t tail;
uint32_t num_texels;
};
struct TilingParams {
uint32_t num_texels = 0;
uint32_t color_transform = 0;
std::array<uint32_t, 2> padding {};
std::array<Mip, MaxMipLevels> mips;
};
struct Tiling {
uint64_t params_offset;
uint32_t num_texels;
uint16_t params_size;
uint16_t pipeline_slot;
};
struct Conversion {
uint32_t src_base;
uint32_t dst_base;
uint32_t width;
uint32_t height;
uint32_t source_pitch;
uint32_t target_pitch;
uint32_t color_transform;
uint32_t to_host;
};
struct Dispatch {
Push push {};
uint32_t pipeline_slot = 0;
uint64_t params_offset = 0;
};
struct StorageBinding {
vk::DescriptorBufferInfo info;
uint32_t base = 0;
@@ -119,12 +110,12 @@ private:
uint64_t active, uint32_t remaining,
uint64_t alignment, uint64_t max_range,
uint32_t max_groups) noexcept;
void Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos, uint64_t source_base, uint64_t target_base,
std::vector<Dispatch>& dispatches, ColorTransform transform = ColorTransform::None);
void Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
std::span<Dispatch> dispatches, bool clear_target);
[[nodiscard]] Tiling Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
const ImageInfo& info, uint32_t levels, uint64_t source_base,
uint64_t target_base);
void Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
const Tiling& tiling, bool clear_target);
[[nodiscard]] vk::Pipeline GetPipeline(uint32_t slot);
GraphicContext& m_graphics;
@@ -378,51 +378,6 @@ static TextureCache::ImageDesc NullTextureDesc(const ShaderRecompiler::IR::Image
return desc;
}
static void PopulateTextureMipLayout(ImageInfo& info) {
if (info.IsVolume() && info.tile_mode != Prospero::TileMode::kLinear) {
TileSurfaceLayout surface {};
const TileSurfaceDescription description {
info.guest_format, info.tile_mode, TileSurfaceDimension::Dim3D, info.extent.width,
info.extent.height, info.extent.depth, info.resources.levels, 1};
if (!TileGetTiledTextureLayout(description, surface)) {
EXIT("unsupported normalized volume texture layout\n");
}
for (uint32_t level = 0; level < info.resources.levels; level++) {
const auto& mip = surface.mips[level];
info.mip_layout[level] = {
mip.offset,
mip.size,
mip.padded_width,
mip.padded_height,
};
}
return;
}
TileSizeOffset levels[16] {};
TilePaddedSize padded[16] {};
TileGetTextureSize(info.guest_format, info.extent.width, info.extent.height,
info.resources.levels, info.tile_mode, nullptr, levels, padded);
const auto texel_shift = info.IsBlock() ? 2u : 0u;
for (uint32_t level = 0; level < info.resources.levels; level++) {
const auto offset =
levels[level].src_size != 0 ? levels[level].src_offset : levels[level].offset;
auto size = static_cast<uint64_t>(levels[level].src_size != 0 ? levels[level].src_size
: levels[level].size);
if (info.IsVolume()) {
size *= std::max(info.extent.depth >> level, 1u);
} else {
size *= info.resources.layers;
}
info.mip_layout[level] = {
offset,
size,
padded[level].width >> texel_shift,
padded[level].height >> texel_shift,
};
}
}
static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, vk::Format format,
const SurfaceFormatInfo& surface_format, bool storage,
@@ -490,45 +445,54 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
return view;
}
static bool ResolveTextureMipView(const TileSurfaceDescription& description, bool metadata,
uint32_t view_levels, uint32_t& levels, uint32_t& base_level) {
TileSurfaceLayout physical {};
TileSurfaceLayout view {};
auto view_description = description;
view_description.levels = levels;
if (!TileGetTiledTextureLayout(description, physical) ||
!TileGetTiledTextureLayout(view_description, view)) {
static bool ResolveTextureMipView(ImageInfo& info, uint32_t physical_levels, bool metadata,
uint32_t view_levels, uint32_t& base_level) {
if (!info.IsTiled()) {
return false;
}
if (physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips))) {
return true;
ImageInfo physical = info;
physical.resources.levels = physical_levels;
physical.UpdateSize();
info.UpdateSize();
const auto same_mip = [&](uint32_t target, uint32_t source) {
const auto& left = physical.mip_layout[target];
const auto& right = info.mip_layout[source];
return left.offset == right.offset && left.size == right.size &&
left.pitch == right.pitch && left.height == right.height &&
left.tail_x == right.tail_x && left.tail_y == right.tail_y &&
left.surface_z == right.surface_z &&
physical.MipExtent(target) == info.MipExtent(source);
};
if (physical.first_tail_level == info.first_tail_level &&
physical.tiled_slice_stride == info.tiled_slice_stride &&
physical.data.size == info.data.size) {
bool matches = true;
for (uint32_t level = 0; level < physical_levels; ++level) {
matches &= same_mip(level, level);
}
if (matches) {
return true;
}
}
if (metadata || ((description.layers > 1 || description.depth > 1) &&
physical.block_slice_size != view.block_slice_size)) {
if (metadata ||
(info.TransferLayers() > 1 && physical.tiled_slice_stride != info.tiled_slice_stride)) {
return false;
}
// T# addresses the last mip. A view can select the same stored subresources
// with different mip indices; inaccessible mips need no host representation.
for (uint32_t base = 0; base + view_levels <= description.levels; ++base) {
for (uint32_t base = 0; base + view_levels <= physical_levels; ++base) {
bool matches = true;
for (uint32_t i = 0; i < view_levels; ++i) {
const auto source = base_level + i;
const auto target = base + i;
if (physical.mips[target] != view.mips[source] ||
(target >= physical.first_tail_level) != (source >= view.first_tail_level) ||
std::max(description.width >> target, 1u) != std::max(description.width >> source, 1u) ||
std::max(description.height >> target, 1u) != std::max(description.height >> source, 1u) ||
std::max(description.depth >> target, 1u) != std::max(description.depth >> source, 1u)) {
if (!same_mip(target, source) ||
(target >= physical.first_tail_level) != (source >= info.first_tail_level)) {
matches = false;
break;
}
}
if (matches) {
levels = description.levels;
info = std::move(physical);
base_level = base;
return true;
}
@@ -613,40 +577,6 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
type == Prospero::ImageType::kColor2DMsaaArray;
const auto image_layers = layered ? depth : 1u;
auto view_base = static_cast<uint32_t>(base_level);
if (levels > physical_levels) {
const TileSurfaceDescription physical {
format, tile, volume ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width, height, volume ? depth : 1u, physical_levels, image_layers};
if (!ResolveTextureMipView(physical, !resource.r128 && descriptor.MetaCompress(),
view_levels, levels, view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
static_cast<uint32_t>(tile));
}
}
uint32_t pitch = 0;
TileSizeAlign size {};
if (multisampled) {
const auto bytes = Prospero::NumBytesPerElement(format);
pitch = depth_tile ? TileGetDepthPitch(width, bytes, last_level)
: TileGetRenderTargetPitch(width, bytes, last_level);
if (pitch == 0 || !TileGetRenderTargetSize(width, height, pitch, bytes, size, last_level) ||
size.size > UINT32_MAX / image_layers) {
EXIT("unsupported multisample texture layout\n");
}
size.size *= image_layers;
} else {
pitch = TileGetTexturePitch(format, width, tile);
TileGetTextureTotalSize(format, width, height, volume ? depth : image_layers,
physical_levels, tile, volume, size);
}
EXIT_NOT_IMPLEMENTED(size.size == 0 || size.align == 0 ||
(address & (static_cast<uint64_t>(size.align) - 1u)) != 0);
if (storage) {
ValidateStorageTexture(resource, descriptor, size.size);
}
auto pixel_format = surface_format.vk_format;
if (resource.depth_compare) {
if (const auto* depth_format = FindGuestDepthFormatPolicy(format)) {
@@ -663,17 +593,35 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
: pixel_format;
const auto block_bytes = Prospero::BlockCompressedBytesPerBlock(format);
TextureCache::ImageDesc desc {};
desc.info.data = {address, size.size};
desc.info.data = {address, 0};
desc.info.pixel_format = pixel_format;
desc.info.guest_format = format;
desc.info.type = TextureBaseType(type);
desc.info.extent = {width, height, volume ? depth : 1u};
desc.info.resources = {levels, image_layers};
desc.info.pitch = pitch;
desc.info.bytes_per_block =
block_bytes != 0 ? block_bytes : Prospero::NumBytesPerElement(format);
desc.info.samples = samples;
desc.info.tile_mode = tile;
if (levels > physical_levels) {
if (!ResolveTextureMipView(desc.info, physical_levels,
!resource.r128 && descriptor.MetaCompress(), view_levels,
view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
static_cast<uint32_t>(tile));
}
} else {
desc.info.UpdateSize();
}
const auto alignment =
tile == Prospero::TileMode::kLinear ? 256u : desc.info.tiling.block.block_size;
EXIT_NOT_IMPLEMENTED(desc.info.data.size == 0 || alignment == 0 ||
(address & (static_cast<uint64_t>(alignment) - 1u)) != 0);
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
!desc.info.IsDepth()) {
TileSizeAlign metadata_size {};
@@ -684,11 +632,6 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
desc.info.metadata.range = {descriptor.MetaAddr() << 8u, metadata_size.size};
desc.info.metadata.dcc_alpha_msb = descriptor.DccAlphaPos();
}
if (samples > 1) {
desc.info.mip_layout[0] = {0, size.size, pitch, height};
} else {
PopulateTextureMipLayout(desc.info);
}
desc.view_info = TextureViewInfo(resource, descriptor, view_format, surface_format, storage,
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
@@ -704,7 +647,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
if (storage) {
EXIT("depth target cannot be bound as a storage image\n");
}
ValidateSampledDepthBinding(resource, descriptor, *image, pixel_format, size.size);
ValidateSampledDepthBinding(resource, descriptor, *image, pixel_format,
desc.info.data.size);
} else if (storage) {
ValidateStorageColorView(image->info.pixel_format, view_format, descriptor.DstSelXYZW());
} else {
@@ -217,17 +217,53 @@ struct PipelineCache::ProgramCache {
};
struct Permutation {
std::vector<uint32_t> function_code;
ShaderRecompiler::IR::ResourceSpecialization specialization;
ShaderRecompiler::IR::CompiledShaderInfo program;
ShaderProgram handle;
};
struct SourceEntry {
explicit SourceEntry(ShaderRecompiler::IR::ResourcePlan plan)
: resource_plan(std::move(plan)) {
SourceEntry(ShaderRecompiler::IR::ResourcePlan plan,
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source)
: call_source(std::move(call_source)), resource_plan(std::move(plan)) {
permutations.reserve(8);
}
std::span<const uint32_t> FunctionCode() const {
return call_source
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
: std::span<const uint32_t>{};
}
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
const auto revision = call_source->revision;
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
resource_plan.source_reads = call_source->reads;
return revision != call_source->revision;
}
template <bool HasCalls>
Permutation* FindPermutation(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
resource_plan, runtime, resources, specialization));
for (auto& candidate: permutations) {
const auto& layout = candidate.program.bindings;
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) ||
candidate.specialization != specialization) {
continue;
}
if constexpr (HasCalls) {
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
}
return &candidate;
}
return nullptr;
}
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
ShaderRecompiler::IR::ResourcePlan resource_plan;
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
@@ -256,7 +292,8 @@ struct PipelineCache::ProgramCache {
const ShaderRecompiler::CompileOptions& options,
ShaderRecompiler::TranslateResult translated,
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword) {
uint32_t push_data_start_dword,
std::span<const uint32_t> function_code) {
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
@@ -273,6 +310,7 @@ struct PipelineCache::ProgramCache {
static_cast<uint64_t>(result.spirv.size()), options.wave_size);
}
return {
.function_code = {function_code.begin(), function_code.end()},
.specialization = std::move(specialization),
.program = std::move(result.program).TakeCompiledInfo(),
.handle = {.id = ++next_shader_id, .module = module},
@@ -307,21 +345,18 @@ struct PipelineCache::ProgramCache {
if constexpr (std::is_same_v<InputInfo, ShaderComputeInputInfo>) {
runtime.workgroup_counts = input_info.workgroup_counts;
}
bool source_changed = false;
if (entry != programs.end()) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
entry->second.resource_plan, runtime, entry->second.resources,
entry->second.specialization));
if (const auto permutation = std::ranges::find_if(
entry->second.permutations, [&](const Permutation& candidate) {
const auto& layout = candidate.program.bindings;
return layout.push_data_start_dword ==
ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) &&
candidate.specialization == entry->second.specialization;
});
permutation != entry->second.permutations.end()) {
input_info.stage = {.program = &permutation->program,
.resources = &entry->second.resources};
auto& cached = entry->second;
Permutation* permutation = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
if (!source_changed) permutation = cached.FindPermutation<true>(runtime, push_data_cursor);
} else {
permutation = cached.FindPermutation<false>(runtime, push_data_cursor);
}
if (permutation != nullptr) {
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
permutation->program.bindings.AdvancePushData(push_data_cursor);
return permutation->handle;
}
@@ -368,16 +403,39 @@ struct PipelineCache::ProgramCache {
options.wave_size = input_info.wave_size;
}
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
auto translated = ShaderRecompiler::TranslateProgram(params.code, options);
ShaderRecompiler::ShaderSource source;
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (current->call) {
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
} else {
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
}
} else {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
}
if (entry == programs.end()) {
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
entry = programs.try_emplace(lookup_key,
ShaderRecompiler::IR::ExtractResourcePlan(translated.program)).first;
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
entry->second.resource_plan, runtime, entry->second.resources,
entry->second.specialization));
} else if (source_changed) {
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
if (const auto* permutation = entry->second.FindPermutation<true>(runtime, push_data_cursor)) {
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
permutation->program.bindings.AdvancePushData(push_data_cursor);
return permutation->handle;
}
}
entry->second.permutations.push_back(CompilePermutation(
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor));
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor,
entry->second.FunctionCode()));
const auto& permutation = entry->second.permutations.back();
input_info.stage = {.program = &permutation.program, .resources = &entry->second.resources};
permutation.program.bindings.AdvancePushData(push_data_cursor);
+2 -2
View File
@@ -156,9 +156,9 @@ public:
KYTY_CLASS_NO_COPY(RenderExecutor);
void DispatchDirect(uint64_t submit_id, CommandBuffer& buffer, uint32_t thread_group_x,
uint32_t thread_group_y, uint32_t thread_group_z, uint32_t mode);
uint32_t thread_group_y, uint32_t thread_group_z, uint32_t mode, bool async_compute);
void DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer, uint64_t args_addr,
uint32_t mode);
uint32_t mode, bool async_compute);
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
void FindBuffers(std::span<PreparedBindings* const> stages);
@@ -226,7 +226,7 @@ static void BindSharedMemory(RenderContext& context, ShaderComputeInputInfo& inp
void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
uint32_t thread_group_x, uint32_t thread_group_y,
uint32_t thread_group_z, uint32_t mode) {
uint32_t thread_group_z, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid());
m_context.GetCommandScheduler().PopPendingOperations();
auto& ctx = buffer.GetRegisters();
@@ -281,6 +281,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& sh_regs = ctx.GetShaderRegisters();
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
const bool use_thread_dimensions = (mode & DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS) != 0;
input_info.dispatch_thread_dimensions = use_thread_dimensions;
input_info.workgroup_counts[0] = thread_group_x;
@@ -437,7 +438,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
uint64_t args_addr, uint32_t mode) {
uint64_t args_addr, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid() || args_addr == 0 || (args_addr & 3u) != 0 ||
(mode & Pm4::COMPUTE_DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS) != 0);
m_context.GetCommandScheduler().PopPendingOperations();
@@ -450,6 +451,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
return;
}
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
buffer.EndRendering();
+28 -30
View File
@@ -312,9 +312,9 @@ static void LogDrawInputState(const CommandBuffer& buffer, const RenderColorInfo
}
}
static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuffer vk_buffer,
const ShaderVertexInputInfo& vs_input_info,
const RenderDepthInfo& depth, const RenderState& rendering) {
static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState& dynamic_state,
const ShaderVertexInputInfo& vs_input_info,
const RenderDepthInfo& depth, const RenderState& rendering) {
KYTY_PROFILER_FUNCTION();
const auto& ctx = buffer.GetRegisters();
@@ -358,8 +358,8 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
scissor.extent = {0, 0};
}
}
vk_buffer.setViewportWithCount(viewport_count, viewports.data());
vk_buffer.setScissorWithCount(viewport_count, scissors.data());
dynamic_state.SetViewports(std::span {viewports}.first(viewport_count));
dynamic_state.SetScissors(std::span {scissors}.first(viewport_count));
float line_width = ctx.GetLineWidth();
if (line_width != 1.0f) {
@@ -372,17 +372,17 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
}
line_width = 1.0f;
}
vk_buffer.setLineWidth(line_width);
dynamic_state.SetLineWidth(line_width);
const auto& blend = ctx.GetBlendColor();
const std::array blend_constants {blend.red, blend.green, blend.blue, blend.alpha};
vk_buffer.setBlendConstants(blend_constants.data());
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
dynamic_state.SetBlendConstants(blend_constants);
dynamic_state.SetDepthTestEnabled(depth.depth_test_enable);
dynamic_state.SetDepthWriteEnabled(depth.depth_write_enable);
dynamic_state.SetDepthCompareOp(depth.depth_compare_op);
#if !defined(__APPLE__)
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetDepthBoundsTestEnabled(depth.depth_bounds_test_enable);
if (depth.depth_bounds_test_enable) {
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
dynamic_state.SetDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
}
#endif
@@ -391,7 +391,7 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
const bool use_front = mode.poly_offset_front_enable && !mode.cull_front;
const bool use_back = mode.poly_offset_back_enable && !mode.cull_back;
const bool depth_bias_enable = use_front || use_back;
vk_buffer.setDepthBiasEnable(depth_bias_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetDepthBiasEnabled(depth_bias_enable);
if (depth_bias_enable) {
// Vulkan has one bias for both faces. Prefer a visible front face when both are enabled.
const float guest_constant_factor =
@@ -400,19 +400,18 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
guest_constant_factor, poly_offset, depth.desc.view_info.format);
const float slope_factor =
(use_front ? poly_offset.front_scale : poly_offset.back_scale) / 16.0f;
vk_buffer.setDepthBias(constant_factor, poly_offset.clamp, slope_factor);
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
}
vk_buffer.setStencilTestEnable(depth.stencil_test_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
if (depth.stencil_test_enable) {
const auto set_stencil = [&](vk::StencilFaceFlagBits face, const vk::StencilOpState& state) {
vk_buffer.setStencilOp(face, state.failOp, state.passOp, state.depthFailOp, state.compareOp);
vk_buffer.setStencilCompareMask(face, state.compareMask);
vk_buffer.setStencilWriteMask(face, state.writeMask);
vk_buffer.setStencilReference(face, state.reference);
};
set_stencil(vk::StencilFaceFlagBits::eFront, depth.stencil_front);
set_stencil(vk::StencilFaceFlagBits::eBack, depth.stencil_back);
const auto& front = depth.stencil_front;
const auto& back = depth.stencil_back;
dynamic_state.SetStencilOps({front.failOp, front.passOp, front.depthFailOp, front.compareOp},
{back.failOp, back.passOp, back.depthFailOp, back.compareOp});
dynamic_state.SetStencilCompareMasks(front.compareMask, back.compareMask);
dynamic_state.SetStencilWriteMasks(front.writeMask, back.writeMask);
dynamic_state.SetStencilReferences(front.reference, back.reference);
}
#if defined(__APPLE__)
@@ -423,9 +422,7 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
for (uint32_t slot = 0; slot < rendering.num_color_attachments; slot++) {
enable[slot] = rendering.color_attachments[slot].image_view != nullptr;
}
if (rendering.num_color_attachments != 0) {
vk_buffer.setColorWriteEnableEXT(rendering.num_color_attachments, enable);
}
dynamic_state.SetColorWriteEnables(std::span {enable}.first(rendering.num_color_attachments));
#endif
}
@@ -1139,10 +1136,11 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
CommitIndexBuffer(vk_buffer, index_binding);
}
SetGraphicsDynamicParams(buffer, vk_buffer, vertex_stages.back(), state.depth_info, rendering);
if (m_context.GetGraphics().attachment_feedback_loop_enabled) {
vk_buffer.setAttachmentFeedbackLoopEnableEXT(feedback_aspects);
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
rendering);
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
LogDrawPhase(draw.Name(), "BeginRendering");
if (!draw.IsIndexed()) {
@@ -6,20 +6,13 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
@@ -1,66 +1,84 @@
layout(local_size_x = 8, local_size_y = 8) in;
#extension GL_EXT_shader_8bit_storage : require
#extension GL_EXT_shader_16bit_storage : require
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(set = 0, binding = 0, std430) readonly buffer Input8 { uint8_t data[]; } input_buffer8;
layout(set = 0, binding = 1, std430) buffer Output8 { uint8_t data[]; } output_buffer8;
layout(set = 0, binding = 0, std430) readonly buffer Input16 { uint16_t data[]; } input_buffer16;
layout(set = 0, binding = 1, std430) buffer Output16 { uint16_t data[]; } output_buffer16;
layout(set = 0, binding = 2, std140) uniform Push {
struct Mip {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint tiled_slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint surface_z;
uint tail_x;
uint tail_y;
uint tail;
uint num_texels;
};
layout(set = 0, binding = 2, std140) uniform TilingInfo {
uint num_texels;
uint color_transform;
} params;
Mip mips[16];
} info;
#include "gpu_tiler_color.inc"
void copy_element(uint src, uint dst) {
if ((ELEMENT_BYTES == 4u || ELEMENT_BYTES == 8u) && params.color_transform != 0u) {
if ((ELEMENT_BYTES == 4u || ELEMENT_BYTES == 8u) && info.color_transform != 0u) {
uvec2 value = uvec2(input_buffer.data[src >> 2], 0u);
if (ELEMENT_BYTES == 8u) value.y = input_buffer.data[(src >> 2) + 1u];
value = transform_color(value, params.color_transform, TILE == 0u);
value = transform_color(value, info.color_transform, TILE == 0u);
output_buffer.data[dst >> 2] = value.x;
if (ELEMENT_BYTES == 8u) output_buffer.data[(dst >> 2) + 1u] = value.y;
} else if (ELEMENT_BYTES >= 4) {
for (uint i = 0; i < ELEMENT_BYTES; i += 4) {
output_buffer.data[(dst + i) >> 2] = input_buffer.data[(src + i) >> 2];
}
} else if (ELEMENT_BYTES == 2) {
output_buffer16.data[dst >> 1] = input_buffer16.data[src >> 1];
} else {
uint mask = ELEMENT_BYTES == 1 ? 0xffu : 0xffffu;
uint value = (input_buffer.data[src >> 2] >> ((src & 3u) * 8u)) & mask;
uint shift = (dst & 3u) * 8u;
atomicAnd(output_buffer.data[dst >> 2], ~(mask << shift));
atomicOr(output_buffer.data[dst >> 2], value << shift);
output_buffer8.data[dst] = input_buffer8.data[src];
}
}
void main() {
uvec3 p = gl_GlobalInvocationID;
if (p.x >= params.width || p.y >= params.height || p.z >= params.depth) {
uint group = gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x;
// Reject padding groups before multiplying: the active range may approach 4 GiB.
if (group > (info.num_texels - 1u) / 64u) {
return;
}
uint texel = group * 64u + gl_LocalInvocationID.x;
if (texel >= info.num_texels) {
return;
}
uint mip = 0u;
uint mip_texels = info.mips[mip].num_texels;
while (texel >= mip_texels) {
texel -= mip_texels;
mip_texels = info.mips[++mip].num_texels;
}
Mip params = info.mips[mip];
uint row = texel / params.width;
uint z = row / params.height;
uvec3 p = uvec3(texel - row * params.width, row - z * params.height, z);
uvec3 extent = block_extent();
uvec3 swizzle = p;
uvec3 block = uvec3(0);
if (params.tail != 0) {
swizzle.xy += uvec2(params.tail_x, params.tail_y);
} else {
block = p / extent;
}
uvec3 block = p / extent;
uvec3 swizzle = p + uvec3(params.tail_x, params.tail_y, params.surface_z);
uint block_index = block.z * params.blocks_per_slice +
block.y * params.blocks_per_row + block.x;
uint tiled = block_index * BLOCK_BYTES +
block_offset(uvec3(swizzle.xy, swizzle.z + params.surface_z));
uint block_index = block.y * params.blocks_per_row + block.x;
uint tiled = block.z * params.tiled_slice_bytes + block_index * BLOCK_BYTES +
block_offset(swizzle);
uint linear = p.z * params.slice_bytes + p.y * params.pitch_bytes +
p.x * ELEMENT_BYTES;
if (TILE == 0) {
@@ -6,20 +6,15 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
uint decode(uint value) {
@@ -35,8 +30,8 @@ void main() {
if (x >= params.width || y >= params.height) {
return;
}
uint src = params.src_base + y * params.pitch_bytes + x * 4u;
uint dst = params.dst_base + y * params.slice_bytes + x * 2u;
uint src = params.src_base + y * params.source_pitch + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
uint shift = (dst & 2u) * 8u;
uint mask = 0xffffu << shift;
uint word = dst >> 2u;
@@ -6,20 +6,15 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
void main() {
@@ -28,12 +23,12 @@ void main() {
if (x >= params.width || y >= params.height) {
return;
}
uint src = params.src_base + y * params.pitch_bytes + x * 2u;
uint src = params.src_base + y * params.source_pitch + x * 2u;
uint packed = input_buffer.data[src >> 2u];
uint value = (packed >> ((src & 2u) * 8u)) & 0xffffu;
uint encoded = D32 != 0u
? floatBitsToUint(float(value) / 65535.0)
: value * 256u + (value + 128u) / 257u;
uint dst = params.dst_base + y * params.slice_bytes + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 4u;
output_buffer.data[dst >> 2u] = encoded;
}
+7 -13
View File
@@ -566,29 +566,23 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
}
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
const auto pitch =
Graphics::TileGetTexturePitch(pixel_format.guest_format, attribute.width, tile_mode);
Graphics::TileSizeAlign total {};
Graphics::TileGetTextureTotalSize(pixel_format.guest_format, attribute.width, attribute.height,
1, 1, tile_mode, false, total);
if (total.size == 0 || total.align != 65536 ||
(buffer.data_address & (total.align - 1u)) != 0) {
EXIT("invalid video-out surface footprint or alignment\n");
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
Graphics::ImageInfo info {};
info.data = {buffer.data_address, total.size};
info.data = {buffer.data_address, 0};
info.pixel_format = pixel_format.format;
info.guest_format = pixel_format.guest_format;
info.type = Graphics::Prospero::ImageType::kColor2D;
info.extent = {attribute.width, attribute.height, 1};
info.resources = {1, 1};
info.pitch = pitch;
info.bytes_per_block = pixel_format.bytes_per_element;
info.samples = 1;
info.tile_mode = tile_mode;
info.bgra16 = pixel_format.bgra16;
info.mip_layout[0] = {0, total.size, pitch, attribute.height};
info.UpdateSize();
if (info.data.size == 0 || info.tiling.block.block_size != 65536 ||
(buffer.data_address & 65535u) != 0) {
EXIT("invalid video-out surface footprint or alignment\n");
}
if (compression != Graphics::VideoOutCompression::Uncompressed) {
Graphics::TileSizeAlign dcc_size {};
if (!Graphics::TileGetDccSize(attribute.width, attribute.height, 1,
@@ -7,6 +7,7 @@
#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/recompiler/frontend/translate/Translate.h"
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h"
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
@@ -20,6 +21,7 @@
#include <array>
#include <chrono>
#include <fmt/format.h>
#include <functional>
#include <map>
#include <span>
#include <utility>
@@ -466,6 +468,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
Decoder::DecodeInstruction(joined_code, front_words - 1u, result.instructions.back());
Decoder::Program back_program;
Decoder::DecodeProgram(back, back_program);
result.has_swap_pc |= back_program.has_swap_pc;
const auto back_pc = front_words * sizeof(uint32_t);
for (auto& inst: back_program.instructions) {
// A back-stage PC-relative data reference requires its guest code address.
@@ -478,11 +481,227 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
return result;
}
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
source.call = ShaderSource::Call {.instruction = i};
}
EXIT_IF(options.input_info.compute == nullptr);
const auto& call = source.decoded.instructions[source.call->instruction];
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
call.dst.kind != Decoder::OperandKind::Sgpr ||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
const auto graph = CFG::BuildGraph(source.decoded);
Program query;
query.stage = options.stage;
query.user_data_base = options.user_data_base;
query.user_data_count = static_cast<uint32_t>(options.user_data.size());
query.wave_size = options.wave_size;
auto& block = *query.block_storage.emplace_back(std::make_unique<Block>());
query.blocks.push_back(&block);
struct Producer { bool ready = false; std::map<uint32_t, Value> values; };
std::map<uint32_t, Producer> producers;
std::function<Value(uint32_t, uint32_t)> Register;
std::function<Value(uint32_t, Value)> Resolve = [&](uint32_t before, Value value) -> Value {
value = value.Resolve();
auto* inst = value.TryInstruction();
if (inst == nullptr) return value;
uint32_t code = UINT32_MAX;
switch (inst->GetOpcode()) {
case ValueOpcode::GetScalarRegister: code = static_cast<uint32_t>(inst->Arg(0).ScalarRegister()); break;
case ValueOpcode::GetVccLo: code = 106u; break;
case ValueOpcode::GetVccHi: code = 107u; break;
default: break;
}
if (code != UINT32_MAX) {
const auto result = Register(before, code);
inst->ReplaceUsesWith(result);
return result;
}
for (size_t i = 0; i < inst->NumArgs(); ++i) inst->SetArg(i, Resolve(before, inst->Arg(i)));
return value;
};
Register = [&](uint32_t before, uint32_t code) -> Value {
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
if (index == UINT32_MAX) {
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
}
auto [entry, inserted] = producers.try_emplace(index);
auto& producer = entry->second;
if (inserted) {
Block translated;
Frontend::Translator translator(query, &translated, 1u,
(options.input_info.compute->float_mode & 0x10u) == 0u,
!options.input_info.compute->async_compute);
translator.TranslateInstruction(source.decoded.instructions[index]);
for (const auto& inst: translated) {
uint32_t destination = UINT32_MAX;
Value value;
switch (inst.GetOpcode()) {
case ValueOpcode::SetScalarRegister:
destination = static_cast<uint32_t>(inst.Arg(0).ScalarRegister()); value = inst.Arg(1); break;
case ValueOpcode::SetVccLo: destination = 106u; value = inst.Arg(0); break;
case ValueOpcode::SetVccHi: destination = 107u; value = inst.Arg(0); break;
default: break;
}
if (destination != UINT32_MAX) producer.values.insert_or_assign(destination, value);
}
for (auto& [destination, value]: producer.values) value = Resolve(index, value);
for (auto& inst: translated) inst.SetParent(&block);
block.Instructions().splice(block.end(), translated.Instructions());
producer.ready = true;
}
EXIT_IF(!producer.ready || !producer.values.contains(code));
return producer.values.at(code).Resolve();
};
for (uint32_t i = 0; i < 2u; ++i) {
const auto value = Register(source.call->instruction, call.src0.reg + i);
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
root.SetArg(0, value);
source.call->target[i] = Value(&root);
}
RewriteToSsa(query.blocks);
ConstantPropagationPass(query.blocks, query.wave_size);
RemoveIdentities(query.blocks);
EliminateDeadCode(query.blocks);
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
query.value_storage.clear();
for (auto& inst: block) {
inst.SetParent(nullptr);
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 || inst.GetOpcode() == ValueOpcode::ReadConstBuffer)
inst.SetFlags(SrtReadFlags {.index = inst.Flags<MemoryFlags>().index});
}
query.value_storage.splice(query.value_storage.end(), block.Instructions());
source.call_targets = std::move(static_cast<ResourcePlan&>(query));
}
} // namespace
ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOptions& options) {
EXIT_IF(code.empty());
ShaderSource source;
if (!options.back_code.empty()) {
source.decoded = DecodeFusedProgram(code, options.back_code, source.code);
} else if (options.stage == ShaderType::Local) {
source.decoded = Decoder::DecodeFrontProgram(code);
auto& handoff = source.decoded.instructions.back();
handoff.opcode = Decoder::Opcode::S_ENDPGM;
handoff.src_count = 0;
} else {
Decoder::DecodeProgram(code, source.decoded);
}
if (source.decoded.has_swap_pc) {
PrepareCallTarget(source, options);
source.code.assign(code.begin(), code.end());
source.decoded.code = source.code;
}
return source;
}
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
uint32_t low = 0, high = 0;
const auto& call = source.decoded.instructions[source.call->instruction];
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
EXIT_IF(address == 0u);
const auto query_reads = reads.size();
if (source.linked) {
auto& linked = *source.linked;
const auto function = std::span(linked.code).subspan(source.code.size());
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
std::ranges::equal(function, linked.observed_function)) {
return linked.decoded;
}
reads.resize(query_reads);
}
ShaderSource::Linked linked {.code = source.code,
.decoded = source.decoded};
const auto& last = source.decoded.instructions.back();
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
// PCs follow decoded instructions; its physical code is independently read and owned.
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
for (uint32_t index = 0;;) {
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
std::array<uint32_t, 2> words {};
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(1)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
const auto family = Decoder::GetInstructionFamily(words[0]);
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
family != Decoder::Family::SOPP);
Decoder::Instruction inst;
Decoder::DecodeInstruction(words, 0u, inst);
if (inst.word_count == 2u) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
std::span(words).last(1)))
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
}
inst.pc = index * sizeof(uint32_t);
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
index += inst.word_count;
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
inst.opcode == Decoder::Opcode::S_ENDPGM);
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
if (returns) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
inst.opcode = Decoder::Opcode::S_BRANCH;
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
} else if (Decoder::IsDirectBranch(inst.opcode)) {
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
inst.branch_target += function_pc;
}
inst.pc += function_pc;
linked.decoded.instructions.push_back(std::move(inst));
if (returns) break;
}
const auto function_end = linked.decoded.instructions.back().pc +
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
const auto& inst = linked.decoded.instructions[i];
if (Decoder::IsDirectBranch(inst.opcode))
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
}
reads.resize(query_reads);
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
linked.decoded.code = linked.code;
linked.observed_function.resize(linked.code.size() - source.code.size());
source.linked = std::move(linked);
++source.revision;
return source.linked->decoded;
}
TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOptions& options) {
if (code.empty()) {
EXIT("shader recompiler input is empty\n");
auto source = PrepareShaderSource(code, options);
return TranslateProgram(source.decoded, options);
}
TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileOptions& options) {
EXIT_IF(decoded.instructions.empty());
if (decoded.has_swap_pc) {
for (const auto& inst: decoded.instructions) {
if (inst.opcode == Decoder::Opcode::S_SWAPPC_B64 && inst.branch_target == UINT32_MAX)
EXIT("shader call at pc 0x%08x must be linked before translation", inst.pc);
}
}
if (options.stage != ShaderType::Compute && options.stage != ShaderType::Vertex &&
options.stage != ShaderType::Pixel && options.stage != ShaderType::Mesh &&
@@ -493,34 +712,11 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
}
const auto compile_begin = std::chrono::steady_clock::now();
const auto phase_ms = [&compile_begin]() {
const auto phase_ms = [&compile_begin]() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - compile_begin)
.count());
std::chrono::steady_clock::now() - compile_begin).count());
};
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " code_words=%" PRIu64 " decode\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(code.size()));
Decoder::Program decoded;
std::vector<uint32_t> joined_code;
if (!options.back_code.empty()) {
decoded = DecodeFusedProgram(code, options.back_code, joined_code);
} else if (options.stage == ShaderType::Local) {
decoded = Decoder::DecodeFrontProgram(code);
// The separately compiled hull half runs in the next Vulkan stage.
auto& handoff = decoded.instructions.back();
handoff.opcode = Decoder::Opcode::S_ENDPGM;
handoff.src_count = 0;
} else {
Decoder::DecodeProgram(code, decoded);
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " decode instructions=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(decoded.instructions.size()), phase_ms());
std::string decoded_dump;
if (options.dump_ir) {
decoded_dump = Decoder::ProgramToString(decoded);
@@ -606,6 +802,12 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
IR::RemoveIdentities(ir.blocks);
IR::EliminateDeadCode(ir.blocks);
}
if (decoded.has_swap_pc) {
// Leaf returns are resolved CFG edges; native PC values must not escape into GPU data.
for (const auto* block: ir.blocks)
for (const auto& inst: *block)
EXIT_NOT_IMPLEMENTED(inst.GetOpcode() == IR::ValueOpcode::GetShaderBase);
}
LowerTessellationMemory(ir, options);
std::string cfg_dump;
if (options.dump_ir) {
@@ -656,7 +858,11 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
const auto resource = inst.Flags<uint32_t>();
first = resource < ir.info.images.size() &&
ir.info.images[resource].indirect_root == resource ? 1u : 0u;
} else if (op != IR::ValueOpcode::GetSamplerResource) {
} else if (op == IR::ValueOpcode::GetSamplerResource) {
const auto resource = inst.Flags<uint32_t>();
first = resource < ir.info.samplers.size() &&
!ir.info.samplers[resource].indirect_resources.empty() ? 1u : 0u;
} else {
continue;
}
for (size_t index = first; index < inst.NumArgs(); index++) {
@@ -4,8 +4,11 @@
#include "common/common.h"
#include "common/stringUtils.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/shader.h"
#include <array>
#include <optional>
#include <span>
#include <vector>
@@ -37,6 +40,32 @@ struct CompileResult {
IR::Program program;
};
// Decoded source and typed scalar call queries are retained once per cache source.
struct ShaderSource {
struct Call {
uint32_t instruction;
std::array<IR::Value, 2> target;
};
struct Linked {
std::vector<uint32_t> code;
Decoder::Program decoded;
std::vector<uint32_t> observed_function;
};
std::vector<uint32_t> code;
Decoder::Program decoded;
IR::ResourcePlan call_targets;
std::optional<Call> call;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
};
[[nodiscard]] ShaderSource PrepareShaderSource(std::span<const uint32_t> code,
const CompileOptions& options);
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime);
[[nodiscard]] TranslateResult TranslateProgram(const Decoder::Program& decoded,
const CompileOptions& options);
[[nodiscard]] TranslateResult TranslateProgram(std::span<const uint32_t> code,
const CompileOptions& options);
[[nodiscard]] CompileResult CompileProgram(TranslateResult translated,
@@ -137,6 +137,15 @@ uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension) {
}
}
namespace {
void DecorateNonUniformDescriptor(EmitterState& state, uint32_t id) {
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilitySampledImageArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform);
}
} // namespace
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip,
uint32_t array_index) {
const auto pointer = ImageDescriptorPointer(state, resource, mip, array_index);
@@ -144,24 +153,29 @@ uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mi
const auto kind = *IR::DescriptorBindingForImage(state.program.info.images[resource]);
state.builder.AddFunction(spv::OpLoad, state.images[IR::ImageBindingIndex(kind)].type,
image, pointer);
if (array_index != 0u) {
DecorateNonUniformDescriptor(state, image);
}
return image;
}
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler) {
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler, uint32_t array_index) {
if (sampler >= state.program.info.samplers.size()) {
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::Samplers, sampler,
"sampler resource index is out of range");
}
const auto pointer = DescriptorElementPointer(
state, state.sampler_pointer_type, state.sampler_variable, ConstantU32(state, sampler),
state, state.sampler_pointer_type, state.sampler_variable,
array_index == 0u ? ConstantU32(state, sampler) : array_index,
IR::DescriptorBindingKind::Samplers, sampler, "sampler descriptor array was not emitted");
const auto sampler_id = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, state.sampler_type, sampler_id, pointer);
if (array_index != 0u) DecorateNonUniformDescriptor(state, sampler_id);
return sampler_id;
}
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip,
uint32_t array_index) {
uint32_t array_index, bool sampler_dynamic) {
const auto& image_resource = state.program.info.images.at(resource);
EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled);
const auto image = LoadImageDescriptor(state, resource, mip, array_index);
@@ -173,11 +187,8 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl
spv::OpTypeSampledImage, state.images[IR::ImageBindingIndex(kind)].type);
}
state.builder.AddFunction(spv::OpSampledImage, sampled_type, sampled_image, image, sampler_id);
if (array_index != 0u) {
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilitySampledImageArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, sampled_image, spv::DecorationNonUniform);
if (array_index != 0u || sampler_dynamic) {
DecorateNonUniformDescriptor(state, sampled_image);
}
return sampled_image;
}
@@ -115,6 +115,16 @@ DppTargetLane EmitDppTargetLane(EmitterState& state, const IR::DppMoveFlags& fla
if (control == 0x141u) {
return EmitDppMirrorTargetLane(state, subid, true);
}
// row_share:N reads lane N of the caller's row of 16.
if (control >= 0x150u && control <= 0x15fu) {
const auto row = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row,
ConstantU32(state, control & 0xfu));
return {target, ConstantBool(state, true)};
}
if (control >= 0x160u && control <= 0x16fu) {
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), target, subid,
@@ -615,6 +615,90 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
return PackImageTexel(ctx, mem, texel);
}
template <typename Emit>
uint32_t EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t result_type,
const Emit& emit) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
const auto& image = state.program.info.images.at(mem.resource);
if (image.indirect_root != mem.resource) return emit(mem.resource, 0u);
const auto* handle = inst.Arg(0).ResolveInstruction();
const auto* source = image.source < state.program.descriptor_sources.size()
? &state.program.descriptor_sources[image.source]
: nullptr;
if (handle == nullptr || source == nullptr || !source->indirect_descriptor.has_value() ||
handle->NumArgs() == 0u) {
ctx.Fail(inst, "has invalid indirect image key provenance");
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct ImageRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<ImageRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return emit(run.resource, 0u);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return emit(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
return runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
}
} // namespace
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -656,29 +740,35 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
if (op == IR::ValueOpcode::ImageRead) {
const auto dimension = image.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
const auto numeric_class = image.numeric_class;
const auto condition = ctx.Arg(inst, 2);
const auto numeric_class = image.numeric_class;
const auto condition = ctx.Arg(inst, 2);
ctx.Define(
inst,
EmitValueOrDefaultIfCondition(
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
[&]() {
const auto descriptor = LoadImageDescriptor(state, mem.resource);
const auto color = state.builder.AllocateId();
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, numeric_class, 4), color,
descriptor, coord, spv::ImageOperandsSampleMask,
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
} else {
state.builder.AddFunction(spv::OpImageFetch,
ImageVectorType(state, numeric_class, 4), color,
descriptor, coord, spv::ImageOperandsLodMask,
LodU32(ctx, mem, *address, dimension));
}
const auto color = EmitImageAccess(
ctx, inst, ImageVectorType(state, numeric_class, 4),
[&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto dimension = candidate.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
const auto descriptor = LoadImageDescriptor(state, resource, 0u, array_index);
const auto fetched = state.builder.AllocateId();
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
descriptor, coord, spv::ImageOperandsSampleMask,
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
} else {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
descriptor, coord, spv::ImageOperandsLodMask,
LodU32(ctx, mem, *address, mem.image_dimension));
}
return fetched;
});
return ResultVector(ctx, UnpackImageTexel(ctx, mem, color), numeric_class,
false, mem);
}));
@@ -808,106 +898,32 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
operand_count = 2;
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index = 0u) {
const auto& samplers = state.program.info.samplers.at(mem.sampler).indirect_resources;
uint32_t sampler_index = 0;
if (!samplers.empty()) {
const auto* handle = inst.Arg(1).ResolveInstruction();
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
ctx.Fail(inst, "has invalid finite sampler key provenance");
sampler_index = EmitIndexSwitch(
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
candidate.cube);
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index);
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index,
sampler_index != 0u);
const auto sample = state.builder.AllocateId();
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
std::span(&dref_value, dref ? 1u : 0u),
std::span<const uint32_t>(operands).first(operand_count));
return sample;
};
if (image.indirect_root != mem.resource) {
const auto sample = EmitSample(mem.resource);
auto result = sample;
if (!dref) {
result = UnpackImageTexel(ctx, mem, sample);
}
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
return;
}
const auto* handle = image_arg.ResolveInstruction();
const auto* source = image.source < state.program.descriptor_sources.size()
? &state.program.descriptor_sources[image.source]
: nullptr;
if (handle == nullptr || source == nullptr || !source->indirect_descriptor.has_value() ||
handle->NumArgs() == 0u) {
ctx.Fail(inst, "has invalid indirect image key provenance");
return;
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
return;
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
struct SampleRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<SampleRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
const auto kind = *IR::DescriptorBindingForImage(candidate);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = candidate.descriptor_index - ordinal;
++run.count;
continue;
}
}
runs.push_back({ordinal, 1u, resource,
candidate.descriptor_index - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return EmitSample(run.resource);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto root_slot = image.descriptor_index;
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return EmitSample(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
auto result = runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
if (!dref) {
result = UnpackImageTexel(ctx, mem, result);
}
@@ -146,6 +146,15 @@ inline constexpr auto EmitFPOrdEqual32 = EmitFloatCompare32<spv::OpFOrdEqual>;
EMIT_NATIVE(FPOrdEqual64, OpFOrdEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThanEqual64, OpFOrdLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThanEqual64, OpFOrdGreaterThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdNotEqual64, OpFOrdNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThan64, OpFOrdLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThan64, OpFOrdGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordEqual64, OpFUnordEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordNotEqual64, OpFUnordNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThan64, OpFUnordLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThan64, OpFUnordGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThanEqual64, OpFUnordLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThanEqual64, OpFUnordGreaterThanEqual, U1, uint32_t, uint32_t)
inline constexpr auto EmitFPUnordEqual32 = EmitFloatCompare32<spv::OpFUnordEqual>;
inline constexpr auto EmitFPOrdNotEqual32 = EmitFloatCompare32<spv::OpFOrdNotEqual>;
inline constexpr auto EmitFPUnordNotEqual32 = EmitFloatCompare32<spv::OpFUnordNotEqual>;
@@ -158,6 +167,7 @@ inline constexpr auto EmitFPUnordLessThanEqual32 = EmitFloatCompare32<spv::OpFUn
inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFOrdGreaterThanEqual>;
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
EMIT_NATIVE(FPIsNan32, OpIsNan, U1, uint32_t)
EMIT_NATIVE(FPIsNan64, OpIsNan, U1, uint32_t)
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
inline constexpr auto EmitFPCmpClass16 = EmitClassMaskF16;
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
@@ -322,10 +322,11 @@ uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0,
uint32_t array_index = 0);
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler);
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler, uint32_t array_index = 0);
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id,
uint32_t mip = 0, uint32_t array_index = 0);
uint32_t mip = 0, uint32_t array_index = 0,
bool sampler_dynamic = false);
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
uint32_t texel);
@@ -1468,7 +1468,8 @@ Graph BuildGraph(const Decoder::Program& program) {
for (uint32_t i = 0; i < program.instructions.size(); i++) {
const auto& inst = program.instructions[i];
const auto next_pc = InstructionEndPc(inst);
if (Decoder::IsDirectBranch(inst.opcode)) {
if (Decoder::IsDirectBranch(inst.opcode) ||
(inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX)) {
if (!IsValidTarget(inst.branch_target, program.instructions, end_pc)) {
ExitBuildFailure(graph, FailureKind::InvalidBranchTarget, UINT32_MAX,
fmt::format("branch at pc 0x{:08x} targets invalid pc 0x{:08x}",
@@ -1501,7 +1502,7 @@ Graph BuildGraph(const Decoder::Program& program) {
if (next_pc <= end_pc) {
labels.push_back(next_pc);
}
} else if (inst.opcode == Opcode::S_ENDPGM) {
} else if (inst.opcode == Opcode::S_ENDPGM || inst.opcode == Opcode::S_SWAPPC_B64) {
labels.push_back(next_pc);
}
}
@@ -1550,6 +1551,9 @@ Graph BuildGraph(const Decoder::Program& program) {
const auto next_pc = InstructionEndPc(last);
if (last.opcode == Opcode::S_ENDPGM) {
block.terminator.kind = TerminatorKind::Return;
} else if (last.opcode == Opcode::S_SWAPPC_B64 && last.branch_target != UINT32_MAX) {
block.terminator.kind = TerminatorKind::Branch;
block.terminator.true_block = block_at_pc(last.branch_target);
} else if (last.opcode == Opcode::S_SETPC_B64) {
const auto& target_info = setpc_targets.at(last.pc);
if (target_info.indirect) {
@@ -1676,6 +1680,36 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
uint32_t destination = UINT32_MAX;
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
return InstructionWritesScalarCode(inst, code) ||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
});
EXIT_IF(found == graph.blocks.end());
const auto* block = &*found;
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
for (uint32_t i = before; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
if (MayWriteScalarRegister(inst, code)) return i;
}
if (block->id == graph.entry_block) return UINT32_MAX;
EXIT_IF(block->predecessors.size() != 1u);
block = graph.FindBlock(block->predecessors[0]);
EXIT_IF(block == nullptr);
before = block->inst_end;
}
EXIT("scalar shader call source has cyclic reaching definitions");
}
std::string BranchConditionToString(BranchCondition condition) {
switch (condition) {
case BranchCondition::Always: return "always";
@@ -113,6 +113,9 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -47,6 +47,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x1du, Opcode::BUFFER_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::BUFFER_STORE_DWORDX4, 4, 32},
{0x1fu, Opcode::BUFFER_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::BUFFER_LOAD_SHORT_D16, 1, 16},
{0x25u, Opcode::BUFFER_LOAD_SHORT_D16_HI, 1, 16},
{0x30u, Opcode::BUFFER_ATOMIC_SWAP, 1, 32},
{0x31u, Opcode::BUFFER_ATOMIC_CMPSWAP, 1, 32},
{0x32u, Opcode::BUFFER_ATOMIC_ADD, 1, 32},
@@ -298,8 +300,10 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X) {
inst.dst.sdwa_sel = 4u;
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X ||
inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16 ||
inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16_HI) {
inst.dst.sdwa_sel = inst.opcode == Opcode::BUFFER_LOAD_SHORT_D16_HI ? 5u : 4u;
}
DecodeVectorGpr(vaddr, inst.src0);
DecodeScalarSource(srsrc * 4u, pc, inst.src1);
@@ -371,7 +375,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
ApplyMemoryInfo(inst, info);
SetRawWords(inst, code, word_index, 2);
if (lds != 0 || inst.glc || inst.slc || seg == 3u) {
if (lds != 0 || inst.glc || (inst.slc && !IsFlatStoreOpcode(inst.opcode)) || seg == 3u) {
SetUnsupported(inst, Family::FLAT, opcode, "FLAT modifiers or segment are not implemented");
return;
}
@@ -47,8 +47,12 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x0au, Opcode::S_WQM_B64},
{0x0bu, Opcode::S_BREV_B32},
{0x0cu, Opcode::S_BREV_B64},
{0x0du, Opcode::S_BCNT0_I32_B32},
{0x0eu, Opcode::S_BCNT0_I32_B64},
{0x0fu, Opcode::S_BCNT1_I32_B32},
{0x10u, Opcode::S_BCNT1_I32_B64},
{0x11u, Opcode::S_FF0_I32_B32},
{0x12u, Opcode::S_FF0_I32_B64},
{0x13u, Opcode::S_FF1_I32_B32},
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
@@ -61,6 +65,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x1eu, Opcode::S_BITSET1_B64},
{0x1fu, Opcode::S_GETPC_B64},
{0x20u, Opcode::S_SETPC_B64},
{0x21u, Opcode::S_SWAPPC_B64},
{0x24u, Opcode::S_AND_SAVEEXEC_B64},
{0x28u, Opcode::S_ORN2_SAVEEXEC_B64},
{0x2du, Opcode::S_QUADMASK_B64},
@@ -88,7 +93,7 @@ constexpr OpcodeMap SOPK_OPCODE_LIST[] = {
{0x08u, Opcode::S_CMP_LE_I32}, {0x09u, Opcode::S_CMP_EQ_U32}, {0x0au, Opcode::S_CMP_LG_U32},
{0x0bu, Opcode::S_CMP_GT_U32}, {0x0cu, Opcode::S_CMP_GE_U32}, {0x0du, Opcode::S_CMP_LT_U32},
{0x0eu, Opcode::S_CMP_LE_U32}, {0x0fu, Opcode::S_ADD_I32}, {0x10u, Opcode::S_MULK_I32},
{0x13u, Opcode::S_SETREG_B32}, {0x17u, Opcode::S_WAITCNT_VSCNT}, {0x18u, Opcode::S_WAITCNT},
{0x12u, Opcode::S_GETREG_B32}, {0x13u, Opcode::S_SETREG_B32}, {0x17u, Opcode::S_WAITCNT_VSCNT}, {0x18u, Opcode::S_WAITCNT},
{0x19u, Opcode::S_WAITCNT}, {0x1au, Opcode::S_WAITCNT},
{0x1bu, Opcode::S_SUBVECTOR_LOOP_BEGIN}, {0x1cu, Opcode::S_SUBVECTOR_LOOP_END},
};
@@ -111,7 +116,9 @@ constexpr OpcodeMap SOPP_OPCODE_LIST[] = {
{0x12u, Opcode::S_TRAP},
{0x16u, Opcode::S_TTRACEDATA},
{0x17u, Opcode::S_CBRANCH_CDBGSYS},
{0x18u, Opcode::S_CBRANCH_CDBGUSER},
{0x19u, Opcode::S_CBRANCH_CDBGSYS_OR_USER},
{0x1au, Opcode::S_CBRANCH_CDBGSYS_AND_USER},
{0x20u, Opcode::S_INST_PREFETCH},
{0x23u, Opcode::S_WAITCNT_DEPCTR},
};
@@ -155,6 +162,10 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
switch (inst.opcode) {
case Opcode::S_SWAPPC_B64:
inst.branch_target = UINT32_MAX;
inst.data_dwords = 2;
break;
case Opcode::S_GETPC_B64:
inst.src_count = 0;
DecodeScalarDestination(sdst, pc, inst.dst);
@@ -202,7 +213,7 @@ void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const uint32_t word = code[word_index];
const uint32_t opcode = (word >> 23u) & 0x1fu;
const uint32_t sdst = (word >> 16u) & 0x7fu;
const auto imm = opcode >= 0x09u && opcode <= 0x0eu
const auto imm = (opcode >= 0x09u && opcode <= 0x0eu) || opcode == 0x12u
? static_cast<int32_t>(word & 0xffffu)
: static_cast<int32_t>(static_cast<int16_t>(word & 0xffffu));
@@ -222,6 +233,7 @@ void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
switch (inst.opcode) {
case Opcode::S_GETREG_B32:
case Opcode::S_MOVK_I32: DecodeScalarDestination(sdst, pc, inst.dst); return;
case Opcode::S_SUBVECTOR_LOOP_BEGIN:
case Opcode::S_SUBVECTOR_LOOP_END:
@@ -186,7 +186,9 @@ bool IsConditionalBranch(Opcode opcode) {
switch (opcode) {
// Conditional shader debugging is disabled.
case Opcode::S_CBRANCH_CDBGSYS:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER: return false;
case Opcode::S_CBRANCH_CDBGUSER:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
case Opcode::S_CBRANCH_CDBGSYS_AND_USER: return false;
case Opcode::S_CBRANCH_SCC0:
case Opcode::S_CBRANCH_SCC1:
case Opcode::S_CBRANCH_VCCZ:
@@ -389,6 +391,7 @@ Program DecodeFrontProgram(std::span<const uint32_t> front) {
while (front_words < front.size()) {
auto& inst = result.instructions.emplace_back();
DecodeInstruction(front, front_words, inst);
result.has_swap_pc |= inst.opcode == Opcode::S_SWAPPC_B64;
front_words += inst.word_count;
if (inst.opcode == Opcode::S_SETPC_B64) {
EXIT_NOT_IMPLEMENTED(inst.src0.kind != OperandKind::Sgpr || inst.src0.reg != 6u);
@@ -406,6 +409,7 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
program.instructions.clear();
program.instructions.reserve(code.size());
program.code = code;
program.has_swap_pc = false;
std::vector<bool> branch_targets;
for (uint32_t word_index = 0; word_index < code.size();) {
@@ -413,6 +417,7 @@ void DecodeProgram(std::span<const uint32_t> code, Program& program) {
DecodeInstruction(code, word_index, program.instructions.back());
const auto& inst = program.instructions.back();
program.has_swap_pc |= inst.opcode == Opcode::S_SWAPPC_B64;
word_index += inst.word_count;
if (IsDirectBranch(inst.opcode)) {
@@ -507,6 +512,10 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
case Opcode::S_BCNT0_I32_B32:
case Opcode::S_BCNT0_I32_B64:
case Opcode::S_FF0_I32_B32:
case Opcode::S_FF0_I32_B64:
case Opcode::S_BCNT1_I32_B32:
case Opcode::S_FLBIT_I32_B32:
case Opcode::S_FF1_I32_B32:
@@ -568,7 +577,9 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::S_CBRANCH_EXECZ:
case Opcode::S_CBRANCH_EXECNZ:
case Opcode::S_CBRANCH_CDBGSYS:
case Opcode::S_CBRANCH_CDBGUSER:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
case Opcode::S_CBRANCH_CDBGSYS_AND_USER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} 0x{:08x}", inst.pc,
magic_enum::enum_name(inst.opcode),
inst.branch_target));
@@ -627,6 +638,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_STORE_FORMAT_D16_X:
case Opcode::BUFFER_LOAD_UBYTE:
case Opcode::BUFFER_LOAD_USHORT:
case Opcode::BUFFER_LOAD_SHORT_D16:
case Opcode::BUFFER_LOAD_SHORT_D16_HI:
case Opcode::BUFFER_LOAD_DWORD:
case Opcode::BUFFER_LOAD_DWORDX2:
case Opcode::BUFFER_LOAD_DWORDX3:
@@ -50,8 +50,12 @@ enum class Opcode {
S_ABSDIFF_I32,
S_BREV_B32,
S_BREV_B64,
S_BCNT0_I32_B32,
S_BCNT0_I32_B64,
S_BCNT1_I32_B32,
S_BCNT1_I32_B64,
S_FF0_I32_B32,
S_FF0_I32_B64,
S_FF1_I32_B32,
S_FF1_I32_B64,
S_FLBIT_I32_B32,
@@ -60,6 +64,7 @@ enum class Opcode {
S_QUADMASK_B64,
S_GETPC_B64,
S_SETPC_B64,
S_SWAPPC_B64,
S_SUBVECTOR_LOOP_BEGIN,
S_SUBVECTOR_LOOP_END,
S_AND_SAVEEXEC_B32,
@@ -373,10 +378,38 @@ enum class Opcode {
V_CMP_NEQ_F32,
V_CMP_NLT_F32,
V_CMP_TRU_F32,
V_CMP_F_F64,
V_CMP_LT_F64,
V_CMP_EQ_F64,
V_CMP_LE_F64,
V_CMP_GT_F64,
V_CMP_LG_F64,
V_CMP_GE_F64,
V_CMP_O_F64,
V_CMP_U_F64,
V_CMP_NGE_F64,
V_CMP_NLG_F64,
V_CMP_NGT_F64,
V_CMP_NLE_F64,
V_CMP_NEQ_F64,
V_CMP_NLT_F64,
V_CMP_TRU_F64,
V_CMPX_F_F64,
V_CMPX_LT_F64,
V_CMPX_EQ_F64,
V_CMPX_LE_F64,
V_CMPX_GT_F64,
V_CMPX_LG_F64,
V_CMPX_GE_F64,
V_CMPX_O_F64,
V_CMPX_U_F64,
V_CMPX_NGE_F64,
V_CMPX_NLG_F64,
V_CMPX_NGT_F64,
V_CMPX_NLE_F64,
V_CMPX_NEQ_F64,
V_CMPX_NLT_F64,
V_CMPX_TRU_F64,
V_CMPX_LT_F32,
V_CMPX_EQ_F32,
V_CMPX_LE_F32,
@@ -458,11 +491,14 @@ enum class Opcode {
V_CMP_NE_U32,
V_CMP_GE_U32,
V_CMP_T_U32,
// Keep V_CMP_F_I64 to V_CMPX_T_U64 contiguous: VectorAluOps.cpp tests this range.
V_CMP_F_I64,
V_CMP_EQ_I64,
V_CMP_LT_I64,
V_CMP_EQ_I64,
V_CMP_LE_I64,
V_CMP_GT_I64,
V_CMP_NE_I64,
V_CMP_GE_I64,
V_CMP_T_I64,
V_CMP_F_U64,
V_CMP_LT_U64,
@@ -476,7 +512,9 @@ enum class Opcode {
V_CMPX_LT_I64,
V_CMPX_EQ_I64,
V_CMPX_LE_I64,
V_CMPX_GT_I64,
V_CMPX_NE_I64,
V_CMPX_GE_I64,
V_CMPX_T_I64,
V_CMPX_F_U64,
V_CMPX_LT_U64,
@@ -518,6 +556,8 @@ enum class Opcode {
BUFFER_LOAD_SBYTE,
BUFFER_LOAD_USHORT,
BUFFER_LOAD_SSHORT,
BUFFER_LOAD_SHORT_D16,
BUFFER_LOAD_SHORT_D16_HI,
BUFFER_LOAD_DWORD,
BUFFER_LOAD_DWORDX2,
BUFFER_LOAD_DWORDX3,
@@ -674,8 +714,11 @@ enum class Opcode {
S_CBRANCH_EXECZ,
S_CBRANCH_EXECNZ,
S_CBRANCH_CDBGSYS,
S_CBRANCH_CDBGUSER,
S_CBRANCH_CDBGSYS_OR_USER,
S_CBRANCH_CDBGSYS_AND_USER,
S_SENDMSG,
S_GETREG_B32,
S_SETREG_B32,
S_SLEEP,
S_TRAP,
@@ -814,6 +857,7 @@ struct Instruction {
struct Program {
std::span<const uint32_t> code;
std::vector<Instruction> instructions;
bool has_swap_pc = false;
};
// Code spans are trusted to contain complete instructions, valid branch targets, and 32-bit PCs.
@@ -39,7 +39,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
{0x11u, Opcode::V_MIN_I32, Vop2SdwaProfile::IntegerFullDestination},
{0x12u, Opcode::V_MAX_I32, Vop2SdwaProfile::IntegerFullDestination},
{0x13u, Opcode::V_MIN_U32, Vop2SdwaProfile::IntegerPartialDestination},
{0x14u, Opcode::V_MAX_U32, Vop2SdwaProfile::IntegerFullDestination},
@@ -236,8 +236,23 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
{0x20u, Opcode::V_CMP_F_F64, false}, {0x21u, Opcode::V_CMP_LT_F64, false},
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
{0x24u, Opcode::V_CMP_GT_F64, false}, {0x25u, Opcode::V_CMP_LG_F64, false},
{0x26u, Opcode::V_CMP_GE_F64, false}, {0x27u, Opcode::V_CMP_O_F64, false},
{0x28u, Opcode::V_CMP_U_F64, false}, {0x29u, Opcode::V_CMP_NGE_F64, false},
{0x2au, Opcode::V_CMP_NLG_F64, false}, {0x2bu, Opcode::V_CMP_NGT_F64, false},
{0x2cu, Opcode::V_CMP_NLE_F64, false}, {0x2du, Opcode::V_CMP_NEQ_F64, false},
{0x2eu, Opcode::V_CMP_NLT_F64, false}, {0x2fu, Opcode::V_CMP_TRU_F64, false},
{0x30u, Opcode::V_CMPX_F_F64, false},
{0x31u, Opcode::V_CMPX_LT_F64, false}, {0x32u, Opcode::V_CMPX_EQ_F64, false},
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x34u, Opcode::V_CMPX_GT_F64, false},
{0x35u, Opcode::V_CMPX_LG_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
{0x37u, Opcode::V_CMPX_O_F64, false}, {0x38u, Opcode::V_CMPX_U_F64, false},
{0x39u, Opcode::V_CMPX_NGE_F64, false}, {0x3au, Opcode::V_CMPX_NLG_F64, false},
{0x3bu, Opcode::V_CMPX_NGT_F64, false}, {0x3cu, Opcode::V_CMPX_NLE_F64, false},
{0x3du, Opcode::V_CMPX_NEQ_F64, false}, {0x3eu, Opcode::V_CMPX_NLT_F64, false},
{0x3fu, Opcode::V_CMPX_TRU_F64, false},
{0x80u, Opcode::V_CMP_F_I32}, {0x81u, Opcode::V_CMP_LT_I32},
{0x82u, Opcode::V_CMP_EQ_I32}, {0x83u, Opcode::V_CMP_LE_I32},
{0x84u, Opcode::V_CMP_GT_I32}, {0x85u, Opcode::V_CMP_NE_I32},
@@ -263,25 +278,26 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xc1u, Opcode::V_CMP_LT_U32}, {0xc2u, Opcode::V_CMP_EQ_U32},
{0xc3u, Opcode::V_CMP_LE_U32}, {0xc4u, Opcode::V_CMP_GT_U32},
{0xc5u, Opcode::V_CMP_NE_U32}, {0xc6u, Opcode::V_CMP_GE_U32},
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false},
{0xa0u, Opcode::V_CMP_F_I64, false}, {0xa7u, Opcode::V_CMP_T_I64, false},
{0xb0u, Opcode::V_CMPX_F_I64, false}, {0xb1u, Opcode::V_CMPX_LT_I64, false},
{0xb2u, Opcode::V_CMPX_EQ_I64, false}, {0xb3u, Opcode::V_CMPX_LE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xc7u, Opcode::V_CMP_T_U32}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
{0xe0u, Opcode::V_CMP_F_U64, false}, {0xe7u, Opcode::V_CMP_T_U64, false},
{0xf0u, Opcode::V_CMPX_F_U64, false}, {0xf1u, Opcode::V_CMPX_LT_U64, false},
{0xf2u, Opcode::V_CMPX_EQ_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf6u, Opcode::V_CMPX_GE_U64, false}, {0xf7u, Opcode::V_CMPX_T_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xa0u, Opcode::V_CMP_F_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa3u, Opcode::V_CMP_LE_I64, false}, {0xa4u, Opcode::V_CMP_GT_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false}, {0xa6u, Opcode::V_CMP_GE_I64, false},
{0xa7u, Opcode::V_CMP_T_I64, false}, {0xb0u, Opcode::V_CMPX_F_I64, false},
{0xb1u, Opcode::V_CMPX_LT_I64, false}, {0xb2u, Opcode::V_CMPX_EQ_I64, false},
{0xb3u, Opcode::V_CMPX_LE_I64, false}, {0xb4u, Opcode::V_CMPX_GT_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xb6u, Opcode::V_CMPX_GE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false}, {0xe0u, Opcode::V_CMP_F_U64, false},
{0xe1u, Opcode::V_CMP_LT_U64, false}, {0xe2u, Opcode::V_CMP_EQ_U64, false},
{0xe3u, Opcode::V_CMP_LE_U64, false}, {0xe4u, Opcode::V_CMP_GT_U64, false},
{0xe5u, Opcode::V_CMP_NE_U64, false}, {0xe6u, Opcode::V_CMP_GE_U64, false},
{0xe7u, Opcode::V_CMP_T_U64, false}, {0xf0u, Opcode::V_CMPX_F_U64, false},
{0xf1u, Opcode::V_CMPX_LT_U64, false}, {0xf2u, Opcode::V_CMPX_EQ_U64, false},
{0xf3u, Opcode::V_CMPX_LE_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xf6u, Opcode::V_CMPX_GE_U64, false},
{0xf7u, Opcode::V_CMPX_T_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
{0xcau, Opcode::V_CMP_EQ_F16}, {0xcbu, Opcode::V_CMP_LE_F16},
{0xccu, Opcode::V_CMP_GT_F16}, {0xcdu, Opcode::V_CMP_LG_F16},
{0xceu, Opcode::V_CMP_GE_F16}, {0xe9u, Opcode::V_CMP_NGE_F16},
@@ -760,9 +776,10 @@ void DecodeVop1Sdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_i
void ApplyDppModifier(Operand& operand, uint32_t modifier, uint32_t encoding) {
operand.dpp = true;
if (encoding == 233u) {
operand.dpp8 = true;
operand.dpp_ctrl = modifier >> 8u;
if (encoding == 233u || encoding == 234u) {
operand.dpp8 = true;
operand.dpp_ctrl = modifier >> 8u;
operand.dpp_fetch_inactive = encoding == 234u;
return;
}
operand.negate = ((modifier >> 20u) & 0x1u) != 0u;
@@ -879,10 +896,38 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMP_F_F32:
case Opcode::V_CMP_LT_F32:
case Opcode::V_CMP_EQ_F32:
case Opcode::V_CMP_F_F64:
case Opcode::V_CMP_LT_F64:
case Opcode::V_CMP_EQ_F64:
case Opcode::V_CMP_LE_F64:
case Opcode::V_CMP_GT_F64:
case Opcode::V_CMP_LG_F64:
case Opcode::V_CMP_GE_F64:
case Opcode::V_CMP_O_F64:
case Opcode::V_CMP_U_F64:
case Opcode::V_CMP_NGE_F64:
case Opcode::V_CMP_NLG_F64:
case Opcode::V_CMP_NGT_F64:
case Opcode::V_CMP_NLE_F64:
case Opcode::V_CMP_NEQ_F64:
case Opcode::V_CMP_NLT_F64:
case Opcode::V_CMP_TRU_F64:
case Opcode::V_CMPX_F_F64:
case Opcode::V_CMPX_LT_F64:
case Opcode::V_CMPX_EQ_F64:
case Opcode::V_CMPX_LE_F64:
case Opcode::V_CMPX_GT_F64:
case Opcode::V_CMPX_LG_F64:
case Opcode::V_CMPX_GE_F64:
case Opcode::V_CMPX_O_F64:
case Opcode::V_CMPX_U_F64:
case Opcode::V_CMPX_NGE_F64:
case Opcode::V_CMPX_NLG_F64:
case Opcode::V_CMPX_NGT_F64:
case Opcode::V_CMPX_NLE_F64:
case Opcode::V_CMPX_NEQ_F64:
case Opcode::V_CMPX_NLT_F64:
case Opcode::V_CMPX_TRU_F64:
case Opcode::V_CMP_LE_F32:
case Opcode::V_CMP_GT_F32:
case Opcode::V_CMP_LG_F32:
@@ -1202,10 +1247,10 @@ VopcSdwaFields DecodeVopcSdwaFields(uint32_t modifier) {
}
bool SupportsVopcSdwa(Opcode opcode) {
return opcode != Opcode::UNSUPPORTED && opcode != Opcode::V_CMP_NE_I64 &&
opcode != Opcode::V_CMP_EQ_F64 &&
opcode != Opcode::V_CMP_LE_F64 && opcode != Opcode::V_CMPX_LE_F64 &&
opcode != Opcode::V_CMPX_GE_F64;
// SDWA operand handling is not implemented for 64-bit compares.
return opcode != Opcode::UNSUPPORTED &&
(opcode < Opcode::V_CMP_F_F64 || opcode > Opcode::V_CMPX_TRU_F64) &&
(opcode < Opcode::V_CMP_F_I64 || opcode > Opcode::V_CMPX_T_U64);
}
void DecodeVopcSdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
@@ -1538,8 +1583,22 @@ void ApplyNativeVop3SourceModifiers(Instruction& inst, uint32_t abs, uint32_t ne
bool IsVopcCompareExec(Opcode opcode) {
switch (opcode) {
case Opcode::V_CMPX_F_F64:
case Opcode::V_CMPX_LT_F64:
case Opcode::V_CMPX_EQ_F64:
case Opcode::V_CMPX_LE_F64:
case Opcode::V_CMPX_GT_F64:
case Opcode::V_CMPX_LG_F64:
case Opcode::V_CMPX_GE_F64:
case Opcode::V_CMPX_O_F64:
case Opcode::V_CMPX_U_F64:
case Opcode::V_CMPX_NGE_F64:
case Opcode::V_CMPX_NLG_F64:
case Opcode::V_CMPX_NGT_F64:
case Opcode::V_CMPX_NLE_F64:
case Opcode::V_CMPX_NEQ_F64:
case Opcode::V_CMPX_NLT_F64:
case Opcode::V_CMPX_TRU_F64:
case Opcode::V_CMPX_LT_F32:
case Opcode::V_CMPX_EQ_F32:
case Opcode::V_CMPX_LE_F32:
@@ -1572,7 +1631,9 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_LT_I64:
case Opcode::V_CMPX_EQ_I64:
case Opcode::V_CMPX_LE_I64:
case Opcode::V_CMPX_GT_I64:
case Opcode::V_CMPX_NE_I64:
case Opcode::V_CMPX_GE_I64:
case Opcode::V_CMPX_T_I64:
case Opcode::V_CMPX_F_U64:
case Opcode::V_CMPX_LT_U64:
@@ -1626,7 +1687,8 @@ void DecodeVop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
return;
}
switch (src0) {
case 233u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 233u:
case 234u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 249u: DecodeVop2Sdwa(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 250u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
default: break;
@@ -1661,7 +1723,8 @@ void DecodeVop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
return;
}
switch (src0) {
case 233u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
case 233u:
case 234u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
case 249u: DecodeVop1Sdwa(pc, code, word_index, opcode, vdst, inst); return;
case 250u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
default: break;
@@ -1,6 +1,7 @@
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include <bit>
#include <utility>
namespace Libs::Graphics::ShaderRecompiler::Frontend {
@@ -27,7 +28,8 @@ void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool valu
}
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
IR::Type type, bool scalar, bool cmpx) {
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
// The equality compares share their IR opcode with the unsigned ones.
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
@@ -42,8 +44,12 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
}
return ReadOperand(operand, type);
};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {read(inst.src0), read(inst.src1)})), scalar,
cmpx);
auto lhs = read(inst.src0);
auto rhs = read(inst.src1);
if (swap_operands) {
std::swap(lhs, rhs);
}
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
}
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
@@ -65,11 +71,14 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx) {
const auto lhs = IR::F32(ReadOperand(inst.src0, IR::Type::F32));
const auto rhs = IR::F32(ReadOperand(inst.src1, IR::Type::F32));
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {lhs})),
IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {rhs})));
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64) {
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
const auto lhs = ReadOperand(inst.src0, type);
const auto rhs = ReadOperand(inst.src1, type);
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
IR::U1(ir.Emit(is_nan_op, {rhs})));
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
}
@@ -221,8 +221,12 @@ void Translator::V_XOR3_B32(const Decoder::Instruction& inst) {
WriteOperand(DestinationOperand(inst), result);
}
void Translator::S_FF1_I32_B64(const Decoder::Instruction& inst) {
const auto source = ExtractU64(ReadU64(inst.src0));
void Translator::S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero) {
auto source = ReadU32Pair(inst.src0);
if (find_zero) {
source[0] = ir.BitwiseNot(source[0]);
source[1] = ir.BitwiseNot(source[1]);
}
const auto low_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[0]}));
const auto high_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[1]}));
const auto high_position = ir.IAdd(high_lsb, IR::U32(IR::Value(32u)));
@@ -973,6 +973,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_LOAD_SBYTE:
case Decoder::Opcode::BUFFER_LOAD_USHORT:
case Decoder::Opcode::BUFFER_LOAD_SSHORT:
case Decoder::Opcode::BUFFER_LOAD_SHORT_D16:
case Decoder::Opcode::BUFFER_LOAD_SHORT_D16_HI:
case Decoder::Opcode::BUFFER_LOAD_DWORD:
case Decoder::Opcode::BUFFER_LOAD_DWORDX2:
case Decoder::Opcode::BUFFER_LOAD_DWORDX3:
@@ -10,13 +10,21 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_MOV_B64: S_MOV_B64(inst); return;
case O::S_WQM_B32: S_WQM(inst, false); return;
case O::S_WQM_B64: S_WQM(inst, true); return;
case O::S_GETPC_B64: S_GETPC_B64(inst); return;
case O::S_GETPC_B64:
case O::S_SWAPPC_B64: S_GETPC_B64(inst); return;
case O::S_SETPC_B64: return;
case O::S_SUBVECTOR_LOOP_BEGIN: S_SUBVECTOR_LOOP(inst, true); return;
case O::S_SUBVECTOR_LOOP_END: S_SUBVECTOR_LOOP(inst, false); return;
case O::S_CSELECT_B32: S_CSELECT_B32(inst); return;
case O::S_CSELECT_B64: ScalarSelect64(inst, inst.src1); return;
case O::S_CMOV_B64: ScalarSelect64(inst, inst.dst); return;
case O::S_GETREG_B32:
if (!graphics_compute || (inst.src0.value != 0x1818u && inst.src0.value != 0x0a18u))
EXIT("unsupported S_GETREG_B32 at pc 0x%08x: selector=0x%04x graphics_compute=%u",
inst.pc, inst.src0.value, static_cast<unsigned>(graphics_compute));
// HW_ID2 identifies graphics compute as QUEUE=6, ME=0.
WriteOperand(inst.dst, IR::Value(inst.src0.value == 0x1818u ? 6u : 0u));
return;
case O::S_SETREG_B32: EmitControlNop(); return;
case O::S_WAITCNT_VSCNT: S_WAITCNT_VSCNT(inst); return;
case O::S_WAITCNT: return;
@@ -170,12 +178,29 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
IR::U32(ir.Emit(IR::ValueOpcode::BitReverse32, {value[0]}))});
return;
}
case O::S_BCNT0_I32_B32:
case O::S_FF0_I32_B32: {
const auto value = ir.BitwiseNot(ReadU32(inst.src0));
const bool count = inst.opcode == O::S_BCNT0_I32_B32;
const auto result = IR::U32(ir.Emit(count ? IR::ValueOpcode::BitCount32
: IR::ValueOpcode::FindILsb32, {value}));
WriteOperand(inst.dst, result);
if (count) {
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
}
return;
}
case O::S_BCNT1_I32_B32:
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
true);
case O::S_BCNT0_I32_B64:
case O::S_BCNT1_I32_B64: {
// Vulkan bit counts operate on 32-bit words; avoid packing only to split again.
const auto value = ReadU32Pair(inst.src0);
auto value = ReadU32Pair(inst.src0);
if (inst.opcode == O::S_BCNT0_I32_B64) {
value[0] = ir.BitwiseNot(value[0]);
value[1] = ir.BitwiseNot(value[1]);
}
const auto low = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[0]}));
const auto high = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[1]}));
const auto result = ir.IAdd(low, high);
@@ -213,7 +238,8 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseOr32, false, true, true);
case O::S_XNOR_B32:
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseXor32, false, true, true);
case O::S_FF1_I32_B64: return S_FF1_I32_B64(inst);
case O::S_FF0_I32_B64: return S_FF_I32_B64(inst, true);
case O::S_FF1_I32_B64: return S_FF_I32_B64(inst, false);
case O::S_FLBIT_I32_B32: return V_FFBH_32(inst, false);
case O::S_FLBIT_I32_B64: return S_FLBIT_I32_B64(inst);
@@ -253,7 +279,9 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_CBRANCH_EXECZ:
case O::S_CBRANCH_EXECNZ:
case O::S_CBRANCH_CDBGSYS:
case O::S_CBRANCH_CDBGUSER:
case O::S_CBRANCH_CDBGSYS_OR_USER:
case O::S_CBRANCH_CDBGSYS_AND_USER:
case O::S_ENDPGM: return;
default: return FailMissingTranslation(inst);
}
@@ -875,30 +875,70 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
case Decoder::Opcode::V_FLOOR_F64:
case Decoder::Opcode::V_FRACT_F64: include_vector(inst.dst, 2u); [[fallthrough]];
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
case Decoder::Opcode::V_CMP_F_F64:
case Decoder::Opcode::V_CMP_LT_F64:
case Decoder::Opcode::V_CMP_EQ_F64:
case Decoder::Opcode::V_CMP_LE_F64:
case Decoder::Opcode::V_CMP_GT_F64:
case Decoder::Opcode::V_CMP_LG_F64:
case Decoder::Opcode::V_CMP_GE_F64:
case Decoder::Opcode::V_CMP_O_F64:
case Decoder::Opcode::V_CMP_U_F64:
case Decoder::Opcode::V_CMP_NGE_F64:
case Decoder::Opcode::V_CMP_NLG_F64:
case Decoder::Opcode::V_CMP_NGT_F64:
case Decoder::Opcode::V_CMP_NLE_F64:
case Decoder::Opcode::V_CMP_NEQ_F64:
case Decoder::Opcode::V_CMP_NLT_F64:
case Decoder::Opcode::V_CMP_TRU_F64:
case Decoder::Opcode::V_CMPX_F_F64:
case Decoder::Opcode::V_CMPX_LT_F64:
case Decoder::Opcode::V_CMPX_EQ_F64:
case Decoder::Opcode::V_CMPX_LE_F64:
case Decoder::Opcode::V_CMPX_GT_F64:
case Decoder::Opcode::V_CMPX_LG_F64:
case Decoder::Opcode::V_CMPX_GE_F64:
case Decoder::Opcode::V_CMP_EQ_I64:
case Decoder::Opcode::V_CMPX_O_F64:
case Decoder::Opcode::V_CMPX_U_F64:
case Decoder::Opcode::V_CMPX_NGE_F64:
case Decoder::Opcode::V_CMPX_NLG_F64:
case Decoder::Opcode::V_CMPX_NGT_F64:
case Decoder::Opcode::V_CMPX_NLE_F64:
case Decoder::Opcode::V_CMPX_NEQ_F64:
case Decoder::Opcode::V_CMPX_NLT_F64:
case Decoder::Opcode::V_CMPX_TRU_F64:
case Decoder::Opcode::V_CMP_F_I64:
case Decoder::Opcode::V_CMP_LT_I64:
case Decoder::Opcode::V_CMP_EQ_I64:
case Decoder::Opcode::V_CMP_LE_I64:
case Decoder::Opcode::V_CMP_GT_I64:
case Decoder::Opcode::V_CMP_NE_I64:
case Decoder::Opcode::V_CMP_GE_I64:
case Decoder::Opcode::V_CMP_T_I64:
case Decoder::Opcode::V_CMP_F_U64:
case Decoder::Opcode::V_CMP_LT_U64:
case Decoder::Opcode::V_CMP_EQ_U64:
case Decoder::Opcode::V_CMP_LE_U64:
case Decoder::Opcode::V_CMP_GT_U64:
case Decoder::Opcode::V_CMP_NE_U64:
case Decoder::Opcode::V_CMP_GE_U64:
case Decoder::Opcode::V_CMP_T_U64:
case Decoder::Opcode::V_CMPX_F_I64:
case Decoder::Opcode::V_CMPX_LT_I64:
case Decoder::Opcode::V_CMPX_EQ_I64:
case Decoder::Opcode::V_CMPX_LE_I64:
case Decoder::Opcode::V_CMPX_GT_I64:
case Decoder::Opcode::V_CMPX_NE_I64:
case Decoder::Opcode::V_CMPX_GE_I64:
case Decoder::Opcode::V_CMPX_T_I64:
case Decoder::Opcode::V_CMPX_F_U64:
case Decoder::Opcode::V_CMPX_LT_U64:
case Decoder::Opcode::V_CMPX_EQ_U64:
case Decoder::Opcode::V_CMPX_LE_U64:
case Decoder::Opcode::V_CMPX_GT_U64:
case Decoder::Opcode::V_CMPX_NE_U64:
case Decoder::Opcode::V_CMPX_GE_U64:
case Decoder::Opcode::V_CMPX_T_U64:
include_vector(inst.src0, 2u);
include_vector(inst.src1, 2u);
break;
@@ -1356,7 +1396,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
(options.input_info.compute->float_mode & 0x10u) == 0;
for (const auto& cfg_block: cfg.blocks) {
auto* block = blocks_by_id.at(cfg_block.id);
Translator translator(result, block, vector_limit, flush_f32_inputs);
Translator translator(result, block, vector_limit, flush_f32_inputs,
options.stage == ShaderType::Compute && !options.input_info.compute->async_compute);
for (uint32_t index = cfg_block.inst_begin; index < cfg_block.inst_end; index++) {
const auto& instruction = decoded.instructions[index];
if (IsCodeTableLoad(cfg, instruction.pc)) {
@@ -10,9 +10,9 @@ namespace Libs::Graphics::ShaderRecompiler::Frontend {
class Translator {
public:
Translator(IR::Program& program, IR::Block* block, uint32_t vector_limit,
bool flush_f32_inputs)
bool flush_f32_inputs, bool graphics_compute)
: program(program), ir(block), current_vector_limit(vector_limit),
flush_f32_inputs(flush_f32_inputs) {}
flush_f32_inputs(flush_f32_inputs), graphics_compute(graphics_compute) {}
void TranslateInstruction(const Decoder::Instruction& inst);
void TranslateEmbeddedFetch(const Decoder::Instruction& inst, uint32_t attribute,
@@ -122,12 +122,13 @@ private:
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
bool scalar, bool cmpx);
bool scalar, bool cmpx, bool swap_operands = false);
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
@@ -186,7 +187,7 @@ private:
void V_AND_OR_B32(const Decoder::Instruction& inst);
void V_OR3_B32(const Decoder::Instruction& inst);
void V_XOR3_B32(const Decoder::Instruction& inst);
void S_FF1_I32_B64(const Decoder::Instruction& inst);
void S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero);
void V_FFBH_32(const Decoder::Instruction& inst, bool sign);
void S_FLBIT_I32_B64(const Decoder::Instruction& inst);
void Integer24(const Decoder::Instruction& inst, bool sign, bool addend);
@@ -264,6 +265,7 @@ private:
uint32_t current_pc = 0;
uint32_t current_vector_limit = 1;
bool flush_f32_inputs;
bool graphics_compute;
};
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -145,6 +145,23 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
case O::V_CMP_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
true);
return;
case O::V_CMP_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
@@ -242,6 +259,74 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
return;
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
return;
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
return;
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
return;
case O::V_CMP_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
return;
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
return;
case O::V_CMP_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
return;
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
return;
case O::V_CMP_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
return;
case O::V_CMP_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
return;
case O::V_CMPX_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
return;
case O::V_CMP_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
return;
case O::V_CMPX_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
return;
case O::V_CMP_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
return;
case O::V_CMPX_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
return;
case O::V_CMP_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
return;
case O::V_CMPX_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
return;
case O::V_CMP_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
return;
case O::V_CMPX_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
return;
case O::V_CMP_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
return;
case O::V_CMPX_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
return;
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
return;
@@ -161,6 +161,7 @@ struct SamplerResource {
uint32_t first_use_pc = 0;
// Native filtering/border variants share the original sampler's runtime descriptor.
uint32_t snapshot_index = 0;
std::vector<uint32_t> indirect_resources;
bool force_point_filtering = false;
bool depth_compare = false;
bool integer_border = false;
@@ -590,6 +591,7 @@ struct ResourcePlan {
std::vector<ResourceBlock> control_flow;
std::vector<SrtRead> srt_reads;
bool requires_specialization_memory = false;
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
@@ -162,6 +162,15 @@ VALUE_OPCODE(FPOrdEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdLessThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdGreaterThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdNotEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdLessThan64, U1, F64, F64)
VALUE_OPCODE(FPOrdGreaterThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordNotEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordLessThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordGreaterThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordLessThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordGreaterThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdNotEqual32, U1, F32, F32)
VALUE_OPCODE(FPUnordNotEqual32, U1, F32, F32)
@@ -174,6 +183,7 @@ VALUE_OPCODE(FPUnordLessThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPUnordGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPIsNan32, U1, F32)
VALUE_OPCODE(FPIsNan64, U1, F64)
VALUE_OPCODE(FPCmpClass32, U1, F32, U32)
VALUE_OPCODE(FPCmpClass16, U1, U32, U32)
@@ -10,7 +10,6 @@
#include <array>
#include <bit>
#include <cstdio>
#include <cstring>
#include <fmt/format.h>
#include <functional>
#include <numeric>
@@ -141,31 +140,6 @@ bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResou
return true;
}
struct ReadCapture {
SrtRuntime source;
std::vector<std::pair<uint64_t, uint64_t>>& ranges;
};
bool CaptureStrictRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<ReadCapture*>(userdata);
if (!capture.source.read_specialization_memory(capture.source.userdata, address, values)) {
return false;
}
capture.ranges.emplace_back(address, values.size_bytes());
return true;
}
bool CaptureOrdinaryRead(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<ReadCapture*>(userdata);
if (capture.source.read_memory != nullptr) {
if (!capture.source.read_memory(capture.source.userdata, address, values)) return false;
} else {
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
}
capture.ranges.emplace_back(address, values.size_bytes());
return true;
}
const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
if (source >= program.descriptor_sources.size()) {
return nullptr;
@@ -366,20 +340,22 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else if (!indirect.selector_first.IsEmpty()) {
ShaderBufferResource material;
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
material.SwizzleEnabled() ||
material.AddTid() || material.OutOfBounds() != 0u ||
uint64_t {selector->offset} + 4u > material.Stride() ||
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
uint64_t {first} + count > material.NumRecords())
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes)
return false;
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
selector->offset + 4u >
uint64_t {UINT32_MAX} + 1u)
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
if (selector->stride == 0u &&
(material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
material.OutOfBounds() != 0u || uint64_t {selector->offset} + 4u > step ||
uint64_t {first} + count > material.NumRecords())) return false;
const auto end = count != 0u
? (uint64_t {first} + count - 1u) * step + selector->offset + 4u : 0u;
if (end > uint64_t {UINT32_MAX} + 1u || end > material.GetSize())
return false;
if (!read_keys(material, uint64_t {first} * step + selector->offset, step, count))
return false;
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
material.Stride(), count)) return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
@@ -659,6 +635,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
return false;
}
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
if (candidate >= base.samplers.size()) return false;
usage[candidate] |= usage[pair.sampler];
}
}
for (uint32_t index = 0; index < base.samplers.size(); index++) {
auto& mapping = plan.mapping[index];
@@ -927,6 +907,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.user_data_count = program.user_data_count;
plan.info = program.info;
plan.memory_info = program.memory_info;
plan.source_reads = program.source_reads;
plan.srt_plan_complete = program.srt_plan_complete;
plan.resource_tracking_complete = program.resource_tracking_complete;
@@ -1007,10 +988,15 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// A proven uniform factor can decide a branch even when its other lanes are unknown.
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
bool capture_control_reads = false;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) return Clone(value);
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
@@ -1040,7 +1026,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.capture_specialization_reads |= capture_indirect_reads || !plan.control_flow.empty();
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those reads
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
@@ -1075,27 +1062,23 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
}
}
if (capture_indirect_reads) plan.resource_tracking_complete &= !program.has_address_writes;
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
return plan;
}
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization) {
if (!program.resource_tracking_complete ||
(program.requires_specialization_memory && runtime.read_specialization_memory == nullptr)) {
((program.requires_specialization_memory || !program.source_reads.empty()) &&
runtime.read_specialization_memory == nullptr)) {
return false;
}
const bool capture_reads = program.capture_specialization_reads;
const bool capture_reads = program.capture_specialization_reads || !program.source_reads.empty();
auto& reads = snapshot.specialization_reads;
reads.clear();
ReadCapture capture {runtime, reads};
SrtRuntime observed = runtime;
if (capture_reads) {
observed.userdata = &capture;
observed.read_specialization_memory = runtime.read_specialization_memory != nullptr
? CaptureStrictRead : nullptr;
observed.read_memory = CaptureOrdinaryRead;
}
reads = program.source_reads;
SrtReadCapture capture {runtime, reads};
const auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed,
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
@@ -1221,7 +1204,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
snapshot.samplers.resize(program.info.samplers.size());
for (uint32_t i = 0; i < program.info.samplers.size(); ++i) {
if (!evaluate(program.info.samplers[i].source, snapshot.samplers[i])) {
auto source = program.info.samplers[i].source;
if (source >= program.descriptor_sources.size()) return false;
const auto& descriptor = program.descriptor_sources[source];
if (descriptor.indirect_descriptor) {
if (descriptor.indirect_descriptor->sources.empty()) return false;
source = descriptor.indirect_descriptor->sources[0];
}
if (!evaluate(source, snapshot.samplers[i])) {
return false;
}
if (program.info.samplers[i].gather_lod) {
@@ -1313,11 +1303,20 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
const auto& binding = sampler_plan.bindings[index];
for (auto& candidate: samplers[index].indirect_resources) {
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
EXIT_IF(candidate == UINT32_MAX);
}
}
for (auto& pair: sampled_pairs) {
const auto type = static_cast<uint32_t>(ClassifySampler(images[pair.image]));
pair.sampler = sampler_plan.mapping[pair.sampler][type];
EXIT_IF(pair.sampler == UINT32_MAX);
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
for (const auto candidate: samplers[pair.sampler].indirect_resources)
samplers[candidate].depth_compare |= images[pair.image].depth_compare;
}
auto& memory_info = program.memory_info;
@@ -1405,7 +1404,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
EXIT_IF(memory.sampler == UINT32_MAX);
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw);
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}
for (auto& memory: memory_info) {
@@ -430,7 +430,7 @@ public:
plan.handle->SetArg(word, Value(0u));
continue;
}
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
if (plan.handle->NumArgs() == 4u) {
plan.handle->SetArg(0, plan.key);
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
} else {
@@ -609,23 +609,37 @@ private:
return selected.IsEmpty() ? value : selected;
}
Value LowerDescriptorPhi(Value value) {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi ||
phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u || phi->GetType() != Type::U32) {
return value;
}
const auto* merge = phi->Parent();
const auto* branch = phi->PhiBlock(0);
if (merge == nullptr || branch == nullptr || phi->PhiBlock(1) == nullptr ||
branch == phi->PhiBlock(1)) {
return value;
}
for (const auto& [original, selected]: m_descriptor_selections) {
if (original == phi) {
return selected;
Value LowerDescriptorValue(Value value) {
value = value.Resolve();
const auto* inst = value.TryInstruction();
if (inst == nullptr) return value;
// The scalar planner owns reads and lowers their address handles separately.
uint32_t memory_index = 0;
if (ScalarReadMemory(*inst, memory_index) != nullptr) return value;
const auto [cached, inserted] = m_descriptor_values.emplace(inst, value);
if (!inserted) return cached->second;
if (inst->GetOpcode() != ValueOpcode::Phi) {
Inst* projected = nullptr;
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
const auto lowered = LowerDescriptorValue(inst->Arg(arg));
if (lowered == inst->Arg(arg).Resolve()) continue;
if (projected == nullptr) {
projected = &m_program.value_storage.emplace_back(inst->GetOpcode(), inst->Flags<uint64_t>());
for (size_t i = 0; i < inst->NumArgs(); ++i) projected->SetArg(i, inst->Arg(i));
}
projected->SetArg(arg, lowered);
}
if (projected != nullptr) cached->second = Value(projected);
return cached->second;
}
if (inst->NumArgs() != 2u || inst->NumPhiBlocks() != 2u || inst->GetType() != Type::U32) {
return value;
}
const auto* merge = inst->Parent();
const auto* branch = inst->PhiBlock(0);
if (merge == nullptr || branch == nullptr || inst->PhiBlock(1) == nullptr ||
branch == inst->PhiBlock(1)) {
return value;
}
if (branch->ImmSuccessors().size() != 2u) {
if (branch->ImmPredecessors().size() != 1u) {
@@ -638,7 +652,7 @@ private:
}
std::array<const Block*, 2> targets;
for (uint32_t arm = 0; arm < 2; arm++) {
const auto* incoming = phi->PhiBlock(arm);
const auto* incoming = inst->PhiBlock(arm);
if (incoming == merge ||
(incoming != branch &&
(incoming->ImmPredecessors().size() != 1u ||
@@ -654,19 +668,20 @@ private:
if (term.kind != CFG::TerminatorKind::ConditionalBranch ||
!((term.true_block == targets[0] && term.false_block == targets[1]) ||
(term.false_block == targets[0] && term.true_block == targets[1])) ||
!ValidateRuntimeValue(m_program, info.condition, RuntimeValueType::Integer) ||
!ValidateRuntimeValue(m_program, phi->Arg(0)) ||
!ValidateRuntimeValue(m_program, phi->Arg(1))) {
!ValidateRuntimeValue(m_program, info.condition, m_shader_writes
? RuntimeValueType::ImmutableInteger : RuntimeValueType::Integer) ||
!ValidateRuntimeValue(m_program, inst->Arg(0)) ||
!ValidateRuntimeValue(m_program, inst->Arg(1))) {
return value;
}
// Retain a host expression; replacing the GPU Phi would break SSA dominance.
const auto true_arg = term.true_block == targets[0] ? 0u : 1u;
auto& selected = m_program.value_storage.emplace_back(ValueOpcode::SelectU32);
selected.SetArg(0, info.condition);
selected.SetArg(1, phi->Arg(true_arg));
selected.SetArg(2, phi->Arg(true_arg ^ 1u));
m_descriptor_selections.emplace_back(phi, Value(&selected));
return Value(&selected);
selected.SetArg(1, inst->Arg(true_arg));
selected.SetArg(2, inst->Arg(true_arg ^ 1u));
cached->second = Value(&selected);
return cached->second;
}
void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust,
@@ -686,7 +701,7 @@ private:
for (uint32_t i = 0; i < width; i++) {
const auto value = base_reg != UINT32_MAX
? NativeDescriptorSource(handle.Arg(i), base_reg + i, pc) : handle.Arg(i);
descriptor.dwords[i] = LowerDescriptorPhi(value);
descriptor.dwords[i] = LowerDescriptorValue(value);
}
if (sample_adjust) {
descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]);
@@ -1463,7 +1478,18 @@ private:
const auto* step = phi->Arg(initial ^ 1u).Resolve().TryInstruction();
if (!zero.IsImmediate() || zero.GetType() != Type::U32 || zero.U32() != 0u ||
step == nullptr || step->GetOpcode() != ValueOpcode::IAdd32 ||
step->Parent() != phi->PhiBlock(initial ^ 1u)) continue;
step->Parent() == nullptr) continue;
// Structurization can forward the native increment through a synthetic continue block.
const auto* incoming = phi->PhiBlock(initial ^ 1u);
const auto* next = phi->Parent();
for (size_t edges = 0; incoming != step->Parent() && edges < m_program.blocks.size(); ++edges) {
if (incoming == nullptr || incoming->terminator.kind != CFG::TerminatorKind::Branch ||
incoming->terminator.true_block != next || incoming->ImmPredecessors().size() != 1u)
break;
next = incoming;
incoming = incoming->ImmPredecessors()[0];
}
if (incoming != step->Parent()) continue;
uint32_t increment = 0;
if ((step->Arg(0).Resolve() == key &&
ImmediateU32(step->Arg(1), increment) && increment == 1u) ||
@@ -1501,6 +1527,30 @@ private:
});
}
bool MatchScalarMaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
const auto* read = key.Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::ReadConstBuffer) return false;
uint32_t memory_index = 0;
const auto* memory = ScalarReadMemory(*read, memory_index);
Value index;
uint32_t offset = 0, stride = 0;
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(memory_index, *read) ||
!MatchTableOffset(read->Arg(1), index, offset, stride) ||
((offset | stride | memory->offset) & 3u) != 0u ||
uint64_t {offset} + memory->offset > UINT32_MAX) return false;
const auto* bound = BoundedLoop(index, read->Parent());
if (bound == nullptr || bound->GetOpcode() != ValueOpcode::ULessThan32 ||
!MakeRuntimeTableSource(*read, material_source)) return false;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
.source = InternSource(material_source), .stride = stride,
.offset = offset + memory->offset});
indirect.selector_first = Value(0u);
indirect.key_count = bound->Arg(1);
return true;
}
void FoldBoundedLoopSelectors() {
for (auto* header: m_program.blocks) {
for (auto& induction: *header) {
@@ -1736,6 +1786,10 @@ private:
}
if ((table_offset & 3u) != 0u ||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
} else if (!plan.table_indexed &&
MatchScalarMaterialKey(key, indirect, material_source)) {
// The host enumerates the guarded scalar loop; its key and T# payload stay on the GPU.
plan.retain_reads = true;
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
@@ -1899,17 +1953,21 @@ private:
return found;
}
bool TryMakeFiniteImage(Inst& handle, IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u)
bool TryMakeFiniteDescriptor(Inst& handle, IndirectDescriptorPlan& plan) {
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
if ((!sampler && handle.GetOpcode() != ValueOpcode::GetImageResource) ||
handle.NumArgs() != (sampler ? 4u : 8u))
return false;
if (std::ranges::any_of(handle.Uses(), [](const Use& use) {
if (std::ranges::any_of(handle.Uses(), [&](const Use& use) {
const auto op = use.user->GetOpcode();
return op != ValueOpcode::ImageSampleRaw && op != ValueOpcode::ImageGatherRaw;
if (op != ValueOpcode::ImageSampleRaw) return sampler || op != ValueOpcode::ImageGatherRaw;
return sampler && (m_program.memory_info[use.user->Flags<MemoryFlags>().index]
.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0u;
})) return false;
bool has_phi = false;
for (size_t word = 0; word < handle.NumArgs(); ++word) has_phi |= handle.Arg(word).Resolve().IsPhi();
if (!has_phi) return false;
// Normalize the eight synchronized descriptor words to one GPU ordinal.
// Normalize synchronized descriptor words to one GPU ordinal.
// Build the complete graph before changing IR so an unsupported leaf is transactional.
struct Choice {
DescriptorSource descriptor;
@@ -1925,7 +1983,15 @@ private:
const auto index = static_cast<uint32_t>(choices.size());
choices.push_back({descriptor});
uint32_t bad = 0;
if (ValidateSource(descriptor, bad)) return index;
if (ValidateSource(descriptor, bad)) {
// Finite inline samplers have literal leaves; live descriptor tables use the
// existing direct sampler planning path.
if (sampler && !std::ranges::all_of(
std::span(descriptor.dwords).first(descriptor.dword_count),
[](Value word) { return word.IsImmediate() && word.GetType() == Type::U32; }))
return UINT32_MAX;
return index;
}
const auto* branch = descriptor.dwords[bad].TryInstruction();
if (branch == nullptr || branch->Parent() == nullptr ||
branch->GetOpcode() != ValueOpcode::Phi ||
@@ -1962,14 +2028,14 @@ private:
return index;
};
DescriptorSource root;
root.dword_count = 8u;
root.dword_count = handle.NumArgs();
for (uint32_t word = 0; word < root.dword_count; ++word) root.dwords[word] = handle.Arg(word);
if (visit(visit, root) == UINT32_MAX || choices.front().branch == nullptr) return false;
DescriptorSource image_source;
image_source.dword_count = 8u;
image_source.dwords.fill(Value(0u));
image_source.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{});
auto& sources = image_source.indirect_descriptor->sources;
DescriptorSource finite_source;
finite_source.dword_count = root.dword_count;
finite_source.dwords.fill(Value(0u));
finite_source.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{});
auto& sources = finite_source.indirect_descriptor->sources;
for (auto& choice: choices) {
if (choice.branch != nullptr) continue;
const auto source = InternSource(choice.descriptor);
@@ -1998,9 +2064,9 @@ private:
}
}
plan.handle = &handle;
plan.source = InternSource(image_source);
plan.source = InternSource(finite_source);
plan.key = choices.front().key;
plan.roots = image_source.dwords;
plan.roots = finite_source.dwords;
plan.reads.fill(nullptr);
return true;
}
@@ -2031,27 +2097,33 @@ private:
inst.NumArgs() == 0u) {
continue;
}
auto* handle = inst.Arg(0).Resolve().TryInstruction();
if (handle == nullptr || FindIndirectDescriptor(*handle) != nullptr) {
continue;
}
const auto flags = inst.Flags<MemoryFlags>();
const auto& memory = m_program.memory_info[flags.index];
DescriptorSource descriptor;
MakeSource(*handle, inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
false, false, memory.resource * 4u, descriptor, flags.pc);
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) continue;
IndirectDescriptorPlan plan;
if (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteImage(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan)) {
for (const auto& previous: m_indirect_descriptors) {
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
!EquivalentValue(m_program, previous.key, plan.key))
Fail(flags.pc, "shared descriptor read has incompatible keys");
const auto plan_handle = [&](Value value, uint32_t width, uint32_t base, bool sampler) {
auto* handle = value.Resolve().TryInstruction();
if (handle == nullptr || FindIndirectDescriptor(*handle) != nullptr) return;
DescriptorSource descriptor;
MakeSource(*handle, width, sampler, false, base, descriptor, flags.pc);
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) return;
IndirectDescriptorPlan plan;
if (sampler ? TryMakeFiniteDescriptor(*handle, plan)
: (TryMakeIndirectImage(*handle, descriptor, plan) ||
TryMakeFiniteDescriptor(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan))) {
for (const auto& previous: m_indirect_descriptors) {
if (plan.reads[0] != nullptr && previous.reads[0] == plan.reads[0] &&
!EquivalentValue(m_program, previous.key, plan.key))
Fail(flags.pc, "shared descriptor read has incompatible keys");
}
m_indirect_descriptors.push_back(std::move(plan));
}
m_indirect_descriptors.push_back(std::move(plan));
}
};
plan_handle(inst.Arg(0), inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
memory.resource * 4u, false);
if (inst.GetOpcode() == ValueOpcode::ImageSampleRaw &&
(memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) == 0u)
plan_handle(inst.Arg(1), 4u, memory.sampler * 4u, true);
}
}
}
@@ -2180,7 +2252,20 @@ private:
return UINT32_MAX;
}
m_info.samplers.push_back({source, pc});
return static_cast<uint32_t>(m_info.samplers.size() - 1);
const auto root = static_cast<uint32_t>(m_info.samplers.size() - 1);
const auto* descriptor = Source(source);
if (descriptor != nullptr && descriptor->indirect_descriptor) {
const auto& sources = descriptor->indirect_descriptor->sources;
if (sources.empty()) Fail(pc, "finite sampler has no candidates");
std::vector<uint32_t> resources {root};
for (size_t i = 1; i < sources.size(); ++i) {
const auto candidate = AddSampler(sources[i], pc);
if (candidate == UINT32_MAX) return UINT32_MAX;
resources.push_back(candidate);
}
m_info.samplers[root].indirect_resources = std::move(resources);
}
return root;
}
void AddSampledPair(uint32_t image, uint32_t sampler, uint32_t pc) {
@@ -2340,8 +2425,15 @@ private:
uint32_t sampler_source = 0;
const bool sample_adjust =
(memory.image_sample_flags & Decoder::ImageSampleFlagAdjust) != 0;
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc,
memory.sampler * 4u, sampler_handle, sampler_source, true, sample_adjust);
sampler_handle = inst.Arg(1).Resolve().TryInstruction();
const auto* selected_sampler = sampler_handle != nullptr
? FindIndirectDescriptor(*sampler_handle) : nullptr;
if (selected_sampler != nullptr) {
sampler_source = selected_sampler->source;
} else {
GetHandle(inst.Arg(1), ValueOpcode::GetSamplerResource, 4, flags.pc,
memory.sampler * 4u, sampler_handle, sampler_source, true, sample_adjust);
}
sampler = AddSampler(sampler_source, flags.pc);
if (sampler == UINT32_MAX) {
Fail(flags.pc, "sampler resource limit exceeded");
@@ -2397,7 +2489,7 @@ private:
std::vector<HandlePatch> m_handle_patches;
std::vector<MemoryPatch> m_memory_patches;
std::vector<IndirectDescriptorPlan> m_indirect_descriptors;
std::vector<std::pair<const Inst*, Value>> m_descriptor_selections;
std::map<const Inst*, Value> m_descriptor_values;
bool m_shader_writes = false;
};
@@ -11,6 +11,35 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
SrtRuntime SrtReadCapture::ObservedRuntime() {
auto runtime = m_source;
runtime.userdata = this;
runtime.read_specialization_memory = m_source.read_specialization_memory != nullptr
? ReadStrict : nullptr;
runtime.read_memory = ReadOrdinary;
return runtime;
}
bool SrtReadCapture::ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<SrtReadCapture*>(userdata);
if (!capture.m_source.read_specialization_memory(capture.m_source.userdata, address, values)) {
return false;
}
capture.m_ranges.emplace_back(address, values.size_bytes());
return true;
}
bool SrtReadCapture::ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values) {
auto& capture = *static_cast<SrtReadCapture*>(userdata);
if (capture.m_source.read_memory != nullptr) {
if (!capture.m_source.read_memory(capture.m_source.userdata, address, values)) return false;
} else {
std::memcpy(values.data(), reinterpret_cast<const void*>(address), values.size_bytes());
}
capture.m_ranges.emplace_back(address, values.size_bytes());
return true;
}
SrtRuntime CleanRuntime(SrtRuntime runtime) {
runtime.read_memory = runtime.read_specialization_memory != nullptr
? runtime.read_specialization_memory
@@ -159,7 +188,7 @@ private:
bool Validate(Value value, bool require_uniform = true) {
value = value.Resolve();
// Host floating-point evaluation does not model shader rounding/denormal modes.
if (m_type == RuntimeValueType::Integer &&
if (m_type != RuntimeValueType::Any &&
TypesOverlap(value.GetType(), Type::F16 | Type::F32 | Type::F32x2)) {
return false;
}
@@ -188,6 +217,13 @@ private:
return valid;
};
const auto op = inst->GetOpcode();
if (m_type == RuntimeValueType::ImmutableInteger &&
(op == ValueOpcode::ReadConst || BufferAccessOf(op) != BufferAccess::None ||
AddressOpcodeInfoOf(op).access != AddressAccess::None ||
ImageOpcodeInfoOf(op).access != ImageAccess::None ||
SharedAccessOf(op) != SharedAccess::None)) {
return finish(false);
}
if (op == ValueOpcode::ReadConst) {
const auto slot = inst->NumArgs() == 2 ? inst->Arg(1).Resolve() : Value {};
if (inst->NumArgs() != 2 || inst->Arg(0).Resolve().TryInstruction() == nullptr ||
@@ -197,7 +233,7 @@ private:
slot.U32() >= m_program.srt_reads.size()) {
return finish(false);
}
if (m_type == RuntimeValueType::Integer) {
if (m_type != RuntimeValueType::Any) {
const auto active_mask = m_active_mask;
m_active_mask = {};
const bool valid = Validate(m_program.srt_reads[slot.U32()].value);
@@ -209,7 +245,7 @@ private:
if (!m_active_mask.IsEmpty() && IsRuntimeSelect(op) && inst->NumArgs() == 3 &&
inst->Arg(0).Resolve() == m_active_mask) {
// Empty EXEC reads lane zero, so ignored operands still require integer types.
if (m_type == RuntimeValueType::Integer && !Validate(inst->Arg(2), false)) {
if (m_type != RuntimeValueType::Any && !Validate(inst->Arg(2), false)) {
return finish(false);
}
return finish(Validate(inst->Arg(1)));
@@ -237,7 +273,7 @@ private:
return finish(true);
}
if (op == ValueOpcode::Phi) {
if (m_type == RuntimeValueType::Integer && !ValidateArguments(*inst, false)) {
if (m_type != RuntimeValueType::Any && !ValidateArguments(*inst, false)) {
return finish(false);
}
const auto invariant = ResolveInvariantPhi(m_program, value);
@@ -251,7 +287,7 @@ private:
inst->Arg(1).GetType() != Type::U1) {
return finish(false);
}
if (m_type == RuntimeValueType::Integer && !Validate(inst->Arg(1), false)) {
if (m_type != RuntimeValueType::Any && !Validate(inst->Arg(1), false)) {
return finish(false);
}
const auto active_mask = m_active_mask;
@@ -959,7 +995,8 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
}
uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() && m_runtime.read_specialization_memory != nullptr &&
if (!block.condition.IsEmpty() &&
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else {
@@ -20,6 +20,20 @@ struct SrtRuntime {
std::span<const uint32_t> workgroup_counts;
};
class SrtReadCapture {
public:
SrtReadCapture(SrtRuntime source, std::vector<std::pair<uint64_t, uint64_t>>& ranges):
m_source(source), m_ranges(ranges) {}
SrtRuntime ObservedRuntime();
private:
static bool ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values);
SrtRuntime m_source;
std::vector<std::pair<uint64_t, uint64_t>>& m_ranges;
};
// Retained reads no longer need the guest instruction PC. A clean read evaluates
// its address and bounds through the strict reader as well as the final DWORD.
struct SrtReadFlags {
@@ -27,7 +41,7 @@ struct SrtReadFlags {
uint32_t clean = 0;
};
enum class RuntimeValueType { Any, Integer };
enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
RuntimeValueType type = RuntimeValueType::Any);
+4 -1
View File
@@ -574,10 +574,12 @@ static void ShaderGetStaticInputInfoPS(
static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& /*sh*/,
const ShaderMappedData& data, ShaderComputeInputInfo& info) {
const bool async_compute = info.async_compute;
const bool dispatch_thread_dimensions = info.dispatch_thread_dimensions;
const auto host_subgroup_size = info.host_subgroup_size;
const auto workgroup_counts = std::to_array(info.workgroup_counts);
info = {};
info.async_compute = async_compute;
info.dispatch_thread_dimensions = dispatch_thread_dimensions;
info.host_subgroup_size = host_subgroup_size;
std::ranges::copy(workgroup_counts, info.workgroup_counts);
@@ -693,7 +695,8 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_
key.push_back(info.thread_ids_num);
key.push_back(info.lds_size_dwords);
key.push_back(info.scratch_size_dwords);
key.push_back(static_cast<uint32_t>(info.dispatch_thread_dimensions));
key.push_back(static_cast<uint32_t>(info.dispatch_thread_dimensions) |
(static_cast<uint32_t>(info.async_compute) << 1u));
for (int i = 0; i < 3; i++) {
key.push_back(info.threads_num[i]);
key.push_back(static_cast<uint32_t>(info.group_id[i]));
+1
View File
@@ -146,6 +146,7 @@ struct ShaderVertexInputInfo {
};
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
bool async_compute = false;
uint8_t float_mode = 0xc0;
uint32_t dispatch_threads_num[3] = {0, 0, 0};
uint32_t workgroup_counts[3] = {0, 0, 0};
+13 -1
View File
@@ -1,5 +1,6 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -42,7 +43,13 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher kyty_emulator)
add_custom_target(stage_updater
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:launcher>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:kyty_updater>
$<TARGET_FILE_DIR:launcher>/$<TARGET_FILE_NAME:kyty_updater>
DEPENDS kyty_updater
VERBATIM)
add_dependencies(launcher kyty_emulator stage_updater)
if(KYTY_CLANG_CL)
add_custom_command(TARGET launcher POST_BUILD
@@ -96,6 +103,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
@@ -112,6 +120,10 @@ if(APPLE)
")
endif()
if(NOT APPLE)
install(TARGETS kyty_updater RUNTIME DESTINATION .)
endif()
if(WIN32)
install(CODE "
execute_process(
+112 -43
View File
@@ -204,6 +204,72 @@
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="grid_size_slider">
<property name="maximumSize">
<size><width>120</width><height>16777215</height></size>
</property>
<property name="toolTip">
<string>Game image size</string>
</property>
<property name="accessibleName">
<string>Game image size</string>
</property>
<property name="minimum">
<number>128</number>
</property>
<property name="maximum">
<number>320</number>
</property>
<property name="singleStep">
<number>16</number>
</property>
<property name="pageStep">
<number>32</number>
</property>
<property name="value">
<number>224</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="gridSizeSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size><width>8</width><height>0</height></size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="grid_view_button">
<property name="minimumSize">
<size><width>36</width><height>36</height></size>
</property>
<property name="text">
<string>Grid view</string>
</property>
<property name="toolTip">
<string>Toggle grid view</string>
</property>
<property name="iconSize">
<size><width>32</width><height>32</height></size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search_line_edit">
<property name="sizePolicy">
@@ -238,50 +304,53 @@
</layout>
</item>
<item>
<widget class="GameListTreeWidget" name="cfgs_list">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Game</string>
<widget class="QStackedWidget" name="game_views">
<widget class="GameListTreeWidget" name="cfgs_list">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</column>
<column>
<property name="text">
<string>Serial</string>
</property>
</column>
<column>
<property name="text">
<string>Game Version</string>
</property>
</column>
<column>
<property name="text">
<string>Firmware Version</string>
</property>
</column>
<column>
<property name="text">
<string>Size</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Status</string>
</property>
</column>
<column>
<property name="text">
<string>Comments</string>
</property>
</column>
<column>
<property name="text">
<string>Game</string>
</property>
</column>
<column>
<property name="text">
<string>Serial</string>
</property>
</column>
<column>
<property name="text">
<string>Game Version</string>
</property>
</column>
<column>
<property name="text">
<string>Firmware Version</string>
</property>
</column>
<column>
<property name="text">
<string>Size</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Status</string>
</property>
</column>
<column>
<property name="text">
<string>Comments</string>
</property>
</column>
</widget>
<widget class="QListView" name="cfgs_grid"/>
</widget>
</item>
</layout>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#000000" stroke-width="2" stroke-linejoin="round">
<rect x="5" y="5" width="8" height="8" rx="1"/>
<rect x="19" y="5" width="8" height="8" rx="1"/>
<rect x="5" y="19" width="8" height="8" rx="1"/>
<rect x="19" y="19" width="8" height="8" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 363 B

@@ -12,9 +12,9 @@
class ConfigurationItem;
class CompatibilityDatabase;
class ControllerPreview;
class QEvent;
class QTreeWidgetItem;
class QPoint;
namespace Ui {
class ConfigurationListWidget;
@@ -29,6 +29,7 @@ public:
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
void SetControllerPreview(ControllerPreview* preview) { m_controller_preview = preview; }
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
@@ -65,13 +66,13 @@ protected slots:
void edit_global_settings();
void edit_input_mapping();
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
void show_context_menu(const QPoint& pos);
void open_game_folder();
void remove_save_data();
void filter_configurations(const QString& text);
private:
void SelectItem(QTreeWidgetItem* witem);
void ShowContextMenu(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
void ImportGameSettings(QWidget* parent);
@@ -87,6 +88,7 @@ private:
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
CompatibilityDatabase* m_compatibility = nullptr;
ControllerPreview* m_controller_preview = nullptr;
};
#endif // CONFIGURATION_LIST_WIDGET_H
-29
View File
@@ -1,29 +0,0 @@
#ifndef CONTROLLER_LIGHTBAR_H
#define CONTROLLER_LIGHTBAR_H
#include <QColor>
#include <QHash>
#include <QObject>
#include <QString>
#include <QTimer>
#include <SDL3/SDL_gamepad.h>
class ControllerLightbar final: public QObject {
public:
explicit ControllerLightbar(QObject* parent = nullptr);
~ControllerLightbar() override;
void SetColor(const QString& color);
void Stop();
private:
void Refresh();
void ApplyColor(SDL_Gamepad* gamepad) const;
QTimer m_timer;
QHash<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
QColor m_color;
};
#endif // CONTROLLER_LIGHTBAR_H
+53
View File
@@ -0,0 +1,53 @@
#ifndef CONTROLLER_PREVIEW_H
#define CONTROLLER_PREVIEW_H
#include <QColor>
#include <QMap>
#include <QObject>
#include <QPointF>
#include <QSet>
#include <QString>
#include <QTimer>
#include <SDL3/SDL_gamepad.h>
class ControllerPreview final: public QObject {
Q_OBJECT
public:
explicit ControllerPreview(QObject* parent = nullptr);
~ControllerPreview() override;
void SetColor(const QString& color);
void SetPreviewEnabled(bool enabled);
void Stop();
void SetSuspended(bool suspended);
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
[[nodiscard]] const QString& Error() const { return m_error; }
signals:
void StateChanged();
private:
void UpdateRunning();
void OpenGamepad(SDL_JoystickID id);
void Refresh();
void ApplyColor(SDL_Gamepad* gamepad) const;
QTimer m_timer;
QMap<SDL_JoystickID, SDL_Gamepad*> m_gamepads;
QColor m_color;
bool m_preview_enabled = false;
bool m_suspended = false;
QSet<QString> m_pressed;
QPointF m_left_stick;
QPointF m_right_stick;
QString m_device_name;
QString m_error;
};
#endif // CONTROLLER_PREVIEW_H
+40
View File
@@ -0,0 +1,40 @@
#ifndef LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
#define LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_
#include <QPointF>
#include <QSet>
#include <QString>
#include <QTransform>
#include <QWidget>
class DualSenseWidget final: public QWidget {
Q_OBJECT
public:
explicit DualSenseWidget(QWidget* parent = nullptr);
[[nodiscard]] QSize sizeHint() const override;
void SetPressed(const QSet<QString>& pressed);
void SetSelected(const QString& selected);
void SetSticks(const QPointF& left, const QPointF& right);
signals:
void ControlClicked(const QString& id);
protected:
void paintEvent(QPaintEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void leaveEvent(QEvent* event) override;
private:
[[nodiscard]] QTransform ViewTransform() const;
[[nodiscard]] QString HitTest(const QPointF& position) const;
QSet<QString> m_pressed;
QString m_selected;
QString m_hovered;
QPointF m_left_stick;
QPointF m_right_stick;
};
#endif /* LAUNCHER_INCLUDE_DUALSENSE_WIDGET_H_ */
+18 -12
View File
@@ -2,32 +2,38 @@
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
#include <QDialog>
#include <QHash>
#include <QStringList>
class ControllerPreview;
class DualSenseWidget;
class QLabel;
class QPushButton;
class QDoubleSpinBox;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
class InputMappingDialog final: public QDialog {
public:
explicit InputMappingDialog(const QStringList& mapping, QWidget* parent = nullptr);
explicit InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
QWidget* parent = nullptr);
~InputMappingDialog() override;
[[nodiscard]] QStringList Mapping() const;
private:
void ChangeBinding();
void ClearBinding();
void ChangeBinding(const QString& id);
void RestoreDefaults();
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
void UpdatePreview();
void UpdateHighlights();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
QDoubleSpinBox* m_sensitivity = nullptr;
bool m_custom_bindings = false;
ControllerPreview* m_preview;
DualSenseWidget* m_controller = nullptr;
QHash<QString, QString> m_bindings;
QHash<QString, QPushButton*> m_buttons;
QString m_selected;
QLabel* m_status = nullptr;
QDoubleSpinBox* m_sensitivity = nullptr;
bool m_custom_bindings = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
+41 -17
View File
@@ -1,12 +1,21 @@
#ifndef UPDATE_CHECKER_H
#define UPDATE_CHECKER_H
#include "updateRelease.h"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QPointer>
#include <QProcess>
class QByteArray;
#include <functional>
#include <memory>
class QNetworkReply;
class QProgressDialog;
class QTemporaryDir;
class QWidget;
class UpdateChecker final: public QObject {
@@ -14,28 +23,43 @@ class UpdateChecker final: public QObject {
public:
explicit UpdateChecker(QWidget* parent);
~UpdateChecker() override;
[[nodiscard]] static bool IsSupported();
void Check(bool manual);
static void ShowPreviousResult(QWidget* parent);
void Check(bool manual);
void SetGameRunning(bool running) { m_game_running = running; }
[[nodiscard]] bool IsInstalling() const { return m_installing; }
signals:
void CheckingChanged(bool checking);
void InstallingChanged(bool installing);
private:
struct UpdateInfo;
void FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done);
void CheckCurrent(const UpdateRelease::Info& current, bool manual);
void OfferUpdate();
void Download();
void ReadDownload(QNetworkReply* reply);
void PreparePackage();
void RunPreparation(const QString& program, const QStringList& arguments, bool validate);
void Restart();
void Finish(QString error = {}, bool report = true);
static UpdateInfo ParseUpdateInfo(const QByteArray& data);
void FetchUpdateInfo(const char* url, bool fallback, bool manual);
void ShowUpdateResult(const UpdateInfo& info, bool manual);
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
QWidget* m_parent;
QNetworkAccessManager m_network;
QPointer<QNetworkReply> m_reply;
QPointer<QProgressDialog> m_progress;
QProcess m_prepare;
std::unique_ptr<QTemporaryDir> m_workspace;
QFile m_download;
QCryptographicHash m_hash {QCryptographicHash::Sha256};
UpdateRelease::Info m_release;
QString m_download_error;
bool m_busy = false;
bool m_installing = false;
bool m_game_running = false;
bool m_canceled = false;
};
#endif // UPDATE_CHECKER_H
+38
View File
@@ -0,0 +1,38 @@
#ifndef UPDATE_RELEASE_H
#define UPDATE_RELEASE_H
#include <QByteArray>
#include <QDateTime>
#include <QString>
#include <QUrl>
namespace UpdateRelease {
inline const QString LATEST_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest");
inline const QString TAG_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/tags/%1");
inline const QString PAGE_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/%1");
inline const QString DOWNLOAD_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/download/%1/%2");
struct Info {
QString tag;
QUrl page_url;
QUrl download_url;
QByteArray sha256;
qint64 size = 0;
QDateTime published_at;
QString error;
};
enum class VersionRelation { Current, Newer, Older, Unknown };
[[nodiscard]] QString PlatformSuffix();
[[nodiscard]] Info Parse(const QByteArray& data, const QString& platform_suffix);
[[nodiscard]] VersionRelation Compare(const Info& latest, const Info& current);
} // namespace UpdateRelease
#endif // UPDATE_RELEASE_H
+1
View File
@@ -4,6 +4,7 @@
<file alias="global-settings.svg">icons/global-settings.svg</file>
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="refresh.svg">icons/refresh.svg</file>
<file alias="grid-view.svg">icons/grid-view.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
<file alias="trophy.svg">icons/trophy.svg</file>
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
+82 -4
View File
@@ -34,6 +34,7 @@
#include <QJsonParseError>
#include <QKeySequence>
#include <QLineEdit>
#include <QListView>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
@@ -46,7 +47,9 @@
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
#include <QSlider>
#include <QStyle>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QTreeWidget>
#include <QUrl>
@@ -141,6 +144,26 @@ static QStringList SettingsStringList(const QVariant& value) {
return text.isEmpty() ? QStringList() : QStringList({text});
}
class GameGridDelegate: public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex&) const override {
return option.decorationSize + QSize(16, 24 + option.fontMetrics.lineSpacing() * 2);
}
protected:
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override {
const auto image_size = option->decorationSize;
QStyledItemDelegate::initStyleOption(option, index);
option->decorationSize = image_size;
option->decorationPosition = QStyleOptionViewItem::Top;
option->decorationAlignment = Qt::AlignCenter;
option->displayAlignment = Qt::AlignHCenter | Qt::AlignTop;
option->features |= QStyleOptionViewItem::WrapText;
}
};
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->setAlternatingRowColors(false);
ui->cfgs_list->setAllColumnsShowFocus(true);
@@ -152,6 +175,21 @@ static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->cfgs_list->header()->setHighlightSections(false);
ui->cfgs_list->header()->setStretchLastSection(true);
// Both presentations use the same items, icons and current game.
ui->cfgs_grid->setModel(ui->cfgs_list->model());
ui->cfgs_grid->setSelectionModel(ui->cfgs_list->selectionModel());
ui->cfgs_grid->setViewMode(QListView::IconMode);
ui->cfgs_grid->setMovement(QListView::Static);
ui->cfgs_grid->setResizeMode(QListView::Adjust);
ui->cfgs_grid->setSelectionMode(QAbstractItemView::SingleSelection);
ui->cfgs_grid->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->cfgs_grid->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->cfgs_grid->setSpacing(8);
ui->cfgs_grid->setUniformItemSizes(true);
ui->cfgs_grid->setWordWrap(true);
ui->cfgs_grid->setItemDelegate(new GameGridDelegate(ui->cfgs_grid));
ui->cfgs_grid->setContextMenuPolicy(Qt::CustomContextMenu);
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
@@ -252,7 +290,27 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
&ConfigurationListWidget::list_itemDoubleClicked);
connect(m_ui->cfgs_list, &QTreeWidget::customContextMenuRequested, this,
&ConfigurationListWidget::show_context_menu);
[this](const QPoint& pos) { ShowContextMenu(m_ui->cfgs_list->itemAt(pos)); });
connect(m_ui->cfgs_grid, &QListView::doubleClicked, this, [this](const QModelIndex& index) {
list_itemDoubleClicked(m_ui->cfgs_list->itemFromIndex(index), GAME_NAME_COLUMN);
});
connect(m_ui->cfgs_grid, &QListView::customContextMenuRequested, this,
[this](const QPoint& pos) {
ShowContextMenu(m_ui->cfgs_list->itemFromIndex(m_ui->cfgs_grid->indexAt(pos)));
});
connect(m_ui->grid_view_button, &QToolButton::toggled, this, [this](bool grid) {
m_ui->grid_size_slider->setVisible(grid);
auto* view = grid ? static_cast<QAbstractItemView*>(m_ui->cfgs_grid) : m_ui->cfgs_list;
m_ui->game_views->setCurrentWidget(view);
view->setCurrentIndex(view->currentIndex().siblingAtColumn(GAME_NAME_COLUMN));
view->scrollTo(view->currentIndex());
view->setFocus();
});
connect(m_ui->grid_view_button, &QToolButton::clicked, this,
&ConfigurationListWidget::WriteSettings);
connect(m_ui->cfgs_list->model(), &QAbstractItemModel::layoutChanged, this, [this]() {
filter_configurations(m_ui->search_line_edit->text());
});
connect(m_ui->search_line_edit, &QLineEdit::textChanged, this,
&ConfigurationListWidget::filter_configurations);
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
@@ -261,6 +319,19 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
ReadSettings();
m_ui->grid_size_slider->setVisible(m_ui->grid_view_button->isChecked());
const auto resize_grid = [this](int width) {
m_ui->cfgs_grid->setIconSize(QSize(width, width * 9 / 16));
};
resize_grid(m_ui->grid_size_slider->value());
connect(m_ui->grid_size_slider, &QSlider::valueChanged, this, [this, resize_grid](int width) {
resize_grid(width);
if (!m_ui->grid_size_slider->isSliderDown()) {
WriteSettings();
}
});
connect(m_ui->grid_size_slider, &QSlider::sliderReleased, this,
&ConfigurationListWidget::WriteSettings);
if (m_compatibility->IsLocal()) {
m_compatibility->Load();
}
@@ -300,6 +371,7 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
set_icon(m_ui->delete_button, QStringLiteral(":/icons/remove-configuration.svg"));
set_icon(m_ui->trophy_overview_button, QStringLiteral(":/icons/trophy.svg"));
set_icon(m_ui->grid_view_button, QStringLiteral(":/icons/grid-view.svg"));
}
void ConfigurationListWidget::WriteSettings() {
@@ -323,6 +395,8 @@ void ConfigurationListWidget::WriteSettings() {
s->beginGroup(CONF_LAUNCHER);
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
s->setValue(CONF_GAME_DIRS, m_game_dirs);
s->setValue("grid_view", m_ui->grid_view_button->isChecked());
s->setValue("grid_icon_width", m_ui->grid_size_slider->value());
s->remove(CONF_GAME_DIR);
s->endGroup();
@@ -363,6 +437,9 @@ void ConfigurationListWidget::ReadSettings() {
ConfigurationEditDialog::ReadSettings(*s);
s->beginGroup(CONF_LAUNCHER);
m_ui->grid_view_button->setChecked(s->value("grid_view", false).toBool());
m_ui->grid_size_slider->setValue(
s->value("grid_icon_width", m_ui->grid_size_slider->value()).toInt());
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIRS)));
if (m_game_dirs.isEmpty()) {
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
@@ -959,7 +1036,7 @@ void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
}
void ConfigurationListWidget::edit_input_mapping() {
InputMappingDialog dialog(m_global_info.host_input_mapping, this);
InputMappingDialog dialog(m_global_info.host_input_mapping, m_controller_preview, this);
if (dialog.exec() == QDialog::Accepted) {
m_global_info.host_input_mapping = dialog.Mapping();
WriteSettings();
@@ -1097,6 +1174,7 @@ void ConfigurationListWidget::filter_configurations(const QString& text) {
item->text(GAME_NAME_COLUMN).contains(query, Qt::CaseInsensitive) ||
item->text(GAME_SERIAL_COLUMN).contains(query, Qt::CaseInsensitive);
item->setHidden(!match);
m_ui->cfgs_grid->setRowHidden(item_index, !match);
if (match && item == m_selected_item) {
selection_is_shown = true;
@@ -1137,8 +1215,8 @@ void ConfigurationListWidget::list_itemDoubleClicked(QTreeWidgetItem* witem, int
}
}
void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->itemAt(pos));
void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
auto* item = static_cast<ConfigurationItem*>(witem);
if (item != nullptr) {
m_ui->cfgs_list->setCurrentItem(item);
-84
View File
@@ -1,84 +0,0 @@
#include "controllerLightbar.h"
#include <QDebug>
#include <SDL3/SDL.h>
ControllerLightbar::ControllerLightbar(QObject* parent): QObject(parent) {
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, this, &ControllerLightbar::Refresh);
}
ControllerLightbar::~ControllerLightbar() {
Stop();
}
void ControllerLightbar::SetColor(const QString& color) {
const QColor parsed(color);
if (!parsed.isValid()) {
Stop();
return;
}
if (m_color == parsed) {
return;
}
if (!m_timer.isActive()) {
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
qWarning() << "Cannot initialize controller color preview:" << SDL_GetError();
return;
}
m_timer.start();
}
m_color = parsed;
for (auto* gamepad: m_gamepads) {
ApplyColor(gamepad);
}
Refresh();
}
void ControllerLightbar::Stop() {
if (!m_timer.isActive()) {
return;
}
m_timer.stop();
for (auto* gamepad: m_gamepads) {
SDL_CloseGamepad(gamepad);
}
m_gamepads.clear();
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
m_color = {};
}
void ControllerLightbar::Refresh() {
// Bluetooth DualSense may defer an accepted LED command until its connection
// animation ends. Polling events also delivers those deferred commands.
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED && !m_gamepads.contains(event.gdevice.which)) {
auto* gamepad = SDL_OpenGamepad(event.gdevice.which);
if (gamepad == nullptr) {
qWarning() << "Cannot open controller color preview:" << SDL_GetError();
continue;
}
if (SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) {
m_gamepads.insert(event.gdevice.which, gamepad);
ApplyColor(gamepad);
} else {
SDL_CloseGamepad(gamepad);
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
SDL_CloseGamepad(gamepad);
}
}
}
}
void ControllerLightbar::ApplyColor(SDL_Gamepad* gamepad) const {
if (!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
static_cast<Uint8>(m_color.green()),
static_cast<Uint8>(m_color.blue()))) {
qWarning() << "Cannot set controller LED:" << SDL_GetError();
}
}
+202
View File
@@ -0,0 +1,202 @@
#include "controllerPreview.h"
#include <QDebug>
#include <SDL3/SDL.h>
#include <cmath>
namespace {
constexpr struct {
SDL_GamepadButton button;
const char* control;
} BUTTONS[] = {
{SDL_GAMEPAD_BUTTON_SOUTH, "Cross"}, {SDL_GAMEPAD_BUTTON_EAST, "Circle"},
{SDL_GAMEPAD_BUTTON_WEST, "Square"}, {SDL_GAMEPAD_BUTTON_NORTH, "Triangle"},
{SDL_GAMEPAD_BUTTON_DPAD_UP, "Up"}, {SDL_GAMEPAD_BUTTON_DPAD_DOWN, "Down"},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "Left"}, {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "Right"},
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "L1"}, {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "R1"},
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "L3"}, {SDL_GAMEPAD_BUTTON_RIGHT_STICK, "R3"},
{SDL_GAMEPAD_BUTTON_START, "Options"},
};
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
const int value = SDL_GetGamepadAxis(gamepad, axis);
return value / (value < 0 ? 32768.0 : 32767.0);
}
} // namespace
ControllerPreview::ControllerPreview(QObject* parent): QObject(parent) {
m_timer.setInterval(100);
connect(&m_timer, &QTimer::timeout, this, &ControllerPreview::Refresh);
}
ControllerPreview::~ControllerPreview() {
Stop();
}
void ControllerPreview::SetColor(const QString& color) {
const QColor parsed(color);
if (m_color == parsed && m_timer.isActive()) {
return;
}
m_color = parsed;
UpdateRunning();
for (auto* gamepad: m_gamepads) {
ApplyColor(gamepad);
}
}
void ControllerPreview::SetPreviewEnabled(bool enabled) {
m_preview_enabled = enabled;
m_timer.setInterval(enabled ? 16 : 100);
UpdateRunning();
}
void ControllerPreview::Stop() {
m_preview_enabled = false;
m_color = {};
UpdateRunning();
}
void ControllerPreview::SetSuspended(bool suspended) {
if (m_suspended != suspended) {
m_suspended = suspended;
UpdateRunning();
}
}
void ControllerPreview::UpdateRunning() {
if (!m_suspended && (m_preview_enabled || m_color.isValid())) {
if (m_timer.isActive()) {
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
m_error = QString::fromUtf8(SDL_GetError());
qWarning() << "Cannot initialize controller preview:" << m_error;
emit StateChanged();
return;
}
m_error.clear();
int count = 0;
auto* ids = SDL_GetGamepads(&count);
for (int index = 0; index < count; ++index) {
OpenGamepad(ids[index]);
}
SDL_free(ids);
m_timer.start();
Refresh();
emit StateChanged();
return;
}
if (m_timer.isActive()) {
m_timer.stop();
for (auto* gamepad: m_gamepads) {
SDL_CloseGamepad(gamepad);
}
m_gamepads.clear();
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
}
m_pressed.clear();
m_left_stick = {};
m_right_stick = {};
m_device_name.clear();
m_error = m_suspended ? tr("Preview is paused while a game is running.") : QString();
emit StateChanged();
}
void ControllerPreview::OpenGamepad(SDL_JoystickID id) {
if (!m_gamepads.contains(id)) {
if (auto* gamepad = SDL_OpenGamepad(id); gamepad != nullptr) {
m_gamepads.insert(id, gamepad);
ApplyColor(gamepad);
}
}
}
void ControllerPreview::Refresh() {
// One SDL event pump serves both the live diagram and lightbar preview.
// It also delivers deferred Bluetooth DualSense LED commands.
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
OpenGamepad(event.gdevice.which);
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (auto* gamepad = m_gamepads.take(event.gdevice.which); gamepad != nullptr) {
SDL_CloseGamepad(gamepad);
}
}
}
QSet<QString> pressed;
QPointF left;
QPointF right;
QString name;
if (!m_gamepads.isEmpty()) {
auto* gamepad = m_gamepads.first();
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
for (const auto& binding: BUTTONS) {
if (SDL_GetGamepadButton(gamepad, binding.button)) {
pressed.insert(QString::fromLatin1(binding.control));
}
}
const auto stick = [&](SDL_GamepadAxis x, SDL_GamepadAxis y, const char* prefix) {
QPointF position(Axis(gamepad, x), Axis(gamepad, y));
if (std::hypot(position.x(), position.y()) < 0.15) {
position = {};
}
const auto id = QString::fromLatin1(prefix);
if (position.x() < -0.25) pressed.insert(id + QStringLiteral("Left"));
if (position.x() > 0.25) pressed.insert(id + QStringLiteral("Right"));
if (position.y() < -0.25) pressed.insert(id + QStringLiteral("Up"));
if (position.y() > 0.25) pressed.insert(id + QStringLiteral("Down"));
return position;
};
left = stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, "LeftStick");
right = stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, "RightStick");
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.05) {
pressed.insert(QStringLiteral("L2"));
}
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.05) {
pressed.insert(QStringLiteral("R2"));
}
bool touching = false;
if (SDL_GetNumGamepadTouchpads(gamepad) > 0) {
for (int finger = 0; finger < SDL_GetNumGamepadTouchpadFingers(gamepad, 0); ++finger) {
bool down = false;
float x = 0;
if (SDL_GetGamepadTouchpadFinger(gamepad, 0, finger, &down, &x, nullptr, nullptr) &&
down) {
pressed.insert(x < 0.5f ? QStringLiteral("TouchPad")
: QStringLiteral("TouchPadRight"));
touching = true;
}
}
}
if (!touching && SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_TOUCHPAD)) {
pressed.insert(QStringLiteral("TouchPad"));
pressed.insert(QStringLiteral("TouchPadRight"));
}
}
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
name != m_device_name) {
m_pressed = pressed;
m_left_stick = left;
m_right_stick = right;
m_device_name = name;
emit StateChanged();
}
}
void ControllerPreview::ApplyColor(SDL_Gamepad* gamepad) const {
if (m_color.isValid() &&
SDL_GetBooleanProperty(SDL_GetGamepadProperties(gamepad),
SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false) &&
!SDL_SetGamepadLED(gamepad, static_cast<Uint8>(m_color.red()),
static_cast<Uint8>(m_color.green()),
static_cast<Uint8>(m_color.blue()))) {
qWarning() << "Cannot set controller LED:" << SDL_GetError();
}
}
+300
View File
@@ -0,0 +1,300 @@
#include "dualsenseWidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <algorithm>
#include <cmath>
namespace {
enum class Shape { Button, Shoulder, Touchpad, Face, Dpad, Stick, Direction };
struct Control {
const char* id;
QRectF bounds;
Shape shape = Shape::Button;
int angle = 0;
};
// Painting and hit testing share a canvas with room above the shell for the shoulders.
const QRectF CANVAS(0, -20, 480, 330);
const Control CONTROLS[] = {
{"L2", {90, -12, 58, 18}, Shape::Shoulder},
{"R2", {332, -12, 58, 18}, Shape::Shoulder},
{"L1", {84, 12, 64, 12}, Shape::Shoulder},
{"R1", {332, 12, 64, 12}, Shape::Shoulder},
{"TouchPad", {150, 25, 90, 100}, Shape::Touchpad},
{"TouchPadRight", {240, 25, 90, 100}, Shape::Touchpad},
{"Options", {330, 60, 9, 20}},
{"Up", {105, 72, 22, 25}, Shape::Dpad, 0},
{"Right", {124, 94, 25, 22}, Shape::Dpad, 90},
{"Down", {105, 113, 22, 25}, Shape::Dpad, 180},
{"Left", {83, 94, 25, 22}, Shape::Dpad, 270},
{"Triangle", {353, 67, 26, 26}, Shape::Face},
{"Circle", {382, 96, 26, 26}, Shape::Face},
{"Cross", {353, 125, 26, 26}, Shape::Face},
{"Square", {324, 96, 26, 26}, Shape::Face},
{"L3", {143, 131, 66, 66}, Shape::Stick},
{"R3", {271, 131, 66, 66}, Shape::Stick},
{"LeftStickUp", {143, 131, 66, 66}, Shape::Direction, 0},
{"LeftStickRight", {143, 131, 66, 66}, Shape::Direction, 90},
{"LeftStickDown", {143, 131, 66, 66}, Shape::Direction, 180},
{"LeftStickLeft", {143, 131, 66, 66}, Shape::Direction, 270},
{"RightStickUp", {271, 131, 66, 66}, Shape::Direction, 0},
{"RightStickRight", {271, 131, 66, 66}, Shape::Direction, 90},
{"RightStickDown", {271, 131, 66, 66}, Shape::Direction, 180},
{"RightStickLeft", {271, 131, 66, 66}, Shape::Direction, 270},
};
QPainterPath TouchpadPath() {
QPainterPath path;
path.moveTo(158, 42);
path.cubicTo(156, 33, 168, 32, 178, 31);
path.cubicTo(219, 28, 261, 28, 302, 31);
path.cubicTo(312, 32, 324, 33, 322, 42);
path.cubicTo(318, 64, 315, 87, 309, 105);
path.cubicTo(306, 117, 299, 122, 286, 122);
path.lineTo(194, 122);
path.cubicTo(181, 122, 174, 117, 171, 105);
path.cubicTo(165, 87, 162, 64, 158, 42);
path.closeSubpath();
return path;
}
QPainterPath ControlPath(const Control& control) {
QPainterPath path;
if (control.shape == Shape::Direction) {
const auto inner = control.bounds.adjusted(7, 7, -7, -7);
const int start = 49 - control.angle;
path.arcMoveTo(control.bounds, start);
path.arcTo(control.bounds, start, 82);
path.arcTo(inner, start + 82, -82);
path.closeSubpath();
} else if (control.shape == Shape::Touchpad) {
path.addRect(control.bounds);
return TouchpadPath().intersected(path);
} else if (control.shape == Shape::Face || control.shape == Shape::Stick) {
path.addEllipse(control.shape == Shape::Stick ? control.bounds.adjusted(7, 7, -7, -7)
: control.bounds);
} else if (control.shape == Shape::Dpad) {
path.moveTo(-8, -12);
path.quadTo(-11, -12, -11, -8);
path.lineTo(-10, 3);
path.quadTo(-10, 6, -7, 8);
path.lineTo(0, 12);
path.lineTo(7, 8);
path.quadTo(10, 6, 10, 3);
path.lineTo(11, -8);
path.quadTo(11, -12, 8, -12);
path.closeSubpath();
QTransform transform;
transform.translate(control.bounds.center().x(), control.bounds.center().y());
transform.rotate(control.angle);
return transform.map(path);
} else if (control.shape == Shape::Shoulder) {
const auto& bounds = control.bounds;
path.moveTo(bounds.bottomLeft());
path.lineTo(bounds.left() + 2, bounds.top() + bounds.height() / 2);
path.cubicTo(bounds.left() + 10, bounds.top(), bounds.right() - 10, bounds.top(),
bounds.right() - 2, bounds.top() + bounds.height() / 2);
path.lineTo(bounds.bottomRight());
path.closeSubpath();
} else {
path.addRoundedRect(control.bounds, 4, 4);
}
return path;
}
void DrawSymbol(QPainter& painter, const Control& control) {
const auto id = QLatin1String(control.id);
painter.save();
painter.translate(control.bounds.center());
painter.setBrush(Qt::NoBrush);
if (control.shape == Shape::Dpad) {
painter.rotate(control.angle);
painter.drawPolyline(QPolygonF({{-4, 2}, {0, -2}, {4, 2}}));
} else if (id == QLatin1String("Triangle")) {
painter.drawPolygon(QPolygonF({{0, -7}, {7, 5}, {-7, 5}}));
} else if (id == QLatin1String("Circle")) {
painter.drawEllipse(QPointF(), 7, 7);
} else if (id == QLatin1String("Square")) {
painter.drawRect(QRectF(-6, -6, 12, 12));
} else if (id == QLatin1String("Cross")) {
painter.drawLine(QPointF(-5, -5), QPointF(5, 5));
painter.drawLine(QPointF(-5, 5), QPointF(5, -5));
}
painter.restore();
}
} // namespace
DualSenseWidget::DualSenseWidget(QWidget* parent): QWidget(parent) {
setMinimumSize(330, 230);
setMouseTracking(true);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setAccessibleName(tr("DualSense controller"));
setAccessibleDescription(tr("Click a control to select its binding. Connected controller input "
"lights up the controls."));
}
QSize DualSenseWidget::sizeHint() const {
return CANVAS.size().toSize();
}
void DualSenseWidget::SetPressed(const QSet<QString>& pressed) {
if (m_pressed != pressed) {
m_pressed = pressed;
update();
}
}
void DualSenseWidget::SetSelected(const QString& selected) {
if (m_selected != selected) {
m_selected = selected;
update();
}
}
void DualSenseWidget::SetSticks(const QPointF& left, const QPointF& right) {
if (m_left_stick != left || m_right_stick != right) {
m_left_stick = left;
m_right_stick = right;
update();
}
}
QTransform DualSenseWidget::ViewTransform() const {
const qreal scale = std::min(width() / CANVAS.width(), height() / CANVAS.height());
QTransform transform;
transform.translate(width() / 2.0, height() / 2.0);
transform.scale(scale, scale);
transform.translate(-CANVAS.center().x(), -CANVAS.center().y());
return transform;
}
QString DualSenseWidget::HitTest(const QPointF& position) const {
const auto point = ViewTransform().inverted().map(position);
for (const auto& control: CONTROLS) {
if (ControlPath(control).contains(point)) {
return QString::fromLatin1(control.id);
}
}
return {};
}
void DualSenseWidget::paintEvent(QPaintEvent*) {
const QColor black("#111111");
const QColor white("#eeeeee");
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setTransform(ViewTransform());
QPainterPath shell;
shell.moveTo(81, 48);
shell.cubicTo(151, 21, 329, 21, 399, 48);
shell.cubicTo(420, 94, 445, 166, 445, 215);
shell.cubicTo(447, 253, 437, 291, 419, 296);
shell.cubicTo(403, 302, 395, 297, 389, 283);
shell.cubicTo(381, 268, 373, 236, 354, 213);
shell.cubicTo(347, 205, 341, 205, 325, 205);
shell.lineTo(155, 205);
shell.cubicTo(139, 205, 133, 205, 126, 213);
shell.cubicTo(107, 236, 99, 268, 91, 283);
shell.cubicTo(85, 297, 77, 302, 61, 296);
shell.cubicTo(43, 291, 33, 253, 35, 215);
shell.cubicTo(35, 166, 60, 94, 81, 48);
shell.closeSubpath();
painter.setBrush(black);
painter.setPen(QPen(white, 1.5));
painter.drawPath(shell);
// Panel seams flow into the inner grips without a second outline across the belly.
QPainterPath seams;
seams.moveTo(153, 35);
seams.cubicTo(153, 69, 176, 105, 158, 127);
seams.cubicTo(123, 166, 108, 226, 76, 297);
seams.moveTo(327, 35);
seams.cubicTo(327, 69, 304, 105, 322, 127);
seams.cubicTo(357, 166, 372, 226, 404, 297);
painter.setBrush(Qt::NoBrush);
painter.drawPath(seams);
painter.setBrush(black);
painter.drawPath(TouchpadPath());
painter.drawRoundedRect(QRectF(141, 60, 9, 20), 4, 4); // Create button, currently unmapped.
painter.drawEllipse(QPointF(240, 166), 8, 8);
QFont font = painter.font();
font.setPixelSize(9);
font.setBold(true);
painter.setFont(font);
for (const auto& control: CONTROLS) {
const auto id = QString::fromLatin1(control.id);
const bool pressed = m_pressed.contains(id);
const bool selected = m_selected == id;
const bool hovered = m_hovered == id;
const bool active = pressed || selected || hovered;
if ((control.shape == Shape::Direction || control.shape == Shape::Touchpad) && !active) {
continue;
}
const QColor fill = pressed ? QColor("#2eb6df") : active ? QColor("#3a6179") : black;
painter.setBrush(fill);
painter.setPen(QPen(active ? QColor("#79e3ff") : white, selected ? 2 : 1.3));
if (control.shape == Shape::Touchpad) {
// Clip one shared surface so independent bindings never add a permanent center seam.
painter.save();
painter.setClipRect(control.bounds);
painter.drawPath(TouchpadPath());
painter.restore();
} else if (control.shape == Shape::Stick) {
painter.setBrush(black);
painter.drawEllipse(control.bounds);
const auto position = id == QLatin1String("L3") ? m_left_stick : m_right_stick;
QPointF offset(std::clamp(position.x(), -1.0, 1.0),
std::clamp(position.y(), -1.0, 1.0));
offset *= 5 / std::max(1.0, std::hypot(offset.x(), offset.y()));
painter.setBrush(fill);
painter.drawEllipse(control.bounds.adjusted(12, 12, -12, -12).translated(offset));
} else {
painter.drawPath(ControlPath(control));
painter.setPen(QPen(white, 1.3));
if (control.shape == Shape::Face || control.shape == Shape::Dpad) {
DrawSymbol(painter, control);
} else if (control.shape == Shape::Shoulder) {
painter.drawText(control.bounds.adjusted(0, 2, 0, 0), Qt::AlignCenter, id);
} else if (id == QLatin1String("Options")) {
painter.drawLine(control.bounds.center() - QPointF(0, 4),
control.bounds.center() + QPointF(0, 4));
}
}
}
}
void DualSenseWidget::mouseMoveEvent(QMouseEvent* event) {
const auto hovered = HitTest(event->position());
if (hovered != m_hovered) {
m_hovered = hovered;
setCursor(hovered.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
setToolTip(hovered);
update();
}
}
void DualSenseWidget::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
const auto id = HitTest(event->position());
if (!id.isEmpty()) {
emit ControlClicked(id);
return;
}
}
QWidget::mousePressEvent(event);
}
void DualSenseWidget::leaveEvent(QEvent*) {
m_hovered.clear();
unsetCursor();
update();
}
+221 -131
View File
@@ -1,61 +1,69 @@
#include "inputMappingDialog.h"
#include <QAbstractItemView>
#include "controllerPreview.h"
#include "dualsenseWidget.h"
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMouseEvent>
#include <QPushButton>
#include <QScreen>
#include <QScrollArea>
#include <QStringList>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
constexpr int BINDING_COLUMN = 1;
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
struct PadControl {
const char* id;
const char* label;
const char* default_binding;
Group group;
int row;
int column;
};
constexpr PadControl PAD_CONTROLS[] = {
{"Up", "D-pad Up", "Up"},
{"Down", "D-pad Down", "Down"},
{"Left", "D-pad Left", "Left"},
{"Right", "D-pad Right", "Right"},
{"LeftStickUp", "Left stick Up", "W"},
{"LeftStickDown", "Left stick Down", "S"},
{"LeftStickLeft", "Left stick Left", "A"},
{"LeftStickRight", "Left stick Right", "D"},
{"RightStickUp", "Right stick Up", "T"},
{"RightStickDown", "Right stick Down", "G"},
{"RightStickLeft", "Right stick Left", "F"},
{"RightStickRight", "Right stick Right", "H"},
{"Triangle", "Triangle", "I"},
{"Circle", "Circle", "L"},
{"Cross", "Cross", "J"},
{"Square", "Square", "K"},
{"L1", "L1", "Q"},
{"R1", "R1", "E"},
{"L2", "L2", "Z"},
{"R2", "R2", "C"},
{"L3", "L3", "Left Shift"},
{"R3", "R3", "Left Ctrl"},
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad left (SELECT)", "Backspace"},
{"TouchPadRight", "Touch pad right (START)", "Tab"},
{"SpeakerVolume", "Speaker volume (cycle)", "1"},
{"VibrationIntensity", "Vibration intensity (cycle)", "2"},
{"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"},
{"Up", "Up", "Up", Dpad, 0, 1},
{"Down", "Down", "Down", Dpad, 2, 1},
{"Left", "Left", "Left", Dpad, 1, 0},
{"Right", "Right", "Right", Dpad, 1, 2},
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
{"RightStickUp", "Up", "T", RightStick, 0, 1},
{"RightStickDown", "Down", "G", RightStick, 2, 1},
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
{"RightStickRight", "Right", "H", RightStick, 1, 2},
{"Triangle", "Triangle", "I", Face, 0, 1},
{"Circle", "Circle", "L", Face, 1, 2},
{"Cross", "Cross", "J", Face, 2, 1},
{"Square", "Square", "K", Face, 1, 0},
{"L1", "L1", "Q", Shoulders, 0, 1},
{"R1", "R1", "E", Shoulders, 0, 2},
{"L2", "L2", "Z", Shoulders, 0, 0},
{"R2", "R2", "C", Shoulders, 0, 3},
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
{"Options", "Options", "Return", Center, 0, 1},
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
};
QString KeypadName(int key) {
@@ -183,6 +191,17 @@ private:
QString m_binding;
};
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
for (auto item = bindings.begin(); item != bindings.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = bindings.erase(item);
} else {
++item;
}
}
bindings.insert(id, binding);
}
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
@@ -191,15 +210,7 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
}
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
for (auto item = result.begin(); item != result.end();) {
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
item = result.erase(item);
} else {
++item;
}
}
result.insert(entry.left(separator), binding);
AssignBinding(result, entry.left(separator), entry.mid(separator + 1));
}
}
return result;
@@ -216,82 +227,146 @@ double ParseMouseSensitivity(const QStringList& mapping) {
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent) {
setWindowTitle(tr("Input Mapping"));
resize(460, 650);
InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPreview* preview,
QWidget* parent)
: QDialog(parent), m_preview(preview) {
setWindowTitle(tr("Controls"));
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
auto* layout = new QVBoxLayout(this);
layout->addWidget(
new QLabel(tr("Map keyboard or mouse buttons to DualSense controls.\n"
"Press F7 in-game to toggle mouse movement on the right stick."),
this));
layout->setSpacing(10);
auto* title = new QLabel(tr("Controller setup"), this);
auto font = title->font();
font.setPointSize(font.pointSize() + 7);
font.setBold(true);
title->setFont(font);
layout->addWidget(title);
auto* hint = new QLabel(
tr("Select a button to remap. Press controller buttons to test."), this);
hint->setWordWrap(true);
layout->addWidget(hint);
m_status = new QLabel(this);
m_status->setObjectName(QStringLiteral("controllerStatus"));
m_status->setWordWrap(true);
layout->addWidget(m_status);
const auto parsed = ParseMapping(mapping);
m_custom_bindings = !parsed.isEmpty();
auto* scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto* content = new QWidget(scroll);
auto* grid = new QGridLayout(content);
grid->setContentsMargins(0, 0, 0, 0);
grid->setSpacing(12);
QGroupBox* groups[GroupCount];
QGridLayout* cells[GroupCount];
const char* titles[] = {"D-Pad",
"Left stick",
"Right stick",
"Face buttons",
"Shoulders and triggers",
"Touch pad and options",
"In-game shortcuts (cycle)"};
for (int index = 0; index < GroupCount; ++index) {
groups[index] = new QGroupBox(tr(titles[index]), content);
cells[index] = new QGridLayout(groups[index]);
cells[index]->setSpacing(5);
}
m_bindings = ParseMapping(mapping);
m_custom_bindings = !m_bindings.isEmpty();
for (const auto& control: PAD_CONTROLS) {
const auto id = QString::fromLatin1(control.id);
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
auto* cell_layout = new QVBoxLayout(cell);
cell_layout->setContentsMargins(3, 5, 3, 3);
auto* button = new QPushButton(cell);
button->setObjectName(QStringLiteral("binding_") + id);
button->setAutoDefault(false);
button->setMinimumWidth(64);
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
tr(control.label));
cell_layout->addWidget(button);
cells[control.group]->addWidget(cell, control.row, control.column);
m_buttons.insert(id, button);
if (!m_custom_bindings) {
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
}
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
}
auto* left = new QVBoxLayout;
left->addWidget(groups[Dpad]);
left->addWidget(groups[LeftStick]);
left->addStretch();
grid->addLayout(left, 0, 0);
auto* center = new QVBoxLayout;
center->addWidget(groups[Shoulders]);
m_controller = new DualSenseWidget(content);
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
center->addWidget(m_controller, 1);
center->addWidget(groups[Center]);
grid->addLayout(center, 0, 1);
auto* right = new QVBoxLayout;
right->addWidget(groups[Face]);
right->addWidget(groups[RightStick]);
right->addStretch();
grid->addLayout(right, 0, 2);
grid->setColumnStretch(1, 1);
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
scroll->setWidget(content);
layout->addWidget(scroll, 1);
auto* sensitivity_layout = new QHBoxLayout;
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
m_sensitivity = new QDoubleSpinBox(this);
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
m_sensitivity->setRange(0.1, 5.0);
m_sensitivity->setSingleStep(0.1);
m_sensitivity->setDecimals(1);
m_sensitivity->setSuffix(QStringLiteral("x"));
m_sensitivity->setSuffix(QStringLiteral("×"));
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
sensitivity_layout->addWidget(m_sensitivity);
sensitivity_layout->addWidget(
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
sensitivity_layout->addStretch();
layout->addLayout(sensitivity_layout);
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
m_bindings->setHeaderLabels({tr("DualSense control"), tr("Host input")});
m_bindings->setRootIsDecorated(false);
m_bindings->setSelectionMode(QAbstractItemView::SingleSelection);
m_bindings->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
for (const auto& control: PAD_CONTROLS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(control.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(control.id));
SetBinding(item, m_custom_bindings ? parsed.value(QString::fromLatin1(control.id))
: QString::fromLatin1(control.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
auto* controls = new QHBoxLayout;
m_change_button = new QPushButton(tr("Change..."), this);
m_clear_button = new QPushButton(tr("Clear"), this);
auto* defaults = new QPushButton(tr("Defaults"), this);
controls->addWidget(m_change_button);
controls->addWidget(m_clear_button);
controls->addWidget(defaults);
controls->addStretch();
layout->addLayout(controls);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
this);
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
defaults->setObjectName(QStringLiteral("defaults"));
defaults->setAutoDefault(false);
layout->addWidget(buttons);
connect(m_bindings, &QTreeWidget::itemDoubleClicked, this,
[this](QTreeWidgetItem*, int) { ChangeBinding(); });
connect(m_bindings, &QTreeWidget::itemSelectionChanged, this, [this]() { UpdateButtons(); });
connect(m_change_button, &QPushButton::clicked, this, [this]() { ChangeBinding(); });
connect(m_clear_button, &QPushButton::clicked, this, [this]() { ClearBinding(); });
connect(defaults, &QPushButton::clicked, this, [this]() { RestoreDefaults(); });
connect(m_controller, &DualSenseWidget::ControlClicked, this,
&InputMappingDialog::ChangeBinding);
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
if (m_preview != nullptr) {
connect(m_preview, &ControllerPreview::StateChanged, this,
&InputMappingDialog::UpdatePreview);
m_preview->SetPreviewEnabled(true);
}
UpdateButtons();
UpdatePreview();
}
InputMappingDialog::~InputMappingDialog() {
if (m_preview != nullptr) {
m_preview->SetPreviewEnabled(false);
}
}
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (m_custom_bindings) {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
for (const auto& control: PAD_CONTROLS) {
const auto id = QString::fromLatin1(control.id);
const auto binding = m_bindings.value(id);
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
result.append(id + QLatin1Char('=') + binding);
}
}
}
@@ -302,58 +377,73 @@ QStringList InputMappingDialog::Mapping() const {
return result;
}
void InputMappingDialog::ChangeBinding() {
auto* item = m_bindings->currentItem();
if (item == nullptr) {
void InputMappingDialog::ChangeBinding(const QString& id) {
if (!m_buttons.contains(id)) {
return;
}
m_selected = id;
UpdateHighlights();
InputCaptureDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
dialog.setWindowTitle(tr("Set binding — %1").arg(m_buttons.value(id)->accessibleName()));
if (dialog.exec() == QDialog::Accepted) {
AssignBinding(m_bindings, id, dialog.Binding());
m_custom_bindings = true;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
auto* other = m_bindings->topLevelItem(index);
if (other != item && other->data(BINDING_COLUMN, Qt::UserRole)
.toString()
.compare(dialog.Binding(), Qt::CaseInsensitive) == 0) {
SetBinding(other, {});
}
}
SetBinding(item, dialog.Binding());
m_custom_bindings = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_custom_bindings = true;
m_selected.clear();
UpdateButtons();
}
void InputMappingDialog::RestoreDefaults() {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_CONTROLS[index].default_binding));
m_bindings.clear();
for (const auto& control: PAD_CONTROLS) {
m_bindings.insert(QString::fromLatin1(control.id),
QString::fromLatin1(control.default_binding));
}
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
m_custom_bindings = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
if (item == nullptr) {
return;
}
item->setData(BINDING_COLUMN, Qt::UserRole, binding);
item->setText(BINDING_COLUMN, binding.isEmpty() ? tr("None") : binding);
UpdateButtons();
}
void InputMappingDialog::UpdateButtons() {
if (m_change_button == nullptr) {
return;
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
auto* button = item.value();
const auto binding = m_bindings.value(item.key());
const auto label = binding.isEmpty() ? tr("None") : binding;
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
}
UpdateHighlights();
}
void InputMappingDialog::UpdatePreview() {
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
m_controller->SetPressed(pressed);
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
m_preview != nullptr ? m_preview->RightStick() : QPointF());
UpdateHighlights();
if (m_preview != nullptr && !m_preview->Error().isEmpty()) {
m_status->setText(tr("Controller preview unavailable: %1").arg(m_preview->Error()));
} else if (m_preview != nullptr && !m_preview->DeviceName().isEmpty()) {
m_status->setText(tr("Connected: %1").arg(m_preview->DeviceName()));
} else {
m_status->setText(tr("No controller connected"));
}
}
void InputMappingDialog::UpdateHighlights() {
m_controller->SetSelected(m_selected);
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
auto colors = palette();
if (down) {
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
colors.setColor(QPalette::ButtonText, colors.color(QPalette::HighlightedText));
} else if (item.key() == m_selected) {
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
}
auto label_font = font();
label_font.setBold(down);
item.value()->setPalette(colors);
item.value()->setFont(label_font);
}
const auto* item = m_bindings->currentItem();
m_change_button->setEnabled(item != nullptr);
m_clear_button->setEnabled(item != nullptr &&
!item->data(BINDING_COLUMN, Qt::UserRole).toString().isEmpty());
}
+16 -7
View File
@@ -4,7 +4,7 @@
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include "controllerLightbar.h"
#include "controllerPreview.h"
#include "gameContent.h"
#include "updateChecker.h"
@@ -89,7 +89,7 @@ private:
Ui::MainDialog* m_ui = {nullptr};
MainDialog* m_main_dialog = nullptr;
UpdateChecker* m_update_checker = nullptr;
ControllerLightbar m_lightbar;
ControllerPreview m_controller_preview;
QString m_interpreter;
QProcess m_process;
@@ -112,6 +112,7 @@ MainDialogPrivate::~MainDialogPrivate() {
void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_ui = new Ui::MainDialog;
m_ui->setupUi(main_dialog);
m_ui->widget->SetControllerPreview(&m_controller_preview);
m_main_dialog = main_dialog;
m_update_checker = new UpdateChecker(main_dialog);
@@ -127,7 +128,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
[this](const QString& color) {
if (m_process.state() == QProcess::NotRunning) {
m_lightbar.SetColor(color);
m_controller_preview.SetColor(color);
}
});
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
@@ -135,6 +136,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
[this](const QString&) { m_update_checker->Check(true); });
connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link,
&QLabel::setDisabled);
connect(m_update_checker, &UpdateChecker::InstallingChanged, this, &MainDialogPrivate::Update);
connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
@@ -145,6 +147,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
});
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
if (state == QProcess::NotRunning) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
@@ -161,6 +164,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
}
void MainDialogPrivate::FindInterpreter() {
UpdateChecker::ShowPreviousResult(m_main_dialog);
QDir search_dir(QApplication::applicationDirPath());
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
@@ -508,13 +512,16 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
}
void MainDialogPrivate::Run() {
if (m_update_checker->IsInstalling()) {
return;
}
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
return;
}
m_running_item->SetRunning(true);
m_lightbar.Stop();
m_controller_preview.SetSuspended(true);
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
m_main_dialog->RunInterpreter(&m_process, *info);
@@ -525,7 +532,8 @@ void MainDialogPrivate::Run() {
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
!m_update_checker->IsInstalling());
if (run_enabled) {
const auto& info = item->GetInfo();
@@ -535,10 +543,11 @@ void MainDialogPrivate::Update() {
m_ui->widget->SetRunEnabled(run_enabled);
if (m_process.state() != QProcess::NotRunning) {
m_lightbar.Stop();
m_controller_preview.SetSuspended(true);
return;
}
m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor());
m_controller_preview.SetSuspended(false);
m_controller_preview.SetColor(m_ui->widget->GetGlobalControllerColor());
}
#include "mainDialog.moc"
+374 -99
View File
@@ -2,143 +2,418 @@
#include "kytyGitVersion.h"
#include <QByteArray>
#include <QAbstractButton>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include <QUrl>
#include <QWidget>
#include <QProcessEnvironment>
#include <QProgressDialog>
#include <QTemporaryDir>
namespace {
constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json";
constexpr char FALLBACK_FEED_URL[] =
"https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest";
constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
QString LauncherRelativePath() {
#if defined(_WIN32)
return QStringLiteral("launcher.exe");
#elif defined(__APPLE__)
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
#else
return QStringLiteral("launcher");
#endif
}
QString InstallDirectory() {
QDir dir(QCoreApplication::applicationDirPath());
#if defined(__APPLE__)
// Official macOS releases are a bundle; never update a flat development build.
if (!dir.cdUp() || !dir.cdUp() || dir.dirName() != QStringLiteral("KytyPS5.app") ||
!dir.cdUp()) {
return {};
}
#endif
return dir.absolutePath();
}
QString HelperName() {
#if defined(_WIN32)
return QStringLiteral("kyty_updater.exe");
#else
return QStringLiteral("kyty_updater");
#endif
}
QNetworkRequest Request(const QUrl& url) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "KytyPS5-Updater");
request.setTransferTimeout(30000);
return request;
}
QString TarProgram() {
#if defined(_WIN32)
// Use the OS tool, not a tar.exe from the download or current directory.
return QDir(qEnvironmentVariable("SystemRoot", QStringLiteral("C:/Windows")))
.filePath(QStringLiteral("System32/tar.exe"));
#else
return QStringLiteral("/usr/bin/tar");
#endif
}
} // namespace
struct UpdateChecker::UpdateInfo {
QString tag;
QUrl page_url;
QString error;
};
UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {}
UpdateChecker::~UpdateChecker() {
if (m_reply) {
m_reply->disconnect(this);
m_reply->abort();
}
// Extraction must stop before QTemporaryDir removes its output.
m_prepare.disconnect(this);
if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
m_prepare.waitForFinished();
}
}
bool UpdateChecker::IsSupported() {
#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG)
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty"));
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")) &&
!UpdateRelease::PlatformSuffix().isEmpty();
#else
return false;
#endif
}
UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) {
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
return {{}, {}, tr("Invalid update feed")};
}
const auto root = document.object();
UpdateInfo info {root.value(QStringLiteral("tag")).toString(),
QUrl(root.value(QStringLiteral("html_url")).toString()), {}};
if (info.tag.isEmpty()) {
info.tag = root.value(QStringLiteral("tag_name")).toString();
}
if (info.tag.isEmpty() || !info.page_url.isValid() ||
info.page_url.scheme() != QStringLiteral("https")) {
info.error = tr("Incomplete update feed");
}
return info;
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_checking_updates) {
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
const auto directory = InstallDirectory();
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
return;
}
m_checking_updates = true;
emit CheckingChanged(true);
FetchUpdateInfo(DEFAULT_FEED_URL, false, manual);
const auto status = result.readLine().trimmed();
auto message = QString::fromUtf8(result.read(64 * 1024)).trimmed();
result.close();
result.remove();
if (status != "success" && status != "failure") {
return;
}
const bool success = status == "success";
if (message.isEmpty()) {
message = success ? tr("The update was installed successfully.")
: tr("The update could not be installed.");
}
QMessageBox(success ? QMessageBox::Information : QMessageBox::Warning, tr("KytyPS5 Update"),
message, QMessageBox::Ok, parent)
.exec();
}
void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) {
QNetworkRequest request(QUrl(QString::fromLatin1(url)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
void UpdateChecker::FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done) {
auto request = Request(url);
request.setRawHeader("Accept", "application/vnd.github+json");
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() {
UpdateInfo info;
if (reply->error() == QNetworkReply::NoError) {
info = ParseUpdateInfo(reply->readAll());
} else {
m_reply = reply;
reply->setReadBufferSize(MAX_METADATA_SIZE + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply]() {
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply, done = std::move(done)]() {
UpdateRelease::Info info;
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
info.error = tr("The update response is too large.");
} else if (reply->error() != QNetworkReply::NoError) {
info.error = reply->errorString();
} else {
info = UpdateRelease::Parse(reply->readAll(), UpdateRelease::PlatformSuffix());
}
m_reply = nullptr;
reply->deleteLater();
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
done(info);
});
}
void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) {
if (!info.error.isEmpty()) {
qWarning() << "Could not check for updates:" << info.error;
if (manual) {
QMessageBox::warning(m_parent, tr("Update Check"),
tr("Could not check for updates:\n%1").arg(info.error));
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_busy) {
return;
}
m_busy = true;
emit CheckingChanged(true);
FetchRelease(QUrl(UpdateRelease::LATEST_URL), [this, manual](UpdateRelease::Info latest) {
m_release = std::move(latest);
if (!m_release.published_at.isValid()) {
Finish(m_release.error, manual);
} else if (m_release.tag == QString::fromLatin1(KYTY_RELEASE_TAG)) {
CheckCurrent(m_release, manual);
} else {
// Date/hash tags cannot order releases published on the same day.
const auto tag = QUrl::toPercentEncoding(QString::fromLatin1(KYTY_RELEASE_TAG));
FetchRelease(QUrl(UpdateRelease::TAG_URL.arg(QString::fromLatin1(tag))),
[this, manual](const UpdateRelease::Info& current) {
CheckCurrent(current, manual);
});
}
});
}
const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (current) {
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("You are using the latest version (%1).").arg(info.tag));
void UpdateChecker::CheckCurrent(const UpdateRelease::Info& current, bool manual) {
if (current.tag != QString::fromLatin1(KYTY_RELEASE_TAG) || !current.published_at.isValid()) {
Finish(tr("Could not verify the currently installed release. Please try again later."),
manual);
return;
}
const auto relation = UpdateRelease::Compare(m_release, current);
if (relation == UpdateRelease::VersionRelation::Newer) {
if (m_release.error.isEmpty()) {
OfferUpdate();
} else {
Finish(m_release.error, manual);
}
return;
}
const auto message =
tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n"
"Open the release page?")
.arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag);
if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message,
QMessageBox::Open | QMessageBox::Cancel,
QMessageBox::Open) == QMessageBox::Open) {
QDesktopServices::openUrl(info.page_url);
if (relation == UpdateRelease::VersionRelation::Unknown) {
Finish(tr("Could not determine whether the available release is newer."), manual);
return;
}
Finish();
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("No newer release is available."));
}
}
void UpdateChecker::OfferUpdate() {
QMessageBox question(QMessageBox::Information, tr("KytyPS5 Update"),
tr("Version %1 is available.\n\nCurrent: %2\n\n"
"Download and install it now? The launcher will restart. Your settings "
"and games are kept.")
.arg(m_release.tag, QString::fromLatin1(KYTY_RELEASE_TAG)),
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Cancel, m_parent);
question.button(QMessageBox::Yes)->setText(tr("Install Update"));
question.button(QMessageBox::Open)->setText(tr("Release Notes"));
question.setDefaultButton(QMessageBox::Yes);
const auto answer = question.exec();
if (answer == QMessageBox::Yes) {
Download();
return;
}
if (answer == QMessageBox::Open) {
QDesktopServices::openUrl(m_release.page_url);
}
Finish();
}
void UpdateChecker::Download() {
if (m_game_running) {
Finish(tr("Close the running game before installing an update."));
return;
}
const auto directory = InstallDirectory();
const auto helper = QDir(QCoreApplication::applicationDirPath()).filePath(HelperName());
if (directory.isEmpty() || !QFileInfo::exists(helper) || !QFileInfo::exists(TarProgram())) {
Finish(tr("This installation cannot update itself. Download the release from GitHub and "
"extract it manually."));
return;
}
m_workspace = std::make_unique<QTemporaryDir>(
QDir(directory).filePath(QStringLiteral(".kyty-update-XXXXXX")));
if (!m_workspace->isValid()) {
Finish(tr("The installation folder is not writable. Move KytyPS5 to a writable folder and "
"try again."));
return;
}
if (!QFile::copy(helper, m_workspace->filePath(HelperName())) ||
!QDir(m_workspace->path()).mkdir(QStringLiteral("payload"))) {
Finish(tr("Could not prepare the update folder."));
return;
}
m_download.setFileName(
m_workspace->filePath(QStringLiteral("package") + UpdateRelease::PlatformSuffix()));
if (!m_download.open(QIODevice::WriteOnly)) {
Finish(m_download.errorString());
return;
}
m_hash.reset();
m_download_error.clear();
m_canceled = false;
m_installing = true;
emit InstallingChanged(true);
m_progress = new QProgressDialog(tr("Downloading %1…").arg(m_release.tag), tr("Cancel"), 0, 100,
m_parent);
m_progress->setWindowTitle(tr("KytyPS5 Update"));
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setAutoClose(false);
m_progress->setAutoReset(false);
m_progress->setMinimumDuration(0);
connect(m_progress, &QProgressDialog::canceled, this, [this]() {
m_canceled = true;
if (m_reply) {
m_reply->abort();
} else if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
}
});
auto* reply = m_network.get(Request(m_release.download_url));
m_reply = reply;
reply->setReadBufferSize(1024 * 1024);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { ReadDownload(reply); });
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
if (m_progress) {
m_progress->setValue(
static_cast<int>(qMin<qint64>(99, received * 100 / m_release.size)));
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
ReadDownload(reply);
const auto error = reply->error();
const auto error_text = reply->errorString();
m_reply = nullptr;
reply->deleteLater();
const auto size = m_download.size();
const bool flushed = m_download.flush();
m_download.close();
if (m_canceled) {
Finish();
} else if (!m_download_error.isEmpty()) {
Finish(m_download_error);
} else if (error != QNetworkReply::NoError) {
Finish(error_text);
} else if (!flushed || size != m_release.size || m_hash.result() != m_release.sha256) {
Finish(tr("The update failed its size or SHA-256 verification. Please try again."));
} else {
PreparePackage();
}
});
}
void UpdateChecker::ReadDownload(QNetworkReply* reply) {
const auto data = reply->readAll();
if (data.isEmpty() || m_canceled || !m_download_error.isEmpty()) {
return;
}
if (m_download.size() + data.size() > m_release.size) {
m_download_error = tr("The downloaded file is larger than the release asset.");
} else if (m_download.write(data) != data.size()) {
m_download_error = m_download.errorString();
} else {
m_hash.addData(data);
}
if (!m_download_error.isEmpty() && !reply->isFinished()) {
reply->abort();
}
}
void UpdateChecker::PreparePackage() {
m_progress->setLabelText(tr("Preparing the update…"));
m_progress->setRange(0, 0);
RunPreparation(TarProgram(),
{QStringLiteral("-xf"), m_download.fileName(), QStringLiteral("-C"),
m_workspace->filePath(QStringLiteral("payload")),
QStringLiteral("--no-same-owner"), QStringLiteral("--no-same-permissions")},
false);
}
void UpdateChecker::RunPreparation(const QString& program, const QStringList& arguments,
bool validate) {
m_prepare.disconnect(this);
m_prepare.setProgram(program);
m_prepare.setArguments(arguments);
m_prepare.setWorkingDirectory(m_workspace->path());
auto environment = QProcessEnvironment::systemEnvironment();
environment.remove(QStringLiteral("TAR_OPTIONS"));
m_prepare.setProcessEnvironment(environment);
connect(&m_prepare, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart) {
Finish(m_prepare.errorString());
}
});
connect(&m_prepare, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[this, validate](int exit_code, QProcess::ExitStatus status) {
if (m_canceled) {
Finish();
} else if (exit_code != 0 || status != QProcess::NormalExit) {
Finish(
tr("Could not prepare the update:\n%1")
.arg(QString::fromUtf8(m_prepare.readAllStandardError()).left(4096)));
} else if (!validate) {
RunPreparation(m_workspace->filePath(HelperName()),
{QStringLiteral("--validate"),
m_workspace->filePath(QStringLiteral("payload")),
InstallDirectory(), LauncherRelativePath()},
true);
} else {
Restart();
}
});
m_prepare.start();
}
void UpdateChecker::Restart() {
if (m_game_running || m_canceled) {
Finish(tr("The update was canceled. Close any running game before trying again."));
return;
}
m_progress->setLabelText(tr("Restarting to install the update…"));
m_progress->setCancelButton(nullptr);
QProcess helper;
helper.setProgram(m_workspace->filePath(HelperName()));
helper.setArguments({QStringLiteral("--install"),
m_workspace->filePath(QStringLiteral("payload")), InstallDirectory(),
LauncherRelativePath(),
QString::number(QCoreApplication::applicationPid()), QDir::currentPath()});
helper.setWorkingDirectory(m_workspace->path());
helper.setStandardOutputFile(m_workspace->filePath(QStringLiteral("helper.log")));
helper.setStandardErrorFile(m_workspace->filePath(QStringLiteral("helper.log")),
QIODevice::Append);
if (!helper.startDetached()) {
Finish(tr("Could not start the update installer."));
return;
}
m_workspace->setAutoRemove(false);
QApplication::quit();
}
void UpdateChecker::Finish(QString error, bool report) {
const auto release_page = m_release.page_url;
m_release = {};
m_download.close();
m_workspace.reset();
if (m_progress) {
m_progress->disconnect(this);
m_progress->hide();
m_progress->deleteLater();
m_progress = nullptr;
}
m_busy = false;
m_installing = false;
emit CheckingChanged(false);
emit InstallingChanged(false);
if (error.isEmpty()) {
return;
}
qWarning() << "KytyPS5 updater:" << error;
if (!report) {
return;
}
QMessageBox message(QMessageBox::Warning, tr("KytyPS5 Update"), error, QMessageBox::Close,
m_parent);
if (!release_page.isEmpty()) {
message.addButton(QMessageBox::Open);
message.button(QMessageBox::Open)->setText(tr("Open Release Page"));
}
if (message.exec() == QMessageBox::Open) {
QDesktopServices::openUrl(release_page);
}
}
+89
View File
@@ -0,0 +1,89 @@
#include "updateRelease.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QRegularExpression>
namespace UpdateRelease {
QString PlatformSuffix() {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
return QStringLiteral("-Windows-x64.zip");
#elif defined(__linux__) && defined(__x86_64__)
return QStringLiteral("-Linux-x86_64.tar.gz");
#elif defined(__APPLE__) && defined(__x86_64__)
return QStringLiteral("-macOS-x86_64.zip");
#else
return {};
#endif
}
Info Parse(const QByteArray& data, const QString& platform_suffix) {
Info info;
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
info.error = QObject::tr("Invalid GitHub release response.");
return info;
}
const auto root = document.object();
info.tag = root.value(QStringLiteral("tag_name")).toString();
const auto published_at =
QDateTime::fromString(root.value(QStringLiteral("published_at")).toString(), Qt::ISODate);
if (info.tag.isEmpty() || !published_at.isValid() ||
root.value(QStringLiteral("draft")).toBool() ||
root.value(QStringLiteral("prerelease")).toBool()) {
info.error = QObject::tr("The release is not a published stable release.");
return info;
}
info.published_at = published_at;
const auto encoded_tag = QString::fromLatin1(QUrl::toPercentEncoding(info.tag));
info.page_url = QUrl(PAGE_URL.arg(encoded_tag));
if (platform_suffix.isEmpty()) {
info.error = QObject::tr("Built-in updates are unavailable for this platform.");
return info;
}
const auto asset_name = info.tag + platform_suffix;
for (const auto& value: root.value(QStringLiteral("assets")).toArray()) {
const auto asset = value.toObject();
if (asset.value(QStringLiteral("name")).toString() != asset_name) {
continue;
}
const auto digest = asset.value(QStringLiteral("digest")).toString();
const auto size = asset.value(QStringLiteral("size")).toInteger();
static const QRegularExpression digest_pattern(
QStringLiteral("\\Asha256:[A-Fa-f0-9]{64}\\z"));
if (!digest_pattern.match(digest).hasMatch() || size <= 0) {
info.error = QObject::tr("The update package size or SHA-256 checksum is invalid.");
return info;
}
const auto encoded_asset = QString::fromLatin1(QUrl::toPercentEncoding(asset_name));
info.download_url = QUrl(DOWNLOAD_URL.arg(encoded_tag, encoded_asset));
info.sha256 = QByteArray::fromHex(digest.mid(7).toLatin1());
info.size = size;
return info;
}
info.error = QObject::tr("This release has no update package for your platform.");
return info;
}
VersionRelation Compare(const Info& latest, const Info& current) {
if (latest.tag.isEmpty() || current.tag.isEmpty()) {
return VersionRelation::Unknown;
}
if (latest.tag == current.tag) {
return VersionRelation::Current;
}
if (!latest.published_at.isValid() || !current.published_at.isValid() ||
latest.published_at == current.published_at) {
return VersionRelation::Unknown;
}
return latest.published_at > current.published_at ? VersionRelation::Newer
: VersionRelation::Older;
}
} // namespace UpdateRelease
+14
View File
@@ -1,3 +1,13 @@
QListView#cfgs_grid {
background: palette(base);
border: 1px solid palette(mid);
color: palette(text);
}
QListView#cfgs_grid::item {
padding: 8px;
}
GameListTreeWidget#cfgs_list {
background: transparent;
border: 1px solid palette(mid);
@@ -39,6 +49,7 @@ QLineEdit#search_line_edit:focus {
border-color: palette(highlight);
}
QToolButton#grid_view_button,
QToolButton#refresh_button,
QToolButton#global_settings_button,
QToolButton#input_mapping_button,
@@ -51,6 +62,7 @@ QToolButton#delete_button {
padding: 3px;
}
QToolButton#grid_view_button:hover,
QToolButton#refresh_button:hover,
QToolButton#global_settings_button:hover,
QToolButton#input_mapping_button:hover,
@@ -61,6 +73,8 @@ QToolButton#delete_button:hover {
border-color: palette(mid);
}
QToolButton#grid_view_button:checked,
QToolButton#grid_view_button:pressed,
QToolButton#refresh_button:pressed,
QToolButton#global_settings_button:pressed,
QToolButton#input_mapping_button:pressed,
+128 -29
View File
@@ -20,6 +20,7 @@
#include "graphics/presentation/videoOut.h"
#include "graphics/presentation/window.h"
#include "graphics/shader/shader.h"
#include "kernel/memory.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -44,6 +45,23 @@ namespace Libs::Graphics {
static RenderContext* g_renderer = nullptr;
namespace Gen5Driver {
static constexpr uint64_t DriverDmemBase = 0x0fe0000000ull;
static constexpr uint32_t DriverDmemSize = 0x200000;
static constexpr uint32_t AgcDmemOffset = 0x40000;
struct DriverDmem {
std::mutex mutex;
int64_t physical_offset = -1;
};
static DriverDmem g_driver_dmem;
static int InitializeDmem();
static void ReleaseDmem();
} // namespace Gen5Driver
template <typename... Args>
static void AgcTrace(const char* format, const Args&... args) {
if (Config::GraphicsDebugDumpEnabled()) {
@@ -70,6 +88,7 @@ void Initialize() {
void Shutdown() {
EXIT_IF(g_renderer == nullptr);
g_renderer->ShutdownGpu();
Gen5Driver::ReleaseDmem();
VideoOut::VideoOutShutdown();
WindowShutdown();
g_renderer = nullptr;
@@ -337,6 +356,38 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver) {
}
printf("version = %u\n", ver);
std::scoped_lock lock {Gen5Driver::g_driver_dmem.mutex};
if (Gen5Driver::g_driver_dmem.physical_offset >= 0) {
return OK;
}
if (const auto result = Gen5Driver::InitializeDmem(); result != OK) {
return result;
}
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = Gen5Driver::DriverDmemBase + Gen5Driver::AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
return OK;
}
@@ -830,6 +881,8 @@ enum class RegIndirectPacket : uint32_t {
Uc,
};
static constexpr uint32_t RegIndirectPacketSizeDw = 5;
static uint32_t reg_indirect_native_op(RegIndirectPacket type) {
switch (type) {
case RegIndirectPacket::Cx: return Pm4::IT_SET_CONTEXT_REG_INDIRECT;
@@ -853,7 +906,7 @@ static uint32_t reg_indirect_pm4_r(RegIndirectPacket type) {
static void reg_indirect_write_packet(uint32_t* cmd, uint64_t vaddr, uint32_t num_regs,
RegIndirectPacket type) {
cmd[0] = KYTY_PM4(5, reg_indirect_native_op(type), reg_indirect_pm4_r(type));
cmd[0] = KYTY_PM4(RegIndirectPacketSizeDw, reg_indirect_native_op(type), reg_indirect_pm4_r(type));
cmd[1] = static_cast<uint32_t>(vaddr) & 0xfffffffcu;
cmd[2] = static_cast<uint32_t>(vaddr >> 32u);
cmd[3] = 0x80000000u;
@@ -1728,7 +1781,7 @@ int KYTY_SYSV_ABI AgcDriverRegisterWorkloadStream(uint32_t stream_id, const void
}
uint32_t* KYTY_SYSV_ABI AgcCbNop(CommandBuffer* buf, uint32_t size_in_dwords) {
if (buf == nullptr || size_in_dwords < 2) {
if (buf == nullptr || size_in_dwords == 0) {
return nullptr;
}
@@ -1739,9 +1792,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbNop(CommandBuffer* buf, uint32_t size_in_dwords) {
}
cmd[0] = KYTY_PM4(size_in_dwords, Pm4::IT_NOP, Pm4::R_ZERO);
if (size_in_dwords > 1) {
memset(cmd + 1, 0, static_cast<size_t>(size_in_dwords - 1) * 4);
}
return cmd;
}
@@ -1789,6 +1839,33 @@ uint32_t KYTY_SYSV_ABI AgcCbDispatchGetSize() {
return 20;
}
static constexpr uint32_t BranchPacketSizeDw = 14;
static void write_branch_target(uint32_t* words, uint64_t address, uint8_t cache_policy,
uint32_t size_in_dwords) {
words[0] = (words[0] & 0x3u) | (static_cast<uint32_t>(address) & ~0x3u);
words[1] = static_cast<uint32_t>(address >> 32u);
words[2] = (words[2] & 0xcff00000u) | (size_in_dwords & 0xfffffu) |
((static_cast<uint32_t>(cache_policy) & 0x3u) << 28u);
}
uint64_t KYTY_SYSV_ABI AgcCbBranchGetSize() {
PRINT_NAME();
return BranchPacketSizeDw * sizeof(uint32_t);
}
int KYTY_SYSV_ABI AgcBranchPatchSetThenTarget(uint32_t* cmd, uint8_t cache_policy,
const volatile uint32_t* target,
uint32_t size_in_dwords) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
if (((cmd[0] >> 8u) & 0xffu) != Pm4::IT_INDIRECT_BUFFER) {
return GRAPHICS5_ERROR_INVALID_PACKET;
}
write_branch_target(cmd + 8, reinterpret_cast<uint64_t>(target), cache_policy, size_in_dwords);
return OK;
}
uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t compare_function,
const volatile uint64_t* compare_addr, uint64_t mask,
uint64_t reference, uint8_t cache_policy1,
@@ -1814,7 +1891,7 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
EXIT_NOT_IMPLEMENTED(buf == nullptr);
auto* cmd = buf->AllocateDW(14);
auto* cmd = buf->AllocateDW(BranchPacketSizeDw);
if (cmd == nullptr) {
LOGF_COLOR(Log::Color::Red, "\t failed to allocate branch packet\n");
@@ -1825,7 +1902,7 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
const auto then_vaddr = reinterpret_cast<uint64_t>(buffer1);
const auto else_vaddr = reinterpret_cast<uint64_t>(buffer2);
cmd[0] = KYTY_PM4(14, Pm4::IT_INDIRECT_BUFFER, 0u);
cmd[0] = KYTY_PM4(BranchPacketSizeDw, Pm4::IT_INDIRECT_BUFFER, 0u);
cmd[1] = (mode & 0x3u) | ((static_cast<uint32_t>(compare_function) & 0x7u) << 8u);
cmd[2] = static_cast<uint32_t>(compare_vaddr & 0xfffffff8u);
cmd[3] = static_cast<uint32_t>((compare_vaddr >> 32u) & 0xffffffffu);
@@ -1833,12 +1910,9 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
cmd[5] = static_cast<uint32_t>((mask >> 32u) & 0xffffffffu);
cmd[6] = static_cast<uint32_t>(reference & 0xffffffffu);
cmd[7] = static_cast<uint32_t>((reference >> 32u) & 0xffffffffu);
cmd[8] = static_cast<uint32_t>(then_vaddr & 0xfffffffcu);
cmd[9] = static_cast<uint32_t>((then_vaddr >> 32u) & 0xffffffffu);
cmd[10] = (size_in_dwords1 & 0xfffffu) | ((static_cast<uint32_t>(cache_policy1) & 0x3u) << 28u);
cmd[11] = static_cast<uint32_t>(else_vaddr & 0xfffffffcu);
cmd[12] = static_cast<uint32_t>((else_vaddr >> 32u) & 0xffffffffu);
cmd[13] = (size_in_dwords2 & 0xfffffu) | ((static_cast<uint32_t>(cache_policy2) & 0x3u) << 28u);
std::fill(cmd + 8, cmd + BranchPacketSizeDw, 0u);
write_branch_target(cmd + 8, then_vaddr, cache_policy1, size_in_dwords1);
write_branch_target(cmd + 11, else_vaddr, cache_policy2, size_in_dwords2);
return cmd;
}
@@ -2353,7 +2427,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2364,6 +2438,11 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirectGetSize() {
PRINT_NAME();
return RegIndirectPacketSizeDw * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs) {
@@ -2377,7 +2456,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2391,7 +2470,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize() {
PRINT_NAME();
return 5u * sizeof(uint32_t);
return RegIndirectPacketSizeDw * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
@@ -2407,7 +2486,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer*
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -3647,12 +3726,7 @@ int KYTY_SYSV_ABI AgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(
}
uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet) {
const auto cmd_id = packet[0];
if ((cmd_id & 0x3fffff00u) == 0x3fff1000u) {
return 1;
}
return KYTY_PM4_LEN(cmd_id);
return Pm4::PacketSizeDw(packet[0]);
}
int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication) {
@@ -3694,12 +3768,7 @@ int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_
const auto cmd_id = packet[0];
packet[0] = (cmd_id & ~1u) | predication_bit;
auto size = KYTY_PM4_LEN(cmd_id);
if ((cmd_id & 0x3fffff00u) == 0x3fff1000u) {
size = 1;
}
packet_va += size * sizeof(uint32_t);
packet_va += Pm4::PacketSizeDw(cmd_id) * sizeof(uint32_t);
packet = reinterpret_cast<uint32_t*>(packet_va);
}
@@ -4206,6 +4275,36 @@ namespace Gen5Driver {
LIB_NAME("Graphics5Driver", "Graphics5Driver");
static int InitializeDmem() {
int64_t physical_offset = -1;
auto result = LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), DriverDmemSize, DriverDmemSize,
12, &physical_offset);
if (result != OK) {
return result;
}
void* address = reinterpret_cast<void*>(DriverDmemBase);
result = LibKernel::Memory::KernelMapNamedDirectMemory(
&address, DriverDmemSize, 0x33, 0, physical_offset, DriverDmemSize, "SceAgcDriver");
if (result != OK) {
EXIT_IF(LibKernel::Memory::KernelCheckedReleaseDirectMemory(physical_offset, DriverDmemSize) != OK);
return result;
}
EXIT_IF(reinterpret_cast<uint64_t>(address) != DriverDmemBase);
std::memset(address, 0, DriverDmemSize);
g_driver_dmem.physical_offset = physical_offset;
return OK;
}
static void ReleaseDmem() {
std::scoped_lock lock {g_driver_dmem.mutex};
if (g_driver_dmem.physical_offset >= 0) {
EXIT_IF(LibKernel::Memory::KernelCheckedReleaseDirectMemory(
g_driver_dmem.physical_offset, DriverDmemSize) != OK);
g_driver_dmem.physical_offset = -1;
}
}
struct TessellationDriverState {
uint64_t tf_ring_base = 0;
uint32_t tf_ring_size = 0;
+5
View File
@@ -101,6 +101,10 @@ uint32_t* KYTY_SYSV_ABI AgcCbBranch(CommandBuffer* buf, uint8_t mode, uint8_t co
const volatile uint32_t* buffer1, uint32_t size_in_dwords1,
uint8_t cache_policy2, const volatile uint32_t* buffer2,
uint32_t size_in_dwords2);
uint64_t KYTY_SYSV_ABI AgcCbBranchGetSize();
int KYTY_SYSV_ABI AgcBranchPatchSetThenTarget(uint32_t* cmd, uint8_t cache_policy,
const volatile uint32_t* target,
uint32_t size_in_dwords);
uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32_t offset,
const uint32_t* values, uint32_t num_values);
uint32_t KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values);
@@ -134,6 +138,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegi
uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
uint64_t KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirectGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
+3
View File
@@ -59,6 +59,8 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("k3GhuSNmBLU", Gen5::AgcCbDispatch);
LIB_FUNC("Abendgtz+3o", Gen5::AgcCbDispatchGetSize);
LIB_FUNC("w1KFAHVqpaU", Gen5::AgcCbBranch);
LIB_FUNC("uZW-mqsxkrM", Gen5::AgcCbBranchGetSize);
LIB_FUNC("7Wa3aeJgeVU", Gen5::AgcBranchPatchSetThenTarget);
LIB_FUNC("n2fD4A+pb+g", Gen5::AgcCbSetShRegisterRangeDirect);
LIB_FUNC("bxGoVxpdSPQ", Gen5::AgcCbSetShRegisterRangeDirectGetSize);
LIB_FUNC("UZbQjYAwwXM", Gen5::AgcCbSetShRegistersDirect);
@@ -95,6 +97,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("1DeUNpRIDDA", Gen5::AgcDcbSetCxRegisterDirectGetSize);
LIB_FUNC("w4-d0n60hdo", Gen5::AgcDcbSetUcRegisterDirect);
LIB_FUNC("ZvwO9euwYzc", Gen5::AgcDcbSetCxRegistersIndirect);
LIB_FUNC("GBCh3zCihoU", Gen5::AgcDcbSetCxRegistersIndirectGetSize);
LIB_FUNC("-HOOCn0JY48", Gen5::AgcDcbSetShRegistersIndirect);
LIB_FUNC("nNlUtdDDvZ0", Gen5::AgcDcbSetShRegistersIndirectGetSize);
LIB_FUNC("hvUfkUIQcOE", Gen5::AgcDcbSetUcRegistersIndirect);
+15
View File
@@ -0,0 +1,15 @@
# The installer is copied outside the application before it runs. Keep it free of
# Qt and application DLL dependencies so it can replace those files on Windows.
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
add_executable(kyty_updater main.cpp UpdateInstaller.cpp)
if(MSVC)
target_compile_options(kyty_updater PRIVATE /EHsc)
else()
target_compile_options(kyty_updater PRIVATE -fexceptions)
endif()
if(MINGW)
target_link_options(kyty_updater PRIVATE -static -municode)
endif()
+249
View File
@@ -0,0 +1,249 @@
#include "UpdateInstaller.h"
#include <algorithm>
#include <stdexcept>
#include <vector>
namespace Kyty::Updater {
namespace {
namespace fs = std::filesystem;
struct Entry {
fs::path relative;
fs::file_type type;
};
struct Plan {
fs::path source;
fs::path target;
std::vector<Entry> entries;
};
bool IsWithin(const fs::path& path, const fs::path& root) {
auto part = path.begin();
for (const auto& component: root) {
if (part == path.end() || *part++ != component) {
return false;
}
}
return true;
}
bool IsRelative(const fs::path& path) {
if (path.empty() || path.is_absolute() || path.has_root_name()) {
return false;
}
for (const auto& component: path) {
if (component == ".." || component == "." || component.empty()) {
return false;
}
}
return true;
}
fs::file_type Type(const fs::path& path) {
std::error_code error;
const auto status = fs::symlink_status(path, error);
if (error && error != std::errc::no_such_file_or_directory) {
throw fs::filesystem_error("Cannot inspect update path", path, error);
}
return status.type();
}
void CheckLink(const fs::path& path, const fs::path& root) {
const auto link = fs::read_symlink(path);
if (link.is_absolute() || !IsWithin(fs::canonical(path), root)) {
throw std::runtime_error("Update contains a link outside its installation: " +
path.string());
}
}
void CheckDestination(const fs::path& target, const Entry& entry) {
const auto destination = target / entry.relative;
const auto type = Type(destination);
if (type == fs::file_type::not_found) {
return;
}
if ((entry.type == fs::file_type::directory) != (type == fs::file_type::directory) ||
(type != fs::file_type::regular && type != fs::file_type::directory &&
type != fs::file_type::symlink)) {
throw std::runtime_error("Update would replace a different file type: " +
destination.string());
}
if (type == fs::file_type::symlink) {
CheckLink(destination, target);
}
}
Plan MakePlan(const fs::path& source, const fs::path& target, const fs::path& launcher) {
if (!IsRelative(launcher) || Type(source) != fs::file_type::directory ||
Type(target) != fs::file_type::directory) {
throw std::runtime_error(
"Update source, installation directory, or launcher path is invalid.");
}
Plan plan {fs::canonical(source), fs::canonical(target), {}};
if (IsWithin(plan.target, plan.source)) {
throw std::runtime_error("Update source must not contain the installation directory.");
}
// macOS releases also have flat runtime files; install only the signed bundle.
fs::path scope;
if (launcher.begin()->extension() == ".app") {
scope = *launcher.begin();
}
const auto source_root = scope.empty() ? plan.source : plan.source / scope;
if (Type(source_root) != fs::file_type::directory) {
throw std::runtime_error("The release does not contain the application bundle.");
}
const auto binary_directory = launcher.parent_path();
#ifdef _WIN32
const fs::path suffix = ".exe";
#else
const fs::path suffix;
#endif
for (const auto& required: {launcher, binary_directory / ("kyty_emulator" + suffix.string()),
binary_directory / ("kyty_updater" + suffix.string())}) {
if (Type(plan.source / required) != fs::file_type::regular) {
throw std::runtime_error("Release is missing an executable: " + required.string());
}
#ifndef _WIN32
const auto executable =
fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec;
if ((fs::status(plan.source / required).permissions() & executable) == fs::perms::none) {
throw std::runtime_error("Release executable lacks execute permissions: " +
required.string());
}
#endif
}
if (!scope.empty()) {
plan.entries.push_back({scope, fs::file_type::directory});
}
for (const auto& item: fs::recursive_directory_iterator(source_root)) {
const auto relative = item.path().lexically_relative(plan.source);
const auto type = item.symlink_status().type();
if (relative.begin()->string().starts_with(".kyty-update")) {
throw std::runtime_error("Release uses a reserved updater filename.");
}
if (type == fs::file_type::symlink) {
CheckLink(item.path(), source_root);
} else if (type != fs::file_type::directory && type != fs::file_type::regular) {
throw std::runtime_error("Release contains an unsupported file: " + relative.string());
}
plan.entries.push_back({relative, type});
}
// Checking directories before their children prevents destination symlink traversal.
std::sort(plan.entries.begin(), plan.entries.end(),
[](const Entry& left, const Entry& right) { return left.relative < right.relative; });
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
}
return plan;
}
struct InstallLock {
fs::path path;
void Acquire(const fs::path& target) {
auto candidate = target / ".kyty-update-lock";
if (!fs::create_directory(candidate)) {
throw std::runtime_error(
"Another update is running, or a previous update was interrupted. "
"Close all updater processes, inspect retained backups, then remove " +
candidate.string());
}
path = std::move(candidate);
}
~InstallLock() {
if (!path.empty()) {
std::error_code ignored;
// An incomplete rollback leaves backups here and keeps the lock for recovery.
fs::remove(path, ignored);
}
}
};
struct Change {
fs::path relative;
bool old_moved = false;
bool installed = false;
};
} // namespace
Result Validate(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
try {
MakePlan(source, target, launcher_relative);
// Check actual write access, including ACLs, and interrupted/concurrent updates.
InstallLock lock;
lock.Acquire(target);
return {true, {}};
} catch (const std::exception& error) {
return {false, error.what()};
}
}
Result Install(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
InstallLock lock;
fs::path backup;
std::vector<Change> changes;
std::vector<fs::path> created_directories;
try {
const auto plan = MakePlan(source, target, launcher_relative);
lock.Acquire(plan.target);
backup = lock.path / "backup";
changes.reserve(plan.entries.size());
created_directories.reserve(plan.entries.size());
// The launcher extracts beside the installation, so files can move directly
// from that staging directory without another full copy of the release.
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
const auto destination = plan.target / entry.relative;
if (entry.type == fs::file_type::directory) {
if (fs::create_directory(destination)) {
created_directories.push_back(destination);
}
continue;
}
changes.push_back({entry.relative});
auto& change = changes.back();
if (Type(destination) != fs::file_type::not_found) {
fs::create_directories((backup / entry.relative).parent_path());
fs::rename(destination, backup / entry.relative);
change.old_moved = true;
}
fs::rename(plan.source / entry.relative, destination);
change.installed = true;
}
std::error_code cleanup_error;
fs::remove_all(backup, cleanup_error);
return {true, cleanup_error ? "Update installed; backup cleanup failed: " + backup.string()
: std::string {}};
} catch (const std::exception& error) {
std::string message = error.what();
bool restored = true;
for (auto change = changes.rbegin(); change != changes.rend(); ++change) {
try {
const auto destination = target / change->relative;
if (change->installed) {
fs::rename(destination, source / change->relative);
}
if (change->old_moved) {
fs::rename(backup / change->relative, destination);
}
} catch (const std::exception& rollback_error) {
restored = false;
message += "\nRollback failed: " + std::string(rollback_error.what());
}
}
for (auto directory = created_directories.rbegin(); directory != created_directories.rend();
++directory) {
std::error_code ignored;
fs::remove(*directory, ignored);
}
if (!backup.empty()) {
if (restored) {
std::error_code ignored;
fs::remove_all(backup, ignored);
} else {
message += "\nBackup retained at: " + backup.string();
}
}
return {false, message};
}
}
} // namespace Kyty::Updater
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <filesystem>
#include <string>
namespace Kyty::Updater {
struct Result {
bool success;
std::string message;
};
// Only release files are replaced. Files absent from the release are preserved.
// Successful installation consumes staged files; stage on the target filesystem.
Result Validate(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
Result Install(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
} // namespace Kyty::Updater
+167
View File
@@ -0,0 +1,167 @@
#include "UpdateInstaller.h"
#include <chrono>
#include <fstream>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <spawn.h>
#include <unistd.h>
extern char** environ;
#endif
namespace {
namespace fs = std::filesystem;
bool WaitForParent(unsigned long pid) {
#ifdef _WIN32
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
if (!process) {
return GetLastError() == ERROR_INVALID_PARAMETER;
}
const auto result = WaitForSingleObject(process, 120000);
CloseHandle(process);
return result == WAIT_OBJECT_0;
#else
if (pid > static_cast<unsigned long>(std::numeric_limits<pid_t>::max())) {
return false;
}
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
while (std::chrono::steady_clock::now() < deadline) {
if (kill(static_cast<pid_t>(pid), 0) != 0) {
return errno == ESRCH;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
#endif
}
bool Launch(const fs::path& launcher, const fs::path& directory) {
#ifdef _WIN32
std::wstring command = L"\"" + launcher.wstring() + L"\"";
STARTUPINFOW startup {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process {};
if (!CreateProcessW(launcher.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
directory.c_str(), &startup, &process)) {
return false;
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return true;
#else
if (chdir(directory.c_str()) != 0) {
return false;
}
setsid();
pid_t child;
char* arguments[] = {const_cast<char*>(launcher.c_str()), nullptr};
return posix_spawn(&child, launcher.c_str(), nullptr, nullptr, arguments, environ) == 0;
#endif
}
void WriteResult(const fs::path& workspace, const fs::path& target,
const Kyty::Updater::Result& result) try {
const auto temporary = workspace / "update-result.txt";
std::ofstream stream(temporary, std::ios::trunc);
stream << (result.success ? "success\n" : "failure\n") << result.message;
stream.close();
if (!stream) {
throw std::runtime_error("Cannot write the update result.");
}
const auto destination = target / ".kyty-update-result";
// Never open the destination: rename safely replaces even an existing symlink.
std::error_code ignored;
fs::remove(destination, ignored);
fs::rename(temporary, destination);
} catch (const std::exception& error) {
// Reporting must never prevent the original or updated launcher restarting.
std::cerr << "Cannot record update result: " << error.what() << '\n';
}
void CleanupWorkspace(const fs::path& source, const fs::path& target) try {
const auto workspace = source.parent_path();
if (source.filename() != "payload" ||
!workspace.filename().string().starts_with(".kyty-update-") ||
fs::canonical(workspace.parent_path()) != fs::canonical(target)) {
return;
}
// Windows cannot unlink the running helper. Remove the archive and payload
// separately so that only this small executable can remain after success.
for (const auto& entry: fs::directory_iterator(workspace)) {
std::error_code ignored;
fs::remove_all(entry.path(), ignored);
}
std::error_code ignored;
fs::remove(workspace, ignored);
} catch (const std::exception& error) {
std::cerr << "Update installed; temporary files could not be removed: " << error.what() << '\n';
}
int Run(const std::vector<fs::path>& arguments) {
const bool validate = arguments.size() == 5 && arguments[1] == "--validate";
const bool install = arguments.size() == 7 && arguments[1] == "--install";
if (!validate && !install) {
std::cerr
<< "Usage: kyty_updater --validate SOURCE TARGET LAUNCHER_REL\n"
" kyty_updater --install SOURCE TARGET LAUNCHER_REL PARENT_PID WORKDIR\n";
return 2;
}
const auto source = fs::absolute(arguments[2]);
const auto target = fs::absolute(arguments[3]);
const auto launcher = arguments[4];
if (validate) {
const auto result = Kyty::Updater::Validate(source, target, launcher);
if (!result.success) {
std::cerr << result.message << '\n';
}
return result.success ? 0 : 1;
}
const auto pid_string = arguments[5].string();
std::size_t consumed = 0;
const auto pid = std::stoul(pid_string, &consumed);
if (pid <= 1 || consumed != pid_string.size() || pid_string.front() == '-') {
throw std::runtime_error("Invalid launcher process identifier.");
}
if (!WaitForParent(pid)) {
WriteResult(source.parent_path(), target,
{false, "Update cancelled: the launcher did not exit within two minutes."});
return 1;
}
auto result = Kyty::Updater::Install(source, target, launcher);
WriteResult(source.parent_path(), target, result);
if (!Launch(target / launcher, fs::absolute(arguments[6]))) {
result.message += "\nThe launcher could not restart. Open it manually.";
WriteResult(source.parent_path(), target, result);
std::cerr << result.message << '\n';
return 1;
}
if (result.success) {
CleanupWorkspace(source, target);
}
return result.success ? 0 : 1;
}
} // namespace
#ifdef _WIN32
int wmain(int argc, wchar_t* argv[]) {
#else
int main(int argc, char* argv[]) {
#endif
try {
return Run(std::vector<fs::path>(argv, argv + argc));
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+24 -18
View File
@@ -243,7 +243,7 @@ void TestSrtAliasesRetainReadPolicy() {
program.descriptor_sources.push_back({.dwords = {Value(&first), Value(0u)},
.dword_count = 2});
DescriptorSource indirect;
indirect.indirect_descriptor.emplace().sources = {0, 1};
indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1};
program.descriptor_sources.push_back(indirect);
auto plan = ExtractResourcePlan(program);
for (const auto &inst : plan.value_storage) {
@@ -435,7 +435,7 @@ void TestUnbasedFlatCacheHitMaterializes() {
"unbased FLAT plan produced unexpected descriptors");
}
void TestWrittenDescriptorUsesStrictReaderOnce() {
void TestWrittenDescriptorPredicateReads(bool memory_condition) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@@ -456,10 +456,10 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
// A host-evaluable branch captures resource reads and needs the writable
// descriptor's clean provenance for the renderer's disjointness proof.
// Only memory-derived host decisions need clean, disjoint descriptor reads.
auto &condition = block.AppendNewInst(ValueOpcode::IEqual32,
{Value(&offset), Value(4u)});
{memory_condition ? Value(&read) : Value(&offset),
Value(memory_condition ? 0x8000u : 4u)});
auto &store_block = AddValueBlock(program);
AddValueBlock(program);
program.blocks[0]->condition = Value(&condition);
@@ -480,8 +480,8 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
{Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
Check(plan.capture_specialization_reads,
"conditional writable descriptor lost its alias proof");
Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor used the wrong memory dependency policy");
struct Reads { uint32_t ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{
@@ -501,16 +501,21 @@ void TestWrittenDescriptorUsesStrictReaderOnce() {
}};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1,
"GPU-dirty dynamic writable descriptor bypassed strict provenance");
reads.clean = true;
reads.strict = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.ordinary == 0 && reads.strict == 1 && snapshot.buffers[0].dwords[0] == 0x8000u &&
snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}},
"writable descriptor was evaluated twice or scalar EXEC suppressed its read");
for (const bool clean : {false, true}) {
reads = {.clean = clean};
// A failed predicate explores both edges and the writable descriptor retries its clean read.
const bool expected = !memory_condition || clean;
Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
reads.ordinary == (memory_condition ? 0u : 1u) &&
reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u),
"writable descriptor changed reader policy or bypassed a dirty memory predicate");
if (!expected) continue;
Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
snapshot.specialization_reads == (memory_condition
? std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}
: std::vector<std::pair<uint64_t, uint64_t>>{}),
"writable descriptor lost its address or captured an immutable predicate");
}
}
void TestFailedMaterializationRejectsStage() {
@@ -671,7 +676,8 @@ int main() {
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();
TestWrittenDescriptorUsesStrictReaderOnce();
for (const bool memory_condition : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
+167 -7
View File
@@ -742,7 +742,9 @@ void TestBoundedComputeImageLoop() {
enum class Variant {
Bounded, Plain, Nonzero, TrueEdge, WrongGuard, EntryBypass, ExitBypass, GuardBlock,
WrongStep, DivergentBound, DivergentKey, Disjunction, WrongPolarity,
IncrementBypass, PreviousBound, Masked, MaskedWrongGuard, MaskResurrection, StatusOverwrite,
IncrementBypass, PreviousBound, Forwarded, ForwardedChain, ForwardedJoin,
ForwardedBranch, ForwardedIncrementBypass,
Masked, MaskedWrongGuard, MaskResurrection, StatusOverwrite,
GuardedDiamond, GuardedDiamondBypass
};
const auto make_plan = [](Variant variant) {
@@ -759,16 +761,21 @@ void TestBoundedComputeImageLoop() {
auto *guard = masked ? fixture.AddBlock() : header;
auto *increment = masked ? fixture.AddBlock() : latch;
auto *final_exit = variant == Variant::PreviousBound ? fixture.AddBlock() : exit;
const bool forwarded = variant >= Variant::Forwarded && variant <= Variant::ForwardedIncrementBypass;
auto *forward = forwarded ? fixture.AddBlock() : increment;
auto *incoming = variant == Variant::ForwardedChain ? fixture.AddBlock() : forward;
auto *detour = variant == Variant::ForwardedJoin ? fixture.AddBlock() : nullptr;
entry->AddBranch(header);
if (!masked) {
header->AddBranch(exit);
header->AddBranch(body);
body->AddBranch(latch);
latch->AddBranch(header);
latch->AddBranch(forwarded ? forward : header);
if (variant == Variant::PreviousBound) latch->AddBranch(final_exit);
if (variant == Variant::EntryBypass) entry->AddBranch(body);
if (variant == Variant::ExitBypass) exit->AddBranch(body);
if (variant == Variant::IncrementBypass || variant == Variant::PreviousBound)
if (variant == Variant::IncrementBypass || variant == Variant::PreviousBound ||
variant == Variant::ForwardedIncrementBypass)
exit->AddBranch(latch);
fixture.program.blocks[0]->terminator = {
.kind = variant == Variant::EntryBypass
@@ -789,16 +796,16 @@ void TestBoundedComputeImageLoop() {
.kind = variant == Variant::PreviousBound
? CFG::TerminatorKind::ConditionalBranch
: CFG::TerminatorKind::Branch,
.true_block = fixture.program.blocks[1u],
.true_block = forwarded ? forward : fixture.program.blocks[1u],
.false_block = final_exit};
fixture.program.blocks[4]->terminator = {
.kind = final_exit != exit || variant == Variant::ExitBypass ||
variant == Variant::IncrementBypass
variant == Variant::IncrementBypass || variant == Variant::ForwardedIncrementBypass
? CFG::TerminatorKind::Branch
: CFG::TerminatorKind::Return,
.true_block =
fixture.program.blocks[final_exit != exit ||
variant == Variant::IncrementBypass
variant == Variant::IncrementBypass || variant == Variant::ForwardedIncrementBypass
? 3u
: 2u]};
if (final_exit != exit)
@@ -822,6 +829,29 @@ void TestBoundedComputeImageLoop() {
count}, 0, compare);
const auto initial_active = fixture.Emit(ValueOpcode::INotEqual32,
{local, Value(0u)}, 0, entry);
if (forwarded) {
forward->AddBranch(incoming == forward ? header : incoming);
forward->terminator = {.kind = CFG::TerminatorKind::Branch,
.true_block = incoming == forward ? header : incoming};
if (incoming != forward) {
incoming->AddBranch(header);
incoming->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = header};
}
if (variant == Variant::ForwardedBranch) {
forward->AddBranch(exit);
forward->condition = initial_active;
forward->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = header, .false_block = exit};
}
if (detour != nullptr) {
latch->AddBranch(detour);
latch->condition = initial_active;
latch->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = forward, .false_block = detour};
detour->AddBranch(forward);
detour->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = forward};
}
}
if (masked) {
auto *active_phi = diamond ? nullptr : &header->AppendNewInst(
ValueOpcode::Phi, {}, static_cast<uint64_t>(Type::U1));
@@ -934,7 +964,9 @@ void TestBoundedComputeImageLoop() {
{key, Value(variant == Variant::WrongStep ? 2u : 1u)},
0, increment);
phi.AddPhiOperand(entry, variant == Variant::DivergentKey ? local : Value(0u));
phi.AddPhiOperand(increment, step);
phi.AddPhiOperand(forwarded ? incoming : increment, step);
Check(!forwarded || step.ResolveInstruction()->Parent() != phi.PhiBlock(1),
"forwarded loop fixture did not separate the increment from its Phi incoming edge");
fixture.block = variant == Variant::GuardBlock ? header : body;
const auto table = fixture.Address(fixture.UserData(0), fixture.UserData(1));
@@ -975,7 +1007,11 @@ void TestBoundedComputeImageLoop() {
make_plan(Variant::TrueEdge);
make_plan(Variant::Masked);
make_plan(Variant::GuardedDiamond);
make_plan(Variant::Forwarded);
make_plan(Variant::ForwardedChain);
for (const auto variant : {Variant::IncrementBypass, Variant::PreviousBound,
Variant::ForwardedJoin, Variant::ForwardedBranch,
Variant::ForwardedIncrementBypass,
Variant::MaskedWrongGuard,
Variant::MaskResurrection, Variant::StatusOverwrite,
Variant::GuardedDiamondBypass}) {
@@ -2612,6 +2648,129 @@ void TestFiniteImagePhiCycle() {
}
}
void TestFiniteInlineSampler() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
Fixture fixture;
auto* entry = fixture.block;
auto* defined = fixture.AddBlock();
auto* merge = fixture.AddBlock();
entry->AddBranch(defined);
entry->AddBranch(merge);
defined->AddBranch(merge);
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
entry->condition = fixture.Emit(ValueOpcode::IEqual32, {lane, Value(0u)});
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = defined, .false_block = merge};
defined->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = merge};
merge->terminator.kind = CFG::TerminatorKind::Return;
fixture.block = merge;
auto& lod = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
auto& filter = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
lod.AddPhiOperand(entry, Value(0u));
lod.AddPhiOperand(defined, Value(0x00fff000u));
filter.AddPhiOperand(defined, Value(0x0a500000u));
filter.AddPhiOperand(entry, Value(0u));
const auto sampler = fixture.Sampler({Value(0u), Value(&lod), Value(&filter), Value(0u)});
const auto direct = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
const auto previous = fixture.Sampler(
{Value(0u), Value(0x00fff000u), Value(0x0a500000u), Value(0u)});
using F = Libs::Graphics::Prospero::BufferFormat;
for (const auto format : {F::k32Float, F::k32SInt}) {
const auto image = fixture.Image({Value(0x1000u), Value(uint32_t(format) << 20u),
Value(0u), Value(Libs::Graphics::DstSel(4, 5, 6, 7) |
(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u)),
Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x440u));
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x1dd0u));
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
fixture.AddMemory(memory, 0x1dd8u));
}
fixture.PlanAndTrack();
const auto root = fixture.program.memory_info[1].sampler;
const auto source = fixture.program.info.samplers[root].source;
const auto& finite = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(finite && finite->sources.size() == 2 && fixture.program.info.samplers.size() == 3,
"finite inline sampler lost either state or aliased the direct sampler");
const auto* key = sampler.Instruction()->Arg(0).Instruction();
Check(key->GetOpcode() == ValueOpcode::Phi && key->Parent() == merge &&
key->PhiBlock(0) == entry && key->PhiBlock(1) == defined &&
key->Arg(0).U32() == 0 && key->Arg(1).U32() == 1,
"inline sampler DWORD correlation did not preserve its GPU selector");
const auto plan = ExtractResourcePlan(fixture.program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.read_specialization_memory = ReadTestMemory};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite inline sampler could not materialize literal choices");
const auto candidates = fixture.program.info.samplers[root].indirect_resources;
Check(candidates.size() == 2 && snapshot.samplers[candidates[0]].dwords[1] == 0 &&
snapshot.samplers[candidates[0]].dwords[2] == 0 &&
snapshot.samplers[candidates[1]].dwords[1] == 0x00fff000u &&
snapshot.samplers[candidates[1]].dwords[2] == 0x0a500000u,
"finite sampler materialization mixed the two correlated DWORD states");
Check(candidates[1] == fixture.program.memory_info[0].sampler && candidates[1] < root,
"finite sampler did not reuse the earlier constant candidate");
ApplyResourceSpecialization(fixture.program, specialization);
for (const uint32_t operation : {1u, 3u}) {
const auto selected = fixture.program.memory_info[operation].sampler;
const auto& choices = fixture.program.info.samplers[selected].indirect_resources;
Check(choices.size() == 2, "sampler class specialization lost a finite candidate");
for (uint32_t ordinal = 0; ordinal < choices.size(); ++ordinal) {
const auto& choice = fixture.program.info.samplers[choices[ordinal]];
Check(choice.snapshot_index == candidates[ordinal] &&
choice.force_point_filtering == (operation == 3u) &&
choice.integer_border == (operation == 3u),
"finite sampler class remapping changed candidate identity or filtering");
}
}
Check(fixture.program.info.samplers[fixture.program.memory_info[2].sampler]
.indirect_resources.empty(),
"direct sampler was changed into a dynamic selection");
auto invalid = ExtractResourcePlan(fixture.program);
invalid.info.samplers[0].source = static_cast<uint32_t>(invalid.descriptor_sources.size());
Check(!MaterializeResources(invalid, runtime, snapshot, specialization),
"invalid sampler source bypassed materialization validation");
Fixture adjusted;
auto* initial = adjusted.block;
auto* alternate = adjusted.AddBlock();
auto* join = adjusted.AddBlock();
initial->AddBranch(alternate);
initial->AddBranch(join);
alternate->AddBranch(join);
initial->condition = adjusted.Emit(ValueOpcode::IEqual32,
{adjusted.Emit(ValueOpcode::LaneId), Value(0u)});
initial->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = alternate, .false_block = join};
alternate->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = join};
join->terminator.kind = CFG::TerminatorKind::Return;
auto& selected = join->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
selected.AddPhiOperand(initial, Value(0u));
selected.AddPhiOperand(alternate, Value(0x00fff000u));
adjusted.block = join;
const auto shared = adjusted.Sampler({Value(0u), Value(&selected), Value(0u), Value(0u)});
const auto image = adjusted.Image({Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u), Value(0u)});
for (const uint32_t flags : {0u, uint32_t(Decoder::ImageSampleFlagAdjust)}) {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = flags;
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
adjusted.AddMemory(memory, 0x1dd0u));
}
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
"finite sampler planning accepted a shared SampleAdjust handle");
}
void TestFiniteImageBitScanSentinel() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const bool nonzero : {false, true}) {
@@ -3572,6 +3731,7 @@ int main() {
Run("writable descriptor phi", TestWritableDescriptorPhi);
Run("conditional sampler phi", TestConditionalSamplerPhi);
Run("finite image phi cycle", TestFiniteImagePhiCycle);
Run("finite inline sampler", TestFiniteInlineSampler);
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
Run("invariant loop phi", TestInvariantLoopPhi);
File diff suppressed because it is too large. Load diff
+601 -33
View File
@@ -15,6 +15,7 @@
#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/recompiler/frontend/translate/Translate.h"
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include "graphics/shader/recompiler/ir/IREmitter.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h"
@@ -1944,14 +1945,18 @@ void TestSopkCompareImmediateExtension() {
void TestDisabledDebugBranches() {
using namespace ShaderRecompiler;
// Both debug branches fall through when conditional shader debugging is disabled.
// Debug branches fall through when conditional shader debugging is disabled.
// Retain the post-ENDPGM helper displacement from PPSA08709 and exercise the
// OR_USER variant encountered in PPSA03671 with the same body.
for (const auto opcode : {Decoder::Opcode::S_CBRANCH_CDBGSYS,
Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER}) {
const std::pair<uint32_t, Decoder::Opcode> branches[] = {
{0x17, Decoder::Opcode::S_CBRANCH_CDBGSYS},
{0x18, Decoder::Opcode::S_CBRANCH_CDBGUSER},
{0x19, Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER},
{0x1a, Decoder::Opcode::S_CBRANCH_CDBGSYS_AND_USER}};
for (const auto [encoding, opcode] : branches) {
std::array<uint32_t, 274> shader;
shader.fill(EncodeSopp(0x00));
shader[0] = EncodeSopp(opcode == Decoder::Opcode::S_CBRANCH_CDBGSYS ? 0x17 : 0x19, 0x10f);
shader[0] = EncodeSopp(encoding, 0x10f);
shader[1] = EncodeVop1(0x01, 1, 129);
shader[2] = EncodeMubuf0(0x1c, 0, false);
shader[3] = EncodeMubuf1(1, 0, 0);
@@ -1961,6 +1966,7 @@ void TestDisabledDebugBranches() {
Decoder::Program decoded;
Decoder::DecodeProgram(shader, decoded);
Check(decoded.instructions.front().opcode == opcode &&
decoded.instructions.front().opcode_id == encoding &&
decoded.instructions.front().branch_target == 0x440u &&
decoded.instructions.back().pc == 0x43cu &&
decoded.instructions.back().opcode == Decoder::Opcode::S_ENDPGM,
@@ -4822,34 +4828,92 @@ void TestNewShaderDecoderArchitecture() {
"VOP2 DPP V_PK_FMAC_F16 lost its implicit packed modifiers");
}
void TestNewShaderRecompilerRejectsDppOn64BitCompares() {
const uint32_t opcodes[] = {
0xa2u, 0xa5u, 0xb5u, 0xe2u, 0xe4u, 0xe5u,
0xf5u}; // eq_i64, ne_i64, cmpx_ne_i64, eq_u64, gt_u64, ne_u64, cmpx_ne_u64
for (const auto opcode : opcodes) {
const uint32_t shader[] = {
EncodeVopc(opcode, 250u, 0u), // DPP escape in SRC0
EncodeVop2Dpp(0u),
0xbf810000u,
};
ShaderRecompiler::Decoder::Program program;
ShaderRecompiler::Decoder::DecodeProgram(shader, program);
Check(program.instructions.size() == 2u,
"64-bit VOPC DPP decode did not consume its modifier word");
const auto &compare = program.instructions.front();
Check(compare.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
"64-bit VOPC illegally accepted a DPP modifier");
Check((compare.unsupported_reason.find("VOPC DPP modifier is not supported for opcode") != std::string::npos),
"64-bit VOPC DPP rejection reason was not explicit");
void TestNewShaderRecompilerInteger64CompareEncodings() {
namespace D = ShaderRecompiler::Decoder;
using O = D::Opcode;
const O families[][8] = {
{O::V_CMP_F_I64, O::V_CMP_LT_I64, O::V_CMP_EQ_I64, O::V_CMP_LE_I64,
O::V_CMP_GT_I64, O::V_CMP_NE_I64, O::V_CMP_GE_I64, O::V_CMP_T_I64},
{O::V_CMPX_F_I64, O::V_CMPX_LT_I64, O::V_CMPX_EQ_I64, O::V_CMPX_LE_I64,
O::V_CMPX_GT_I64, O::V_CMPX_NE_I64, O::V_CMPX_GE_I64, O::V_CMPX_T_I64},
{O::V_CMP_F_U64, O::V_CMP_LT_U64, O::V_CMP_EQ_U64, O::V_CMP_LE_U64,
O::V_CMP_GT_U64, O::V_CMP_NE_U64, O::V_CMP_GE_U64, O::V_CMP_T_U64},
{O::V_CMPX_F_U64, O::V_CMPX_LT_U64, O::V_CMPX_EQ_U64, O::V_CMPX_LE_U64,
O::V_CMPX_GT_U64, O::V_CMPX_NE_U64, O::V_CMPX_GE_U64, O::V_CMPX_T_U64}};
const uint32_t bases[] = {0xa0u, 0xb0u, 0xe0u, 0xf0u};
for (uint32_t family = 0; family < std::size(families); ++family) {
const bool update_exec = (family % 2) != 0;
for (uint32_t predicate = 0; predicate < 8; ++predicate) {
const auto encoding = bases[family] + predicate;
for (const bool vop3 : {false, true}) {
const std::array<uint32_t, 2> words = vop3
? std::array{EncodeVop3Word0(encoding, 20),
EncodeVop3Word1(257, 259, 0)}
: std::array{EncodeVopc(encoding, 257, 3), 0xbf810000u};
D::Instruction compare;
D::DecodeInstruction(words, 0, compare);
const auto destination = update_exec ? D::OperandKind::ExecLo
: vop3 ? D::OperandKind::Sgpr : D::OperandKind::VccLo;
Check(compare.family == (vop3 ? D::Family::VOP3 : D::Family::VOPC) &&
compare.opcode == families[family][predicate] &&
compare.opcode_id == encoding &&
compare.word_count == (vop3 ? 2u : 1u) &&
compare.dst.kind == destination &&
(!vop3 || update_exec || compare.dst.reg == 20) &&
compare.src_count == 2 &&
compare.src0.kind == D::OperandKind::Vgpr && compare.src0.reg == 1 &&
compare.src1.kind == D::OperandKind::Vgpr && compare.src1.reg == 3,
"64-bit integer compare decoded incorrectly");
for (const bool high_src0 : {false, true}) {
// Only the compare's pair width makes v13 reachable by MOVRELD,
// including predicates F/T that never read their operands.
const uint32_t src0 = high_src0 ? 268u : 266u;
const uint32_t src1 = high_src0 ? 10u : 12u;
std::vector<uint32_t> shader{EncodeVop1(0x42, 12, 256)};
if (vop3) {
shader.push_back(EncodeVop3Word0(encoding, 20));
shader.push_back(EncodeVop3Word1(src0, src1 + 256, 0));
} else {
shader.push_back(EncodeVopc(encoding, src0, src1));
}
shader.push_back(EncodeSopp(0x01));
D::Program decoded;
D::DecodeProgram(shader, decoded);
ShaderComputeInputInfo compute{};
ShaderRecompiler::Frontend::TranslateOptions options{.stage = ShaderType::Compute};
options.input_info.compute = &compute;
const auto program = ShaderRecompiler::Frontend::TranslateProgram(
decoded, ShaderRecompiler::CFG::BuildGraph(decoded), options);
bool includes_high_word = false;
for (const auto *block : program.blocks) {
for (const auto &inst : *block) {
includes_high_word |=
inst.GetOpcode() == ShaderRecompiler::IR::ValueOpcode::SetVectorRegister &&
ShaderRecompiler::IR::RegIndex(inst.Arg(0).VectorRegister()) == 13;
}
}
Check(includes_high_word, "64-bit compare omitted a VGPR pair's high word");
}
}
for (const bool dpp : {false, true}) {
const uint32_t shader[] = {
EncodeVopc(encoding, dpp ? 250u : 249u, 3),
dpp ? EncodeVop2Dpp(1) : EncodeVopcSdwa(1),
0xbf810000u};
D::Program program;
D::DecodeProgram(shader, program);
Check(program.instructions.size() == 2u &&
program.instructions.back().opcode == O::S_ENDPGM,
"64-bit VOPC decode did not consume its modifier word");
const auto &compare = program.instructions.front();
const auto reason = dpp ? "VOPC DPP modifier is not supported for opcode"
: "VOPC SDWA modifier is not supported for opcode";
Check(compare.opcode == O::UNSUPPORTED && compare.word_count == 2u &&
compare.unsupported_reason.find(reason) != std::string::npos,
"64-bit VOPC did not explicitly reject an illegal modifier");
}
}
}
const uint32_t sdwa[] = {EncodeVopc(0xa5u, 249u, 0u), 0x06060000u};
ShaderRecompiler::Decoder::Instruction compare;
ShaderRecompiler::Decoder::DecodeInstruction(sdwa, 0u, compare);
Check(compare.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED &&
compare.word_count == 2u &&
compare.unsupported_reason.find("VOPC SDWA modifier is not supported") != std::string::npos,
"V_CMP_NE_I64 accepted an illegal SDWA encoding");
}
void TestNewShaderRecompilerCapturedVopcSdwaCmpxClass() {
@@ -10308,6 +10372,218 @@ void TestNewShaderRecompilerBranchConditionForms() {
}
}
void TestGraphicsComputeGetreg() {
using namespace ShaderRecompiler;
for (const auto [word, expected] : {std::pair{0xb96a1818u, 6u}, std::pair{0xb96a0a18u, 0u}}) {
Decoder::Instruction decoded;
Decoder::DecodeInstruction(std::span(&word, 1), 0, decoded);
Check(decoded.opcode == Decoder::Opcode::S_GETREG_B32 &&
decoded.dst.kind == Decoder::OperandKind::VccLo &&
decoded.src0.value == (word & 0xffffu), "GETREG field or VCC destination decoded incorrectly");
for (const auto wave : {32u, 64u}) {
IR::Program program;
program.wave_size = wave;
IR::Block block;
Frontend::Translator translator(program, &block, 1, false, true);
translator.TranslateInstruction(decoded);
bool wrote_vcc = false;
for (const auto& inst : block) {
Check(inst.GetOpcode() != IR::ValueOpcode::SetScc, "GETREG changed SCC");
if (inst.GetOpcode() == IR::ValueOpcode::SetVccLo) {
Check(inst.Arg(0).IsImmediate() && inst.Arg(0).U32() == expected,
"graphics compute hardware field has the wrong value");
wrote_vcc = true;
}
}
Check(wrote_vcc, "GETREG did not write VCC_LO");
}
}
const uint32_t shader[] = {0xb96a1818u, EncodeSopp(0x01)};
HW::ComputeShaderInfo regs{};
regs.cs_regs.data_addr = reinterpret_cast<uint64_t>(shader);
ShaderMappedData mapped{};
mapped.code_size_bytes = sizeof(shader);
ShaderMapUserData(regs.cs_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
ShaderComputeInputInfo graphics{}, async{};
async.async_compute = true;
(void)PrepareProgram(regs, sh, graphics);
(void)PrepareProgram(regs, sh, async);
Check(!graphics.async_compute && async.async_compute &&
MakeStageStaticKey(graphics) != MakeStageStaticKey(async) &&
MakeStageStaticKey(graphics).size() == MakeStageStaticKey(async).size(),
"compute input reset or cache identity lost queue provenance");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
auto options = MakeCompileOptions(ShaderType::Compute);
options.input_info.compute = &async;
ExpectFatal([&] { (void)TranslateProgram(shader, options); }, "async GETREG used graphics constants");
auto vertex = MakeCompileOptions(ShaderType::Vertex);
ExpectFatal([&] { (void)TranslateProgram(shader, vertex); }, "vertex GETREG used compute constants");
options.input_info.compute = &graphics;
const uint32_t unsupported[] = {0xb96af818u, EncodeSopp(0x01)};
Decoder::Program decoded;
Decoder::DecodeProgram(unsupported, decoded);
Check(decoded.instructions[0].src0.value == 0xf818u, "GETREG selector was sign extended");
ExpectFatal([&] { (void)TranslateProgram(unsupported, options); }, "unsupported GETREG field was accepted");
#endif
}
void TestMemoryFedScalarLeafCall() {
using namespace ShaderRecompiler;
constexpr uint64_t base = 0xfe0040000ull;
const uint32_t shader[] = {
0xbeea03ffu, 0xe0040000u, 0xbeeb03ffu, 0x0000000fu,
0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u,
0xbf8cc07fu, 0xbf130e80u, 0xbf840004u, 0xf4240402u, 0xfa000028u,
0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u};
Decoder::Program decoded;
Decoder::DecodeProgram(shader, decoded);
Check(decoded.has_swap_pc, "decoded scalar call was not recorded");
const uint32_t literal[] = {EncodeSMovB32(0, 255), 0xbe8e210eu, EncodeSopp(0x01)};
Decoder::DecodeProgram(literal, decoded);
Check(!decoded.has_swap_pc && decoded.instructions.size() == 2,
"reused decoder retained a call or treated a literal as SWAPPC");
const uint32_t null_call[] = {EncodeSop1(0x21, 125, 14), EncodeSopp(0x01)};
Decoder::DecodeProgram(null_call, decoded);
Check(!decoded.has_swap_pc && decoded.instructions.front().opcode == Decoder::Opcode::S_SETPC_B64,
"NULL-destination SWAPPC was recorded as a call");
struct Memory {
std::array<uint32_t, 128> words{};
uint64_t end = base + 0x10c;
bool clean = true;
} memory;
std::copy_n(std::array{0xe0040060u, 0x0010000fu, 8u, 0x5204u}.begin(),
4, memory.words.begin());
memory.words[32] = static_cast<uint32_t>(base + 0x103); // SWAPPC aligns the old pair.
memory.words[33] = static_cast<uint32_t>(base >> 32u);
memory.words[64] = EncodeSMovB32(3, 255);
memory.words[65] = 0x12345678;
memory.words[66] = EncodeSop1(0x21, 125, 14);
const auto read = [](void *data, uint64_t address, std::span<uint32_t> out) {
const auto& m = *static_cast<Memory*>(data);
if (!m.clean || address < base || address > m.end || out.size_bytes() > m.end - address)
return false;
std::memcpy(out.data(), reinterpret_cast<const uint8_t*>(m.words.data()) + address - base,
out.size_bytes());
return true;
};
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
const auto options = MakeCompileOptions(ShaderType::Compute);
auto source = PrepareShaderSource(shader, options);
Check(source.call && source.decoded.has_swap_pc &&
source.decoded.instructions[source.call->instruction].branch_target == UINT32_MAX,
"memory-fed SWAPPC did not retain an unresolved target");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal([&] { (void)TranslateProgram(source.decoded, options); },
"unresolved SWAPPC silently fell through");
#endif
const auto& linked = RefreshShaderSource(source, runtime);
Check(linked.has_swap_pc && source.revision == 1 && linked.instructions.back().branch_target == 0x40 &&
linked.instructions[source.call->instruction].branch_target == 0x44 &&
source.reads.back() == std::pair<uint64_t, uint64_t>{base + 0x100, 12},
"aliased call target, exact leaf range, or continuation was lost");
const auto* storage = linked.instructions.data();
const auto* scratch = source.linked->observed_function.data();
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 1 && source.linked->decoded.instructions.data() == storage &&
source.linked->observed_function.data() == scratch,
"unchanged leaf rebuilt decoded instructions or refresh storage");
auto translated = TranslateProgram(linked, options);
translated.program.source_reads = source.reads;
const auto plan = IR::ExtractResourcePlan(translated.program);
Check(plan.source_reads == source.reads && plan.capture_specialization_reads,
"linked code dependencies were omitted from resource alias validation");
std::vector<uint32_t> writer(std::begin(shader), std::end(shader) - 1);
writer.insert(writer.end(), {EncodeVop1(0x01, 1, 3), EncodeMubuf0(0x1c, 0, false),
EncodeMubuf1(1, 6, 0), EncodeSopp(0x01)});
std::array<uint32_t, 64> user_data{};
user_data[25] = static_cast<uint32_t>(base >> 32u);
user_data[26] = 4;
user_data[27] = 3u << 28u;
auto write_options = options;
write_options.user_data = user_data;
auto write_runtime = runtime;
write_runtime.user_data = user_data;
auto write_source = PrepareShaderSource(writer, write_options);
auto write_program = TranslateProgram(RefreshShaderSource(write_source, write_runtime), write_options);
write_program.program.source_reads = write_source.reads;
auto write_plan = IR::ExtractResourcePlan(write_program.program);
IR::ResourceSnapshot snapshot;
IR::ResourceSpecialization specialization;
const auto written = std::ranges::find_if(write_plan.info.buffers,
[](const auto& buffer) { return buffer.written; });
Check(written != write_plan.info.buffers.end(), "caller store lost its writable binding");
const auto output_index = static_cast<size_t>(written - write_plan.info.buffers.begin());
for (const auto offset : {0x80u, 0x100u, 0x1f0u}) {
user_data[24] = static_cast<uint32_t>(base + offset);
Check(IR::MaterializeResources(write_plan, write_runtime, snapshot, specialization) &&
snapshot.buffers[output_index].dwords[0] == user_data[24] &&
snapshot.buffers[output_index].dwords[2] == 4 &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{base + 0x80, 4}) != snapshot.specialization_reads.end() &&
std::ranges::find(snapshot.specialization_reads,
std::pair<uint64_t, uint64_t>{base + 0x100, 12}) != snapshot.specialization_reads.end(),
"call dependencies or writable ranges were lost before binding validation");
}
// Relocating identical code updates dependencies without rebuilding the source.
std::copy_n(memory.words.begin() + 64, 3, memory.words.begin() + 96);
memory.words[32] = static_cast<uint32_t>(base + 0x180);
memory.end = base + 0x18c;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 1 && source.reads.back().first == base + 0x180,
"identical relocated leaf recompiled or retained its old memory dependency");
memory.words[97] = 0x87654321;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 2 && source.linked->code.back() == EncodeSop1(0x21, 125, 14),
"changed leaf bytes retained stale source");
// A newly selected shorter function may end before the old bulk-read length.
memory.words[96] = EncodeSMovB32(3, 129);
memory.words[97] = EncodeSop1(0x21, 125, 14);
memory.end = base + 0x188;
(void)RefreshShaderSource(source, runtime);
Check(source.revision == 3 && source.reads.back().second == 8,
"shorter exact-boundary leaf was not decoded after a failed old-span read");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
memory.clean = false;
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"unreadable call dependencies reused stale linked code");
memory.clean = true;
auto ordinary = runtime;
ordinary.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, ordinary); },
"call target query accepted an ordinary non-strict memory reader");
std::vector<uint32_t> escaping(std::begin(shader), std::end(shader) - 1);
escaping.insert(escaping.end(), {EncodeVop1(0x01, 1, 14),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), EncodeSopp(0x01)});
ExpectFatal([&] {
auto escaping_source = PrepareShaderSource(escaping, options);
(void)TranslateProgram(RefreshShaderSource(escaping_source, runtime), options);
}, "saved native return PC escaped into GPU data");
memory.words[96] = EncodeSMovB32(14, 129);
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"leaf clobbering its return pair was linked");
// AND_B64 writes the high half despite the decoder's generic scalar width.
const uint32_t high_writer[] = {
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto pair_source = PrepareShaderSource(high_writer, options);
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
uint32_t high = 1;
Check(pair_walker.Evaluate(pair_source.call->target[1], high) && high == 0,
"scalar pair producer used a stale preceding high-half definition");
const uint32_t joined[] = {
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
EncodeSMovB32(14, 130), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
ExpectFatal([&] { (void)PrepareShaderSource(joined, options); },
"ambiguous reaching definitions were accepted for a call target");
const uint32_t multiple[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
"call metadata bypassed the multiple-call rejection");
#endif
}
void TestNewShaderRecompilerSetpcBranch() {
const uint32_t shader[] = {
EncodeSop1(0x1f, 4, 0), // s_getpc_b64 s[4:5]
@@ -10350,7 +10626,10 @@ void TestFusedShaderHandoffPreservesRegisters() {
options.back_code = back;
for (const auto handoff: {EncodeSop1(0x20, 0, 6), 0xbefd2106u}) {
front[2] = handoff; // SETPC or captured SWAPPC with NULL destination
auto translated = TranslateProgram(front, options);
auto source = PrepareShaderSource(front, options);
Check(!source.decoded.has_swap_pc && !source.call,
"fused SETPC handoff entered scalar call preparation");
auto translated = TranslateProgram(source.decoded, options);
uint32_t allocations = 0;
for (const auto* block: translated.program.blocks) {
for (const auto& inst: *block) {
@@ -10365,6 +10644,17 @@ void TestFusedShaderHandoffPreservesRegisters() {
}
Check(allocations == 1u, "fused shader omitted the back shader allocation");
}
const uint32_t call_front[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x20, 0, 6)};
Check(Decoder::DecodeFrontProgram(call_front).has_swap_pc,
"front decoder omitted its scalar call");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
ExpectFatal([&] { (void)PrepareShaderSource(call_front, options); },
"fused front call bypassed the non-compute rejection");
const uint32_t call_back[] = {EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
options.back_code = call_back;
ExpectFatal([&] { (void)PrepareShaderSource(front, options); },
"fused back call metadata was not propagated");
#endif
}
void TestMeshExportStorage() {
@@ -13682,6 +13972,278 @@ void TestGpuProducedWritableDescriptor() {
"GPU-updated address or record count created a shader permutation");
}
void TestImmutableDescriptorPredicate() {
using namespace ShaderRecompiler::IR;
ResourcePlan plan;
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
auto &inst = plan.value_storage.emplace_back(op);
uint32_t index = 0;
for (const auto arg : args) inst.SetArg(index++, arg);
return Value(&inst);
};
const auto handle = emit(ValueOpcode::GetBufferResource,
{Value(0x1000u), Value(4u << 16u), Value(16u), Value(0u)});
for (const bool coherent : {false, true}) {
plan.memory_info.push_back({.kind = ResourceKind::Buffer,
.idxen = !coherent, .coherent = coherent});
const auto loaded = emit(ValueOpcode::LoadBufferU32,
{handle, Value(0u), Value(0u), Value(0u), Value(true)});
loaded.ResolveInstruction()->SetFlags(MemoryFlags{
.index = static_cast<uint32_t>(plan.memory_info.size() - 1u)});
const auto mask = emit(ValueOpcode::INotEqual32, {loaded, Value(0u)});
const auto selected = emit(ValueOpcode::SelectU32, {mask, Value(1u), Value(0u)});
const auto reduced = emit(ValueOpcode::ReadFirstLane, {selected, mask});
Check(ValidateRuntimeValue(plan, reduced, RuntimeValueType::Integer),
"integer EXEC reduction lost its active-lane proof");
Check(!ValidateRuntimeValue(plan, reduced, RuntimeValueType::ImmutableInteger),
"memory-dependent EXEC reduction was classified as immutable");
}
}
void TestUniformSelectedWritableDescriptor() {
using namespace ShaderRecompiler::IR;
// SAROS b9da5e64f4c5b10a builds s[8:11] from two scalar address expressions,
// then sets the stride in the high address word after the branch merge.
const uint32_t shader[] = {
0xbfa00002u, 0xbf070280u, 0xbf84000du, 0xf4001a82u,
0xfa00009cu, 0xbf8cc07fu, 0x8f6b856au, 0xf4001a82u,
0xfa000080u, 0xbf8cc07fu, 0x98eb6b6au, 0xb7eb0080u,
0x80086b03u, 0xbf800000u, 0x82098000u, 0xbf820006u,
0xf4041a82u, 0xfa000040u, 0xbf8cc07fu, 0x8008066au,
0xbf800000u, 0x8209016bu, 0xd7460001u, 0x04010c07u,
0x7e000280u, 0xf4001a82u, 0xfa000084u, 0xbf8cc07fu,
0x810a846au, 0xbe8b03ffu, 0x00016204u, 0x4a020284u,
0xbe891d92u, 0xe0702000u, 0x80020001u, 0xbf810000u,
};
std::array<uint32_t, 8> user_data{0x30u, 0x1cu, 1u, 0xfffffff0u,
0x1000u, 0u, 0xfffffffcu, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.buffers.size() == 1 && plan.info.buffers[0].written,
"uniform address selection lost its writable buffer");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const bool alternate = *static_cast<uint32_t *>(data) == 0;
if (words.size() != 1) return false;
switch (address) {
case 0x1040: words[0] = 8u; return alternate;
case 0x1044: words[0] = 1u; return alternate;
case 0x1080: words[0] = 3u; return !alternate;
case 0x109c: words[0] = 2u; return !alternate;
case 0x1084: words[0] = 10u; return true;
default: return false;
}
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[2], .read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {1u, 0u, 1u}) {
user_data[2] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"uniform selection evaluated an unavailable untaken scalar source");
const auto &descriptor = snapshot.buffers[0].dwords;
Check(descriptor[0] == (flag ? 0xe0u : 4u) &&
descriptor[1] == (flag ? 0x40031u : 0x4001eu) &&
descriptor[2] == 14u && descriptor[3] == 0x16204u,
"uniform writable descriptor lost its branch, carry, or post-merge stride");
}
}
void TestUniformSelectedDescriptorLoadAddress() {
using namespace ShaderRecompiler::IR;
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 128), // s_cmp_eq_u32 s0, 0
EncodeSopp(0x04, 2), // s_cbranch_scc0 alternate
EncodeSop1(0x04, 6, 2), // s_mov_b64 s[6:7], s[2:3]
EncodeSopp(0x02, 1), // s_branch load
EncodeSop1(0x04, 6, 4), // s_mov_b64 s[6:7], s[4:5]
EncodeSmem0(0x02, 8, 3), 125u << 25u, // s_load_dwordx4 s[8:11], s[6:7], 0
EncodeMubuf0(0x1c), EncodeMubuf1(0, 2, 0),
EncodeSopp(0x01),
};
std::array<uint32_t, 6> user_data{0u, 0u, 0x1000u, 0u, 0x2000u, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u &&
plan.info.buffers[0].written && !plan.capture_specialization_reads,
"conditional scalar address duplicated or detached descriptor reads");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto flag = *static_cast<uint32_t *>(data);
const uint64_t base = flag ? 0x2000u : 0x1000u;
const std::array<uint32_t, 4> descriptor{0x3000u + flag * 0x1000u,
4u << 16u, 16u, 0x16204u};
if (words.size() != 1u || address < base || address >= base + 16u ||
(address & 3u) != 0u) return false;
words[0] = descriptor[(address - base) / 4u];
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[0]};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {0u, 1u, 0u}) {
user_data[0] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[0] == 0x3000u + flag * 0x1000u &&
snapshot.specialization_reads.empty(),
"conditional descriptor load read the untaken pointer");
}
}
void TestBoundedScalarMaterialImageKeys() {
using namespace ShaderRecompiler::IR;
// SAROS 9fba2edffc549531: min(header count,64), scalar rows of 160 bytes,
// and separate float/uint image fields at 0x7c/0x94 in the same image heap.
std::vector<uint32_t> shader {
0xf4080106u, 0xfa0000a0u, 0xf4080806u, 0xfa0000b0u,
0xbf8cc07fu, 0xf4201550u, 0xfa000024u, 0xbf8cc07fu,
EncodeSop2(0x07, 85, 85, 0xc0), EncodeSMovB32(86, 128),
EncodeSMovB32(87, 126),
};
const auto loop = shader.size();
shader.insert(shader.end(), {0x7da2aaf9u, 0x86860056u, EncodeSopp(0x08),
0x931cff56u, 0x000000a0u, 0xf42006c2u, 0x38000094u,
0xf4200682u, 0x3800007cu, 0xbf8cc07fu, 0x8f58851bu, 0x8f3c851au,
0xf42c1304u, 0xb0000000u, 0xf42c0d04u, 0x78000000u, 0xbf8cc07fu,
0xf0000110u, 0x00130106u, 0xf0000110u, 0x000d0206u,
EncodeVop1(0x01, 3, 78), // Preserve a T# dimension word as native scalar payload.
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0),
EncodeMubuf0(0x1c, 4, false), EncodeMubuf1(2, 0, 0),
EncodeMubuf0(0x1c, 8, false), EncodeMubuf1(3, 0, 0),
EncodeSop2(0x02, 86, 86, 129), EncodeSMovB32(126, 87),
EncodeSopp(0x02)});
shader.back() = EncodeSopp(0x02, static_cast<uint16_t>(loop - shader.size()));
shader[loop + 2u] = EncodeSopp(0x08, shader.size() - loop - 3u);
shader.push_back(EncodeSopp(0x01));
std::array<uint32_t, 14> user_data{};
user_data[8] = 0x100000u;
user_data[9] = 32u << 16u;
user_data[10] = 198656u;
user_data[11] = 0x5204u;
user_data[12] = 0x1000u;
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.images.size() == 2 && plan.capture_specialization_reads,
"scalar material fields lost their distinct image roots or clean read capture");
for (const auto &image : plan.info.images) {
const auto &source = plan.descriptor_sources[image.source].indirect_descriptor;
Check(source && source->selector && source->selector->stride == 160u &&
(source->selector->offset == 0x7cu || source->selector->offset == 0x94u) &&
source->table_stride == 32u && !source->table_scalar &&
source->key_count.ResolveInstruction()->GetOpcode() == ValueOpcode::UMin32,
"scalar material image selection fell back to the whole heap");
}
Check(std::ranges::any_of(translated.program.blocks, [&](const auto *block) {
return std::ranges::any_of(*block, [&](const auto &inst) {
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) return false;
const auto &memory = translated.program.memory_info[inst.template Flags<MemoryFlags>().index];
return memory.component_count == 8u && memory.component_index == 2u &&
!memory.planning_only && inst.HasUses();
});
}), "scalar material projection removed a live T# dimension payload");
struct Memory {
uint32_t count = 2;
uint32_t stride = 160;
uint32_t records = 409;
uint64_t unavailable = 0;
bool clean = true;
bool mixed = false;
bool same_key = false;
uint32_t keys = 0;
uint32_t images = 0;
} memory;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (!m.clean || address == m.unavailable) return false;
if (words.size() == 1 && address >= 0x10a0u && address < 0x10c0u) {
const uint32_t descriptors[] = {0x2000u, m.stride << 16u, m.records, 0x5204u,
0x4000u, 16u << 16u, 8u, 0x4dfacu};
words[0] = descriptors[(address - 0x10a0u) / 4u];
return true;
}
if (address == 0x4024u && words.size() == 1) { words[0] = m.count; return true; }
for (const uint32_t row : {0u, 1u}) {
for (const uint32_t field : {0x7cu, 0x94u}) {
if (address != 0x2000u + row * 160u + field || words.size() != 1) continue;
++m.keys;
words[0] = (row == 0 || m.same_key ? 5582u : 5567u) + (field == 0x94u ? 6u : 0u);
return true;
}
}
for (const uint32_t key : {5567u, 5582u, 5573u, 5588u}) {
if (address != 0x100000u + key * 32u || words.size() != 8) continue;
++m.images;
std::ranges::fill(words, 0u);
words[0] = key;
const bool integer = key == 5573u || key == 5588u;
words[1] = static_cast<uint32_t>(integer && !(m.mixed && key == 5573u)
? Prospero::BufferFormat::k32UInt : Prospero::BufferFormat::k32Float) << 20u;
words[3] = DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Prospero::ImageType::kColor3D) << 28u);
return true;
}
return false;
};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t stride : {160u, 16u}) {
memory = {.stride = stride, .records = stride == 160u ? 409u : 20u};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == 4 && memory.images == 4 && snapshot.images.size() == 4,
"scalar material selection used V# stride as its instruction stride or scanned unused keys");
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
const auto &selector = *plan.descriptor_sources[plan.info.images[root].source].indirect_descriptor->selector;
const uint32_t first = selector.offset == 0x94u ? 5573u : 5567u;
const auto offset = specialization.images[root].indirect_mapping_offset;
Check(snapshot.flattened_srt[offset] == 2u &&
snapshot.flattened_srt[offset + 1u] == first &&
snapshot.flattened_srt[offset + 3u] == first + 15u,
"scalar material image mapping changed descriptor-index keys into byte offsets");
}
}
for (const bool same_key : {false, true}) {
memory = {.count = same_key ? 2u : 1u, .same_key = same_key};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == (same_key ? 4u : 2u) && memory.images == 2 &&
snapshot.images.size() == 2 &&
std::ranges::all_of(specialization.images, [](const auto &image) {
return image.indirect_root == ImageResource::NoIndirectImage;
}), "single-candidate scalar material loop retained an indirect image mapping");
}
memory = {.count = 0};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.keys == 0 && memory.images == 0 && snapshot.images.size() == 2,
"empty scalar material loop retained image probes");
memory = {.clean = false};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"dirty scalar material proof was accepted");
memory = {.unavailable = 0x2094u};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unavailable selected material key was accepted");
memory = {.stride = 1, .records = 311};
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.keys == 0,
"scalar material byte range escaped V# bounds");
memory = {.mixed = true};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"selected material fields bypassed image compatibility");
memory = {};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"native scalar material image selection did not recover after rejected candidates");
ApplyResourceSpecialization(translated.program, specialization);
Check(translated.program.info.images.size() == 4 &&
translated.program.info.images[0].indirect_resources.size() == 2 &&
translated.program.info.images[1].indirect_resources.size() == 2,
"native image reads lost their distinct material field dispatches");
}
void TestTypedDescriptorRealCarryAndScalarLoads() {
const uint32_t carry_shader[] = {
EncodeSop1(0x1f, 0, 0), // s_getpc_b64 s[0:1]
@@ -15052,7 +15614,7 @@ int main() {
TestVopcCmpxClassF16Decoder();
TestNewShaderRecompilerCapturedVopcSdwaCmpxLtU16();
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
TestNewShaderRecompilerRejectsDppOn64BitCompares();
TestNewShaderRecompilerInteger64CompareEncodings();
TestFloatComparisonInputModes();
TestPsInputCountRegisterDecode();
TestPixelAncillaryLayerInput();
@@ -15120,6 +15682,8 @@ int main() {
TestNewShaderRecompilerPixelImageSampleLodSelection();
TestNewShaderRecompilerBranchConditionForms();
TestNewShaderRecompilerSetpcBranch();
TestGraphicsComputeGetreg();
TestMemoryFedScalarLeafCall();
TestFusedShaderHandoffPreservesRegisters();
TestMeshExportStorage();
TestMergedShaderUserDataSnapshot();
@@ -15153,6 +15717,10 @@ int main() {
TestTypedDescriptorRealWideMoveTranslation();
TestComputeImageFill();
TestGpuProducedWritableDescriptor();
TestUniformSelectedWritableDescriptor();
TestUniformSelectedDescriptorLoadAddress();
TestBoundedScalarMaterialImageKeys();
TestImmutableDescriptorPredicate();
TestTypedDescriptorRealCarryAndScalarLoads();
TestSrtWalkerRealSmemTranslation();
TestSrtWalkerVccBaseTranslation();